From f94cf21e7ae8f85a90645cd36bda0db014c5b2df Mon Sep 17 00:00:00 2001
From: chenzhihao <1798906853@qq.com>
Date: Fri, 20 Mar 2026 18:18:49 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=93=9D=E7=89=99?=
=?UTF-8?q?=E8=B0=83=E8=AF=95=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=88=9D=E5=A7=8B?=
=?UTF-8?q?=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 BLVRQPage.json 配置文件,设置导航栏样式。
- 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。
- 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
---
Document/BLV_RQ蓝牙通信控制页面实施文档.md | 682 +++++++++++++++
Document/RF_RQ2603 蓝牙通讯协议.md | 192 +++++
app.js | 1 +
app.json | 7 +-
lib/RequestingCenter.js | 1 +
.../BluetoothDebugging/BLVRQPage/BLVRQPage.js | 773 ++++++++++++++++++
.../BLVRQPage/BLVRQPage.json | 8 +
.../BLVRQPage/BLVRQPage.wxml | 129 +++
.../BLVRQPage/BLVRQPage.wxss | 715 ++++++++++++++++
.../BluetoothDebugging/BluetoothDebugging.js | 702 +++++++++++++---
.../BluetoothDebugging.wxml | 24 +-
.../BluetoothDebugging.wxss | 45 +-
pages/basics/HostUpgrade/HostUpgrade.js | 3 +-
13 files changed, 3127 insertions(+), 155 deletions(-)
create mode 100644 Document/BLV_RQ蓝牙通信控制页面实施文档.md
create mode 100644 Document/RF_RQ2603 蓝牙通讯协议.md
create mode 100644 pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
create mode 100644 pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
create mode 100644 pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
create mode 100644 pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
diff --git a/Document/BLV_RQ蓝牙通信控制页面实施文档.md b/Document/BLV_RQ蓝牙通信控制页面实施文档.md
new file mode 100644
index 0000000..13fcc6e
--- /dev/null
+++ b/Document/BLV_RQ蓝牙通信控制页面实施文档.md
@@ -0,0 +1,682 @@
+# BLV_RQ 蓝牙通信控制页面实施文档
+
+- 归档日期:2026年3月19日
+- 适用项目:`Wx_BLWConfigTools_V02_Prod`
+- 参考协议:`Document/RF_RQ2603 蓝牙通讯协议.md`
+- 当前状态:已确认,待进入代码实现
+
+---
+
+## 一、实施目标
+
+为 `BLV_RQ` 设备新增独立的蓝牙通信控制页面,用于完成以下能力:
+
+1. 展示 BLE 连接信息
+2. 读取设备版本信息
+3. 设置蓝牙名称
+4. 读取按键配置
+5. 设置按键功能与按键映射
+6. 基于映射关系执行按键控制
+7. 查看、清空、导出通信日志
+
+页面采用**从上到下的单屏结构**,页面自身不做上下滚动;若日志区需要展示更多内容,仅允许日志内容区内部滚动。
+
+---
+
+## 二、已确认实施约束
+
+1. **单独新建一个新页面路径**
+2. **设备型号固定写死为 `BLV_RQ`**
+3. **按键支持映射设置**
+4. **蓝牙名称输入规则为 ASCII + 最多 8 字节自定义名,仅允许数字和英文字符**
+5. **允许按键类型由用户先选,读取配置后再按设备返回结果自动修正**
+6. **允许通信日志导出**
+7. **未配置映射的按键允许继续下发(方案 B)**
+8. **采纳建议:未配置映射项下发时,优先“保持设备当前返回值不变”**
+
+---
+
+## 三、页面路径规划
+
+建议新增页面路径:
+
+`pages/basics/BluetoothDebugging/BLVRQPage/`
+
+页面文件包括:
+
+- `BLVRQPage.js`
+- `BLVRQPage.wxml`
+- `BLVRQPage.wxss`
+- `BLVRQPage.json`
+
+该页面仅服务于 `BLV_RQ` 设备,不复用 `W13` 页面逻辑。
+
+---
+
+## 四、页面结构设计
+
+## 1. 页面总结构
+
+页面自上而下分为三大区域:
+
+1. 蓝牙信息栏
+2. 设备管理框
+3. 通信日志框组
+
+页面整体不滚动,建议通过高度配比保证三块区域稳定展示:
+
+- 蓝牙信息栏:约 26%
+- 设备管理框:约 46%
+- 通信日志框组:约 28%
+
+---
+
+## 2. 蓝牙信息栏
+
+### 展示内容
+
+- 蓝牙名称
+- MAC / DeviceId
+- ServiceId
+- Tx 特征值
+- Rx 特征值
+- 连接状态
+- 蓝牙信号 / RSSI
+
+### 交互内容
+
+- `断开` 按钮
+- `重连` 按钮
+- 蓝牙名称输入框
+- `设置蓝牙名称` 按钮
+
+### 交互规则
+
+#### 蓝牙名称输入限制
+
+输入内容必须满足:
+
+- 仅允许数字和英文字符
+- 长度为 1~8 字节
+- ASCII 编码
+
+建议使用如下校验规则:
+
+`^[A-Za-z0-9]{1,8}$`
+
+#### 设置蓝牙名称按钮启用条件
+
+仅当同时满足以下条件时可点击:
+
+- 当前已连接设备
+- 输入内容非空
+- 输入内容满足校验规则
+
+#### 设置蓝牙名称交互流程
+
+1. 用户输入新名称
+2. 点击 `设置蓝牙名称`
+3. 下发 `0x05` 指令
+4. 回包成功后刷新显示名称
+5. 记录 TX / RX / UI 日志
+
+---
+
+## 3. 设备管理框
+
+设备管理框分为两部分:
+
+### 3.1 设备信息栏
+
+包含:
+
+- 设备型号:`BLV_RQ`(固定写死)
+- 软件版本
+- 硬件版本
+- `读取版本` 按钮
+- 按键类型下拉框
+- `读取按键配置` 按钮
+- `设置按键配置` 按钮
+
+### 3.2 设备按键栏
+
+根据按键类型动态生成按键卡片。
+
+布局要求:
+
+- 一行 2 个卡片
+- Z 型排列
+- 奇数卡片时最后一个左对齐
+
+每个按键卡片包含:
+
+- 按键序号(表示硬件按键位置)
+- 按键控制按钮
+- 按键功能下拉框
+- 映射下拉框
+
+建议标题展示方式:
+
+- `按键1(硬件)`
+- `按键2(硬件)`
+- ...
+
+可附加展示小字:
+
+- `当前映射:键值X`
+
+---
+
+## 五、按钮与控件启用逻辑
+
+### 1. 读取版本按钮
+
+启用条件:
+
+- 已连接设备
+
+### 2. 读取按键配置按钮
+
+启用条件:
+
+- 已连接设备
+- 已选择按键类型(1~8键)
+
+### 3. 设置按键配置按钮
+
+启用条件:
+
+- 已连接设备
+- 已选择按键类型(1~8键)
+
+### 4. 按键控制按钮
+
+启用条件:
+
+- 已连接设备
+- 当前按键存在有效映射
+
+### 5. 设置蓝牙名称按钮
+
+启用条件:
+
+- 已连接设备
+- 名称输入合法
+
+---
+
+## 六、按键类型、功能与映射设计
+
+## 1. 按键类型下拉框
+
+选项:
+
+- 请选择按键类型
+- 1键
+- 2键
+- 3键
+- 4键
+- 5键
+- 6键
+- 7键
+- 8键
+
+### 规则
+
+- 用户先选择按键类型后,才允许读取/设置按键配置
+- 读取设备配置后,如果设备返回键数与当前选择不同:
+ - 页面提示:`设备返回为 X 键,已按设备实际值修正`
+ - 页面自动修正下拉框与卡片数量
+
+---
+
+## 2. 按键功能下拉框
+
+依据协议中的功能码 `0x00 ~ 0x0F`:
+
+- 不设置(0x00)
+- 普通按键(0x01)
+- 清理按键(0x02)
+- 投诉按键(0x03)
+- 预留(0x04)
+- 预留(0x05)
+- ...
+- 预留(0x0F)
+
+页面展示建议使用:
+
+- `普通按键 (0x01)`
+- `投诉按键 (0x03)`
+
+---
+
+## 3. 映射下拉框
+
+映射选项需根据当前按键类型动态生成。
+
+例如:
+
+- 当前为 4 键:只允许映射 `键值1 ~ 键值4`
+- 当前为 8 键:允许映射 `键值1 ~ 键值8`
+
+### 映射校验规则
+
+根据协议要求:
+
+- 不同硬件位置不能映射到相同键值
+- 映射值必须落在当前按键类型范围内
+
+因此页面在设置前必须校验:
+
+1. 是否存在重复映射
+2. 是否存在越界映射
+3. 是否存在未配置映射
+
+---
+
+## 七、协议交互设计
+
+---
+
+## 1. 读取版本号(0x01)
+
+### 发送
+
+- `Frame_Type = 0x01`
+- `P0 = 0x01`
+
+### 回包
+
+- `P0 = 软件版本号`
+- `P1 = 硬件版本号`
+
+### 页面交互
+
+- 点击 `读取版本`
+- 成功回包后更新:
+ - 软件版本
+ - 硬件版本
+
+---
+
+## 2. 设置蓝牙名称(0x05)
+
+### 发送
+
+- `Frame_Type = 0x05`
+- `P0 = 名称长度`
+- `P1~Pn = ASCII字符`
+
+### 回包
+
+- `P0 = 0x01`:设置成功
+- `P0 = 0x02`:设置失败
+
+### 页面交互
+
+- 输入名称
+- 点击 `设置蓝牙名称`
+- 成功后提示并刷新名称显示
+
+---
+
+## 3. 读取按键配置(0x04)
+
+### 前置条件
+
+- 已连接设备
+- 已选择按键类型
+
+### 发送
+
+- `Frame_Type = 0x04`
+- `P0 = 0x01`
+
+### 回包
+
+- `P0 = 硬件按键总数`
+- `P1~P8 = 各硬件按键配置字节`
+
+### 配置字节解析规则
+
+每个配置字节:
+
+- Bit0~3:映射键值
+- Bit4~7:按键功能
+
+### 页面处理
+
+- 自动解析并回填每张卡片的:
+ - `mappedKey`
+ - `functionCode`
+
+---
+
+## 4. 设置按键配置(0x02)
+
+协议规定 `0x02` 分为两个子命令:
+
+### 子命令 1:设置按键功能
+
+- `P0 = 0x01`
+- `P1~P8 = 逻辑键值功能码`
+
+### 子命令 2:设置按键映射
+
+- `P0 = 0x02`
+- `P1~P8 = 硬件位置映射到的键值`
+
+### 页面交互流程
+
+用户点击 `设置按键配置` 后,执行以下步骤:
+
+#### 第一步:前端校验
+
+检查:
+
+1. 是否已选择按键类型
+2. 是否存在重复映射
+3. 是否存在越界映射
+4. 是否存在未配置映射
+5. 功能配置是否完整
+
+#### 第二步:弹出二次确认框
+
+##### 场景 A:存在未配置映射按键
+
+弹窗提示示例:
+
+> 以下按键未配置映射:按键2、按键5
+> 若继续下发,将保持这些按键当前设备返回值不变。
+> 是否继续?
+
+##### 场景 B:无缺失项
+
+弹窗提示示例:
+
+> 确认下发当前按键功能与映射配置?
+
+#### 第三步:用户确认后继续下发(方案 B)
+
+执行顺序建议为:
+
+1. 先下发功能配置(`P0 = 0x01`)
+2. 再下发映射配置(`P0 = 0x02`)
+
+#### 第四步:未配置映射项处理策略(采纳建议)
+
+对于未配置映射的按键,不直接写入空值或 `0x00`,而是:
+
+- **保持设备当前返回值不变**
+
+即:
+
+- 页面在成功读取配置后缓存设备原始映射值
+- 用户未配置映射的项,设置时继续带上原设备返回值
+
+该策略的优点:
+
+- 最稳妥
+- 避免把设备有效映射误清空
+- 与“允许继续”的方案 B 兼容
+
+#### 第五步:结果反馈
+
+- 两条命令都成功:提示 `设置成功`
+- 任一命令失败:提示 `设置失败`
+- 日志中记录失败发生在功能设置还是映射设置阶段
+
+---
+
+## 5. 按键控制(0x06)
+
+### 交互逻辑
+
+用户通过映射下拉框配置卡片映射值后,点击某个按键卡片中的控制按钮时:
+
+- 发送的不是硬件按键序号
+- 而是该卡片当前映射到的**逻辑键值**
+
+### 示例
+
+- 硬件按键1 当前映射为键值3
+- 点击“按键1”的控制按钮
+- 实际发送键值3的模拟触发命令
+
+### 发送
+
+- `Frame_Type = 0x06`
+- `P0 = 1`
+- `P1 = 1 << (mappedKey - 1)`
+
+### 回包
+
+- `P0 = 0x01`:成功
+- `P0 = 0x02`:失败
+
+### 限制
+
+- 未连接不可触发
+- 未配置映射不可触发
+- 点击未映射按键时提示:`当前按键未配置映射`
+
+---
+
+## 八、日志设计
+
+## 1. 日志分类
+
+建议日志类型:
+
+- `UI`:用户操作
+- `CFG`:状态变化
+- `TX`:发送命令
+- `RX`:接收回包
+- `WARN`:警告
+- `ERR`:错误
+
+## 2. 日志内容建议
+
+每条日志包含:
+
+- 时间戳
+- 日志类型
+- 命令字
+- Hex 内容
+- 文本说明
+
+### 示例
+
+- `[TX][0x04] 读取按键配置`
+- `[RX][0x04] 返回4键配置`
+- `[WARN] 未配置映射:按键2、按键5,继续按原设备值下发`
+- `[TX][0x06] 触发键值3`
+
+## 3. 日志功能
+
+顶部功能按钮:
+
+- `清空日志`
+- `导出日志`
+
+## 4. 导出格式
+
+允许导出为 `txt` 文件。
+
+建议文件名:
+
+`BLV_RQ_Log_yyyyMMdd_HHmmss.txt`
+
+导出内容建议包含:
+
+- 当前设备信息
+- 当前连接参数
+- 全量日志内容
+
+---
+
+## 九、推荐数据模型
+
+```js
+data: {
+ bleInfo: {
+ devName: '',
+ mac: '',
+ serviceId: '',
+ txCharId: '',
+ rxCharId: '',
+ connected: false,
+ rssi: null,
+ signalText: '-'
+ },
+
+ deviceInfo: {
+ model: 'BLV_RQ',
+ softwareVersion: '-',
+ hardwareVersion: '-'
+ },
+
+ keyConfig: {
+ selectedKeyCount: 0,
+ keyCountOptions: ['请选择按键类型', '1键', '2键', '3键', '4键', '5键', '6键', '7键', '8键'],
+ keys: [
+ {
+ hwIndex: 1,
+ mappedKey: 1,
+ functionCode: 1,
+ functionLabel: '普通按键',
+ originalMappedKey: 1
+ }
+ ]
+ },
+
+ logs: []
+}
+```
+
+其中:
+
+- `hwIndex`:硬件按键位置
+- `mappedKey`:当前界面配置映射值
+- `originalMappedKey`:设备读取回来的原始映射值,用于方案 B 放行时保留原值
+
+---
+
+## 十、测试范围
+
+## 1. 蓝牙信息栏
+
+- 能显示蓝牙名称、MAC、特征值、连接状态、信号
+- 断开按钮有效
+- 重连按钮有效
+
+## 2. 蓝牙名称设置
+
+- 非英文数字不可通过
+- 超过 8 字节不可通过
+- 合法名称可设置成功
+- 成功失败提示正确
+
+## 3. 版本读取
+
+- 可正常读取软件版本、硬件版本
+
+## 4. 按键配置读取
+
+- 未选按键类型时按钮禁用
+- 选定后可读取
+- 返回值可正确解析
+- 设备返回键数与页面选择不一致时可自动修正
+
+## 5. 按键配置设置
+
+- 功能可正确下发
+- 映射可正确下发
+- 重复映射必须拦截
+- 越界映射必须拦截
+- 未配置映射时弹窗提示并允许继续
+- 未配置映射项按原设备值下发
+
+## 6. 按键控制
+
+- 点击卡片时基于映射后的逻辑键值发送命令
+- 未配置映射不可触发
+
+## 7. 日志
+
+- TX / RX / WARN / ERR 记录完整
+- 清空日志可用
+- 导出日志可用
+
+---
+
+## 十一、首版实施范围
+
+首版实施包含:
+
+1. 新建 `BLVRQPage`
+2. BLE 信息展示
+3. 断开 / 重连
+4. 读取版本
+5. 设置蓝牙名称
+6. 选择按键类型
+7. 读取按键配置
+8. 设置按键功能
+9. 设置按键映射
+10. 按映射执行单键控制
+11. 日志显示 / 清空 / 导出
+
+首版暂不做:
+
+1. 多键同时触发
+2. 批量压测模式
+3. 原始报文自由编辑器
+
+---
+
+## 十二、建议实施顺序
+
+### 第 1 阶段:页面骨架
+
+- 新建 `BLVRQPage`
+- 搭建静态页面结构
+- 接收 BLE 上下文参数
+
+### 第 2 阶段:基础蓝牙能力
+
+- 连接信息展示
+- 断开 / 重连
+- RSSI 刷新
+- 蓝牙名称设置
+- 版本读取
+
+### 第 3 阶段:按键配置能力
+
+- 按键类型下拉
+- 配置读取
+- 卡片渲染
+- 功能与映射编辑
+- 二次确认弹窗
+- 方案 B 放行逻辑
+
+### 第 4 阶段:控制与日志增强
+
+- 单键触发
+- 日志导出
+- 异常处理优化
+
+---
+
+## 十三、结论
+
+本实施文档已完成以下关键决策固化:
+
+- `BLV_RQ` 单独新建页面
+- 设备型号固定为 `BLV_RQ`
+- 页面支持按键功能与映射双配置
+- 设置时采用二次确认
+- 未配置映射按键采用 **方案 B:允许继续**
+- 继续时采纳建议:**未配置映射项保持设备当前返回值不变**
+- 页面整体不滚动,仅日志区允许内部滚动
+
+该文档可作为后续代码实现的直接依据。
diff --git a/Document/RF_RQ2603 蓝牙通讯协议.md b/Document/RF_RQ2603 蓝牙通讯协议.md
new file mode 100644
index 0000000..ef192be
--- /dev/null
+++ b/Document/RF_RQ2603 蓝牙通讯协议.md
@@ -0,0 +1,192 @@
+# RF_RQ2603 蓝牙通讯协议
+
+## 一、设备与通讯基础
+
+- **主控蓝牙模块**:清月 BT201 (型号:KT1025A)
+- **通讯模式**:BLE 透传模式
+- **默认 BLE 名称**:BT201-BLE
+- **BLE 单包最大数据**:20 Byte
+- **串口参数 (MCU ↔ 蓝牙模块)**:115200, 8N1
+
+## 二、数据帧格式
+
+所有数据包遵循以下结构:
+
+| 字节位置 | 字段 | 长度 | 说明 |
+| --- | --- | --- | --- |
+| B0~B1 | Head (帧头) | 2 | 固定为 `0xDD 0xD0` |
+| B2 | Len (长度) | 1 | 整包总字节数(包含 Head 和 CRC) |
+| B3~B4 | CRC (校验) | 2 | CRC16;计算时此处填 `0x00` |
+| B5 | Frame_Type (命令字) | 1 | 指令代码 |
+| B6~B19 | PARA (参数) | ≤14 | 指令相关参数(可变长) |
+
+> **注意**
+> - `Len`:从 B0 到包尾的总字节数。
+> - `CRC` 计算范围:B0/B1/B2/B5/B6…(包尾),计算时 **B3~B4 需填 0x00**。
+
+## 三、指令集明细
+
+### 3.1 读取版本号(0x01)
+
+**PC → MCU**
+
+| 字段 | 值 |
+| --- | --- |
+| Frame_Type | `0x01` |
+| P0 | `0x01`(读取) |
+
+**MCU → PC**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x01` |
+| P0 | 软件版本号 |
+| P1 | 硬件版本号 |
+
+---
+
+### 3.2 设置蓝牙名称(0x05)
+
+**PC → MCU**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x05` |
+| P0 | 有效名称字节长度 N(1~12) |
+| P1~P(N) | 名称字符(ASCII) |
+
+> 默认名称:`BLV_RQ_XXXXXXXX`,最多支持 8 字节自定义名称。
+
+**MCU → PC**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x05` |
+| P0 | `0x01`:设置成功
`0x02`:设置失败 |
+
+---
+
+### 3.3 读取按键配置(0x04)
+
+**PC → MCU**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x04` |
+| P0 | `0x01`(读取) |
+
+**MCU → PC**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x04` |
+| P0 | 硬件按键总数(0x01~0x08) |
+| P1~P8 | 硬件按键 1~8 配置(每项 1 字节) |
+
+**按键配置字节结构**
+- Bit0~3:映射键值(0x1~0x8)
+- Bit4~7:按键功能
+
+**按键功能码(Bit4~7)**
+- `0x00`:不设置(保持原功能)
+- `0x01`:普通按键
+- `0x02`:清理按键(带二次确认)
+- `0x03`:投诉按键(带二次确认)
+- `0x04~0x0F`:预留/其他功能
+
+---
+
+### 3.4 设置按键配置(0x02)
+
+**PC → MCU**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x02` |
+| P0 | 硬件按键数量(`0x01~0x08`) |
+| P1 | 按键1功能 |
+| P2 | 按键2功能 |
+| P3 | 按键3功能 |
+| P4 | 按键4功能 |
+| P5 | 按键5功能 |
+| P6 | 按键6功能 |
+| P7 | 按键7功能 |
+| P8 | 按键8功能 |
+
+> 说明:
+> - `P0` 为实际需要配置的硬件按键数量 `N`
+> - 只需发送前 `N+1` 个字节(`P0~PN`)
+> - 例如:`P0=0x04`(4个按键),则只需发送 `P0, P1, P2, P3, P4` 共 5 个字节
+> - 未使用的 `P5~P8` 可省略
+
+**按键功能码定义**
+
+| 功能码 | 功能名称 | 行为说明 |
+| --- | --- | --- |
+| `0x00` | 不设置 | 保持该按键原有功能不变 |
+| `0x01` | 普通按键 | 标准按键功能,按下即触发 |
+| `0x02` | 清理按键 | 带二次确认的功能按键 |
+| `0x03` | 投诉按键 | 带二次确认的功能按键 |
+| `0x04` | 功能4 | 预留功能4 |
+| `0x05` | 功能5 | 预留功能5 |
+| `0x06` | 功能6 | 预留功能6 |
+| `0x07` | 功能7 | 预留功能7 |
+| `0x08` | 功能8 | 预留功能8 |
+| `0x09` | 功能9 | 预留功能9 |
+| `0x0A` | 功能10 | 预留功能10 |
+| `0x0B` | 功能11 | 预留功能11 |
+| `0x0C` | 功能12 | 预留功能12 |
+| `0x0D` | 功能13 | 预留功能13 |
+| `0x0E` | 功能14 | 预留功能14 |
+| `0x0F` | 功能15 | 预留功能15 |
+
+**MCU → PC**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x02` |
+| P0 | `0x01`:设置成功
`0x02`:设置失败 |
+
+---
+
+### 3.5 模拟按键触发(0x06)
+
+**PC → MCU**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x06` |
+| P0 | 模拟按键数量 K |
+| P1 | 按键状态位图(Bit0~Bit7):
`Bit0=1`:键值1按下
`Bit1=1`:键值2按下
`...`
`Bit7=1`:键值8按下 |
+
+> P0 的值应与 P1 中置 1 的位数一致。
+
+**MCU → PC**
+
+| 字段 | 说明 |
+| --- | --- |
+| Frame_Type | `0x06` |
+| P0 | `0x01`:成功
`0x02`:失败 |
+
+---
+
+## 四、关键概念
+
+- **硬件按键**:设备上固定的物理按钮(位置 1~8)。
+- **映射键值**:物理按键实际触发的逻辑按键编号(1~8),映射可配置。
+
+### 功能与读取配置的关系
+
+- 设置按键功能(Command `0x02`)作用于 **硬件按键位置**。
+- `P0` 为当前实际配置的硬件按键数量。
+- 读取配置(Command `0x04`)返回每个硬件位置当前的映射键值及其功能。
+
+### 其它说明
+
+- 参数区(PARA)最大 14 字节,需确保单包数据不超限。
+
+### 默认状态
+
+- 蓝牙名称:`BLV_RQ_XXXXXXXX`
+- 按键功能:默认为“普通按键”(0x01)
+- 按键映射:默认硬件位置 n 映射到键值 n(1 对 1 映射)
diff --git a/app.js b/app.js
index c45bd45..42f35e2 100644
--- a/app.js
+++ b/app.js
@@ -45,6 +45,7 @@ App({
CreateTime:null,
HotelCode:null,
CreatDate:null,
+ pendingBleNavigation: null,
},
toast:function(type,title,success,time){
if(type==1){
diff --git a/app.json b/app.json
index b386b81..8bf590e 100644
--- a/app.json
+++ b/app.json
@@ -1,8 +1,11 @@
{
"pages": [
-
+
"pages/autho/index",
- "pages/basics/BluetoothDebugging/B13page/B13page",
+ "pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage",
+
+
+ "pages/basics/BluetoothDebugging/B13page/B13page",
"pages/login/login",
"pages/index/index",
"pages/mycenter/index",
diff --git a/lib/RequestingCenter.js b/lib/RequestingCenter.js
index 18332ee..2b0db33 100644
--- a/lib/RequestingCenter.js
+++ b/lib/RequestingCenter.js
@@ -184,6 +184,7 @@ export async function GetRoomType(params){
}
//获取房型下的房间
export async function GetRoomTypeNode(params){
+
params.token = gettoken();
console.log(params)
return await reqeust1({
diff --git a/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
new file mode 100644
index 0000000..e4f9c8c
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
@@ -0,0 +1,773 @@
+const app = getApp()
+
+function ab2hex(buffer) {
+ const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || [])
+ return Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')
+}
+
+function asciiToBytes(text) {
+ const bytes = []
+ for (let i = 0; i < text.length; i += 1) {
+ bytes.push(text.charCodeAt(i) & 0xFF)
+ }
+ return bytes
+}
+
+function bytesToAscii(bytes) {
+ return String.fromCharCode(...(bytes || []).filter(v => v > 0))
+}
+
+function crc16Modbus(bytes) {
+ let crc = 0xFFFF
+ for (let i = 0; i < bytes.length; i += 1) {
+ crc ^= bytes[i]
+ for (let j = 0; j < 8; j += 1) {
+ const lsb = crc & 0x0001
+ crc >>= 1
+ if (lsb) crc ^= 0xA001
+ }
+ }
+ return crc & 0xFFFF
+}
+
+function buildPacket(frameType, params = []) {
+ const payload = Array.isArray(params) ? params.slice() : Array.from(params || [])
+ const len = 6 + payload.length
+ const bytes = [0xDD, 0xD0, len & 0xFF, 0x00, 0x00, frameType & 0xFF, ...payload]
+ const crc = crc16Modbus(bytes)
+ bytes[3] = crc & 0xFF
+ bytes[4] = (crc >> 8) & 0xFF
+ return Uint8Array.from(bytes)
+}
+
+function parsePacket(buffer) {
+ const bytes = Array.from(new Uint8Array(buffer || []))
+ if (bytes.length < 6) return null
+ if (bytes[0] !== 0xDD || bytes[1] !== 0xD0) return null
+ const len = bytes[2]
+ if (len !== bytes.length) return null
+ const inputCrc = bytes[3] | (bytes[4] << 8)
+ const crcBytes = bytes.slice()
+ crcBytes[3] = 0x00
+ crcBytes[4] = 0x00
+ const calcCrc = crc16Modbus(crcBytes)
+ if (inputCrc !== calcCrc) {
+ return { invalid: true, bytes, reason: 'CRC校验失败' }
+ }
+ return {
+ bytes,
+ len,
+ frameType: bytes[5],
+ params: bytes.slice(6)
+ }
+}
+
+function nowText() {
+ const d = new Date()
+ const pad = (n, l = 2) => String(n).padStart(l, '0')
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
+}
+
+function formatBleError(err) {
+ if (!err) return '未知错误'
+ const parts = []
+ if (typeof err.errCode !== 'undefined') parts.push(`errCode=${err.errCode}`)
+ if (err.errMsg) parts.push(err.errMsg)
+ if (err.message && err.message !== err.errMsg) parts.push(err.message)
+ return parts.length ? parts.join(' | ') : String(err)
+}
+
+function createFunctionOptions() {
+ const names = {
+ 0x00: '不设置',
+ 0x01: '普通按键功能',
+ 0x02: '清理按键',
+ 0x03: '投诉按键'
+ }
+ return Array.from({ length: 16 }, (_, i) => ({
+ code: i,
+ label: `${i}: ${names[i] || '无效'}`
+ }))
+}
+
+function createKeys(count) {
+ return Array.from({ length: count }, (_, i) => ({
+ hwIndex: i + 1,
+ mappedKey: i + 1,
+ mappedKeyIndex: i + 1,
+ originalMappedKey: i + 1,
+ functionCode: 0x01,
+ functionIndex: 1
+ }))
+}
+
+function createKeySlots(keys, totalSlots = 8) {
+ const list = Array.isArray(keys) ? keys : []
+ return Array.from({ length: totalSlots }, (_, i) => {
+ const key = list[i]
+ if (key) {
+ return {
+ slotIndex: i,
+ isPlaceholder: false,
+ ...key
+ }
+ }
+ return {
+ slotIndex: i,
+ isPlaceholder: true,
+ hwIndex: i + 1
+ }
+ })
+}
+
+Page({
+ data: {
+ bleInfo: {
+ devName: '',
+ mac: '',
+ serviceId: '',
+ txCharId: '',
+ rxCharId: '',
+ readCharId: '',
+ writeCharId: '',
+ connected: false,
+ rssi: null,
+ signalText: '-'
+ },
+ bleNameInput: '',
+ canSetBleName: false,
+ deviceInfo: {
+ model: 'BLV_RQ',
+ softwareVersion: '-',
+ hardwareVersion: '-'
+ },
+ keyConfig: {
+ selectedKeyCount: 0,
+ selectedKeyTypeIndex: 0,
+ keyCountOptions: ['请选择按键类型', '1键', '2键', '3键', '4键', '5键', '6键', '7键', '8键'],
+ keyCountValues: [0, 1, 2, 3, 4, 5, 6, 7, 8],
+ keys: [],
+ keySlots: createKeySlots([])
+ },
+ functionOptions: createFunctionOptions(),
+ functionOptionLabels: createFunctionOptions().map(item => item.label),
+ canOperateKeyConfig: false,
+ isConnecting: false,
+ readingVersion: false,
+ readingKeyConfig: false,
+ writingKeyConfig: false,
+ settingBleName: false,
+ keySendingIndex: -1,
+ logs: [],
+ logScrollTo: '',
+ confirmDialog: {
+ visible: false,
+ message: ''
+ }
+ },
+
+ onLoad(options) {
+ const devName = options && options.DevName ? decodeURIComponent(options.DevName) : 'BLV_RQ设备'
+ const mac = options && options.mac ? decodeURIComponent(options.mac) : ''
+ const connected = options && (options.connected === '1' || options.connected === 'true')
+ const serviceId = options && options.serviceId ? decodeURIComponent(options.serviceId) : ''
+ const txCharId = options && options.txCharId ? decodeURIComponent(options.txCharId) : ''
+ const rxCharId = options && options.rxCharId ? decodeURIComponent(options.rxCharId) : ''
+ const readCharId = options && options.readCharId ? decodeURIComponent(options.readCharId) : rxCharId
+ const writeCharId = options && options.writeCharId ? decodeURIComponent(options.writeCharId) : txCharId
+ const resumeConnectedSession = connected || !!(mac && serviceId && txCharId && rxCharId)
+ this.setData({
+ bleInfo: {
+ devName,
+ mac,
+ serviceId,
+ txCharId,
+ rxCharId,
+ readCharId,
+ writeCharId,
+ connected: resumeConnectedSession,
+ rssi: null,
+ signalText: ''
+ },
+ canOperateKeyConfig: false
+ })
+ this.appendLog('CFG', `进入BLV_RQ页面:${devName}`)
+ this.updateSetBleNameState('')
+ if (app.globalData && app.globalData.pendingBleNavigation && app.globalData.pendingBleNavigation.target === 'BLV_RQ') {
+ app.globalData.pendingBleNavigation = null
+ }
+ if (resumeConnectedSession) {
+ this.activateConnectedSession('页面初始化')
+ } else {
+ this.ensureBleReady('页面初始化')
+ }
+ },
+
+ onShow() {
+ if (this.data.bleInfo.connected) {
+ this.bindConnectionStateListener()
+ this.startBleNotifications()
+ this.startStatusPolling()
+ this.refreshBleStatus()
+ return
+ }
+ this.ensureBleReady('页面显示')
+ },
+
+ onUnload() {
+ this.stopStatusPolling()
+ this.teardownBleNotifications()
+ this.teardownConnectionStateListener()
+ },
+
+ appendLog(type, text, extra = {}) {
+ const map = { TX: 'tx', RX: 'rx', WARN: 'warn', ERR: 'err', UI: 'ui', CFG: 'cfg' }
+ let displayText = text
+ let hexText = ''
+ let noteText = text
+ if (type === 'TX' && text.includes('|')) {
+ const parts = text.split('|')
+ noteText = parts[0].trim()
+ hexText = parts.slice(1).join('|').trim()
+ displayText = hexText || noteText
+ } else if (type === 'RX') {
+ hexText = text.replace(/^\[[^\]]+\]\s*/, '').trim()
+ displayText = hexText
+ }
+ const item = {
+ id: Date.now() + Math.random(),
+ time: nowText(),
+ type,
+ typeClass: map[type] || 'cfg',
+ text: extra.cmd ? `[${extra.cmd}] ${text}` : text,
+ displayText,
+ hexText,
+ noteText
+ }
+ const logs = [...this.data.logs, item].slice(-300)
+ this.setData({ logs, logScrollTo: `log-${item.id}` })
+ },
+
+ updateSetBleNameState(value) {
+ const can = /^[A-Za-z0-9]{1,8}$/.test(value || '') && !!this.data.bleInfo.connected
+ this.setData({ bleNameInput: value, canSetBleName: can })
+ },
+
+ onBleNameInput(e) {
+ const raw = (e.detail.value || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 8)
+ this.updateSetBleNameState(raw)
+ },
+
+ onKeyCountChange(e) {
+ const selectedKeyTypeIndex = Number(e.detail.value || 0)
+ const count = this.data.keyConfig.keyCountValues[selectedKeyTypeIndex] || 0
+ const keys = createKeys(count)
+ this.setData({
+ 'keyConfig.selectedKeyTypeIndex': selectedKeyTypeIndex,
+ 'keyConfig.selectedKeyCount': count,
+ 'keyConfig.keys': keys,
+ 'keyConfig.keySlots': createKeySlots(keys),
+ canOperateKeyConfig: !!this.data.bleInfo.connected && count > 0
+ })
+ this.appendLog('UI', count ? `选择按键类型:${count}键` : '清空按键类型选择')
+ },
+
+ onFunctionChange(e) {
+ const index = Number(e.currentTarget.dataset.index)
+ const value = Number(e.detail.value || 0)
+ const keys = [...this.data.keyConfig.keys]
+ if (!keys[index]) return
+ keys[index].functionIndex = value
+ keys[index].functionCode = this.data.functionOptions[value].code
+ this.setData({ 'keyConfig.keys': keys, 'keyConfig.keySlots': createKeySlots(keys) })
+ },
+
+ async onReadVersion() {
+ if (!this.data.bleInfo.connected) return
+ this.setData({ readingVersion: true })
+ await this.sendCommand(0x01, [0x01], '读取版本')
+ this.setData({ readingVersion: false })
+ },
+
+ async onSetBleName() {
+ if (!this.data.canSetBleName) {
+ wx.showToast({ title: '名称不合法', icon: 'none' })
+ return
+ }
+ const bytes = asciiToBytes(this.data.bleNameInput)
+ this.setData({ settingBleName: true })
+ await this.sendCommand(0x05, [bytes.length, ...bytes], '设置蓝牙名称')
+ this.setData({ settingBleName: false })
+ },
+
+ async onReadKeyConfig() {
+ if (!this.data.bleInfo.connected) {
+ wx.showToast({ title: '请先连接设备', icon: 'none' })
+ return
+ }
+ this.setData({ readingKeyConfig: true })
+ await this.sendCommand(0x04, [0x01], '读取按键配置')
+ this.setData({ readingKeyConfig: false })
+ },
+
+ onSubmitKeyConfig() {
+ const validation = this.validateKeyConfig()
+ if (validation.blocked) {
+ wx.showModal({ title: '配置校验失败', content: validation.message, showCancel: false })
+ return
+ }
+ const message = `确认下发当前${validation.keyCount}键的按键功能配置?`
+ this._pendingSubmit = validation
+ this.setData({ 'confirmDialog.visible': true, 'confirmDialog.message': message })
+ },
+
+ onCancelSubmit() {
+ this._pendingSubmit = null
+ this.setData({ 'confirmDialog.visible': false, 'confirmDialog.message': '' })
+ },
+
+ async onConfirmSubmit() {
+ const validation = this._pendingSubmit || this.validateKeyConfig()
+ this.onCancelSubmit()
+ const keys = validation.keys
+ const keyCount = validation.keyCount
+ const functionPayload = [keyCount]
+ for (let i = 0; i < keyCount; i += 1) {
+ const item = keys[i]
+ functionPayload.push(item ? item.functionCode : 0x00)
+ }
+ this.setData({ writingKeyConfig: true })
+ const step1 = await this.sendCommand(0x02, functionPayload, '设置按键功能', { expectResponse: true, silentError: true })
+ if (!step1) {
+ this.setData({ writingKeyConfig: false })
+ wx.showToast({ title: '功能设置失败', icon: 'none' })
+ return
+ }
+ this.setData({ writingKeyConfig: false })
+ wx.showToast({ title: '设置成功', icon: 'success' })
+ },
+
+ async onTriggerKey(e) {
+ const index = Number(e.currentTarget.dataset.index)
+ const item = this.data.keyConfig.keys[index]
+ if (!item || !item.mappedKey) {
+ wx.showToast({ title: '当前按键未配置映射', icon: 'none' })
+ return
+ }
+ const keyCount = Number(this.data.keyConfig.selectedKeyCount || 0)
+ if (!keyCount) {
+ wx.showToast({ title: '请先选择按键类型', icon: 'none' })
+ return
+ }
+ const bitmap = 1 << (item.mappedKey - 1)
+ this.setData({ keySendingIndex: index })
+ await this.sendCommand(0x06, [keyCount, bitmap], `触发按键${item.hwIndex} -> 键值${item.mappedKey}(${keyCount}键)`)
+ this.setData({ keySendingIndex: -1 })
+ },
+
+ onClearLogs() {
+ this.setData({ logs: [], logScrollTo: '' })
+ },
+
+ onExportLogs() {
+ try {
+ const fs = wx.getFileSystemManager && wx.getFileSystemManager()
+ if (!fs || !wx.env || !wx.env.USER_DATA_PATH) {
+ wx.showToast({ title: '当前环境不支持导出', icon: 'none' })
+ return
+ }
+ const name = `BLV_RQ_Log_${this.getExportTimestamp()}.txt`
+ const path = `${wx.env.USER_DATA_PATH}/${name}`
+ const content = this.buildLogExportText()
+ fs.writeFileSync(path, content, 'utf8')
+ this.appendLog('UI', `日志已导出:${name}`)
+ if (typeof wx.openDocument === 'function') {
+ wx.openDocument({ filePath: path, showMenu: true })
+ } else {
+ wx.showToast({ title: '日志已导出', icon: 'success' })
+ }
+ } catch (err) {
+ this.appendLog('ERR', `导出日志失败:${(err && err.message) || err}`)
+ wx.showToast({ title: '导出失败', icon: 'none' })
+ }
+ },
+
+ getExportTimestamp() {
+ const d = new Date()
+ const pad = (n) => String(n).padStart(2, '0')
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
+ },
+
+ buildLogExportText() {
+ const info = this.data.bleInfo
+ const device = this.data.deviceInfo
+ const lines = [
+ 'BLV_RQ 通信日志导出',
+ `导出时间:${new Date().toLocaleString()}`,
+ `蓝牙名称:${info.devName || '-'}`,
+ `MAC:${info.mac || '-'}`,
+ `连接状态:${info.connected ? '已连接' : '未连接'}`,
+ `信号值:${info.signalText || '-'}`,
+ `设备型号:${device.model || '-'}`,
+ `软件版本:${device.softwareVersion || '-'}`,
+ `硬件版本:${device.hardwareVersion || '-'}`,
+ '----------------------------------------'
+ ]
+ this.data.logs.forEach(item => {
+ if (item.type === 'TX') {
+ lines.push(`[${item.time}] TX -> ${item.displayText || item.text}`)
+ } else if (item.type === 'RX') {
+ lines.push(`[${item.time}] RX <- ${item.displayText || item.text}`)
+ } else {
+ lines.push(`[${item.time}] [${item.type}] ${item.text}`)
+ }
+ })
+ return lines.join('\n')
+ },
+
+ onDisconnect(showToast = false) {
+ const { mac } = this.data.bleInfo
+ if (!mac || typeof wx.closeBLEConnection !== 'function') return
+ this.stopStatusPolling()
+ wx.closeBLEConnection({
+ deviceId: mac,
+ complete: () => {
+ this.setData({
+ 'bleInfo.connected': false,
+ 'bleInfo.rssi': null,
+ 'bleInfo.signalText': '',
+ canOperateKeyConfig: false
+ })
+ this.updateSetBleNameState(this.data.bleNameInput)
+ this.appendLog('CFG', '蓝牙连接已断开')
+ if (showToast) wx.showToast({ title: '已断开连接', icon: 'none' })
+ }
+ })
+ },
+
+ onConnectionAction() {
+ if (this.data.isConnecting) return
+ if (this.data.bleInfo.connected) {
+ this.onDisconnect(true)
+ return
+ }
+ this.onReconnect()
+ },
+
+ onReconnect() {
+ this.appendLog('UI', '请求重连蓝牙')
+ this.ensureBleReady('手动重连', true)
+ },
+
+ validateKeyConfig() {
+ const keys = (this.data.keyConfig.keys || []).map(item => ({ ...item }))
+ const selectedCount = this.data.keyConfig.selectedKeyCount
+ if (!selectedCount) {
+ return {
+ blocked: true,
+ message: '请先选择按键类型',
+ keys,
+ keyCount: 0
+ }
+ }
+ return {
+ blocked: false,
+ message: '',
+ keys,
+ keyCount: selectedCount
+ }
+ },
+
+ async sendCommand(frameType, params, text, options = {}) {
+ const { mac, serviceId, txCharId, writeCharId } = this.data.bleInfo
+ const targetWriteCharId = writeCharId || txCharId
+ if (!mac || !serviceId || !targetWriteCharId) {
+ wx.showToast({ title: '蓝牙通道未就绪', icon: 'none' })
+ return false
+ }
+ const packet = buildPacket(frameType, params)
+ this.appendLog('TX', `${text} | ${ab2hex(packet)}`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
+ return new Promise((resolve) => {
+ this._pendingResponse = {
+ frameType,
+ resolve,
+ silentError: !!options.silentError,
+ timer: setTimeout(() => {
+ this._pendingResponse = null
+ this.appendLog('WARN', `${text}超时`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
+ resolve(false)
+ }, 4000)
+ }
+ wx.writeBLECharacteristicValue({
+ deviceId: mac,
+ serviceId,
+ characteristicId: targetWriteCharId,
+ value: packet.buffer,
+ fail: (err) => {
+ if (this._pendingResponse && this._pendingResponse.timer) clearTimeout(this._pendingResponse.timer)
+ this._pendingResponse = null
+ this.appendLog('ERR', `${text}发送失败:${(err && err.errMsg) || '未知错误'}`)
+ if (!options.silentError) wx.showToast({ title: '发送失败', icon: 'none' })
+ resolve(false)
+ }
+ })
+ })
+ },
+
+ startBleNotifications() {
+ const { mac, serviceId, txCharId, rxCharId } = this.data.bleInfo
+ const notifyTargets = [txCharId, rxCharId].filter(Boolean)
+ if (!mac || !serviceId || !notifyTargets.length || typeof wx.notifyBLECharacteristicValueChange !== 'function') return
+ let pending = notifyTargets.length
+ notifyTargets.forEach((characteristicId) => {
+ wx.notifyBLECharacteristicValueChange({
+ state: true,
+ deviceId: mac,
+ serviceId,
+ characteristicId,
+ complete: () => {
+ pending -= 1
+ if (pending <= 0) {
+ this.bindBleListener()
+ }
+ }
+ })
+ })
+ },
+
+ ensureBleReady(source = '状态校验', showToast = false) {
+ const { mac } = this.data.bleInfo
+ if (!mac || typeof wx.createBLEConnection !== 'function') return
+ if (this._connecting) return
+ this._connecting = true
+ this.setData({ isConnecting: true })
+ this.appendLog('CFG', `${source}:开始校验蓝牙连接`)
+ wx.createBLEConnection({
+ deviceId: mac,
+ success: () => {
+ this._connecting = false
+ this.activateConnectedSession(source, showToast)
+ },
+ fail: (err) => {
+ this._connecting = false
+ const reason = formatBleError(err)
+ const message = String(reason || '').toLowerCase()
+ if (message.includes('already connect') || message.includes('already connected') || reason === '已连接' || reason === '设备已连接') {
+ this.appendLog('CFG', `${source}检测到设备已连接,复用当前蓝牙会话`)
+ this.activateConnectedSession(source, showToast)
+ return
+ }
+ this.setData({
+ isConnecting: false,
+ 'bleInfo.connected': false,
+ 'bleInfo.rssi': null,
+ 'bleInfo.signalText': '',
+ canOperateKeyConfig: false
+ })
+ this.updateSetBleNameState(this.data.bleNameInput)
+ this.appendLog('ERR', `${source}失败:${reason}`)
+ if (showToast) wx.showToast({ title: '重连失败', icon: 'none' })
+ }
+ })
+ },
+
+ activateConnectedSession(source = '状态校验', showToast = false) {
+ this.setData({
+ isConnecting: false,
+ 'bleInfo.connected': true,
+ canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
+ })
+ this.bindConnectionStateListener()
+ this.startBleNotifications()
+ this.startStatusPolling()
+ this.refreshBleStatus()
+ this.updateSetBleNameState(this.data.bleNameInput)
+ this.appendLog('CFG', `${source}成功:蓝牙已连接`)
+ this.autoLoadDeviceData()
+ if (showToast) wx.showToast({ title: '重连成功', icon: 'success' })
+ },
+
+ async autoLoadDeviceData() {
+ await this.onReadVersion()
+ },
+
+ bindConnectionStateListener() {
+ if (this._connectionStateHandler || typeof wx.onBLEConnectionStateChange !== 'function') return
+ this._connectionStateHandler = (res) => {
+ if (!res || res.deviceId !== this.data.bleInfo.mac) return
+ const connected = !!res.connected
+ this.setData({
+ isConnecting: false,
+ 'bleInfo.connected': connected,
+ 'bleInfo.signalText': connected ? this.data.bleInfo.signalText : '',
+ 'bleInfo.rssi': connected ? this.data.bleInfo.rssi : null,
+ canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
+ })
+ this.updateSetBleNameState(this.data.bleNameInput)
+ if (connected) {
+ this.startStatusPolling()
+ this.refreshBleStatus()
+ this.appendLog('CFG', '蓝牙连接状态:已连接')
+ } else {
+ this.stopStatusPolling()
+ this.appendLog('WARN', '蓝牙连接状态:已断开')
+ }
+ }
+ wx.onBLEConnectionStateChange(this._connectionStateHandler)
+ },
+
+ teardownConnectionStateListener() {
+ if (this._connectionStateHandler && typeof wx.offBLEConnectionStateChange === 'function') {
+ wx.offBLEConnectionStateChange(this._connectionStateHandler)
+ }
+ this._connectionStateHandler = null
+ },
+
+ startStatusPolling() {
+ this.stopStatusPolling()
+ this._statusTimer = setInterval(() => {
+ this.refreshBleStatus()
+ }, 3000)
+ },
+
+ stopStatusPolling() {
+ if (this._statusTimer) {
+ clearInterval(this._statusTimer)
+ this._statusTimer = null
+ }
+ },
+
+ bindBleListener() {
+ if (this._bleChangeHandler || typeof wx.onBLECharacteristicValueChange !== 'function') return
+ this._bleChangeHandler = (res) => {
+ const packet = parsePacket(res && res.value)
+ if (!packet) return
+ if (packet.invalid) {
+ this.appendLog('WARN', `收到异常数据:${ab2hex(packet.bytes)}`)
+ return
+ }
+ this.appendLog('RX', ab2hex(packet.bytes), { cmd: `0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}` })
+ this.handlePacket(packet)
+ }
+ wx.onBLECharacteristicValueChange(this._bleChangeHandler)
+ },
+
+ teardownBleNotifications() {
+ if (this._bleChangeHandler && typeof wx.offBLECharacteristicValueChange === 'function') {
+ wx.offBLECharacteristicValueChange(this._bleChangeHandler)
+ }
+ this._bleChangeHandler = null
+ },
+
+ handlePacket(packet) {
+ switch (packet.frameType) {
+ case 0x01:
+ this.handleVersionPacket(packet.params)
+ break
+ case 0x04:
+ this.handleKeyConfigPacket(packet.params)
+ break
+ case 0x05:
+ this.handleSimpleAck(packet.params, '设置蓝牙名称')
+ break
+ case 0x02:
+ this.handleSimpleAck(packet.params, '设置按键配置')
+ break
+ case 0x06:
+ this.handleSimpleAck(packet.params, '按键控制')
+ break
+ default:
+ this.appendLog('CFG', `收到未处理命令:0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}`)
+ break
+ }
+ if (this._pendingResponse && this._pendingResponse.frameType === packet.frameType) {
+ clearTimeout(this._pendingResponse.timer)
+ const resolve = this._pendingResponse.resolve
+ this._pendingResponse = null
+ resolve(true)
+ }
+ },
+
+ handleVersionPacket(params = []) {
+ const softwareVersion = params.length > 0 ? String(params[0]) : '-'
+ const hardwareVersion = params.length > 1 ? String(params[1]) : '-'
+ this.setData({
+ 'deviceInfo.softwareVersion': softwareVersion,
+ 'deviceInfo.hardwareVersion': hardwareVersion
+ })
+ this.appendLog('CFG', `版本更新:软件${softwareVersion},硬件${hardwareVersion}`)
+ },
+
+ handleKeyConfigPacket(params = []) {
+ const count = Number(params[0] || 0)
+ const safeCount = count > 0 && count <= 8 ? count : this.data.keyConfig.selectedKeyCount
+ const keys = createKeys(safeCount)
+ for (let i = 0; i < safeCount; i += 1) {
+ const cfg = params[i + 1] || 0
+ const mappedKey = cfg & 0x0F
+ const functionCode = (cfg >> 4) & 0x0F
+ keys[i].mappedKey = mappedKey || null
+ keys[i].originalMappedKey = mappedKey || null
+ keys[i].mappedKeyIndex = mappedKey || 0
+ keys[i].functionCode = functionCode
+ keys[i].functionIndex = functionCode
+ }
+ const keyTypeIndex = this.data.keyConfig.keyCountValues.indexOf(safeCount)
+ this.setData({
+ readingKeyConfig: false,
+ 'keyConfig.selectedKeyTypeIndex': keyTypeIndex >= 0 ? keyTypeIndex : 0,
+ 'keyConfig.selectedKeyCount': safeCount,
+ 'keyConfig.keys': keys,
+ 'keyConfig.keySlots': createKeySlots(keys),
+ canOperateKeyConfig: true
+ })
+ if (count && count !== this.data.keyConfig.selectedKeyCount) {
+ wx.showToast({ title: `已按设备返回${count}键修正`, icon: 'none' })
+ }
+ this.appendLog('CFG', `读取到按键配置:${safeCount}键`)
+ },
+
+ handleSimpleAck(params = [], text) {
+ const ok = Number(params[0] || 0) === 0x01
+ this.appendLog(ok ? 'CFG' : 'WARN', `${text}${ok ? '成功' : '失败'}`)
+ if (text === '设置蓝牙名称' && ok) {
+ this.setData({ 'bleInfo.devName': this.data.bleNameInput })
+ }
+ if (text === '设置蓝牙名称') this.setData({ settingBleName: false })
+ if (text === '按键控制') this.setData({ keySendingIndex: -1 })
+ },
+
+ refreshBleStatus() {
+ const { mac } = this.data.bleInfo
+ if (!mac || typeof wx.getBLEDeviceRSSI !== 'function') return
+ wx.getBLEDeviceRSSI({
+ deviceId: mac,
+ success: (res) => {
+ const rssi = typeof res.RSSI === 'number' ? res.RSSI : null
+ const signalText = rssi == null ? '-' : `${rssi} dBm`
+ this.setData({
+ 'bleInfo.rssi': rssi,
+ 'bleInfo.signalText': signalText,
+ 'bleInfo.connected': true,
+ canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
+ })
+ this.updateSetBleNameState(this.data.bleNameInput)
+ },
+ fail: (err) => {
+ const reason = formatBleError(err)
+ const connected = !!this.data.bleInfo.connected
+ this.setData({
+ 'bleInfo.connected': connected,
+ 'bleInfo.rssi': null,
+ 'bleInfo.signalText': connected ? '-' : '',
+ canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
+ })
+ if (!connected) {
+ this.stopStatusPolling()
+ }
+ this.appendLog('WARN', `信号刷新失败:${reason}`)
+ this.updateSetBleNameState(this.data.bleNameInput)
+ }
+ })
+ }
+})
diff --git a/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
new file mode 100644
index 0000000..518243f
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
@@ -0,0 +1,8 @@
+{
+ "navigationBarBackgroundColor": "#fff",
+ "navigationBarTextStyle": "black",
+ "navigationStyle": "custom",
+ "usingComponents": {
+ "cu-custom": "/colorui/components/cu-custom"
+ }
+}
diff --git a/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
new file mode 100644
index 0000000..e252c2b
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
@@ -0,0 +1,129 @@
+
+
+ {{bleInfo.devName || 'BLV_RQ设备'}}
+
+
+
+
+
+
+
+
+ 🔵
+ 蓝牙名称:
+ {{bleInfo.devName || '-'}}
+
+
+
+
+
+
+
+
+ 连接状态:
+ {{isConnecting ? '连接中' : (bleInfo.connected ? '已连接' : '未连接')}}
+
+
+ 信号强度:
+ {{bleInfo.signalText || '-'}}
+
+
+
+
+
+ MAC地址:
+ {{bleInfo.mac || '-'}}
+
+
+
+
+
+ 蓝牙名称:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 软件版本:
+ {{deviceInfo.softwareVersion}}
+
+
+ 硬件版本:
+ {{deviceInfo.hardwareVersion}}
+
+
+
+
+
+
+
+
+ 按键类型:
+
+ {{keyConfig.keyCountOptions[keyConfig.selectedKeyTypeIndex]}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 按键{{item.hwIndex}}
+
+ {{functionOptionLabels[item.functionIndex] || '请选择功能'}}
+
+
+
+
+ 预留{{item.hwIndex}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.time}}
+ {{item.type === 'TX' ? 'TX ->' : (item.type === 'RX' ? 'RX <-' : '[' + item.type + ']')}}
+ {{(item.type === 'TX' || item.type === 'RX') ? (item.displayText || item.hexText || item.text) : item.text}}
+
+
+
+
+
+
+
+ 设置确认
+ {{confirmDialog.message}}
+
+
+
+
+
+
+
diff --git a/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
new file mode 100644
index 0000000..b89d0d1
--- /dev/null
+++ b/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
@@ -0,0 +1,715 @@
+page {
+ background: #f5f6f8;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.container {
+ height: 100vh;
+ background: #f5f6f8;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.page-body,
+.content {
+ height: calc(100vh - 88rpx);
+ min-height: 0;
+ padding: 8rpx 10rpx 0;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+ flex: 1;
+ overflow: hidden;
+}
+
+.panel,
+.cfg-card,
+.log-card,
+.device-row {
+ width: 100%;
+ background: #ffffff;
+ border: none;
+ border-radius: 14rpx;
+ padding: 12rpx;
+ box-sizing: border-box;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.ble-panel,
+.device-panel,
+.log-panel {
+ width: 100%;
+}
+
+.ble-panel,
+.device-panel {
+ flex: 0 0 auto;
+}
+
+.log-panel,
+.log-card {
+ flex: 1 1 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12rpx;
+ margin-bottom: 8rpx;
+}
+
+.blank-head {
+ min-height: 0;
+ margin-bottom: 6rpx;
+}
+
+.panel-title {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #0f172a;
+ line-height: 1.2;
+}
+
+.panel-title.no-margin {
+ margin: 0;
+}
+
+.console-row {
+ display: flex;
+ align-items: stretch;
+ gap: 8rpx;
+ margin-bottom: 8rpx;
+}
+
+.console-row:last-child {
+ margin-bottom: 0;
+}
+
+.console-cell {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ padding: 0;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ box-sizing: border-box;
+}
+
+.device-name-cell {
+ flex: 0.98;
+}
+
+.status-cell,
+.signal-cell {
+ flex: 1;
+}
+
+.action-cell,
+.button-cell,
+.action-cell-inline {
+ width: 140rpx;
+ justify-content: center;
+}
+
+.read-version-cell {
+ width: 108rpx;
+}
+
+.full-line {
+ width: 100%;
+}
+
+.input-cell.wide,
+.select-cell {
+ flex: 0.88;
+}
+
+.firmware-cell {
+ flex: 1.54;
+}
+
+.software-version-cell,
+.hardware-version-cell {
+ min-width: 0;
+}
+
+.software-version-cell .cell-label,
+.hardware-version-cell .cell-label {
+ flex-shrink: 0;
+}
+
+.software-version-cell .cell-value,
+.hardware-version-cell .cell-value {
+ font-size: 24rpx;
+}
+
+.key-type-cell {
+ flex: 0.62;
+}
+
+.device-icon {
+ font-size: 30rpx;
+}
+
+.cell-label {
+ font-size: 26rpx;
+ color: #606266;
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+}
+
+.cell-value {
+ flex: 1;
+ min-width: 0;
+ font-size: 27rpx;
+ font-weight: 600;
+ color: #333333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-flex;
+ align-items: center;
+}
+
+.status-chip {
+ min-width: 120rpx;
+ height: 100%;
+ min-height: 48rpx;
+ align-self: stretch;
+ padding: 0 12rpx;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 10rpx;
+ font-size: 24rpx;
+ font-weight: 700;
+ box-sizing: border-box;
+}
+
+.status-chip.online {
+ color: #047857;
+ background: rgba(16, 185, 129, 0.12);
+ border: 1rpx solid rgba(16, 185, 129, 0.28);
+}
+
+.status-chip.offline {
+ color: #b45309;
+ background: rgba(245, 158, 11, 0.12);
+ border: 1rpx solid rgba(245, 158, 11, 0.26);
+}
+
+.status-chip.pending {
+ color: #1d4ed8;
+ background: rgba(37, 99, 235, 0.12);
+ border: 1rpx solid rgba(37, 99, 235, 0.26);
+}
+
+.name-input {
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ min-height: 48rpx;
+ align-self: stretch;
+ padding: 0 12rpx;
+ background: #f7f8fa;
+ border: 1rpx solid #e5e9f2;
+ border-radius: 10rpx;
+ font-size: 24rpx;
+ box-sizing: border-box;
+}
+
+.action-btn {
+ border-radius: 12rpx;
+ font-weight: 600;
+ white-space: nowrap;
+ padding: 0 12rpx;
+ font-size: 28rpx;
+ height: 100%;
+ min-height: 52rpx;
+ align-self: stretch;
+ line-height: 1.2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ box-sizing: border-box;
+}
+
+.read-version-cell .action-btn {
+ font-size: 26rpx;
+ padding: 0 8rpx;
+}
+
+.action-btn::after {
+ border: none;
+}
+
+.action-btn.primary {
+ background: #0ea5e9;
+ color: #ffffff;
+ box-shadow: 0 2rpx 6rpx rgba(14, 165, 233, 0.35);
+}
+
+.action-btn.success {
+ background: #21c161;
+ color: #ffffff;
+}
+
+.action-btn.warn {
+ background: #ff8f00;
+ color: #ffffff;
+}
+
+.action-btn.default {
+ background: #ffffff;
+ color: #2bab99;
+ border: 1rpx solid rgba(43, 171, 153, 0.18);
+}
+
+.full-btn {
+ width: 100%;
+}
+
+.count-picker,
+.inline-key-picker {
+ flex: 1;
+ min-width: 0;
+}
+
+.picker-view {
+ min-height: 48rpx;
+ height: 100%;
+ align-self: stretch;
+ padding: 0 12rpx;
+ display: flex;
+ align-items: center;
+ background: #f7f8fa;
+ border: 1rpx solid #e5e9f2;
+ border-radius: 10rpx;
+ font-size: 23rpx;
+ color: #0f172a;
+ box-sizing: border-box;
+}
+
+.picker-view.compact {
+ width: 100%;
+}
+
+.is-disabled {
+ opacity: 0.48;
+ filter: grayscale(0.2);
+}
+
+.name-input.is-disabled,
+.picker-view.is-disabled {
+ background: #f1f5f9;
+ border-color: #e2e8f0;
+ color: #98a2b3;
+}
+
+.key-card-wrap {
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+.key-grid.strict-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8rpx;
+}
+
+.key-card-inline {
+ min-width: 0;
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ padding: 0;
+ background: #ffffff;
+ border: none;
+ border-radius: 14rpx;
+ box-sizing: border-box;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.reserve-card {
+ border-style: dashed;
+ border-color: rgba(148, 163, 184, 0.5);
+ background: rgba(248, 250, 252, 0.55);
+ min-height: 86rpx;
+ opacity: 0.72;
+ align-items: center;
+ justify-content: center;
+}
+
+.key-card-content {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 6rpx;
+ min-width: 0;
+}
+
+.reserve-card-empty {
+ width: 100%;
+ min-height: 86rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.reserve-card-text {
+ font-size: 24rpx;
+ color: #94a3b8;
+ letter-spacing: 2rpx;
+}
+
+.inline-key-label {
+ min-width: 70rpx;
+ font-size: 22rpx;
+ font-weight: 700;
+ color: #0f172a;
+ white-space: nowrap;
+}
+
+.inline-send-btn {
+ min-width: 104rpx;
+}
+
+.dialog-btn {
+ min-width: 140rpx;
+}
+
+.log-inline-head {
+ align-items: center;
+}
+
+.log-toolbar-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10rpx;
+ margin-bottom: 8rpx;
+}
+
+.log-toolbar-title-spacer {
+ flex: 1;
+ min-width: 0;
+}
+
+.right-actions {
+ margin-left: auto;
+}
+
+.toolbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+}
+
+.log-scroll {
+ flex: 1 1 0;
+ min-height: 0;
+ max-height: none;
+ background: #fdfdfd;
+ border: 1rpx solid #e5e9f2;
+ border-radius: 12rpx;
+ padding: 12rpx;
+ box-sizing: border-box;
+}
+
+.log-item.simple-log {
+ display: flex;
+ align-items: flex-start;
+ gap: 8rpx;
+ padding: 0 0 8rpx 0;
+ border-bottom: none;
+ border-left: none;
+}
+
+.log-item.simple-log:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.log-time {
+ color: #9aa0a6;
+ font-size: 22rpx;
+ font-family: inherit;
+ white-space: nowrap;
+ min-width: 78rpx;
+}
+
+.log-dir {
+ font-size: 22rpx;
+ font-family: inherit;
+ font-weight: 700;
+ white-space: nowrap;
+ min-width: 52rpx;
+}
+
+.log-dir.tx {
+ color: #60a5fa;
+}
+
+.log-dir.rx {
+ color: #34d399;
+}
+
+.log-dir.warn {
+ color: #fbbf24;
+}
+
+.log-dir.err {
+ color: #f87171;
+}
+
+.log-dir.ui,
+.log-dir.cfg {
+ color: #c4b5fd;
+}
+
+.log-text {
+ flex: 1;
+ min-width: 0;
+ color: #333333;
+ font-size: 24rpx;
+ line-height: 1.35;
+ word-break: break-all;
+ font-family: inherit;
+}
+
+.log-text.packet {
+ color: #333333;
+ font-size: 24rpx;
+ line-height: 1.45;
+ font-family: inherit;
+ word-break: break-all;
+ padding: 0;
+ border: none;
+ border-radius: 10rpx;
+ background: transparent;
+}
+
+.log-text.plain {
+ color: #333333;
+ font-size: 24rpx;
+ line-height: 1.45;
+ font-family: inherit;
+ word-break: break-all;
+}
+
+.dialog-mask {
+ position: fixed;
+ inset: 0;
+ background: rgba(15, 23, 42, 0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999;
+}
+
+.dialog-card {
+ width: 78%;
+ background: #ffffff;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ box-sizing: border-box;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
+}
+
+.dialog-title {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: #1f2d3d;
+ margin-bottom: 12rpx;
+}
+
+.dialog-content {
+ font-size: 26rpx;
+ color: #475467;
+ line-height: 1.7;
+ margin-bottom: 16rpx;
+ white-space: pre-line;
+}
+
+.dialog-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12rpx;
+}
+
+@media (max-width: 767px) {
+ .page-body {
+ padding: 12rpx;
+ gap: 8rpx;
+ }
+
+ .console-row,
+ .panel-head,
+ .toolbar-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .keep-inline-row {
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
+
+ .keep-inline-head {
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
+
+ .keep-inline-actions {
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
+
+ .keep-inline-row .cell-label {
+ font-size: 20rpx;
+ }
+
+ .keep-inline-row .cell-value,
+ .keep-inline-row .name-input,
+ .keep-inline-row .picker-view,
+ .keep-inline-row .inline-key-label {
+ font-size: 20rpx;
+ }
+
+ .name-action-row .device-name-cell {
+ flex: 1 1 0;
+ min-width: 0;
+ }
+
+ .name-action-row .action-cell {
+ flex: 0 0 140rpx;
+ width: 140rpx;
+ flex-shrink: 0;
+ }
+
+ .status-signal-row .status-cell,
+ .status-signal-row .signal-cell,
+ .firmware-row .firmware-cell,
+ .rename-console-row .input-cell.wide,
+ .rename-console-row .button-cell,
+ .config-action-row .select-cell,
+ .config-action-row .action-cell-inline {
+ flex: 1 1 0;
+ width: auto;
+ min-width: 0;
+ }
+
+ .rename-console-row .input-cell.wide {
+ flex: 1.08;
+ }
+
+ .rename-console-row .button-cell,
+ .config-action-row .action-cell-inline {
+ flex: 0 0 128rpx;
+ width: 128rpx;
+ }
+
+ .firmware-row .firmware-cell {
+ flex: 1.08;
+ }
+
+ .config-action-row .select-cell {
+ flex: 0.78;
+ }
+
+ .config-action-row .action-cell-inline {
+ flex: 0 0 128rpx;
+ width: 128rpx;
+ }
+
+ .firmware-row .read-version-cell {
+ flex: 0 0 112rpx;
+ width: 112rpx;
+ }
+
+ .status-signal-row .status-cell {
+ flex: 0 0 230rpx;
+ }
+
+ .status-signal-row .signal-cell {
+ flex: 1;
+ }
+
+ .keep-inline-row .action-btn,
+ .keep-inline-actions .action-btn {
+ font-size: 24rpx;
+ }
+
+ .name-action-row .action-btn,
+ .rename-console-row .action-btn,
+ .config-action-row .action-btn,
+ .firmware-row .action-btn {
+ padding: 0 6rpx;
+ }
+
+ .firmware-row .read-version-cell .action-btn {
+ padding: 0 4rpx;
+ }
+
+ .keep-inline-head .panel-title {
+ font-size: 30rpx;
+ }
+
+ .read-version-cell {
+ width: 96rpx;
+ }
+
+ .read-version-cell .action-btn {
+ font-size: 24rpx;
+ padding: 0;
+ }
+
+ .keep-inline-actions {
+ gap: 8rpx;
+ }
+
+ .key-grid.strict-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .key-card-inline {
+ min-height: 82rpx;
+ }
+
+ .reserve-card-empty {
+ min-height: 82rpx;
+ }
+
+ .inline-key-label {
+ min-width: 0;
+ }
+
+ .log-item.simple-log {
+ gap: 6rpx;
+ }
+
+ .log-time {
+ min-width: 74rpx;
+ }
+
+ .log-dir {
+ min-width: 50rpx;
+ }
+}
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.js b/pages/basics/BluetoothDebugging/BluetoothDebugging.js
index cd14426..4cc8b98 100644
--- a/pages/basics/BluetoothDebugging/BluetoothDebugging.js
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.js
@@ -2,11 +2,18 @@
const app = getApp()
const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08])
+const DEVICE_EXPIRE_MS = 15000
+const SEARCH_DURATION_MS = 6000
+const SEARCH_RESTART_DELAY_MS = 350
+const RESUME_BACKGROUND_SEARCH_DELAY_MS = 2500
Page({
data: {
ConnectedDevName:"",
- activeTab: 'W13', // 默认选中W13
+ deviceTypeLabels: ['请选择设备类型', '主机', 'W13', 'BLV_RQ'],
+ deviceTypeValues: ['NONE', 'HOST', 'W13', 'BLV_RQ'],
+ deviceTypeIndex: 0,
+ activeTab: 'NONE',
autho: null,
Hotelinfo: {},
deviceList: [],
@@ -14,6 +21,150 @@ Page({
coid:0
},
+ getSignalByRSSI(rssi) {
+ if (typeof rssi !== 'number' || Number.isNaN(rssi)) return 0
+ return Math.max(0, Math.min(100, 100 + rssi))
+ },
+
+ getConnectedDevice() {
+ return (this.data.deviceList || []).find(item => item.connected)
+ },
+
+ markDeviceDisconnected(deviceId, options = {}) {
+ if (!deviceId) return
+ const now = Date.now()
+ const removeIfStale = !!options.removeIfStale
+ const nextList = (this.data.deviceList || [])
+ .map(item => item.id === deviceId
+ ? {
+ ...item,
+ connected: false,
+ RSSI: null,
+ signal: 0,
+ disconnectedAt: now
+ }
+ : item)
+ .filter(item => {
+ if (!removeIfStale) return true
+ if (item.id !== deviceId) return true
+ const lastSeenAt = item.lastSeenAt || 0
+ return now - lastSeenAt <= DEVICE_EXPIRE_MS
+ })
+ const nextData = { deviceList: nextList }
+ if (this.data.currentDeviceId === deviceId) {
+ nextData.currentDeviceId = null
+ }
+ this.setData(nextData)
+ },
+
+ reconcileConnectedDevices(connectedDevices = [], deviceType = this.data.activeTab) {
+ const connectedIds = new Set((connectedDevices || []).map(item => item.deviceId))
+ const now = Date.now()
+ const nextList = (this.data.deviceList || []).map(item => {
+ const name = item.name || item.localName || ''
+ if (!this.matchDeviceType(name, deviceType)) return item
+ if (connectedIds.has(item.id)) return item
+ if (!item.connected) return item
+ return {
+ ...item,
+ connected: false,
+ RSSI: null,
+ signal: 0,
+ disconnectedAt: now
+ }
+ })
+ const hasCurrentConnected = this.data.currentDeviceId && connectedIds.has(this.data.currentDeviceId)
+ this.setData({
+ deviceList: nextList,
+ currentDeviceId: hasCurrentConnected ? this.data.currentDeviceId : null
+ })
+ },
+
+ bindSearchPageBleStateListener() {
+ if (this._bleStateChangeHandler || typeof wx.onBLEConnectionStateChange !== 'function') return
+ this._bleStateChangeHandler = (res) => {
+ if (!res || !res.deviceId) return
+ if (res.connected) {
+ this.refreshConnectedDeviceRSSI(res.deviceId)
+ return
+ }
+ this.markDeviceDisconnected(res.deviceId, { removeIfStale: true })
+ }
+ wx.onBLEConnectionStateChange(this._bleStateChangeHandler)
+ },
+
+ teardownSearchPageBleStateListener() {
+ if (this._bleStateChangeHandler && typeof wx.offBLEConnectionStateChange === 'function') {
+ wx.offBLEConnectionStateChange(this._bleStateChangeHandler)
+ }
+ this._bleStateChangeHandler = null
+ },
+
+ startConnectedDeviceMonitor() {
+ if (this._connectedDeviceMonitorTimer) return
+ this._connectedDeviceMonitorTimer = setInterval(() => {
+ const connectedDevice = this.getConnectedDevice()
+ if (!connectedDevice) return
+ this.refreshConnectedDeviceRSSI(connectedDevice.id)
+ }, 3000)
+ },
+
+ stopConnectedDeviceMonitor() {
+ if (this._connectedDeviceMonitorTimer) {
+ clearInterval(this._connectedDeviceMonitorTimer)
+ this._connectedDeviceMonitorTimer = null
+ }
+ },
+
+ applyDeviceTypeChange(selectedType, index) {
+ const nextList = (this.data.deviceList || []).filter((item) => {
+ const name = item.name || item.localName || ''
+ return this.matchDeviceType(name, selectedType)
+ })
+
+ this.setData({
+ deviceTypeIndex: index,
+ activeTab: selectedType,
+ deviceList: nextList
+ })
+
+ if (this._discoveryStopTimer) {
+ clearTimeout(this._discoveryStopTimer)
+ this._discoveryStopTimer = null
+ }
+ if (this._resumeBackgroundSearchTimer) {
+ clearTimeout(this._resumeBackgroundSearchTimer)
+ this._resumeBackgroundSearchTimer = null
+ }
+ this._searchToken = (this._searchToken || 0) + 1
+ this._pauseBackgroundSearch = true
+ this.stopBluetoothDiscovery()
+ this.syncScanTimerByType(selectedType)
+
+ if (this.isSearchableDeviceType(selectedType)) {
+ this.searchBluetooth({ source: 'selector', restart: true, clearList: false, showLoading: true })
+ }
+ },
+
+ refreshConnectedDeviceRSSI(deviceId) {
+ if (!deviceId || typeof wx.getBLEDeviceRSSI !== 'function') return
+ wx.getBLEDeviceRSSI({
+ deviceId,
+ success: (res) => {
+ const rssi = typeof res.RSSI === 'number' ? res.RSSI : null
+ if (rssi == null) return
+ const signal = this.getSignalByRSSI(rssi)
+ const list = (this.data.deviceList || []).map(item => item.id === deviceId
+ ? { ...item, connected: true, RSSI: rssi, signal, lastSeenAt: Date.now() }
+ : item)
+ this.setData({ deviceList: list })
+ },
+ fail: () => {
+ this.markDeviceDisconnected(deviceId, { removeIfStale: true })
+ }
+ })
+ },
+
// 返回上一页
goBack() {
// 返回前主动断开当前BLE连接,避免连接遗留
@@ -23,32 +174,67 @@ Page({
wx.navigateBack()
},
- // 切换导航选项卡
- switchTab(e) {
- const tab = e.currentTarget.dataset.tab
- const current = this.data.activeTab
- if (tab === current) {
+ markPendingBleNavigation(target, deviceId, session = {}) {
+ if (!app.globalData) return
+ app.globalData.pendingBleNavigation = {
+ target,
+ deviceId: deviceId || '',
+ session: { ...session },
+ keepConnection: true,
+ timestamp: Date.now()
+ }
+ },
+
+ scheduleResumeBackgroundSearch() {
+ if (this._resumeBackgroundSearchTimer) {
+ clearTimeout(this._resumeBackgroundSearchTimer)
+ this._resumeBackgroundSearchTimer = null
+ }
+ this._resumeBackgroundSearchTimer = setTimeout(() => {
+ this._resumeBackgroundSearchTimer = null
+ this._pauseBackgroundSearch = false
+ }, RESUME_BACKGROUND_SEARCH_DELAY_MS)
+ },
+
+ syncSearchLoadingByList(searchToken = this._searchToken) {
+ const hasDevices = Array.isArray(this.data.deviceList) && this.data.deviceList.length > 0
+ if (hasDevices) {
+ this._shouldShowSearching = false
+ if (this._searchToken === searchToken) {
+ wx.hideLoading()
+ }
return
}
- const hasConnected = this.data.deviceList.some(d => d.connected)
- if (hasConnected) {
+ if (this._shouldShowSearching && this._searchToken === searchToken) {
+ wx.showLoading({ title: '搜索中...', mask: true })
+ }
+ },
+
+ onDeviceTypeChange(e) {
+ const index = Number(e.detail.value || 0)
+ const selectedType = this.data.deviceTypeValues[index] || 'NONE'
+ if (selectedType === this.data.activeTab) {
+ return
+ }
+ const connectedDevice = this.getConnectedDevice()
+ if (connectedDevice) {
wx.showModal({
- title: '提示',
- content: '当前有已连接的蓝牙设备,切换将断开并重新搜索,是否继续?',
+ title: '切换设备类型',
+ content: '当前有蓝牙设备处于已连接状态,切换设备类型后将断开该连接,是否继续?',
success: (res) => {
- if (res.confirm) {
- this.disconnectAllDevices()
- this.setData({ activeTab: tab })
- // 切换后立即搜索一次
- this.searchBluetooth()
+ if (!res.confirm) {
+ this.setData({ deviceTypeIndex: this.data.deviceTypeValues.indexOf(this.data.activeTab) })
+ return
}
+ try { this.disconnectCurrentDevice() } catch (e) {}
+ this.applyDeviceTypeChange(selectedType, index)
}
})
- } else {
- this.setData({ activeTab: tab })
- this.searchBluetooth()
+ return
}
+
+ this.applyDeviceTypeChange(selectedType, index)
},
disconnectAllDevices() {
@@ -88,21 +274,52 @@ Page({
},
// 搜索蓝牙设备
- searchBluetooth() {
- const filterPrefix = this.data.activeTab
+ searchBluetooth(options = {}) {
+ const deviceType = this.data.activeTab
+ const source = options.source || 'manual'
+ const restart = options.restart !== false
+ const clearList = !!options.clearList
+ const showLoading = !!options.showLoading
+ if (!this.isSearchableDeviceType(deviceType)) {
+ if (deviceType === 'HOST') {
+ wx.showToast({ title: '该设备暂未发布', icon: 'none' })
+ } else {
+ wx.showToast({ title: '未选择设备类型', icon: 'none' })
+ }
+ return
+ }
+ if (clearList) {
+ this.setData({ deviceList: [] })
+ }
- // 先断开当前连接设备(如果有)
- // this.disconnectCurrentDevice()
+ if (source !== 'timer') {
+ this._manualSearchInProgress = true
+ this._pauseBackgroundSearch = true
+ if (this._resumeBackgroundSearchTimer) {
+ clearTimeout(this._resumeBackgroundSearchTimer)
+ this._resumeBackgroundSearchTimer = null
+ }
+ }
- // // 清空旧列表并启动搜索
- // this.setData({ deviceList: [] })
+ this._shouldShowSearching = showLoading
+ this._hasFoundDeviceInCurrentSearch = false
+ this._searchToken = (this._searchToken || 0) + 1
+ const currentSearchToken = this._searchToken
+ this.syncSearchLoadingByList(currentSearchToken)
this.ensureBluetoothReady()
- .then(() => this.startBluetoothDevicesDiscovery(filterPrefix))
+ .then(() => this.startBluetoothDevicesDiscovery(deviceType, { restart, source, searchToken: currentSearchToken }))
.catch((err) => {
+ this._shouldShowSearching = false
+ if (source !== 'timer') {
+ this._manualSearchInProgress = false
+ this.scheduleResumeBackgroundSearch()
+ }
console.error('蓝牙初始化失败', err)
- wx.hideLoading()
+ if (this._searchToken === currentSearchToken) {
+ wx.hideLoading()
+ }
wx.showToast({ title: '请开启蓝牙和定位权限', icon: 'none' })
})
},
@@ -135,10 +352,18 @@ Page({
})
},
- startBluetoothDevicesDiscovery(prefix) {
+ startBluetoothDevicesDiscovery(deviceType, options = {}) {
+ const restart = options.restart !== false
+ const source = options.source || 'manual'
+ const searchToken = options.searchToken || 0
// 先取消旧的发现监听,避免多次注册造成干扰
this.teardownDeviceFoundListener()
+ if (this._discoveryStopTimer) {
+ clearTimeout(this._discoveryStopTimer)
+ this._discoveryStopTimer = null
+ }
this._foundCount = 0
+ this._scanDeviceType = deviceType
const now = Date.now()
// 防护:避免短时间内频繁触发扫描(系统限制)
if (this._lastScanAt && now - this._lastScanAt < 2000) {
@@ -146,33 +371,58 @@ Page({
return
}
this._lastScanAt = now
- console.info('[BLE] start scan, prefix:', prefix || 'ALL')
+ console.info('[BLE] start scan, deviceType:', deviceType || 'NONE')
const doStart = () => {
+ const beginDiscovery = () => {
+ wx.startBluetoothDevicesDiscovery({
+ allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
+ success: () => {
+ this._isDiscoveringSearch = true
+ this.setupDeviceFoundListener(deviceType)
+ // 定时停止,避免长时间占用
+ this._discoveryStopTimer = setTimeout(() => {
+ this._discoveryStopTimer = null
+ this.stopBluetoothDiscovery()
+ }, SEARCH_DURATION_MS)
+ },
+ fail: (err) => {
+ console.error('开始搜索蓝牙设备失败', err)
+ // 若为系统提示搜索过于频繁,可稍后重试一次
+ const code = err && (err.errCode || (err.errMsg && Number((err.errMsg.match(/\d+/)||[])[0])))
+ const message = (err && (err.errMsg || err.message) || '').toLowerCase()
+ if (code === 10008) {
+ try { this.appendLog && this.appendLog('WARN', 'startBluetoothDevicesDiscovery failed: scanning too frequently, retrying shortly') } catch (e) {}
+ setTimeout(() => { try { doStart() } catch (e) {} }, 1500)
+ return
+ }
+ if ((code === 1509008 || message.indexOf('location permission is denied') >= 0) && restart) {
+ setTimeout(() => { try { beginDiscovery() } catch (e) {} }, 800)
+ return
+ }
+ if (source !== 'timer') {
+ this._manualSearchInProgress = false
+ this.scheduleResumeBackgroundSearch()
+ }
+ this._shouldShowSearching = false
+ if (this._searchToken === searchToken) {
+ wx.hideLoading()
+ }
+ }
+ })
+ }
+
+ if (!restart) {
+ beginDiscovery()
+ return
+ }
+
// 先停止可能已有的搜索,待停止完成后再启动,避免竞态
wx.stopBluetoothDevicesDiscovery({
complete: () => {
- wx.startBluetoothDevicesDiscovery({
- allowDuplicatesKey: true, // 允许重复上报,提升二次搜索的发现率
- success: () => {
- this.setupDeviceFoundListener(prefix)
- // 定时停止,避免长时间占用
- setTimeout(() => {
- this.stopBluetoothDiscovery()
- }, 6000)
- },
- fail: (err) => {
- console.error('开始搜索蓝牙设备失败', err)
- // 若为系统提示搜索过于频繁,可稍后重试一次
- const code = err && (err.errCode || (err.errMsg && Number((err.errMsg.match(/\d+/)||[])[0])))
- if (code === 10008) {
- try { this.appendLog && this.appendLog('WARN', 'startBluetoothDevicesDiscovery failed: scanning too frequently, retrying shortly') } catch (e) {}
- setTimeout(() => { try { doStart() } catch (e) {} }, 1500)
- return
- }
- wx.hideLoading()
- }
- })
+ setTimeout(() => {
+ beginDiscovery()
+ }, SEARCH_RESTART_DELAY_MS)
}
})
}
@@ -183,8 +433,7 @@ Page({
success: (res) => {
if (res && res.discovering) {
try { this.appendLog && this.appendLog('CFG', 'adapter already discovering, attach listener') } catch (e) {}
- this.setupDeviceFoundListener(prefix)
- wx.hideLoading()
+ this.setupDeviceFoundListener(deviceType)
return
}
doStart()
@@ -198,11 +447,15 @@ Page({
}
},
- setupDeviceFoundListener(prefix) {
+ setupDeviceFoundListener(deviceType) {
this._deviceFoundHandler = (res) => {
const devices = (res && res.devices) || []
+ if (devices.length && this._shouldShowSearching && !this._hasFoundDeviceInCurrentSearch) {
+ this._hasFoundDeviceInCurrentSearch = true
+ this.scheduleResumeBackgroundSearch()
+ }
if (devices.length) this._foundCount = (this._foundCount || 0) + devices.length
- this.handleDeviceFound(devices, prefix)
+ this.handleDeviceFound(devices, deviceType)
}
if (typeof wx.onBluetoothDeviceFound === 'function') {
wx.onBluetoothDeviceFound(this._deviceFoundHandler)
@@ -222,8 +475,9 @@ Page({
try { this.appendLog && this.appendLog('CFG', 'startScanTimer') } catch (e) {}
this._scanTimer = setInterval(() => {
try {
+ if (this._manualSearchInProgress || this._pauseBackgroundSearch) return
// 触发一次搜索,内部有防抖保护
- this.searchBluetooth()
+ this.searchBluetooth({ source: 'timer', restart: false, clearList: false, showLoading: false })
} catch (e) { /* ignore */ }
}, 3000)
},
@@ -236,51 +490,111 @@ Page({
}
},
- handleDeviceFound(devices, prefix) {
+ syncScanTimerByType(deviceType) {
+ const type = deviceType || this.data.activeTab
+ if (this.isSearchableDeviceType(type)) {
+ this.startScanTimer()
+ } else {
+ this.stopScanTimer()
+ }
+ },
+
+ isSearchableDeviceType(deviceType) {
+ return deviceType === 'W13' || deviceType === 'BLV_RQ'
+ },
+
+ matchDeviceType(name, deviceType) {
+ const value = (name || '').trim()
+ if (!value) return false
+ if (deviceType === 'W13') {
+ return /^BLV_(W13|C13)/i.test(value)
+ }
+ if (deviceType === 'BLV_RQ') {
+ return /^BLV_RQ/i.test(value)
+ }
+ return false
+ },
+
+ handleDeviceFound(devices, deviceType) {
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)
+ const matched = this.matchDeviceType(name, deviceType || this.data.activeTab)
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 oldItem = existsIndex >= 0 ? list[existsIndex] : null
+ const hasFreshRSSI = typeof dev.RSSI === 'number' && dev.RSSI !== 0
+ const rssi = hasFreshRSSI ? dev.RSSI : (oldItem && typeof oldItem.RSSI === 'number' ? oldItem.RSSI : (typeof dev.RSSI === 'number' ? dev.RSSI : null))
+ const signal = this.getSignalByRSSI(rssi)
const mapped = {
id: dev.deviceId,
name,
mac: dev.deviceId,
signal,
- connected: false,
+ connected: oldItem ? !!oldItem.connected : false,
RSSI: rssi,
localName: dev.localName || '',
- serviceUUIDs: dev.serviceUUIDs || []
+ serviceUUIDs: dev.serviceUUIDs || [],
+ lastSeenAt: Date.now()
}
if (existsIndex >= 0) {
// 设备已存在时仅更新信号值与 RSSI,避免覆盖其它已保存字段
list[existsIndex] = {
...list[existsIndex],
+ name,
+ mac: dev.deviceId,
signal,
- RSSI: rssi
+ connected: typeof list[existsIndex].connected === 'boolean' ? list[existsIndex].connected : false,
+ RSSI: rssi,
+ localName: dev.localName || list[existsIndex].localName || '',
+ serviceUUIDs: dev.serviceUUIDs || list[existsIndex].serviceUUIDs || [],
+ lastSeenAt: Date.now()
}
} else {
list.push(mapped)
+ console.log('[BluetoothDebugging] 新增设备', {
+ name: mapped.name,
+ mac: mapped.mac,
+ serviceUUIDs: mapped.serviceUUIDs
+ })
}
})
this.setData({ deviceList: list })
+ this.syncSearchLoadingByList()
},
// 停止蓝牙搜索
stopBluetoothDiscovery() {
+ const stoppingToken = this._searchToken || 0
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
+ this._isDiscoveringSearch = false
+ this._manualSearchInProgress = false
+ this.scheduleResumeBackgroundSearch()
+ this._shouldShowSearching = false
+ if (this._searchToken === stoppingToken) {
+ wx.hideLoading()
+ }
+ const scanType = this._scanDeviceType || this.data.activeTab
+ const now = Date.now()
+ const nextList = (this.data.deviceList || []).filter((item) => {
+ const name = item.name || item.localName || ''
+ const isCurrentType = this.matchDeviceType(name, scanType)
+ if (!isCurrentType) return true
+ if (item.connected) {
+ return item.id === this.data.currentDeviceId && now - (item.lastSeenAt || 0) <= DEVICE_EXPIRE_MS
+ }
+ const lastSeenAt = item.lastSeenAt || 0
+ return now - lastSeenAt <= DEVICE_EXPIRE_MS
+ })
+ this.setData({ deviceList: nextList })
+ this.syncSearchLoadingByList(stoppingToken)
+ this._scanDeviceType = null
// 取消自动显示搜索完成提示,避免打扰
}
})
@@ -291,6 +605,16 @@ Page({
// this.teardownDeviceFoundListener()
// 页面卸载时停止定时扫描
try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
+ if (this._discoveryStopTimer) {
+ clearTimeout(this._discoveryStopTimer)
+ this._discoveryStopTimer = null
+ }
+ if (this._resumeBackgroundSearchTimer) {
+ clearTimeout(this._resumeBackgroundSearchTimer)
+ this._resumeBackgroundSearchTimer = null
+ }
+ this._pauseBackgroundSearch = false
+ this._manualSearchInProgress = false
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
wx.stopBluetoothDevicesDiscovery({ complete: () => {} })
}
@@ -298,6 +622,8 @@ Page({
clearInterval(this._fixedLoopTimer)
this._fixedLoopTimer = null
}
+ this.stopConnectedDeviceMonitor()
+ this.teardownSearchPageBleStateListener()
},
// 连接设备
@@ -324,10 +650,21 @@ Page({
: base
console.log('navigateTo:', withParams)
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
+ this.markPendingBleNavigation('B13', mac, { devName, serviceId: svc, txCharId: tx, rxCharId: rx })
+ wx.navigateTo({ url: withParams })
+ } else if (this.data.activeTab === 'BLV_RQ') {
+ const devName = device.name || 'BLV_RQ设备'
+ 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 || ''
+ const base = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(mac)}&connected=1`
+ const withParams = (svc && tx && rx)
+ ? `${base}&serviceId=${encodeURIComponent(svc)}&txCharId=${encodeURIComponent(tx)}&rxCharId=${encodeURIComponent(rx)}`
+ : base
+ try { this._navigatingToBLVRQ = true } catch (e) { /* ignore */ }
+ this.markPendingBleNavigation('BLV_RQ', mac, { devName, serviceId: svc, txCharId: tx, rxCharId: rx })
wx.navigateTo({ url: withParams })
-
-
-
} else {
wx.showToast({ title: '已连接当前设备', icon: 'none' })
}
@@ -376,6 +713,7 @@ Page({
success: () => {
const list = this.data.deviceList.map((d) => ({ ...d, connected: d.id === device.id }))
this.setData({ deviceList: list, currentDeviceId: device.id })
+ this.startConnectedDeviceMonitor()
// 设置MTU为256,提升传输效率(若支持)
if (typeof wx.setBLEMTU === 'function') {
wx.setBLEMTU({
@@ -386,7 +724,17 @@ Page({
})
}
// 连接成功后发现服务与特征
- this.discoverBleChannels(device)
+ if (this.data.activeTab === 'BLV_RQ') {
+ this.markPendingBleNavigation('BLV_RQ', device.id, {
+ devName: device.name || 'BLV_RQ设备',
+ serviceId: this.data.currentServiceId,
+ txCharId: this.data.currentTxCharId,
+ rxCharId: this.data.currentRxCharId
+ })
+ this.discoverBLVRQChannels(device)
+ } else {
+ this.discoverBleChannels(device)
+ }
},
fail: (err) => {
wx.hideLoading()
@@ -397,6 +745,7 @@ Page({
try { this.appendLog && this.appendLog('CFG', 'createBLEConnection: already connected, treating as connected') } catch (e) {}
const list = this.data.deviceList.map((d) => ({ ...d, connected: d.id === device.id }))
this.setData({ deviceList: list, currentDeviceId: device.id })
+ this.startConnectedDeviceMonitor()
// 继续发现服务与特征以恢复页面状态
try { this.discoverBleChannels(device) } catch (e) {}
return
@@ -468,6 +817,11 @@ Page({
try { this._navigatingToB13 = true } catch (e) { /* ignore */ }
wx.navigateTo({ url })
// this.sendFixedCommand(deviceId, serviceId, ffe1.uuid)
+ } else if (this.data.activeTab === 'BLV_RQ') {
+ const devName = device.name || 'BLV_RQ设备'
+ const url = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&connected=1&serviceId=${encodeURIComponent(serviceId)}&txCharId=${encodeURIComponent(ffe1.uuid)}&rxCharId=${encodeURIComponent(ffe2.uuid)}`
+ try { this._navigatingToBLVRQ = true } catch (e) { /* ignore */ }
+ wx.navigateTo({ url })
}
}
})
@@ -494,6 +848,91 @@ Page({
})
},
+ discoverBLVRQChannels(device) {
+ const deviceId = device.id
+ wx.getBLEDeviceServices({
+ deviceId,
+ success: (srvRes) => {
+ const services = srvRes.services || []
+ const targetService = services.find((s) => {
+ const uuid = String(s.uuid || '').toLowerCase()
+ return uuid.startsWith('0000fff0')
+ })
+
+ if (!targetService) {
+ wx.hideLoading()
+ wx.showModal({ title: '提示', content: '未找到FFF0目标服务', showCancel: false })
+ return
+ }
+
+ wx.getBLEDeviceCharacteristics({
+ deviceId,
+ serviceId: targetService.uuid,
+ success: (chRes) => {
+ const chars = chRes.characteristics || []
+ const fff1 = chars.find(c => this._matchUuid(c.uuid, 'FFF1'))
+ const fff2 = chars.find(c => this._matchUuid(c.uuid, 'FFF2'))
+ const fff3 = chars.find(c => this._matchUuid(c.uuid, 'FFF3'))
+
+ if (!fff1 || !fff2 || !fff3) {
+ wx.hideLoading()
+ wx.showModal({ title: '提示', content: '未找到FFF1/FFF2/FFF3完整特征', showCancel: false })
+ return
+ }
+
+ const notifyTargets = [fff1, fff2].filter(c => c.properties && c.properties.notify)
+ let pending = notifyTargets.length
+ const finalize = () => {
+ wx.hideLoading()
+ wx.showToast({ title: '连接成功', icon: 'success' })
+ const devName = device.name || 'BLV_RQ设备'
+ this.setData({
+ currentServiceId: targetService.uuid,
+ currentTxCharId: fff1.uuid,
+ currentRxCharId: fff2.uuid,
+ currentReadCharId: fff2.uuid,
+ currentWriteCharId: fff1.uuid
+ })
+ const url = `/pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage?DevName=${encodeURIComponent(devName)}&mac=${encodeURIComponent(deviceId)}&connected=1&serviceId=${encodeURIComponent(targetService.uuid)}&txCharId=${encodeURIComponent(fff1.uuid)}&rxCharId=${encodeURIComponent(fff2.uuid)}&readCharId=${encodeURIComponent(fff2.uuid)}&writeCharId=${encodeURIComponent(fff1.uuid)}`
+ this.markPendingBleNavigation('BLV_RQ', deviceId, { devName, serviceId: targetService.uuid, txCharId: fff1.uuid, rxCharId: fff2.uuid })
+ wx.navigateTo({ url })
+ }
+
+ if (!pending) {
+ finalize()
+ return
+ }
+
+ notifyTargets.forEach((charItem) => {
+ wx.notifyBLECharacteristicValueChange({
+ state: true,
+ deviceId,
+ serviceId: targetService.uuid,
+ characteristicId: charItem.uuid,
+ complete: () => {
+ pending -= 1
+ if (pending <= 0) {
+ finalize()
+ }
+ }
+ })
+ })
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ console.error('获取BLV_RQ特征失败', err)
+ wx.showToast({ title: '获取特征失败', icon: 'none' })
+ }
+ })
+ },
+ fail: (err) => {
+ wx.hideLoading()
+ console.error('获取BLV_RQ服务失败', err)
+ wx.showToast({ title: '获取服务失败', icon: 'none' })
+ }
+ })
+ },
+
_matchUuid(uuid, needle) {
if (!uuid || !needle) return false
const u = String(uuid).replace(/-/g, '').toUpperCase()
@@ -572,7 +1011,7 @@ Page({
const idx = this.data.deviceList.findIndex(d => d.connected)
if (idx >= 0) {
// 标记断开状态
- const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false }))
+ const list = this.data.deviceList.map((d, i) => ({ ...d, connected: false, RSSI: null, signal: 0 }))
this.setData({ deviceList: list })
}
// 如果保留了设备ID,尝试调用系统断开
@@ -585,6 +1024,7 @@ Page({
}
this.setData({ currentDeviceId: null })
}
+ this.stopConnectedDeviceMonitor()
},
onLoad() {
@@ -613,10 +1053,12 @@ Page({
wx.setNavigationBarTitle({ title: '蓝牙调试' })
}
- // 页面加载时,根据当前选中的选项卡加载设备
- this.loadDevicesByTab(this.data.activeTab)
- // 同步执行一次蓝牙搜索(按 W13 过滤规则)
- this.searchBluetooth()
+ this.setData({
+ deviceTypeIndex: 0,
+ activeTab: 'NONE',
+ deviceList: []
+ })
+ this.syncScanTimerByType('NONE')
},
onShow() {
@@ -625,60 +1067,85 @@ Page({
// 已从 B13 返回:清理导航标记但仍继续执行恢复流程,确保已连接设备状态可被恢复并展示
this._navigatingToB13 = false
}
+ if (this._navigatingToBLVRQ) {
+ this._navigatingToBLVRQ = false
+ }
- try { this.appendLog && this.appendLog('CFG', 'onShow: resume, ensuring adapter and forcing discovery') } catch (e) {}
- // 息屏唤醒后可能适配器被关闭,先确保打开后短延迟再搜索一次
+ const deviceType = this.data.activeTab
+ this.bindSearchPageBleStateListener()
+ this.syncScanTimerByType(deviceType)
+ this.startConnectedDeviceMonitor()
+
+ if (!this.isSearchableDeviceType(deviceType)) {
+ try { this.appendLog && this.appendLog('CFG', 'onShow: no searchable device type, skip discovery') } catch (e) {}
+ return
+ }
+
+ try { this.appendLog && this.appendLog('CFG', 'onShow: resume adapter and restore devices without auto scan') } catch (e) {}
this.ensureBluetoothReady()
.then(() => {
- setTimeout(() => {
- try {
- this.startBluetoothDevicesDiscovery(this.data.activeTab)
- } catch (e) { /* ignore */ }
- }, 300)
-
- // 尝试获取系统已连接设备,优先恢复之前连接的设备显示(防止已连接但不广播的设备无法被扫描到)
try {
const svc = this.data.currentServiceId
- const isW13 = this.data.activeTab === 'W13'
- const prefix = this.data.activeTab
- const matchByTab = (name) => {
- const n = name || ''
- if (isW13) return /^BLV_(W13|C13)_.+$/i.test(n)
- return prefix ? n.startsWith(prefix) : true
- }
+ const matchByType = (name) => this.matchDeviceType(name, deviceType)
if (typeof wx.getConnectedBluetoothDevices === 'function' && svc) {
wx.getConnectedBluetoothDevices({ services: [svc], success: (res) => {
const devices = (res && res.devices) || []
+ this.reconcileConnectedDevices(devices, deviceType)
if (devices.length) {
- // 将已连接设备合并到列表并按过滤规则筛选
const list = [...this.data.deviceList]
devices.forEach(d => {
-
const name = d.name || d.localName || ''
- if (!matchByTab(name)) return
+ if (!matchByType(name)) return
const idx = list.findIndex(x => x.id === d.deviceId)
- const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: true, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
+ const prev = idx >= 0 ? list[idx] : null
+ const hasSystemRSSI = typeof d.RSSI === 'number' && d.RSSI !== 0
+ const rssi = hasSystemRSSI ? d.RSSI : (prev && typeof prev.RSSI === 'number' ? prev.RSSI : null)
+ const mapped = {
+ id: d.deviceId,
+ name,
+ mac: d.deviceId,
+ connected: true,
+ RSSI: rssi,
+ signal: this.getSignalByRSSI(rssi),
+ serviceUUIDs: d.serviceUUIDs || []
+ }
+ mapped.lastSeenAt = Date.now()
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
else list.unshift(mapped)
+ this.refreshConnectedDeviceRSSI(d.deviceId)
})
this.setData({ deviceList: list })
try { this.appendLog && this.appendLog('CFG', 'restored connected devices from system') } catch (e) {}
}
}})
} else if (typeof wx.getBluetoothDevices === 'function') {
- // 作为兜底,查询最近缓存的设备并按过滤规则合并
wx.getBluetoothDevices({ success: (res) => {
const devices = (res && res.devices) || []
+ const connectedDevices = devices.filter(d => !!d.connected)
+ this.reconcileConnectedDevices(connectedDevices, deviceType)
if (devices.length) {
const list = [...this.data.deviceList]
devices.forEach(d => {
const name = d.name || d.localName || ''
- if (!matchByTab(name)) return
+ if (!matchByType(name)) return
const idx = list.findIndex(x => x.id === d.deviceId)
- const mapped = { id: d.deviceId, name, mac: d.deviceId, connected: !!d.connected, RSSI: d.RSSI || 0, serviceUUIDs: d.serviceUUIDs || [] }
+ const prev = idx >= 0 ? list[idx] : null
+ const hasSystemRSSI = typeof d.RSSI === 'number' && d.RSSI !== 0
+ const rssi = hasSystemRSSI ? d.RSSI : (prev && typeof prev.RSSI === 'number' ? prev.RSSI : null)
+ const mapped = {
+ id: d.deviceId,
+ name,
+ mac: d.deviceId,
+ connected: !!d.connected,
+ RSSI: rssi,
+ signal: this.getSignalByRSSI(rssi),
+ serviceUUIDs: d.serviceUUIDs || []
+ }
+ mapped.lastSeenAt = Date.now()
if (idx >= 0) list[idx] = { ...list[idx], ...mapped }
else list.push(mapped)
+ if (mapped.connected) this.refreshConnectedDeviceRSSI(d.deviceId)
})
this.setData({ deviceList: list })
}
@@ -689,42 +1156,43 @@ Page({
.catch((err) => {
try { this.appendLog && this.appendLog('WARN', 'onShow ensureBluetoothReady failed') } catch (e) {}
})
- // 进入页面时启动定时扫描(每3秒一次)
- try { this.startScanTimer && this.startScanTimer() } catch (e) {}
},
onHide() {
- // 如果正在导航到 B13 页面,保留连接会话,但应停止扫描/发现以节省资源
- if (this._navigatingToB13) {
- try { this.appendLog && this.appendLog('CFG', 'onHide during navigation to B13: stop scan but keep connection') } catch (e) {}
- // 停止定时扫描
- try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
- // 停止发现(但不断开已连接设备),随后直接返回以保留连接状态
- try {
- if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
- wx.stopBluetoothDevicesDiscovery({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'stopBluetoothDevicesDiscovery onHide (navigating to B13)') } catch (e) {} } })
- }
- } catch (e) { /* ignore */ }
- return
- }
-
- // 非导航到 B13 的离开:停止定时扫描并断开连接、关闭适配器以重置状态
+ this.stopConnectedDeviceMonitor()
try { this.stopScanTimer && this.stopScanTimer() } catch (e) {}
-
+ if (this._discoveryStopTimer) {
+ clearTimeout(this._discoveryStopTimer)
+ this._discoveryStopTimer = null
+ }
+ if (this._resumeBackgroundSearchTimer) {
+ clearTimeout(this._resumeBackgroundSearchTimer)
+ this._resumeBackgroundSearchTimer = null
+ }
+ this._pauseBackgroundSearch = false
+ this._manualSearchInProgress = false
try {
- // 停止发现,避免后台扫描
if (typeof wx.stopBluetoothDevicesDiscovery === 'function') {
wx.stopBluetoothDevicesDiscovery({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'stopBluetoothDevicesDiscovery onHide') } catch (e) {} } })
}
} catch (e) { /* ignore */ }
+ const pendingBleNavigation = app.globalData && app.globalData.pendingBleNavigation
+ const keepBleSession = this._navigatingToB13 || this._navigatingToBLVRQ || (
+ pendingBleNavigation &&
+ pendingBleNavigation.keepConnection &&
+ Date.now() - (pendingBleNavigation.timestamp || 0) < 5000
+ )
+ if (keepBleSession) {
+ try { this.appendLog && this.appendLog('CFG', 'onHide during navigation: keep connection and skip disconnect') } catch (e) {}
+ return
+ }
+
try {
- // 断开当前连接(如果有)
this.disconnectCurrentDevice()
} catch (e) { /* ignore */ }
try {
- // 关闭蓝牙适配器以重置底层状态(部分 Android 机型息屏后需要此步骤)
if (typeof wx.closeBluetoothAdapter === 'function') {
wx.closeBluetoothAdapter({ complete: () => { try { this.appendLog && this.appendLog('CFG', 'closeBluetoothAdapter called onHide') } catch (e) {} } })
}
diff --git a/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml
index c2105c4..d31dfd8 100644
--- a/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml
+++ b/pages/basics/BluetoothDebugging/BluetoothDebugging.wxml
@@ -7,25 +7,13 @@