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
+
+
+
+
+
+
+
+ 通讯日志
+ 清空
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.time}}
+ {{item.text}}
+
+
+ 暂无日志记录
+
+
+
+
+
+
+
+
+
+ 蓝牙名称:
+ {{bleName || '-'}}
+
+
+ MAC:
+ {{bleMac || '-'}}
+ 版本:
+ {{bleVersion || '-'}}
+
+
+ 读取蓝牙信息
+
+
+
+
+
+ {{importFileName || '未选择文件'}}
+
+
+
+
+
+
+
+
+
+
+
+ 端口配置
+
+
+
+
+
+
+
+
+ {{item.portLabel}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{timeUnits[item.detectUnit]}}
+
+
+
+
+
+
+
+
+
+ 条件配置
+
+
+
+
+
+
+
+
+
+
+
+
+ 条件组:
+ {{grp.group}}
+
+ 超时:
+
+ {{grp.timeout || 1}}
+
+ 单位:
+
+ {{timeUnits[grp.timeoutUnit]}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 条件序号:
+ {{it.seq}}
+
+
+ 持续判定时间:
+
+ {{it.judgeTime || 1}}
+
+
+
+ 单位:
+
+ {{timeUnits[it.judgeUnit]}}
+
+
+
+
+
+ 有无人标记
+ 有卡取电
+ 开门磁
+ 门口红外
+ 卫红外
+ 浴红外
+
+
+
+
+
+ {{tagOptions[it.tag]}}
+
+
+
+
+ {{stateOptions[it.cardPower || 0]}}
+
+
+
+
+ {{stateOptions[it.doorMag]}}
+
+
+
+
+ {{stateOptions[it.irHall]}}
+
+
+
+
+ {{stateOptions[it.bathRadar]}}
+
+
+
+
+ {{stateOptions[it.bathroomRadar]}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.wxss b/pages/basics/BluetoothDebugging/B13page/B13page.wxss
new file mode 100644
index 0000000..ff526a0
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/B13page/B13page.wxss
@@ -0,0 +1,117 @@
+.cond-items { display:flex; flex-direction: column; gap: 10rpx; }
+.cond-item-card { background:#fff; border: 1rpx solid #edf0f5; border-radius: 12rpx; padding: 10rpx; }
+.item-head .item-top { display:flex; align-items:center; gap: 10rpx; flex-wrap: wrap; }
+.item-field { display:flex; align-items:center; gap: 6rpx; }
+.item-field .label { font-size: 24rpx; color:#606266; }
+.item-actions { display:flex; align-items:center; gap: 8rpx; }
+.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; }
+.content { padding: 8rpx 10rpx 0rpx; box-sizing: border-box; display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
+.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10rpx; margin-bottom: 12rpx; }
+.grid-item { background: #fff; border-radius: 14rpx; padding: 10rpx 0; display: flex; flex-direction: column; align-items: center; }
+.circle { width: 40rpx; height: 40rpx; border-radius: 50%; background: #e9ecef; margin-bottom: 6rpx; }
+.circle.green { background: #21c161; }
+.circle.red { background: #ff3b30; }
+.circle.gray { background: #e9ecef; }
+.label { font-size: 24rpx; color: #606266; }
+
+.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8rpx; margin-bottom: 10rpx; }
+.card { background: #fff; border-radius: 14rpx; padding: 14rpx 6rpx; display: flex; flex-direction: column; align-items: center; }
+.icon { width: 30rpx; height: 30rpx; border-radius: 8rpx; margin-bottom: 4rpx; }
+.icon.orange { background: #ff8f00; }
+.icon.red { background: #ff3b30; }
+.icon.green { background: #21c161; }
+.icon.blue { background: #0ea5e9; }
+.icon.gray { background: #9aa0a6; }
+.card-title { font-size: 24rpx; color: #555; }
+
+.slider-card { background: #fff; border-radius: 14rpx; padding: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); margin-bottom: 10rpx; }
+.slider-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6rpx; }
+.slider-head .title { font-size: 26rpx; color: #333; }
+.slider-head .value { font-size: 24rpx; color: #21c161; }
+
+.log-card { background: #fff; border-radius: 14rpx; padding: 14rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
+.log-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
+.log-head .title { font-size: 26rpx; color: #333; }
+.log-head .action { font-size: 24rpx; color: #2bab99; }
+
+.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); }
+
+.log-input-row { display: flex; align-items: flex-end; gap: 8rpx; }
+.log-input { flex: 1; background: #f5f6f8; border-radius: 12rpx; padding: 10rpx 12rpx; font-size: 24rpx; border: 1rpx solid #e5e9f2; height: 100rpx; line-height: 34rpx; box-sizing: border-box; white-space: pre-wrap; word-break: break-all; }
+.send-btn { padding: 0 22rpx; height: 56rpx; line-height: 56rpx; border-radius: 12rpx; }
+
+.log-scroll { flex: 1; border: 1rpx solid #e5e9f2; border-radius: 12rpx; padding: 12rpx; background: #fdfdfd; box-sizing: border-box; min-height: 200rpx; max-height: 42vh; }
+
+/* 设备信息行 */
+.device-row { display:flex; align-items:flex-start; justify-content: space-between; gap: 12rpx; background:#fff; border-radius: 14rpx; padding: 10rpx 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); flex-wrap: wrap; row-gap: 6rpx; }
+.device-left { display:flex; flex-direction: column; gap: 6rpx; flex: 1; min-width: 60%; }
+.device-line { display:flex; align-items:center; gap: 8rpx; flex-wrap: wrap; }
+.device-row .dr-label { color:#606266; font-size: 24rpx; }
+.device-row .dr-value { color:#333; font-size: 26rpx; }
+.dr-btn { white-space: nowrap; margin-left: auto; height: 84rpx; line-height: 84rpx; padding: 0 28rpx; display: flex; align-items: center; justify-content: center; text-align: center; flex-shrink: 0; align-self: stretch; }
+.dr-btn-view { background: #0ea5e9; color: #ffffff; border-radius: 12rpx; box-shadow: 0 2rpx 6rpx rgba(14,165,233,0.35); }
+.dr-btn-view:active { opacity: 0.85; }
+
+/* 配置表样式 */
+.cfg-card { background:#fff; border-radius: 14rpx; padding: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); }
+.cfg-head { font-size: 26rpx; color:#333; margin-bottom: 10rpx; }
+.head-row { display:flex; align-items:center; justify-content: space-between; gap: 12rpx; }
+.head-actions { display:flex; gap: 10rpx; }
+.cond-card .head-actions { gap: 8rpx; }
+.cond-card .head-actions button { padding: 0 14rpx; height: 48rpx; line-height: 48rpx; }
+.toolbar { display:flex; flex-wrap: wrap; gap: 8rpx; align-items: center; justify-content: space-between; }
+.toolbar .import-box { min-width: 260rpx; padding: 6rpx 10rpx; border: 1rpx solid #e5e9f2; border-radius: 10rpx; background:#f7f8fa; font-size: 22rpx; color:#606266; }
+.toolbar .toolbar-actions { display:flex; align-items:center; gap: 6rpx; }
+.toolbar .toolbar-actions button { padding: 0 12rpx; }
+.table.header, .table.body { display:flex; flex-direction: column; gap: 6rpx; }
+.table.port .tr { display:grid; grid-template-columns: 1.4fr 1.2fr 1.4fr 1.4fr 0.8fr 1.1fr 1fr; gap: 6rpx; padding: 6rpx 0; }
+.table.body.port { gap: 2rpx; }
+.table.body.port .tr { gap: 4rpx; padding: 2rpx 0; }
+.table.cond-item .tr-top { display:grid; grid-template-columns: 0.8fr 1fr 0.8fr; gap: 4rpx; padding: 2rpx 0; }
+.cond-card .table.cond-item .tr-top .td { display:flex; align-items:center; gap: 6rpx; flex-wrap: nowrap; }
+.cond-card .table.cond-item .tr-top .td .label { white-space: nowrap; }
+.cond-card .table.cond-item .tr-top .td .picker-text { flex: none; white-space: nowrap; }
+.cond-card .table.cond-item .tr-top .td { min-width: 0; }
+.cond-card .table.cond-item .tr-top .td .label { font-size: 22rpx; }
+.cond-card .table.cond-item .tr-top .td:first-child .label { font-weight: 600; color:#8B4513; }
+.cond-card .table.cond-item .tr-top .td .picker-text { height: 40rpx; padding: 0 6rpx; font-size: 22rpx; }
+.cond-card .table.cond-item .tr-top .td:first-child .picker-text { width: 90rpx; text-align: center; }
+.cond-card .table.cond-item .tr-top .td:nth-child(2) .picker-text { width: 95rpx; }
+.cond-card .table.cond-item .tr-top .td:nth-child(3) .picker-text { width: 100rpx; }
+.cond-card .seq-value { font-size: 24rpx; font-weight: 600; color: #8B4513; }
+.table.cond-item .tr-mid { display:grid; grid-template-columns: 1.5fr minmax(100rpx,0.9fr) minmax(100rpx,0.9fr) 1fr 1fr 1fr; gap: 6rpx; padding: 4rpx 0; }
+.table.cond-item .tr-bottom { display:grid; grid-template-columns: 1.5fr minmax(100rpx,0.9fr) minmax(100rpx,0.9fr) 1fr 1fr 1fr; gap: 6rpx; padding: 6rpx 0; }
+.cond-card .table.cond-item .tr-mid .th:first-child { white-space: nowrap; }
+.cond-card .table.cond-item .tr-bottom .td:first-child .picker-text { white-space: nowrap; }
+.table .th, .table .td { font-size: 24rpx; color:#555; }
+.table .th.name, .table .td.name { color:#333; }
+.picker-text { height: 48rpx; padding: 0 12rpx; border: 1rpx solid #e5e9f2; border-radius: 10rpx; background:#f7f8fa; display: flex; align-items: center; }
+.cond-card .picker-text { height: 44rpx; padding: 0 8rpx; }
+.picker-text.readonly { background: #f0f1f3; font-weight: 600; }
+.input.picker-text { height: 48rpx; line-height: 48rpx; padding: 0 12rpx; }
+.cond-card input.picker-text { height: 44rpx; line-height: 44rpx; padding: 0 8rpx; }
+.td.group-seq { display:flex; align-items:center; gap: 6rpx; }
+.td.group-seq .picker-text { flex: 1; }
+.td.group-seq .sep { color:#9aa0a6; }
+/* 条件组样式 */
+.cond-groups { display:flex; flex-direction: column; gap: 10rpx; }
+.cond-scroll { max-height: 680rpx; border: 1rpx solid #e5e9f2; border-radius: 12rpx; padding: 6rpx; box-sizing: border-box; background: #fff; }
+.group-card { background:#fafafa; border: 1rpx solid #edf0f5; border-radius: 12rpx; padding: 10rpx; }
+.group-head .group-info { display:flex; align-items:center; gap: 12rpx; }
+.group-title { font-size: 24rpx; color:#1e3a8a; font-weight: 600; }
+.group-seq { font-size: 24rpx; color:#1e3a8a; font-weight: 600; }
+.group-time { display:flex; align-items:center; gap: 8rpx; }
+.cond-card .group-time .label { white-space: nowrap; font-size: 22rpx; }
+.cond-card .group-time .timeout-picker .picker-text { width: 120rpx; height: 44rpx; padding: 0 8rpx; font-size: 22rpx; text-align: center; white-space: nowrap; }
+.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; }
+.log-item { margin-bottom: 10rpx; }
+.log-item:last-child { margin-bottom: 0; }
+.log-time { display: block; font-size: 22rpx; color: #9aa0a6; margin-bottom: 4rpx; }
+.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; }
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.js b/pages/basics/BluetoothDebugging/BluetoothDebugging.js
new file mode 100644
index 0000000..803f689
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.js
@@ -0,0 +1,535 @@
+// pages/bluetooth-connect/bluetooth-connect.js
+const app = getApp()
+
+const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08])
+
+Page({
+ data: {
+ ConnectedDevName:"",
+ activeTab: 'W13', // 默认选中W13
+ autho: null,
+ Hotelinfo: {},
+ deviceList: [],
+ currentDeviceId: null,
+ coid:0
+ },
+
+ // 返回上一页
+ goBack() {
+ // 返回前主动断开当前BLE连接,避免连接遗留
+ try {
+ this.disconnectCurrentDevice()
+ } catch (e) {}
+ wx.navigateBack()
+ },
+
+ // 切换导航选项卡
+ switchTab(e) {
+ const tab = e.currentTarget.dataset.tab
+ const current = this.data.activeTab
+ if (tab === current) {
+ return
+ }
+
+ const hasConnected = this.data.deviceList.some(d => d.connected)
+ if (hasConnected) {
+ wx.showModal({
+ title: '提示',
+ content: '当前有已连接的蓝牙设备,切换将断开并重新搜索,是否继续?',
+ success: (res) => {
+ if (res.confirm) {
+ this.disconnectAllDevices()
+ this.setData({ activeTab: tab })
+ // 切换后立即搜索一次
+ this.searchBluetooth()
+ }
+ }
+ })
+ } else {
+ this.setData({ activeTab: tab })
+ this.searchBluetooth()
+ }
+ },
+
+ disconnectAllDevices() {
+ const list = this.data.deviceList.map(d => ({ ...d, connected: false }))
+ this.setData({ deviceList: list })
+ },
+
+ // 根据选项卡加载设备数据
+ loadDevicesByTab(tab) {
+ // 这里可以根据tab从服务器获取对应的设备列表
+ console.log('加载设备列表,选项卡:', tab)
+
+ // 模拟不同选项卡的设备数据
+ let deviceList = []
+ if (tab === 'host') {
+ deviceList = [
+ {
+ id: 1,
+ name: '主机设备1',
+ signal: 95,
+ connected: true
+ },
+ {
+ id: 2,
+ name: '主机设备2',
+ signal: 80,
+ connected: false
+ }
+ ]
+ } else {
+ deviceList = this.data.deviceList
+ }
+
+ this.setData({ deviceList })
+ },
+
+ // 搜索蓝牙设备
+ searchBluetooth() {
+ const filterPrefix = this.data.activeTab
+
+ wx.showLoading({
+ title: '搜索中...',
+ mask: true
+ })
+
+ // 先断开当前连接设备(如果有)
+ this.disconnectCurrentDevice()
+
+ // 清空旧列表并启动搜索
+ this.setData({ deviceList: [] })
+
+ this.ensureBluetoothReady()
+ .then(() => this.startBluetoothDevicesDiscovery(filterPrefix))
+ .catch((err) => {
+ console.error('蓝牙初始化失败', err)
+ wx.hideLoading()
+ wx.showToast({ title: '请开启蓝牙和定位权限', icon: 'none' })
+ })
+ },
+
+ ensureBluetoothReady() {
+ return new Promise((resolve, reject) => {
+ wx.openBluetoothAdapter({
+ mode: 'central',
+ success: () => {
+ resolve()
+ },
+ fail: (err) => {
+ // 10001 系统蓝牙未打开;10002 无权限
+ if (err && err.errCode === 10001) {
+ wx.showModal({
+ title: '蓝牙未开启',
+ content: '请先打开手机蓝牙后重试',
+ showCancel: false
+ })
+ } else {
+ wx.showModal({
+ title: '权限提示',
+ content: '请授权蓝牙与定位权限后重试',
+ showCancel: false
+ })
+ }
+ reject(err)
+ }
+ })
+ })
+ },
+
+ startBluetoothDevicesDiscovery(prefix) {
+ // 先取消旧的发现监听,避免多次注册造成干扰
+ this.teardownDeviceFoundListener()
+ this._foundCount = 0
+ console.info('[BLE] start scan, prefix:', prefix || 'ALL')
+ // 先停止可能已有的搜索,待停止完成后再启动,避免竞态
+ wx.stopBluetoothDevicesDiscovery({
+ complete: () => {
+ wx.startBluetoothDevicesDiscovery({
+ allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
+ success: () => {
+ this.setupDeviceFoundListener(prefix)
+ // 定时停止,避免长时间占用
+ setTimeout(() => {
+ this.stopBluetoothDiscovery()
+ }, 6000)
+ },
+ fail: (err) => {
+ console.error('开始搜索蓝牙设备失败', err)
+ wx.hideLoading()
+ wx.showToast({ title: '搜索失败', icon: 'none' })
+ }
+ })
+ }
+ })
+ },
+
+ setupDeviceFoundListener(prefix) {
+ this._deviceFoundHandler = (res) => {
+ const devices = (res && res.devices) || []
+ if (devices.length) this._foundCount = (this._foundCount || 0) + devices.length
+ this.handleDeviceFound(devices, prefix)
+ }
+ if (typeof wx.onBluetoothDeviceFound === 'function') {
+ wx.onBluetoothDeviceFound(this._deviceFoundHandler)
+ }
+ },
+
+ teardownDeviceFoundListener() {
+ if (this._deviceFoundHandler && typeof wx.offBluetoothDeviceFound === 'function') {
+ wx.offBluetoothDeviceFound(this._deviceFoundHandler)
+ }
+ this._deviceFoundHandler = null
+ },
+
+ handleDeviceFound(devices, prefix) {
+ const list = [...this.data.deviceList]
+ devices.forEach((dev) => {
+ const name = dev.name || dev.localName || ''
+ if (!name) return
+ const isW13 = this.data.activeTab === 'W13'
+ const matched = isW13 ? /^BLV_(W13|C13)_.+$/i.test(name) : (prefix ? name.startsWith(prefix) : true)
+ if (!matched) return
+
+ const existsIndex = list.findIndex((d) => d.id === dev.deviceId)
+ const rssi = dev.RSSI || 0
+ const signal = Math.max(0, Math.min(100, 100 + rssi))
+ const mapped = {
+ id: dev.deviceId,
+ name,
+ mac: dev.deviceId,
+ signal,
+ connected: false,
+ RSSI: rssi,
+ localName: dev.localName || '',
+ serviceUUIDs: dev.serviceUUIDs || []
+ }
+
+ if (existsIndex >= 0) {
+ list[existsIndex] = { ...list[existsIndex], ...mapped }
+ } else {
+ list.push(mapped)
+ }
+ })
+
+ this.setData({ deviceList: list })
+ },
+
+ // 停止蓝牙搜索
+ stopBluetoothDiscovery() {
+ wx.stopBluetoothDevicesDiscovery({
+ complete: () => {
+ console.info('[BLE] stop scan, found events:', this._foundCount || 0, 'list size:', this.data.deviceList.length)
+ wx.hideLoading()
+ const count = this.data.deviceList.length
+ wx.showToast({ title: `发现${count}个设备`, icon: 'success', duration: 1500 })
+ }
+ })
+ },
+
+ onUnload() {
+ // 页面卸载时清理蓝牙扫描与监听
+ // this.teardownDeviceFoundListener()
+ if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
+ wx.stopBluetoothDevicesDiscovery({ complete: () => {} })
+ }
+ if (this._fixedLoopTimer) {
+ clearInterval(this._fixedLoopTimer)
+ this._fixedLoopTimer = null
+ }
+ },
+
+ // 连接设备
+ onDeviceTap(e) {
+ const index = e.currentTarget.dataset.index
+ const device = this.data.deviceList[index]
+ let coid= this.data.coid
+ if (!device) return
+
+ const currentIndex = this.data.deviceList.findIndex(d => d.connected)
+
+ // 如果点击的就是已连接设备,直接进入对应页面并携带已保存的BLE参数
+ if (currentIndex === index && currentIndex >= 0) {
+ if (this.data.activeTab === 'W13') {
+ const devName = device.name || 'W13设备'
+ const mac = this.data.currentDeviceId || device.id || ''
+ const svc = this.data.currentServiceId || device.serviceId || ''
+ const tx = this.data.currentTxCharId || device.txCharId || ''
+ const rx = this.data.currentRxCharId || device.rxCharId || ''
+ // 至少携带 mac;若已持有 svc/tx/rx 则一并带上,避免重复发现
+ const base = `/pages/basics/BluetoothDebugging/B13page/B13page?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(mac)}`
+ const withParams = (svc && tx && rx)
+ ? `${base}&serviceId=${encodeURIComponent(svc)}&txCharId=${encodeURIComponent(tx)}&rxCharId=${encodeURIComponent(rx)}`
+ : base
+ console.log(url)
+ console.log(withParams)
+ wx.navigateTo({ url: withParams })
+
+
+
+ } else {
+ wx.showToast({ title: '已连接当前设备', icon: 'none' })
+ }
+ return
+ }
+
+ if (currentIndex >= 0 && currentIndex !== index) {
+ wx.showModal({
+ title: '切换设备',
+ content: '已连接其他设备,是否切换到当前设备?',
+ success: (res) => {
+ if (res.confirm) {
+ this.connectToDevice(index)
+ }
+ }
+ })
+ } else if (currentIndex < 0) {
+ wx.showModal({
+ title: '连接设备',
+ content: '是否连接此蓝牙设备?',
+ success: (res) => {
+ if (res.confirm) {
+ this.connectToDevice(index)
+ }
+ }
+ })
+ } else {
+ this.connectToDevice(index)
+ }
+ },
+
+ connectToDevice(index) {
+ const device = this.data.deviceList[index]
+ if (!device || !device.id) {
+ wx.showToast({ title: '设备信息缺失', icon: 'none' })
+ return
+ }
+
+ 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) => {
+ wx.hideLoading()
+ console.error('BLE 连接失败', err)
+ wx.showToast({ title: '连接失败', icon: 'none' })
+ }
+ })
+ },
+
+ // 发现包含 FFE1(写) / FFE2(订阅) 的服务与特征,并启用 FFE2 通知
+ discoverBleChannels(device) {
+ const deviceId = device.id
+ wx.getBLEDeviceServices({
+ deviceId,
+ success: (srvRes) => {
+ const services = srvRes.services || []
+ if (!services.length) {
+ wx.hideLoading()
+ wx.showToast({ title: '未发现服务', icon: 'none' })
+ return
+ }
+ let found = false
+ let pending = services.length
+ // 优先自定义/未知服务(UUID 含 FFE)其余按原顺序
+ 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 => {
+ const serviceId = s.uuid
+ wx.getBLEDeviceCharacteristics({
+ deviceId,
+ serviceId,
+ 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
+ // 启用FFE2通知
+ wx.notifyBLECharacteristicValueChange({
+ state: true,
+ deviceId,
+ serviceId,
+ characteristicId: ffe2.uuid,
+ complete: () => {
+ wx.hideLoading()
+ wx.showToast({ title: '连接成功', icon: 'success' })
+ // 保存当前服务与特征,供已连接设备直接进入页面使用
+ this.setData({
+ currentServiceId: serviceId,
+ currentTxCharId: ffe1.uuid,
+ currentRxCharId: ffe2.uuid
+ })
+ // 连接成功后发送指定命令帧
+ // this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
+
+ if (this.data.activeTab === 'W13') {
+ 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 })
+ // this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
+ }
+ }
+ })
+ }
+ },
+ fail: () => {
+ // 不中断流程,继续其他服务
+ },
+ complete: () => {
+ pending -= 1
+ if (!found && pending === 0) {
+ wx.hideLoading()
+ wx.showModal({ title: '提示', content: '未找到FFE1/FFE2特征', showCancel: false })
+ }
+ }
+ })
+ })
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ console.error('获取服务失败', err)
+ wx.showToast({ title: '获取服务失败', icon: 'none' })
+ }
+ })
+ },
+
+ _matchUuid(uuid, needle) {
+ if (!uuid || !needle) return false
+ const u = String(uuid).replace(/-/g, '').toUpperCase()
+ const n = String(needle).toUpperCase().replace(/^0X/, '')
+ // 模糊匹配:包含指定段即可(兼容16/128位 UUID)
+ return u.includes(n)
+ },
+
+ sendFixedCommand(deviceId, serviceId, txCharId) {
+ if (!deviceId || !serviceId || !txCharId || typeof wx.writeBLECharacteristicValue !== 'function') return
+ console.info(`[BLE] sendFixedCommand params device=${deviceId} svc=${serviceId} tx=${txCharId}`)
+ try {
+ wx.writeBLECharacteristicValue({
+ deviceId,
+ serviceId,
+ characteristicId: txCharId,
+ value: FIXED_CONNECT_CMD.buffer,
+ complete: () => {
+ console.info(`[BLE] sent fixed cmd device=${deviceId} svc=${serviceId} tx=${txCharId}`)
+ }
+ })
+ } catch (e) {
+ console.warn('[BLE] send fixed cmd failed', e)
+ }
+ },
+
+ // 测试函数:每5秒发送一次固定命令
+ startFixedCmdLoop() {
+ const deviceId = this.data.currentDeviceId
+ const serviceId = this.data.currentServiceId
+ const txCharId = this.data.currentTxCharId
+ if (!deviceId || !serviceId || !txCharId) {
+ wx.showToast({ title: '未连接BLE', icon: 'none' })
+ return
+ }
+ if (this._fixedLoopTimer) {
+ clearInterval(this._fixedLoopTimer)
+ this._fixedLoopTimer = null
+ }
+ const sendOnce = () => {
+ try {
+ wx.writeBLECharacteristicValue({
+ deviceId,
+ serviceId,
+ characteristicId: txCharId,
+ value: FIXED_CONNECT_CMD.buffer,
+ fail: (err) => {
+ console.warn('[BLE] loop fixed cmd fail', err && (err.errMsg || err.message) || err)
+ }
+ })
+ } catch (e) {
+ console.warn('[BLE] loop fixed cmd exception', e && (e.errMsg || e.message) || e)
+ }
+ }
+ sendOnce()
+ this._fixedLoopTimer = setInterval(sendOnce, 5000)
+ wx.showToast({ title: '已启动固定命令循环', icon: 'none' })
+ },
+
+ // 可选:停止循环发送
+ stopFixedCmdLoop() {
+ if (this._fixedLoopTimer) {
+ clearInterval(this._fixedLoopTimer)
+ this._fixedLoopTimer = null
+ wx.showToast({ title: '已停止循环', icon: 'none' })
+ }
+ },
+
+ // 断开当前连接设备(如果有真实连接)
+ disconnectCurrentDevice() {
+ const idx = this.data.deviceList.findIndex(d => d.connected)
+ if (idx >= 0) {
+ // 标记断开状态
+ const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false }))
+ this.setData({ deviceList: list })
+ }
+ // 如果保留了设备ID,尝试调用系统断开
+ const devId = this.data.currentDeviceId
+ if (devId && typeof wx.closeBLEConnection === 'function') {
+ try {
+ wx.closeBLEConnection({ deviceId: devId, complete: () => {} })
+ } catch (e) {
+ // 忽略断开异常,继续搜索
+ }
+ this.setData({ currentDeviceId: null })
+ }
+ },
+
+ onLoad() {
+ const { autho } = app.globalData || {}
+ let currentHotel = null
+
+ if (autho && Array.isArray(autho) && autho.length > 0) {
+ // 优先取第一个分组里的第一个酒店;后续可按需要改为 options.HotelId 精确匹配
+ const firstGroup = autho[0]
+ if (firstGroup && Array.isArray(firstGroup.Hotels) && firstGroup.Hotels.length > 0) {
+ currentHotel = firstGroup.Hotels[0]
+ }
+ }
+
+ if (currentHotel) {
+ this.setData({
+ autho,
+ Hotelinfo: currentHotel
+ })
+
+ const title = currentHotel.HotelName
+ ? `${currentHotel.HotelName}${currentHotel.Code ? ' (' + currentHotel.Code + ')' : ''}`
+ : '蓝牙调试'
+ wx.setNavigationBarTitle({ title })
+ } else {
+ wx.setNavigationBarTitle({ title: '蓝牙调试' })
+ }
+
+ // 页面加载时,根据当前选中的选项卡加载设备
+ this.loadDevicesByTab(this.data.activeTab)
+ // 同步执行一次蓝牙搜索(按 W13 过滤规则)
+ this.searchBluetooth()
+ }
+})
\ No newline at end of file
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.json b/pages/basics/BluetoothDebugging/BluetoothDebugging.json
new file mode 100644
index 0000000..407e373
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "蓝牙连接",
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
\ No newline at end of file
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml
new file mode 100644
index 0000000..c2105c4
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml
@@ -0,0 +1,60 @@
+
+
+
+
+ {{Hotelinfo.HotelName}}({{Hotelinfo.Code}})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.name || '未命名设备'}}
+ {{item.mac || item.localName || '未知MAC地址'}}
+
+
+
+
+
+
+ Bluetooth · {{item.connected ? '已连接' : '未连接'}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.wxss b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxss
new file mode 100644
index 0000000..99e1c38
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxss
@@ -0,0 +1,193 @@
+/* pages/basics/BluetoothDebugging/BluetoothDebugging.wxss */
+.container {
+ min-height: 100vh;
+ background-color: #f4f4f4;
+ color: #333333;
+ padding: 12rpx;
+}
+
+
+
+/* 内容栏顶部 */
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+
+/* 左侧导航栏 */
+.nav-tabs {
+ display: flex;
+ background: #ffffff;
+ border: 1rpx solid #ddd;
+ border-radius: 50rpx;
+ padding: 2rpx;
+ gap: 2rpx;
+}
+
+.nav-tab {
+ padding: 12rpx 26rpx;
+ border-radius: 50rpx;
+ font-size: 28rpx;
+ transition: all 0.3s ease;
+ color: #666666;
+ background-color: #ffffff;
+}
+
+.nav-tab.active {
+ background: linear-gradient(90deg, #00C6FF, #0072FF);
+ color: #fff;
+ font-weight: bold;
+ box-shadow: 0 2rpx 10rpx rgba(0, 114, 255, 0.3);
+}
+
+/* 右侧搜索按钮 */
+.search-btn {
+ background: linear-gradient(90deg, #00D977, #00B16A);
+ color: #fff;
+ padding: 16rpx 30rpx;
+ border-radius: 50rpx;
+ font-size: 26rpx;
+ font-weight: bold;
+ border: none;
+}
+
+/* 蓝牙设备列表 */
+.device-scroll {
+ max-height: calc(100vh - 260rpx);
+ padding: 4rpx 0;
+ box-sizing: border-box;
+}
+
+.device-card {
+ display: flex;
+ align-items: center;
+ gap: 14rpx;
+ background-color: #ffffff;
+ border: 1rpx solid #e5e9f2;
+ border-radius: 20rpx;
+ padding: 12rpx 14rpx;
+ margin-bottom: 14rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
+ transition: all 0.25s ease;
+}
+
+.device-card.connected {
+ border: 2rpx solid #23c16b;
+ background: linear-gradient(135deg, #f3fff7 0%, #ffffff 100%);
+ box-shadow: 0 10rpx 24rpx rgba(35, 193, 107, 0.18);
+}
+
+.device-avatar {
+ width: 78rpx;
+ height: 78rpx;
+ border-radius: 50%;
+ background-color: #f1f4fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: 'weui';
+ color: #0f9bd7;
+ box-shadow: inset 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
+}
+
+.device-avatar.online {
+ background: radial-gradient(circle at 30% 30%, #bdf4d6, #e8fff2 60%, #ffffff 100%);
+ color: #1bbf67;
+}
+
+.avatar-img {
+ width: 46rpx;
+ height: 46rpx;
+}
+
+.device-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 6rpx;
+}
+
+.device-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10rpx;
+}
+
+.device-name-group {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+ flex: 1;
+ min-width: 0;
+}
+
+.device-name {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #1f2d3d;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.device-mac {
+ font-size: 22rpx;
+ color: #6b7280;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.signal-chip {
+ min-width: 70rpx;
+ height: 42rpx;
+ padding: 0 12rpx;
+ border-radius: 20rpx;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 26rpx;
+ font-family: 'weui';
+ font-weight: 600;
+}
+
+.signal-img {
+ width: 30rpx;
+ height: 30rpx;
+}
+
+.signal-chip.strong {
+ background-color: rgba(35, 193, 107, 0.12);
+ color: #23c16b;
+}
+
+.signal-chip.medium {
+ background-color: rgba(245, 158, 11, 0.14);
+ color: #f59e0b;
+}
+
+.signal-chip.weak {
+ background-color: rgba(244, 63, 94, 0.12);
+ color: #f43f5e;
+}
+
+.device-bottom {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 4rpx;
+}
+
+.device-state {
+ font-size: 24rpx;
+ color: #8c9399;
+}
+
+.rssi {
+ font-size: 28rpx;
+ color: #1f2d3d;
+ font-weight: 600;
+}
\ No newline at end of file
diff --git a/utils/w13Packet.js b/utils/w13Packet.js
new file mode 100644
index 0000000..988a8db
--- /dev/null
+++ b/utils/w13Packet.js
@@ -0,0 +1,163 @@
+// W13 BLE 协议公共组包模块
+// 导出通用的组包构造与CRC16计算(支持 CCITT-FALSE 与 MODBUS)
+
+const DEFAULT_HEAD = [0xCC, 0xC0];
+const MAX_LEN = 548; // 文档声明的最大总长度(含包头与CRC)
+
+export const COMMANDS = {
+ READ_VERSION: 0x01,
+ SET_CONDITION_1: 0x08,
+ SET_CONDITION_2: 0x09,
+ OTA_START: 0x0B,
+ ENABLE_BLE_LOG: 0x0C,
+ RADAR_STATUS: 0x11,
+ TEST_KEYS: 0x13,
+};
+
+function toUint16LE(v) {
+ const x = v & 0xFFFF;
+ return [x & 0xFF, (x >>> 8) & 0xFF];
+}
+
+function ensureUint8Array(data) {
+ if (!data) return new Uint8Array(0);
+ if (data instanceof Uint8Array) return data;
+ if (Array.isArray(data)) return Uint8Array.from(data.map(x => x & 0xFF));
+ if (typeof data === 'string') {
+ // 默认按ASCII编码;如需GBK请先外部转换后传入字节数组
+ const arr = new Uint8Array(data.length);
+ for (let i = 0; i < data.length; i++) arr[i] = data.charCodeAt(i) & 0xFF;
+ return arr;
+ }
+ throw new TypeError('Unsupported payload type');
+}
+
+// CRC-16/CCITT-FALSE: poly=0x1021, init=0xFFFF, refin=false, refout=false, xorout=0x0000
+export function crc16Ccitt(bytes, start = 0, end = bytes.length) {
+ let crc = 0xFFFF;
+ for (let i = start; i < end; i++) {
+ crc ^= (bytes[i] << 8);
+ for (let j = 0; j < 8; j++) {
+ if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1;
+ crc &= 0xFFFF;
+ }
+ }
+ return crc & 0xFFFF;
+}
+
+// CRC-16/MODBUS: poly=0xA001, init=0xFFFF, refin=true, refout=true, xorout=0x0000
+export function crc16Modbus(bytes, start = 0, end = bytes.length) {
+ let crc = 0xFFFF;
+ for (let i = start; i < end; i++) {
+ crc ^= bytes[i];
+ for (let j = 0; j < 8; j++) {
+ const lsb = crc & 0x0001;
+ crc >>= 1;
+ if (lsb) crc ^= 0xA001;
+ }
+ }
+ return crc & 0xFFFF;
+}
+
+function computeCrc(bytes, type, range) {
+ const [start, end] = range || [0, bytes.length];
+ if (type === 'MODBUS') return crc16Modbus(bytes, start, end);
+ return crc16Ccitt(bytes, start, end);
+}
+
+/**
+ * 构造W13协议数据帧
+ * 字段:Head(2) + Len(2) + CRC(2) + Frame(2) + FramNum(2) + Frame_Type(1) + Parameters(N)
+ * Len为整包总长度(含Head与CRC),小端;CRC16默认CCITT-FALSE,计算范围为整包除CRC字段自身。
+ *
+ * @param {Object} opts
+ * @param {number} opts.frame 帧号(0~65535)
+ * @param {number} opts.framNum 帧总数(0~65535)
+ * @param {number} opts.type 命令字(0~255)
+ * @param {Uint8Array|number[]|string} [opts.params] 参数字节或ASCII字符串
+ * @param {('CCITT'|'MODBUS')} [opts.crcType] CRC算法选择,默认CCITT
+ * @param {number[]} [opts.head] 包头,默认[0xCC,0xC0]
+ * @returns {Uint8Array}
+ */
+export function buildPacket(opts) {
+ const head = (opts && opts.head) || DEFAULT_HEAD;
+ const frame = (opts && opts.frame) != null ? (opts.frame >>> 0) : 1;
+ const framNum = (opts && opts.framNum) != null ? (opts.framNum >>> 0) : 1;
+ const type = (opts && opts.type) != null ? (opts.type & 0xFF) : 0x00;
+ const params = ensureUint8Array(opts && opts.params);
+ const crcType = (opts && opts.crcType) === 'MODBUS' ? 'MODBUS' : 'CCITT';
+
+ // 预分配:固定头(2) + 长度(2) + CRC(2) + Frame(2) + FramNum(2) + Type(1) + 参数
+ const fixedLen = 2 + 2 + 2 + 2 + 2 + 1;
+ const totalLen = fixedLen + params.length;
+ if (totalLen > MAX_LEN) throw new RangeError(`Packet too long: ${totalLen} > ${MAX_LEN}`);
+
+ const buf = new Uint8Array(totalLen);
+ let off = 0;
+
+ // Head
+ buf[off++] = head[0] & 0xFF;
+ buf[off++] = head[1] & 0xFF;
+
+ // Len (占位后写入)
+ const lenLE = toUint16LE(totalLen);
+ buf[off++] = lenLE[0];
+ buf[off++] = lenLE[1];
+
+ // CRC (占位,稍后计算写入)
+ const crcPos = off;
+ buf[off++] = 0x00;
+ buf[off++] = 0x00;
+
+ // Frame
+ const frameLE = toUint16LE(frame);
+ buf[off++] = frameLE[0];
+ buf[off++] = frameLE[1];
+
+ // FramNum
+ const framNumLE = toUint16LE(framNum);
+ buf[off++] = framNumLE[0];
+ buf[off++] = framNumLE[1];
+
+ // Frame_Type
+ buf[off++] = type;
+
+ // Parameters
+ buf.set(params, off);
+
+ // 计算CRC:整包除CRC字段自身
+ // 为避免实现差异,计算时将CRC位置的两个字节视为0
+ buf[crcPos] = 0x00;
+ buf[crcPos + 1] = 0x00;
+ const crc = computeCrc(buf, crcType, [0, buf.length]);
+ const crcLE = toUint16LE(crc);
+ buf[crcPos] = crcLE[0];
+ buf[crcPos + 1] = crcLE[1];
+
+ return buf;
+}
+
+/**
+ * 便捷构造:使用命令常量与简易参数数组
+ * @param {number} cmd COMMANDS中的命令字
+ * @param {number[]|Uint8Array|string} payload 参数
+ * @param {{frame?:number, framNum?:number, crcType?:('CCITT'|'MODBUS'), head?:number[]}} [options]
+ */
+export function buildCommand(cmd, payload, options = {}) {
+ return buildPacket({
+ frame: options.frame != null ? options.frame : 1,
+ framNum: options.framNum != null ? options.framNum : 1,
+ type: cmd & 0xFF,
+ params: payload,
+ crcType: options.crcType === 'MODBUS' ? 'MODBUS' : 'CCITT',
+ head: options.head || DEFAULT_HEAD,
+ });
+}
+
+/**
+ * 示例:构造“读版本号”命令帧 (Frame_Type=0x01, P0=0x00)
+ * @returns {Uint8Array}
+ */
+export function buildReadVersion() {
+ return buildCommand(COMMANDS.READ_VERSION, [0x00]);
+}