From d9f8358191c15ed23000c306549105c9132e958a Mon Sep 17 00:00:00 2001 From: chenzhihao <1798906853@qq.com> Date: Tue, 13 Jan 2026 15:37:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=93=9D=E7=89=99=E9=80=9A=E8=AE=AF=E5=88=9D?= =?UTF-8?q?=E6=AD=A5=E8=B0=83=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/W13无卡取电设备 - 蓝牙通讯协议.md | 154 +++ app.json | 4 +- img/lanya.png | Bin 0 -> 3849 bytes img/xinhaodi.png | Bin 0 -> 959 bytes img/xinhaogao.png | Bin 0 -> 2307 bytes img/xinhaozhong.png | Bin 0 -> 1761 bytes pages/NewHome/NewHome.js | 6 + .../BluetoothDebugging/B13page/B13page.js | 945 ++++++++++++++++++ .../BluetoothDebugging/B13page/B13page.json | 8 + .../BluetoothDebugging/B13page/B13page.wxml | 296 ++++++ .../BluetoothDebugging/B13page/B13page.wxss | 117 +++ .../BluetoothDebugging/BluetoothDebugging.js | 535 ++++++++++ .../BluetoothDebugging.json | 5 + .../BluetoothDebugging.wxml | 60 ++ .../BluetoothDebugging.wxss | 193 ++++ utils/w13Packet.js | 163 +++ 16 files changed, 2485 insertions(+), 1 deletion(-) create mode 100644 Document/W13无卡取电设备 - 蓝牙通讯协议.md create mode 100644 img/lanya.png create mode 100644 img/xinhaodi.png create mode 100644 img/xinhaogao.png create mode 100644 img/xinhaozhong.png create mode 100644 pages/basics/BluetoothDebugging/B13page/B13page.js create mode 100644 pages/basics/BluetoothDebugging/B13page/B13page.json create mode 100644 pages/basics/BluetoothDebugging/B13page/B13page.wxml create mode 100644 pages/basics/BluetoothDebugging/B13page/B13page.wxss create mode 100644 pages/basics/BluetoothDebugging/BluetoothDebugging.js create mode 100644 pages/basics/BluetoothDebugging/BluetoothDebugging.json create mode 100644 pages/basics/BluetoothDebugging/BluetoothDebugging.wxml create mode 100644 pages/basics/BluetoothDebugging/BluetoothDebugging.wxss create mode 100644 utils/w13Packet.js diff --git a/Document/W13无卡取电设备 - 蓝牙通讯协议.md b/Document/W13无卡取电设备 - 蓝牙通讯协议.md new file mode 100644 index 0000000..05473c3 --- /dev/null +++ b/Document/W13无卡取电设备 - 蓝牙通讯协议.md @@ -0,0 +1,154 @@ +# W13无卡取电设备 - 蓝牙通讯协议 + +> 版本同步记录: +> - 同步来源:Document/W13无卡取电设备 - 蓝牙通讯协议(1).pdf +> - 同步日期:2026-01-12 +> - 说明:依据PDF抽取与差异比对,更新命令与参数细节,使Markdown与PDF一致。 + +## 1. 通讯方式 +使用蓝牙BLE通讯 + +## 2. 帧结构定义 + +### 2.1 帧字段说明 +| 字节范围 | 功能 | 长度(Bytes) | 取值范围(&H) | 备注 | +|---------|------|-------------|---------|------| +| B0~B1 | Head | 2 | 0xCC 0xC0 | 固定包头 | +| B2~B3 | Len | 2 | 00~548 | 数据的总长度,包括包头和CRC校验,低地址在前 | +| B4~B5 | CRC | 2 | 00~FF | 整包CRC16校验 | +| B6~B7 | Frame | 2 | 00~FF | 帧号 | +| B8~B9 | FramNum | 2 | 00~FF | 帧总数 | +| B10 | Frame_Type | 1 | 00~FF | 帧类型,命令字 | +| B11~B1023 | PARA_0~1012 | Max 1013 | 00~FF | 参数,不同类型有不同的参数字(不定长) | + +### 2.2 帧结构示意图 + +```mermaid +sequenceDiagram + participant PC as 上位机 + participant Device as 无卡取电设备 + + PC->>Device: 发送命令帧 + Note over PC,Device: 帧结构:Head + Len + CRC + Frame + FramNum + Frame_Type + Parameters + Device->>PC: 返回响应帧 + Note over Device,PC: 帧结构:Head + Len + CRC + Frame + FramNum + Frame_Type + Response +``` + +## 3. 详细命令列表 + +命令总览表(快速索引): + +| 序号 | 功能 | 方向 | 命令字 | 备注 | +|------|------|------|--------|------| +| 1 | 读版本号 | PC→MCU / MCU→PC | 0x01 | P0=0x00,请求;返回软件/硬件版本 | +| 2 | 设置无卡取电条件信息 | PC→MCU / MCU→PC | 0x08 | 条件参数设置;返回P0=0x01/0x02 | +| 3 | 设置无卡取电端口信息 | PC→MCU / MCU→PC | 0x09 | 端口配置;返回P0=0x01/0x02 | +| 4 | OTA升级开始 | PC→MCU | 0x0B | P0=0x01,进入bootloader等待OTA | +| 5 | 开启蓝牙打印 | PC→MCU / MCU→PC | 0x0C | 打印开关bit0..bit4;返回P0=0x01/0x02 | +| 6 | 雷达状态获得 | PC→MCU / MCU→PC | 0x11 | 开/关读取;返回端口状态位与有人/无人 | +| 7 | 测试按键功能 | PC→MCU / MCU→PC | 0x13 | 点按控制与状态返回 | +| 8 | 事件设置(门磁/卫浴灯) | PC→MCU / MCU→PC | 0x16 | 控制位bit0/bit1,事件时序参数与单位 | + +### 3.1 读版本号 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x01 | P0: 0x00 | 读取版本号命令 | +| MCU→PC | 0x01 | P0: 软件版本号
P1: 硬件版本号 | 返回版本信息 | + +### 3.2 设置无卡取电条件信息(命令1) +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x08 | P0: 有无逻辑标记
P1: 条件组
P2: 条件序号
P3~P4: 条件判定时间
P5: 条件判定时间单位
P6~P9: 端口1~10状态
P10: 触发阈值
P11~P12: 条件超时时间
P13: 条件超时时间单位 | 设置无卡取电条件 | +| MCU→PC | 0x08 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | + +### 3.3 设置无卡取电条件信息(命令2) +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x09 | P0: 端口设备类型
P1: 端口设备地址
P2~P3: 端口设备回路
P4: 有人->无人阈值
P5: 虚拟端口号
P6: 回路是否启用检测统计
P7~P8: 回路检测统计时间
P9: 回路检测统计时间单位
P10: 无人->有人阈值 | 设置无卡取电条件 | +| MCU→PC | 0x09 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | + +### 3.4 OTA升级开始 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x0B | P0: 0x01(开始升级) | 启动OTA升级,设备进入 bootloader,等待 OTA 升级 APP 连接 | + +### 3.5 开启蓝牙打印 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x0C | P0: bit0(系统调试信息打印开关)
bit1(设备驱动层打印调试信息打印开关)
bit2(蓝牙信息打印开关)
bit3(PC通讯打印开关)
bit4(临时调试信息打印开关) | 设置打印开关 | +| MCU→PC | 0x0C | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | + +### 3.6 雷达状态获得 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x11 | P0: 0x01(开启读取端口状态) / 0x02(关闭读取) | 请求雷达状态;端口状态值:0=释放,1=触发 | +| MCU→PC | 0x11 | P0: 有效端口数量
P1: 有无人状态(0x01=有人,0x02=无人)
P2: bit0(端口1状态), bit1(端口2状态), bit2(端口3状态), bit3(端口4状态)...(0=释放,1=触发) | 返回雷达状态 | + +补充说明: +- 端口状态值:0=释放,1=触发。 +- 典型端口位含义:bit0=端口1(门磁),bit1=端口2(洗手间),bit2=端口3(卧室),bit3=端口4(门口),后续端口依次类推。 + +### 3.7 测试按键功能 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x13 | P0: 0x01(按键点按控制)
P1: bit0(按键1触发), bit1(按键2触发), bit2(按键3触发), bit3(按键4触发), bit4(按键5触发), bit5(按键6触发) | 测试按键功能(对应按键仅支持点按,不具备开关状态) | +| MCU→PC | 0x13 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | + +### 3.8 设置门磁开关走廊灯、卫浴雷达开关卫浴灯事件 +| 方向 | 命令字 | 参数 | 备注 | +|------|-------|------|------| +| PC→MCU | 0x16 | P0: 控制位(bit0=门磁开关走廊灯事件;bit1=卫浴灯开关事件)
门磁开关走廊灯事件:
P1: 事件触发延迟时间数值
P2: 时间单位(1=秒,2=分,3=时)
P3: 事件持续时间数值
P4: 时间单位(1=秒,2=分,3=时)
P5: 事件释放延迟时间数值
P6: 时间单位(1=秒,2=分,3=时)
卫浴灯开关事件:
P7: 事件触发延迟时间数值
P8: 时间单位(1=秒,2=分,3=时)
P9: 事件持续时间数值
P10: 时间单位(1=秒,2=分,3=时)
P11: 事件释放延迟时间数值
P12: 时间单位(1=秒,2=分,3=时) | 设置事件参数(用于控制门磁亮走廊灯、卫浴雷达亮卫浴灯等) | +| MCU→PC | 0x16 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | + +## 4. 命令交互流程图 + +### 4.1 读版本号流程 +```mermaid +flowchart TD + A[上位机发送读版本号命令
Frame_Type=0x01, P0=0x00] --> B[设备接收命令] + B --> C{命令解析正确?} + C -->|是| D[设备准备版本信息] + C -->|否| E[设备返回错误响应] + D --> F[设备发送响应帧
Frame_Type=0x01, 包含软硬件版本号] + E --> G[设备发送错误响应帧
Frame_Type=0x01, P0=0x02] + F --> H[上位机接收版本信息] + G --> I[上位机处理错误] +``` + +### 4.2 设置无卡取电条件流程 +```mermaid +flowchart TD + A[上位机发送设置条件命令
Frame_Type=0x08/0x09] --> B[设备接收命令] + B --> C{参数验证通过?} + C -->|是| D[设备保存条件设置] + C -->|否| E[设备标记参数错误] + D --> F[设备发送成功响应
P0=0x01] + E --> G[设备发送失败响应
P0=0x02] + F --> H[上位机确认设置成功] + G --> I[上位机重新发送或处理错误] +``` + +## 5. 数据传输流程 + +```mermaid +sequenceDiagram + participant PC as 上位机 + participant BLE as 蓝牙模块 + participant Device as 设备主控 + + PC->>BLE: 发送BLE数据 + BLE->>Device: 转发数据帧 + Device->>BLE: 处理并返回响应 + BLE->>PC: 转发响应帧 +``` + +## 6. 异常处理 + +| 错误类型 | 错误码 | 处理方式 | +|---------|-------|---------| +| 参数错误 | 0x02 | 重新发送正确参数 | +| 命令不支持 | - | 检查命令字是否正确 | +| 通讯超时 | - | 重新发送命令 | + +--- + diff --git a/app.json b/app.json index 461b1a2..6f1c6b8 100644 --- a/app.json +++ b/app.json @@ -14,7 +14,9 @@ "pages/test/test", "pages/basics/FacialDeviceBinding/FacialDeviceBinding", "pages/basics/progress/progress", - "pages/basics/progress/RoomTypeControlLog/RoomTypeControlLog" + "pages/basics/progress/RoomTypeControlLog/RoomTypeControlLog", + "pages/basics/BluetoothDebugging/BluetoothDebugging", + "pages/basics/BluetoothDebugging/B13page/B13page" ], "window": { "backgroundTextStyle": "light", diff --git a/img/lanya.png b/img/lanya.png new file mode 100644 index 0000000000000000000000000000000000000000..983284612120bea81a4439d21f75485a51aa8500 GIT binary patch literal 3849 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 B2oV4P literal 0 HcmV?d00001 diff --git a/img/xinhaogao.png b/img/xinhaogao.png new file mode 100644 index 0000000000000000000000000000000000000000..85f4c6ed717744f1e0ad15c5ed5ee946f9389a4f GIT binary patch literal 2307 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>f#Z~?i(^Q|oVS+`_RiQT=<;x0 zE7MGdJq=C|>P=XVE3_uKKX}I@bI6L#`G)dK9+n%7J_=38^cg=ZEa;Bduuph>|BEV4 zXAIUk)YR43Te`E9XH$9leRq^EZiIZzkmFG{rbE7$C=B| zw8uZ1fB)Z~Z(>Sv9e=ktAOBonqwT_HD%LCMB1)iuvF zKR;Cc_wDWF+vf9x+WVeAtO)Cn$hrbIHbxGXkF+q(-%l z2GeMg7#2C_r$gP}U%#IIb+6%E$iM$c{Qlai-^YRQzXsw2%E~2$*{$er5B~B4Fc&!PC{xWt~$(69B)QutNX< literal 0 HcmV?d00001 diff --git a/img/xinhaozhong.png b/img/xinhaozhong.png new file mode 100644 index 0000000000000000000000000000000000000000..0a02c71e475c1c424e369268ddc8dddf3bb7113c GIT binary patch literal 1761 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fo+ASi(^Q|oVR!1`btMiv^^|e z(6FQVs|M?lXvQ+uMD2#R3|bw*j&~Sy*q1Q*us^duxLNdpF2kPlPm-5TEt=;dnEof~ z&h!#HoqsKlciKMwvyhX)m4!(H9W}kW_WJ9|%a<=#?7r)l+&*hrq@0Y*oo%_*OQ+kn zIt26{nqgySS9hV+ve!lA)u#_1KAhi{yZZ+tlcQ)?@{--hqHa%gbDR54ZU6S%-52+D zI0PL1`}glsyOsW1&c6Bk`uh6ki2@w+?q1)0ym{M8O@Z4De-%Gyi<}5)jLDuoPi}M6 z%S8i9z^Ja{q5H$HMSKlxjg5^Q;U5)3rjEd=dM}QIXi9JtruVY1UUXJw&*jh zzfdMqSyGjKUi4(>8-~UQ#zzZR{ZzX2OXj50Y)3(kUlwk&uHVY?&0f5HN2p3vyR*Uz zX3NWW?4wk#?+I78qXv$Ue< za(Cvj7OivW+MmK}HGk=|#UAT-UcKqFMe@wuyLU4$n~2|9R$fqGkbS#Ml2vcQ#Aoce z-yMuEyKd-Vd|9*3WdBvOozL9Tje8l_UVr@(STb$}7F`p>h)_{Zz^lUZTn zj^fOhZk{pS-~FYn{!fU4GV7-Wxm8trHu2t&e|OyVLd_M{f;DFsl$bs4h{*izm7R3B zzT(Tf_wV($-+tS{=NHn_WEuQ zaluG?hkw@|UtN-WyZ4)`z3>SOC9zw3ug>x)Uj5QX{D4uJ$MfQ4sq5~p_IUi6{kT)Q zaldSk%H-q6kGJmIApbH|YS-qwGIDbBwmsW@+}xh$iF7Q-<&GI`b{kFIq;_xW>F~?{ zm#VUG^UL#(eUg_LUc50?Z(oo1Tz_%Tn!oXDH|m|qd~?q2f7{e|QmeP+uK)6Dxrr15 g7YB<08v4ijN1562XXW?9z^aSE)78&qol`;+03?{J8UO$Q literal 0 HcmV?d00001 diff --git a/pages/NewHome/NewHome.js b/pages/NewHome/NewHome.js index 232aad4..75786a1 100644 --- a/pages/NewHome/NewHome.js +++ b/pages/NewHome/NewHome.js @@ -32,6 +32,12 @@ Page({ color: 'pink', icon: 'btn' }, + { + title: '蓝牙调试', + name: 'BluetoothDebugging', + color: 'cyan', + icon: 'tagfill' + }, { title: '红外转发码库下载', name: 'InfraredLibraryDownload', diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.js b/pages/basics/BluetoothDebugging/B13page/B13page.js new file mode 100644 index 0000000..ae85dce --- /dev/null +++ b/pages/basics/BluetoothDebugging/B13page/B13page.js @@ -0,0 +1,945 @@ + +const { buildCommand, buildReadVersion, COMMANDS } = require('../../../../utils/w13Packet.js') +const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08]) +Page({ + data: { + TabCur: 1, // 1: 蓝牙调试 2: 蓝牙升级 + DevName: '', + bleName: '', + // 预设占位数据,未获取真实设备信息时用于展示 + bleMac: '00:00:00:00:00:00', + bleAMC: '-', + bleVersion: '-', + openDelay: 20, + bathDelay: 20, + logs: [], + hexSend: false, + hexShow: false, + withTimestamp: false, + wrapCRLF: false, + sendText: '', + importFileName: '', + // BLE上下文(从上一页传入) + deviceId: '', + serviceId: '', + txCharId: '', + rxCharId: '', + logList: [], + timeUnits: ['时', '分', '秒'], + hourValues: Array.from({ length: 24 }, (_, i) => i + 1), + msValues: Array.from({ length: 60 }, (_, i) => i + 1), + ports: [ + { name: '无卡取电 CH1', portLabel: '开门磁', alias: '开门磁', deviceType: 0, deviceAddr: 0, loop: 1, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '无卡取电 CH2', portLabel: '门口红外', alias: '门口红外', deviceType: 0, deviceAddr: 0, loop: 2, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '无卡取电 CH3', portLabel: '床头红外', alias: '床头红外', deviceType: 0, deviceAddr: 0, loop: 3, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '无卡取电 CH4', portLabel: '卫浴红外', alias: '卫浴红外', deviceType: 0, deviceAddr: 0, loop: 4, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 } + ], + // 条件“有无人标记”选项,参考截图:无人至有人/短暂离开/长时间离开/有人至无人 + tagOptions: ['无人至有人', '短暂离开', '长时间离开', '有人至无人'], + stateOptions: ['不判断', '触发', '释放', '开启', '关闭'], + // 默认条件:与截图一致(组1..6,各1条) + conditions: [ + { group: 1, seq: 1, tag: 0, cardPower: 0, doorMag: 1, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 0, timeoutUnit: 2 }, + { group: 2, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, + { group: 2, seq: 2, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, + { group: 2, seq: 3, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, + { group: 3, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 0, timeoutUnit: 2 }, + { group: 4, seq: 1, tag: 1, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 0, judgeUnit: 2, timeout: 2, timeoutUnit: 2 }, + { group: 5, seq: 1, tag: 2, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }, + { group: 6, seq: 1, tag: 3, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 } + ], + // 二级菜单:按组折叠 + condGroups: [], + // 雷达指示灯(bit0=门磁,bit1=卫浴,bit2=卧室,bit3=走廊) + radarLights: [ + { key: 'door', label: '门磁', colorClass: 'gray', triggered: false }, + { key: 'bath', label: '卫浴', colorClass: 'gray', triggered: false }, + { key: 'bed', label: '卧室', colorClass: 'gray', triggered: false }, + { key: 'hall', label: '走廊', colorClass: 'gray', triggered: false } + ] + }, + + onLoad(options) { + const raw = options && (options.DevName || options.name) || '' + // 处理通过 URL 传递的编码,避免中文显示为乱码 + let devName = '' + try { + devName = decodeURIComponent(raw) + } catch (e) { + devName = raw + } + if (!devName) devName = 'B13设备' + const rawMac = options && options.mac ? options.mac : '' + const bleMac = rawMac ? decodeURIComponent(rawMac) : '00:00:00:00:00:00' + const deviceId = rawMac ? decodeURIComponent(rawMac) : '' + const serviceId = options && options.serviceId ? decodeURIComponent(options.serviceId) : '' + const txCharId = options && options.txCharId ? decodeURIComponent(options.txCharId) : '' + const rxCharId = options && options.rxCharId ? decodeURIComponent(options.rxCharId) : '' + this.setData({ DevName: devName, bleName: devName, bleMac, deviceId, serviceId, txCharId, rxCharId }) + // 页面进入时打印当前蓝牙连接状态 + this.logBleStatus() + // 构建条件组 + this.buildCondGroups() + // 自动发现特征并启动雷达订阅/读取 + this.ensureBleChannels(() => { + this.startRadarStatusWatch() + }) + wx.setNavigationBarTitle({ title: devName }) + this.logBleStatus() + + }, + + onShow() { + // 页面显示时也打印一次,方便返回/二次进入场景 + this.logBleStatus() + }, + + onUnload() { + this.teardownBleListener() + }, + + // 顶部标签切换 + tabSelect(e) { + const id = Number(e.currentTarget.dataset.id || 1) + this.setData({ TabCur: id }) + }, + + onOpenDelayChange(e) { + this.setData({ openDelay: e.detail.value }) + }, + onBathDelayChange(e) { + this.setData({ bathDelay: e.detail.value }) + }, + + // 便捷示例:读版本号 + onSendReadVersion() { + try { + const pkt = buildReadVersion() + const text = this.data.hexShow ? this.toHex(pkt) : `[${Array.from(pkt).join(', ')}]` + this.appendLog('TX', `读版本号: ${text}`) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + }, + + // 开始OTA升级(命令0x0B, P0=0x01) + onStartOta() { + try { + const pkt = buildCommand(COMMANDS.OTA_START, [0x01]) + const text = this.toHex(pkt) + this.appendLog('TX', `OTA开始: ${text}`) + wx.showToast({ title: '已发送OTA开始', icon: 'success' }) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + }, + + // 开启蓝牙打印(命令0x0C,示例掩码0x1F) + onEnableBleLog() { + try { + const mask = 0x1F + const pkt = buildCommand(COMMANDS.ENABLE_BLE_LOG, [mask]) + const text = this.toHex(pkt) + this.appendLog('TX', `开启打印(0x${mask.toString(16).toUpperCase()}): ${text}`) + wx.showToast({ title: '已发送打印开关', icon: 'success' }) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + }, + + toHex(u8) { + return Array.from(u8).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ') + }, + + // 控制台输出当前蓝牙连接状态 + logBleStatus() { + const { deviceId, serviceId, txCharId, rxCharId } = this.data || {} + console.info(`[BLE] B13page enter: device=${deviceId || '-'} svc=${serviceId || '-'} tx=${txCharId || '-'} rx=${rxCharId || '-'}`) + const onUnknown = () => console.info('[BLE] connection state: unknown (no deviceId or API missing)') + if (!deviceId) { + onUnknown() + return + } + const logConnected = (connected) => console.info(`[BLE] connection state: ${connected ? 'connected' : 'disconnected'}`) + // 先尝试新版接口 + if (typeof wx.getBLEConnectionState === 'function') { + try { + wx.getBLEConnectionState({ + deviceId, + success: (res) => logConnected(!!(res && res.connected)), + fail: () => console.warn('[BLE] get connection state failed') + }) + } catch (e) { + console.warn('[BLE] get connection state exception') + } + return + } + // 兼容旧端:检查已连接设备列表 + if (typeof wx.getConnectedBluetoothDevices === 'function') { + try { + wx.getConnectedBluetoothDevices({ + success: (res) => { + const list = (res && res.devices) || [] + const hit = list.some(d => (d.deviceId || '').toUpperCase() === deviceId.toUpperCase()) + logConnected(hit) + }, + fail: () => console.warn('[BLE] getConnectedBluetoothDevices failed') + }) + } catch (e) { + console.warn('[BLE] getConnectedBluetoothDevices exception') + } + return + } + onUnknown() + }, + + // 发送前确认当前蓝牙连接状态(兼容无 getBLEConnectionState 场景) + ensureBleConnected(next, attempt = 0) { + const { deviceId } = this.data || {} + if (!deviceId) { + wx.showToast({ title: '未连接BLE', icon: 'none' }) + return + } + + const proceed = () => { if (typeof next === 'function') next() } + + // 优先使用 getBLEConnectionState + if (typeof wx.getBLEConnectionState === 'function') { + try { + wx.getBLEConnectionState({ + deviceId, + success: (res) => { + const connected = !!(res && res.connected) + if (!connected) { + // 某些机型首次查询可能短暂返回断开,允许一次快速重试 + if (attempt < 1) { + this.appendLog('WARN', 'BLE未连接,重试查询...') + setTimeout(() => this.ensureBleConnected(next, attempt + 1), 250) + return + } + this.appendLog('WARN', 'BLE未连接,取消发送') + wx.showToast({ title: '蓝牙未连接', icon: 'none' }) + return + } + proceed() + }, + fail: () => { + this.appendLog('WARN', '查询BLE状态失败,尝试兜底') + this._fallbackCheckConnected(deviceId, proceed) + } + }) + } catch (e) { + this.appendLog('WARN', '查询BLE状态异常,尝试兜底') + this._fallbackCheckConnected(deviceId, proceed) + } + return + } + + // 兼容旧端:直接兜底检查 + this._fallbackCheckConnected(deviceId, proceed) + }, + + // 兜底检查:使用已连接设备列表;若接口不可用则直接继续 + _fallbackCheckConnected(deviceId, proceed) { + if (typeof wx.getConnectedBluetoothDevices !== 'function') { + proceed() + return + } + try { + wx.getConnectedBluetoothDevices({ + success: (res) => { + const list = (res && res.devices) || [] + const norm = (s) => (s || '').replace(/-/g, '').toUpperCase() + const target = norm(deviceId) + const hit = list.some(d => norm(d.deviceId) === target) + if (hit) { + proceed() + return + } + // 若列表为空或匹配不到,但已有 service/char,放行并记录警告(部分机型/安卓可能返回空列表) + if (list.length === 0 || (this.data.serviceId && this.data.txCharId)) { + this.appendLog('WARN', '未在已连接列表中找到,假定已连接尝试发送') + proceed() + return + } + this.appendLog('WARN', '未在已连接设备列表中找到,取消发送') + wx.showToast({ title: '蓝牙未连接', icon: 'none' }) + }, + fail: () => { + this.appendLog('WARN', '查询已连接设备失败,尝试继续发送') + proceed() + } + }) + } catch (e) { + this.appendLog('WARN', '查询已连接设备异常,尝试继续发送') + proceed() + } + }, + + ab2hex(buffer) { + if (!buffer) return '' + return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ') + }, + + _matchUuid(uuid, needle) { + if (!uuid || !needle) return false + const u = String(uuid).replace(/-/g, '').toUpperCase() + const n = String(needle).toUpperCase().replace(/^0X/, '') + return u.includes(n) + }, + + hexToBytes(hex) { + const clean = (hex || '').replace(/\s+/g, '').toUpperCase() + if (clean.length === 0) return new Uint8Array(0) + if (clean.length % 2 !== 0 || /[^0-9A-F]/.test(clean)) return null + const out = new Uint8Array(clean.length / 2) + for (let i = 0; i < clean.length; i += 2) out[i/2] = parseInt(clean.substr(i,2), 16) + return out + }, + + strToBytes(str) { + const s = String(str || '') + const out = new Uint8Array(s.length) + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xFF + return out + }, + + writeBleBytes(u8, label) { + const { deviceId, serviceId, txCharId } = this.data || {} + if (!deviceId) { + wx.showToast({ title: '未连接BLE', icon: 'none' }) + return + } + const doWrite = () => { + // 若缺少特征/服务,先自动发现再发送 + if (!serviceId || !txCharId) { + this.ensureBleChannels(() => this.writeBleBytes(u8, label)) + return + } + + const bytes = (u8 instanceof Uint8Array) ? u8 : new Uint8Array(u8 || []) + this.appendLog('CFG', `device=${deviceId || '-'} svc=${serviceId || '-'} tx=${txCharId || '-'}`) + // 发送完成时打印当前蓝牙连接状态 + this.logBleStatus() + wx.writeBLECharacteristicValue({ + deviceId, + serviceId, + characteristicId: txCharId, + value: bytes.buffer, + success: () => { + // 发送完成时打印当前蓝牙连接状态 + this.logBleStatus() + }, + fail: (err) => { + const msg = (err && err.errMsg) ? err.errMsg : '发送失败' + this.appendLog('WARN', `${label || '发送'} ${msg}`) + wx.showToast({ title: '发送失败', icon: 'none' }) + } + }) + } + + // 写入前检查当前蓝牙连接状态(兼容旧接口) + this.ensureBleConnected(doWrite) + }, + + ensureBleChannels(done) { + const { deviceId, serviceId, txCharId, rxCharId } = this.data || {} + // 若未携带 deviceId,尝试从系统已连接设备列表中兜底获取 + let devId = deviceId + if (!devId && typeof wx.getConnectedBluetoothDevices === 'function') { + try { + wx.getConnectedBluetoothDevices({ + success: (res) => { + const list = (res && res.devices) || [] + if (list.length > 0) { + const first = list[0] + devId = first && (first.deviceId || first.deviceId) + if (devId) this.setData({ deviceId: devId }) + // 继续后续流程 + this.ensureBleChannels(done) + } else { + wx.showToast({ title: '未发现已连接设备', icon: 'none' }) + } + }, + fail: () => wx.showToast({ title: '获取连接设备失败', icon: 'none' }) + }) + } catch (e) { + wx.showToast({ title: '蓝牙接口异常', icon: 'none' }) + } + return + } + if (!devId) { + wx.showToast({ title: '未连接BLE', icon: 'none' }) + return + } + if (serviceId && txCharId && rxCharId) { + if (typeof done === 'function') done() + return + } + if (typeof wx.getBLEDeviceServices !== 'function') { + wx.showToast({ title: 'BLE接口不可用', icon: 'none' }) + return + } + wx.showLoading({ title: '发现服务...', mask: true }) + wx.getBLEDeviceServices({ + deviceId: devId, + success: (srvRes) => { + const services = srvRes.services || [] + let found = false + let pending = services.length + const score = (s) => { + const u = (s.uuid || '').toUpperCase() + return u.includes('FFE') ? 2 : (s.isPrimary === true ? 1 : 0) + } + const sorted = services.slice().sort((a, b) => score(b) - score(a)) + sorted.forEach(s => { + wx.getBLEDeviceCharacteristics({ + deviceId: devId, + serviceId: s.uuid, + success: (chRes) => { + const chars = chRes.characteristics || [] + const ffe1 = chars.find(c => this._matchUuid(c.uuid, 'FFE1')) + const ffe2 = chars.find(c => this._matchUuid(c.uuid, 'FFE2')) + if (!found && ffe1 && ffe2) { + found = true + this.setData({ serviceId: s.uuid, txCharId: ffe1.uuid, rxCharId: ffe2.uuid }) + // 立即开启通知 + this.enableNotify() + wx.hideLoading() + if (typeof done === 'function') done() + } + }, + complete: () => { + pending -= 1 + if (!found && pending === 0) { + wx.hideLoading() + wx.showToast({ title: '未找到FFE1/FFE2', icon: 'none' }) + } + } + }) + }) + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '获取服务失败', icon: 'none' }) + } + }) + }, + + startRadarStatusWatch() { + // 开启订阅(若已传入rx特征),保证能接收数据 + this.enableNotify() + this.setupBleListener() + this.sendRadarStatusCommand(true) + }, + + enableNotify() { + const { deviceId, serviceId, rxCharId } = this.data || {} + if (!deviceId || !serviceId || !rxCharId || typeof wx.notifyBLECharacteristicValueChange !== 'function') { + this.appendLog('WARN', '通知前置条件不足') + return + } + this.appendLog('CFG', `enableNotify device=${deviceId} svc=${serviceId} rx=${rxCharId}`) + const tryEnable = (attempt) => { + try { + wx.notifyBLECharacteristicValueChange({ + state: true, + deviceId, + serviceId, + characteristicId: rxCharId, + success: () => this.appendLog('UI', `已开启通知 device=${deviceId} svc=${serviceId} rx=${rxCharId}`), + fail: (err) => { + const code = (err && (err.errCode ?? err.code)) + const msg = (err && (err.errMsg || err.message)) || '未知原因' + const detail = code !== undefined ? `code=${code} ${msg}` : msg + this.appendLog('WARN', `开启通知失败(重试${attempt}) ${detail}`) + wx.showToast({ title: '通知失败', icon: 'none' }) + if (attempt < 2) { + // 触发一次服务特征刷新后再重试 + this.ensureBleChannels(() => setTimeout(() => tryEnable(attempt + 1), 120)) + } + } + }) + } catch (e) { + const msg = (e && (e.errMsg || e.message)) || '异常' + this.appendLog('WARN', `开启通知异常 ${msg}`) + wx.showToast({ title: '通知异常', icon: 'none' }) + } + } + tryEnable(0) + }, + + sendRadarStatusCommand(enable) { + try { + const payload = [enable ? 0x01 : 0x02] + const pkt = buildCommand(COMMANDS.RADAR_STATUS, payload) + this.transmitPacket(pkt, `雷达状态${enable ? '开启读取' : '关闭读取'}`) + } catch (err) { + wx.showToast({ title: '雷达命令构建失败', icon: 'none' }) + } + }, + + transmitPacket(pkt, label) { + + const hex = this.toHex(pkt) + this.appendLog('TX', `${label}: ${hex}`) + // 如果页面接入了BLE连接参数,则尝试写入;未配置则仅记录日志 + const { deviceId, serviceId, txCharId } = this.data || {} + if (!deviceId || !serviceId || !txCharId || typeof wx.writeBLECharacteristicValue !== 'function') return + try { + wx.writeBLECharacteristicValue({ + deviceId, + serviceId, + characteristicId: txCharId, + value: (pkt && pkt.buffer) ? pkt.buffer : new Uint8Array(pkt || []).buffer, + fail: (err) => { + const code = err && (err.errCode ?? err.code) + const msg = (err && (err.errMsg || err.message)) || '未知原因' + const detail = code !== undefined ? `code=${code} ${msg}` : msg + this.appendLog('WARN', `写入BLE失败 device=${deviceId} svc=${serviceId} tx=${txCharId} ${detail}`) + wx.showToast({ title: '发送失败', icon: 'none' }) + } + }) + } catch (err) { + const msg = (err && (err.errMsg || err.message)) || '异常' + this.appendLog('WARN', `写入BLE异常 ${msg}`) + } + }, + + /** + * 注册 BLE 通知监听 + * - 先取消旧监听,避免重复回调造成日志噪音或重复解析 + * - 仅处理目标 rxCharId 的通知(按 UUID 片段过滤,兼容 16/128 位 UUID) + * - 兼容两类上报载荷:ArrayBuffer 与十六进制字符串 + * - 将原始数据规范化为 Uint8Array,记录日志后交由协议解析器处理 + */ + setupBleListener() { + // 移除旧监听,防止重复触发(页面重复进入或多次初始化的场景) + this.teardownBleListener() + // 运行环境不支持通知回调则直接返回(避免报错) + if (typeof wx.onBLECharacteristicValueChange !== 'function') return + // 定义并缓存通知回调,便于后续 off 解绑 + this._onBleChange = (res) => { + const { rxCharId } = this.data || {} + // 过滤:如果能拿到通知的特征 ID,仅处理与当前订阅 rxCharId 匹配的通知 + if (rxCharId && res && res.characteristicId) { + const cid = String(res.characteristicId).replace(/-/g, '').toUpperCase() + const rx = String(rxCharId).replace(/-/g, '').toUpperCase() + if (!cid.includes(rx)) return + } + // 数据抽取:res 可能包含 value(ArrayBuffer) 或自定义的十六进制字符串字段 + const buffer = res && res.value + const hexStr = res && (res.hexStr || res.hex || res.data) + let hex = '' + let u8 = null + // ArrayBuffer → Uint8Array,并生成十六进制视图文本 + if (buffer) { + u8 = new Uint8Array(buffer) + hex = this.ab2hex(buffer) + } else if (typeof hexStr === 'string') { + // 规范化十六进制字符串:去空格、转大写,并按字节切分 + hex = hexStr.replace(/\s+/g, '').toUpperCase().replace(/(..)/g, '$1 ').trim() + const clean = hex.replace(/\s+/g, '') + const arr = [] + for (let i = 0; i < clean.length; i += 2) arr.push(parseInt(clean.substr(i, 2), 16) || 0) + u8 = Uint8Array.from(arr) + } + // 若解析失败(没有有效数据),直接忽略此次通知 + if (!u8) return + // 按显示偏好记录日志(HEX 或原始文本) + const viewText = this.data.hexShow ? hex : `[${hex}]` + this.appendLog('RX', viewText) + // 将规范化数据交给协议解析器,进行业务处理与 UI 更新 + this.handleIncomingPacket(u8) + } + // 注册系统 BLE 通知回调 + wx.onBLECharacteristicValueChange(this._onBleChange) + }, + + teardownBleListener() { + if (this._onBleChange && typeof wx.offBLECharacteristicValueChange === 'function') { + wx.offBLECharacteristicValueChange(this._onBleChange) + } + this._onBleChange = null + }, + + handleIncomingPacket(u8) { + if (!u8 || u8.length < 11) return + const headOk = u8[0] === 0xCC && u8[1] === 0xC0 + const frameType = u8[10] + if (!headOk) return + if (frameType === (COMMANDS.RADAR_STATUS & 0xFF)) { + const parsed = this.parseRadarStatus(u8) + if (parsed) { + this.updateRadarLights(parsed.bits) + this.appendLog('PARSE', `雷达状态: 有效端口${parsed.portCount} 有人标记=${parsed.human === 0x01 ? '有人' : '无人'} 位=0b${parsed.bits.toString(2).padStart(8, '0')}`) + } + } + }, + + parseRadarStatus(u8) { + // u8: Head Len CRC Frame FramNum Type Params... + if (!u8 || u8.length < 13) return null + const params = u8.slice(11) + const portCount = params[0] || 0 + const human = params[1] + const bits = params[2] || 0 + return { portCount, human, bits } + }, + + updateRadarLights(bits) { + const next = (this.data.radarLights || []).map((it, idx) => { + const triggered = ((bits >> idx) & 0x01) === 1 + return { + ...it, + triggered, + colorClass: triggered ? 'red' : 'green' + } + }) + this.setData({ radarLights: next }) + }, + + // 数值约束 + clamp(v, min, max) { + v = Number(v || 0) + if (isNaN(v)) return min + if (v < min) return min + if (v > max) return max + return v + }, + clampDetectByUnit(unit, v) { + // unit: 0=时(1..24) 1=分(1..60) 2=秒(1..60) + const max = unit === 0 ? 24 : 60 + return this.clamp(v, 1, max) + }, + + // === 条件组(二级菜单)逻辑 === + buildCondGroups() { + const map = new Map() + const list = (this.data.conditions || []).slice().sort((a, b) => (a.group - b.group) || (a.seq - b.seq)) + list.forEach(it => { + if (!map.has(it.group)) { + map.set(it.group, { group: it.group, timeout: it.timeout, timeoutUnit: it.timeoutUnit, expanded: false, items: [] }) + } + map.get(it.group).items.push({ ...it, expanded: true }) + }) + this.setData({ condGroups: Array.from(map.values()) }) + }, + onToggleGroup(e) { + const idx = Number(e.currentTarget.dataset.idx) + const groups = this.data.condGroups.slice() + if (groups[idx]) groups[idx].expanded = !groups[idx].expanded + this.setData({ condGroups: groups }) + }, + onGroupNumberInput(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + let val = Number(e.detail.value || 0) + const groups = this.data.condGroups.slice() + if (groups[idx]) { + if (field === 'timeout') { + const unit = Number(groups[idx].timeoutUnit || 0) + val = this.clampDetectByUnit(unit, val || 1) + } + groups[idx][field] = val + } + this.setData({ condGroups: groups }) + }, + onGroupPickerChange(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + const val = Number(e.detail.value || 0) + const groups = this.data.condGroups.slice() + if (groups[idx]) { + if (field === 'timeout') { + // 选择的是索引,真实值=索引+1 + groups[idx].timeout = (val + 1) + } else { + groups[idx][field] = val + } + if (field === 'timeoutUnit') { + const t = Number(groups[idx].timeout || 0) + groups[idx].timeout = this.clampDetectByUnit(val, t || 1) + } + } + this.setData({ condGroups: groups }) + }, + onItemNumberInput(e) { + const gidx = Number(e.currentTarget.dataset.gidx) + const iidx = Number(e.currentTarget.dataset.iidx) + const field = e.currentTarget.dataset.field + // 序号为只读,不允许通过输入更新 + if (field === 'seq') return + let val = Number(e.detail.value || 0) + const groups = this.data.condGroups.slice() + const grp = groups[gidx] + if (grp && grp.items[iidx]) { + if (field === 'judgeTime') { + const unit = Number(grp.items[iidx].judgeUnit || 0) + val = this.clampDetectByUnit(unit, val || 1) + } + grp.items[iidx][field] = val + } + this.setData({ condGroups: groups }) + }, + onItemPickerChange(e) { + const gidx = Number(e.currentTarget.dataset.gidx) + const iidx = Number(e.currentTarget.dataset.iidx) + const field = e.currentTarget.dataset.field + const val = Number(e.detail.value || 0) + const groups = this.data.condGroups.slice() + const grp = groups[gidx] + if (grp && grp.items[iidx]) { + if (field === 'judgeTime') { + // 选择的是索引,真实值=索引+1 + grp.items[iidx].judgeTime = (val + 1) + } else { + grp.items[iidx][field] = val + } + if (field === 'judgeUnit') { + const t = Number(grp.items[iidx].judgeTime || 0) + grp.items[iidx].judgeTime = this.clampDetectByUnit(val, t || 1) + } + } + this.setData({ condGroups: groups }) + }, + onToggleItem(e) { + const gidx = Number(e.currentTarget.dataset.gidx) + const iidx = Number(e.currentTarget.dataset.iidx) + const groups = this.data.condGroups.slice() + const grp = groups[gidx] + if (grp && grp.items[iidx]) { + grp.items[iidx].expanded = !grp.items[iidx].expanded + } + this.setData({ condGroups: groups }) + }, + + // 端口配置交互 + onPortAliasInput(e) { + const idx = Number(e.currentTarget.dataset.idx) + const val = String(e.detail.value || '') + const list = this.data.ports.slice() + if (list[idx]) list[idx].alias = val + this.setData({ ports: list }) + }, + onPortNumberInput(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + let val = Number(e.detail.value || 0) + const list = this.data.ports.slice() + if (list[idx]) { + if (field === 'loop') { + val = this.clamp(val, 1, 12) + } else if (field === 'thresholdUp' || field === 'thresholdDown') { + val = this.clamp(val, 0, 100) + } else if (field === 'detectTime') { + const unit = Number(list[idx].detectUnit || 0) + val = this.clampDetectByUnit(unit, val) + } + list[idx][field] = val + } + this.setData({ ports: list }) + }, + onPortSwitch(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + const checked = !!e.detail.value + const list = this.data.ports.slice() + if (list[idx]) list[idx][field] = checked + this.setData({ ports: list }) + }, + onPortUnitChange(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + const val = Number(e.detail.value || 0) + const list = this.data.ports.slice() + if (list[idx]) { + list[idx][field] = val + // 切换单位时同时校正检测时间范围 + const dt = Number(list[idx].detectTime || 0) + list[idx].detectTime = this.clampDetectByUnit(val, dt || 1) + } + this.setData({ ports: list }) + }, + onSavePorts() { + this.data.ports.forEach((p, i) => { + const P0 = p.deviceType & 0xFF + const P1 = p.deviceAddr & 0xFF + const loopLE = [p.loop & 0xFF, (p.loop >>> 8) & 0xFF] + const P4 = p.thresholdDown & 0xFF + const P5 = (i + 1) & 0xFF + const P6 = p.enabled ? 0x01 : 0x00 + const dtLE = [p.detectTime & 0xFF, (p.detectTime >>> 8) & 0xFF] + const P9 = p.detectUnit & 0xFF + const P10 = p.thresholdUp & 0xFF + const payload = [P0, P1, ...loopLE, P4, P5, P6, ...dtLE, P9, P10] + const pkt = buildCommand(COMMANDS.SET_CONDITION_2, payload, { frame: i + 1, framNum: this.data.ports.length }) + this.appendLog('TX', `端口配置[${p.name}]: ${this.toHex(pkt)}`) + }) + wx.showToast({ title: '端口配置已发送', icon: 'success' }) + }, + + // 读取端口配置(占位示例,后续可接入真实读取命令) + onReadPorts() { + this.appendLog('UI', '请求读取端口配置') + wx.showToast({ title: '已请求读取端口配置', icon: 'none' }) + }, + + // 条件配置交互 + onCondNumberInput(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + const val = Number(e.detail.value || 0) + const list = this.data.conditions.slice() + if (list[idx]) list[idx][field] = val + this.setData({ conditions: list }) + }, + onCondPickerChange(e) { + const idx = Number(e.currentTarget.dataset.idx) + const field = e.currentTarget.dataset.field + const val = Number(e.detail.value || 0) + const list = this.data.conditions.slice() + if (list[idx]) list[idx][field] = val + this.setData({ conditions: list }) + }, + onSaveConditions() { + // 将二级菜单分组扁平化回 conditions + const flat = [] + (this.data.condGroups || []).forEach(grp => { + (grp.items || []).forEach(it => { + flat.push({ ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit }) + }) + }) + this.setData({ conditions: flat }) + flat.forEach((c, i) => { + const P0 = c.tag & 0xFF + const P1 = c.group & 0xFF + const P2 = c.seq & 0xFF + const jtLE = [c.judgeTime & 0xFF, (c.judgeTime >>> 8) & 0xFF] + const P5 = c.judgeUnit & 0xFF + const bit = (v) => (v === 1 ? 1 : 0) + const b0 = bit(c.doorMag) + const b1 = bit(c.irHall) + const b2 = bit(c.bathRadar) + const b3 = bit(c.bathroomRadar) + const P6 = (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)) & 0xFF + const P7 = 0x00 + const P8 = 0x00 + const P9 = 0x00 + const P10 = 0x00 + const toLE = [c.timeout & 0xFF, (c.timeout >>> 8) & 0xFF] + const P13 = c.timeoutUnit & 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 }) + this.appendLog('TX', `条件配置[组${c.group}/序${c.seq}]: ${this.toHex(pkt)}`) + }) + wx.showToast({ title: '条件配置已发送', icon: 'success' }) + }, + + // 功能栏示例事件 + onDeleteCondGroup() { this.appendLog('UI', '操作: 删除条件组'); wx.showToast({ title: '删除条件组', icon: 'none' }) }, + onDeleteCondition() { this.appendLog('UI', '操作: 删除条件'); wx.showToast({ title: '删除条件', icon: 'none' }) }, + onAddCondGroup() { this.appendLog('UI', '操作: 添加条件组'); wx.showToast({ title: '添加条件组', icon: 'none' }) }, + onAddCondition() { this.appendLog('UI', '操作: 添加条件'); wx.showToast({ title: '添加条件', icon: 'none' }) }, + onExport() { this.appendLog('UI', '操作: 导出'); wx.showToast({ title: '导出', icon: 'none' }) }, + + onImport() { + const now = new Date() + const stamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` + this.setData({ importFileName: `已导入 ${stamp}` }) + this.appendLog('UI', '操作: 导入') + wx.showToast({ title: '导入', icon: 'none' }) + }, + + // 一键下发:同时下发端口配置与条件配置 + onOneKeySend() { + try { + // 先发送端口配置 + this.onSavePorts() + // 再发送条件配置 + this.onSaveConditions() + wx.showToast({ title: '已一键下发', icon: 'success' }) + this.appendLog('UI', '操作: 一键下发') + } catch (err) { + wx.showToast({ title: '下发失败', icon: 'none' }) + } + }, + + onCheckboxChange(e) { + const key = e.currentTarget.dataset.key + if (!key) return + const checked = (e.detail.value || []).includes(key) + this.setData({ [key]: checked }) + }, + + onInputChange(e) { + this.setData({ sendText: e.detail.value }) + }, + + /** + * 发送蓝牙数据 + * + * 处理用户输入并发送到蓝牙设备: + * 1. 检查输入内容是否为空 + * 2. 根据HEX发送模式转换数据格式 + * 3. 记录发送日志 + * 4. 通过蓝牙发送数据 + * + * @function onSend + * @memberof B13page + * @description 发送数据到已连接的蓝牙设备 + */ + onSend() { + + + + const content = this.data.sendText.trim() + if (!content) { + wx.showToast({ title: '请输入内容', icon: 'none' }) + return + } + + let viewText = content + let bytes = null + if (this.data.hexSend) { + const u8 = this.hexToBytes(content) + if (!u8) { + wx.showToast({ title: 'HEX格式错误', icon: 'none' }) + return + } + bytes = u8 + viewText = this.toHex(u8) + } else { + const text = this.data.wrapCRLF ? (content + '\r\n') : content + bytes = this.strToBytes(text) + viewText = text + } + + // 记录日志(按显示偏好) + const show = this.data.hexShow && bytes ? this.toHex(bytes) : viewText + this.appendLog('TX', show) + this.writeBleBytes(bytes, '发送') + + this.setData({ sendText: '' }) + }, + + + + onClearLogs() { + this.setData({ logList: [] }) + }, + + /** + * 追加一条日志到页面列表 + * - 可选带时间戳前缀(withTimestamp) + * - 头插方式存储,最新在前 + */ + appendLog(direction, text) { + 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}` + const id = `${Date.now()}-${Math.random().toString(16).slice(2)}` + const next = [{ id, time: timeStr, text: finalText }, ...this.data.logList] + this.setData({ logList: next }) + } +}) diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.json b/pages/basics/BluetoothDebugging/B13page/B13page.json new file mode 100644 index 0000000..518243f --- /dev/null +++ b/pages/basics/BluetoothDebugging/B13page/B13page.json @@ -0,0 +1,8 @@ +{ + "navigationBarBackgroundColor": "#fff", + "navigationBarTextStyle": "black", + "navigationStyle": "custom", + "usingComponents": { + "cu-custom": "/colorui/components/cu-custom" + } +} diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.wxml b/pages/basics/BluetoothDebugging/B13page/B13page.wxml new file mode 100644 index 0000000..6d70f3a --- /dev/null +++ b/pages/basics/BluetoothDebugging/B13page/B13page.wxml @@ -0,0 +1,296 @@ + + + + {{DevName || 'B13设备'}} + + + + + + 蓝牙调试 + 蓝牙升级 + + + + + + + + + + {{item.label}} + + + + + + + + 房间有人 + + + + 房间无人 + + + + 门开 + + + + + 门关 + + + + 卫浴有人 + + + + 卫浴无人 + + + + + + + 开门延时 + {{openDelay}}s + + + + + + + 卫浴延时 + {{bathDelay}}s + + + + + + + + 通讯日志 + 清空 + + + + + + + + + + + + + + + + + + +