feat: 添加蓝牙调试页面的初始实现

- 新增 BLVRQPage.json 配置文件,设置导航栏样式。
- 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。
- 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
This commit is contained in:
2026-03-20 18:18:49 +08:00
parent 5010b1bdbb
commit f94cf21e7a
13 changed files with 3127 additions and 155 deletions

View File

@@ -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允许继续**
- 继续时采纳建议:**未配置映射项保持设备当前返回值不变**
- 页面整体不滚动,仅日志区允许内部滚动
该文档可作为后续代码实现的直接依据。

View File

@@ -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 | 有效名称字节长度 N1~12 |
| P1~P(N) | 名称字符ASCII |
> 默认名称:`BLV_RQ_XXXXXXXX`,最多支持 8 字节自定义名称。
**MCU → PC**
| 字段 | 说明 |
| --- | --- |
| Frame_Type | `0x05` |
| P0 | `0x01`:设置成功<br>`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`:设置成功<br>`0x02`:设置失败 |
---
### 3.5 模拟按键触发0x06
**PC → MCU**
| 字段 | 说明 |
| --- | --- |
| Frame_Type | `0x06` |
| P0 | 模拟按键数量 K |
| P1 | 按键状态位图Bit0~Bit7<br>`Bit0=1`键值1按下<br>`Bit1=1`键值2按下<br>`...`<br>`Bit7=1`键值8按下 |
> P0 的值应与 P1 中置 1 的位数一致。
**MCU → PC**
| 字段 | 说明 |
| --- | --- |
| Frame_Type | `0x06` |
| P0 | `0x01`:成功<br>`0x02`:失败 |
---
## 四、关键概念
- **硬件按键**:设备上固定的物理按钮(位置 1~8
- **映射键值**物理按键实际触发的逻辑按键编号1~8映射可配置。
### 功能与读取配置的关系
- 设置按键功能Command `0x02`)作用于 **硬件按键位置**
- `P0` 为当前实际配置的硬件按键数量。
- 读取配置Command `0x04`)返回每个硬件位置当前的映射键值及其功能。
### 其它说明
- 参数区PARA最大 14 字节,需确保单包数据不超限。
### 默认状态
- 蓝牙名称:`BLV_RQ_XXXXXXXX`
- 按键功能默认为“普通按键”0x01
- 按键映射:默认硬件位置 n 映射到键值 n1 对 1 映射)

1
app.js
View File

@@ -45,6 +45,7 @@ App({
CreateTime:null,
HotelCode:null,
CreatDate:null,
pendingBleNavigation: null,
},
toast:function(type,title,success,time){
if(type==1){

View File

@@ -2,7 +2,10 @@
"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",

View File

@@ -184,6 +184,7 @@ export async function GetRoomType(params){
}
//获取房型下的房间
export async function GetRoomTypeNode(params){
params.token = gettoken();
console.log(params)
return await reqeust1({

View File

@@ -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)
}
})
}
})

View File

@@ -0,0 +1,8 @@
{
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom",
"usingComponents": {
"cu-custom": "/colorui/components/cu-custom"
}
}

View File

@@ -0,0 +1,129 @@
<view class="container">
<cu-custom bgColor="bg-gradual-blue" isBack="true">
<view slot="content">{{bleInfo.devName || 'BLV_RQ设备'}}</view>
</cu-custom>
<view class="page-body content">
<view class="panel ble-panel device-row">
<view class="panel-head inline-head blank-head"></view>
<view class="console-row name-action-row keep-inline-row">
<view class="console-cell device-name-cell">
<text class="device-icon">🔵</text>
<text class="cell-label">蓝牙名称:</text>
<text class="cell-value">{{bleInfo.devName || '-'}}</text>
</view>
<view class="console-cell action-cell">
<button class="action-btn {{bleInfo.connected ? 'warn' : 'primary'}} full-btn" bindtap="onConnectionAction" disabled="{{isConnecting || !bleInfo.mac}}">{{isConnecting ? '连接中...' : (bleInfo.connected ? '断开连接' : '重新连接')}}</button>
</view>
</view>
<view class="console-row status-signal-row keep-inline-row">
<view class="console-cell status-cell">
<text class="cell-label">连接状态:</text>
<view class="status-chip {{isConnecting ? 'pending' : (bleInfo.connected ? 'online' : 'offline')}}">{{isConnecting ? '连接中' : (bleInfo.connected ? '已连接' : '未连接')}}</view>
</view>
<view class="console-cell signal-cell">
<text class="cell-label">信号强度:</text>
<text class="cell-value">{{bleInfo.signalText || '-'}}</text>
</view>
</view>
<view class="console-row mac-row">
<view class="console-cell full-line">
<text class="cell-label">MAC地址</text>
<text class="cell-value">{{bleInfo.mac || '-'}}</text>
</view>
</view>
<view class="console-row rename-console-row keep-inline-row">
<view class="console-cell input-cell wide">
<text class="cell-label">蓝牙名称:</text>
<input class="name-input {{bleInfo.connected ? '' : 'is-disabled'}}" placeholder="输入1-8位英文或数字" maxlength="8" value="{{bleNameInput}}" bindinput="onBleNameInput" disabled="{{!bleInfo.connected || settingBleName}}" />
</view>
<view class="console-cell button-cell">
<button class="action-btn primary full-btn" bindtap="onSetBleName" loading="{{settingBleName}}" disabled="{{!canSetBleName || settingBleName}}">设置名称</button>
</view>
</view>
</view>
<view class="panel device-panel cfg-card">
<view class="panel-head inline-head blank-head"></view>
<view class="console-row firmware-row keep-inline-row">
<view class="console-cell firmware-cell software-version-cell">
<text class="cell-label">软件版本:</text>
<text class="cell-value">{{deviceInfo.softwareVersion}}</text>
</view>
<view class="console-cell firmware-cell hardware-version-cell">
<text class="cell-label">硬件版本:</text>
<text class="cell-value">{{deviceInfo.hardwareVersion}}</text>
</view>
<view class="console-cell action-cell-inline read-version-cell">
<button class="action-btn default full-btn" bindtap="onReadVersion" loading="{{readingVersion}}" disabled="{{!bleInfo.connected || readingVersion || readingKeyConfig || writingKeyConfig}}">读取版本</button>
</view>
</view>
<view class="console-row config-action-row keep-inline-row">
<view class="console-cell select-cell key-type-cell">
<text class="cell-label">按键类型:</text>
<picker class="count-picker" mode="selector" range="{{keyConfig.keyCountOptions}}" value="{{keyConfig.selectedKeyTypeIndex}}" bindchange="onKeyCountChange" disabled="{{!bleInfo.connected || writingKeyConfig}}">
<view class="picker-view {{bleInfo.connected ? '' : 'is-disabled'}}">{{keyConfig.keyCountOptions[keyConfig.selectedKeyTypeIndex]}}</view>
</picker>
</view>
<view class="console-cell action-cell-inline">
<button class="action-btn primary full-btn" bindtap="onReadKeyConfig" loading="{{readingKeyConfig}}" disabled="{{!bleInfo.connected || readingKeyConfig || writingKeyConfig}}">读取配置</button>
</view>
<view class="console-cell action-cell-inline">
<button class="action-btn success full-btn" bindtap="onSubmitKeyConfig" loading="{{writingKeyConfig}}" disabled="{{!canOperateKeyConfig || writingKeyConfig || readingKeyConfig}}">写入配置</button>
</view>
</view>
<view class="key-card-wrap">
<view class="key-grid strict-grid">
<view class="key-card-inline {{item.isPlaceholder ? 'reserve-card' : (bleInfo.connected ? '' : 'is-disabled')}}" wx:for="{{keyConfig.keySlots}}" wx:key="slotIndex">
<view wx:if="{{!item.isPlaceholder}}" class="key-card-content">
<view class="inline-key-label">按键{{item.hwIndex}}</view>
<picker class="inline-key-picker" mode="selector" range="{{functionOptionLabels}}" value="{{item.functionIndex}}" bindchange="onFunctionChange" data-index="{{index}}" disabled="{{!bleInfo.connected || writingKeyConfig}}">
<view class="picker-view compact {{bleInfo.connected ? '' : 'is-disabled'}}">{{functionOptionLabels[item.functionIndex] || '请选择功能'}}</view>
</picker>
<button class="action-btn default inline-send-btn" bindtap="onTriggerKey" data-index="{{index}}" loading="{{keySendingIndex === index}}" disabled="{{!bleInfo.connected || !item.mappedKey || keySendingIndex === index}}">{{keySendingIndex === index ? '发送中...' : '发送'}}</button>
</view>
<view wx:if="{{item.isPlaceholder}}" class="reserve-card-empty">
<text class="reserve-card-text">预留{{item.hwIndex}}</text>
</view>
</view>
</view>
</view>
</view>
<view class="panel log-panel log-card">
<view class="panel-head inline-head log-inline-head keep-inline-head blank-head"></view>
<view class="log-toolbar-row keep-inline-actions-row">
<view class="log-toolbar-title-spacer"></view>
<view class="toolbar-actions right-actions keep-inline-actions">
<button class="action-btn default" bindtap="onClearLogs">清空日志</button>
<button class="action-btn primary" bindtap="onExportLogs">导出日志</button>
</view>
</view>
<scroll-view class="log-scroll" scroll-y="true" scroll-into-view="{{logScrollTo}}">
<view class="log-item simple-log {{item.typeClass}}" wx:for="{{logs}}" wx:key="id" id="{{'log-' + item.id}}">
<text class="log-time">{{item.time}}</text>
<text class="log-dir {{item.typeClass}}">{{item.type === 'TX' ? 'TX ->' : (item.type === 'RX' ? 'RX <-' : '[' + item.type + ']')}}</text>
<text class="log-text {{item.type === 'TX' || item.type === 'RX' ? 'packet' : 'plain'}}">{{(item.type === 'TX' || item.type === 'RX') ? (item.displayText || item.hexText || item.text) : item.text}}</text>
</view>
</scroll-view>
</view>
</view>
<view class="dialog-mask" wx:if="{{confirmDialog.visible}}">
<view class="dialog-card">
<view class="dialog-title">设置确认</view>
<view class="dialog-content">{{confirmDialog.message}}</view>
<view class="dialog-actions">
<button class="action-btn default dialog-btn" bindtap="onCancelSubmit">取消</button>
<button class="action-btn primary dialog-btn" bindtap="onConfirmSubmit">继续下发</button>
</view>
</view>
</view>
</view>

View File

@@ -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;
}
}

View File

@@ -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) {} } })
}

View File

@@ -7,25 +7,13 @@
<!-- 内容栏顶部 -->
<view class="content-header">
<!-- 左侧导航栏 -->
<view class="nav-tabs">
<view
class="nav-tab {{activeTab === 'host' ? 'active' : ''}}"
data-tab="host"
bindtap="switchTab"
>
主机
<picker class="device-type-picker" mode="selector" range="{{deviceTypeLabels}}" value="{{deviceTypeIndex}}" bindchange="onDeviceTypeChange">
<view class="device-type-display {{deviceTypeIndex === 0 ? 'placeholder' : ''}}">
<text class="device-type-text">{{deviceTypeLabels[deviceTypeIndex]}}</text>
<text class="device-type-arrow">▼</text>
</view>
<view
class="nav-tab {{activeTab === 'W13' ? 'active' : ''}}"
data-tab="W13"
bindtap="switchTab"
>
W13
</view>
</view>
</picker>
<!-- 右侧搜索按钮 -->
<view class="search-btn" bindtap="searchBluetooth">
搜索蓝牙
</view>

View File

@@ -14,32 +14,43 @@
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
gap: 14rpx;
}
/* 左侧导航栏 */
.nav-tabs {
.device-type-picker {
flex: 1;
min-width: 0;
}
.device-type-display {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
min-height: 70rpx;
padding: 0 22rpx;
background: #ffffff;
border: 1rpx solid #ddd;
border-radius: 50rpx;
padding: 2rpx;
gap: 2rpx;
border: 1rpx solid #dce3ef;
border-radius: 18rpx;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
color: #1f2d3d;
}
.nav-tab {
padding: 12rpx 26rpx;
border-radius: 50rpx;
.device-type-display.placeholder {
color: #8c9399;
}
.device-type-text {
font-size: 28rpx;
transition: all 0.3s ease;
color: #666666;
background-color: #ffffff;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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);
.device-type-arrow {
font-size: 20rpx;
color: #7b8794;
}
/* 右侧搜索按钮 */

View File

@@ -789,6 +789,7 @@ Page({
},
RefreshTheRoom:async function(e){
let that = this
await GetHostsInfo({
HotelID: this.data.Hotelinfo.HotelId
@@ -918,7 +919,7 @@ Page({
}, showtime)
},
async bindPickerChange(e) {
debugger
let Completed =this.data.Completed
let _this =this
if (Completed) {