feat: 添加蓝牙调试页面的初始实现
- 新增 BLVRQPage.json 配置文件,设置导航栏样式。 - 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。 - 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
This commit is contained in:
682
Document/BLV_RQ蓝牙通信控制页面实施文档.md
Normal file
682
Document/BLV_RQ蓝牙通信控制页面实施文档.md
Normal 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:允许继续**
|
||||
- 继续时采纳建议:**未配置映射项保持设备当前返回值不变**
|
||||
- 页面整体不滚动,仅日志区允许内部滚动
|
||||
|
||||
该文档可作为后续代码实现的直接依据。
|
||||
192
Document/RF_RQ2603 蓝牙通讯协议.md
Normal file
192
Document/RF_RQ2603 蓝牙通讯协议.md
Normal 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 | 有效名称字节长度 N(1~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 映射到键值 n(1 对 1 映射)
|
||||
1
app.js
1
app.js
@@ -45,6 +45,7 @@ App({
|
||||
CreateTime:null,
|
||||
HotelCode:null,
|
||||
CreatDate:null,
|
||||
pendingBleNavigation: null,
|
||||
},
|
||||
toast:function(type,title,success,time){
|
||||
if(type==1){
|
||||
|
||||
7
app.json
7
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",
|
||||
|
||||
@@ -184,6 +184,7 @@ export async function GetRoomType(params){
|
||||
}
|
||||
//获取房型下的房间
|
||||
export async function GetRoomTypeNode(params){
|
||||
|
||||
params.token = gettoken();
|
||||
console.log(params)
|
||||
return await reqeust1({
|
||||
|
||||
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal file
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"cu-custom": "/colorui/components/cu-custom"
|
||||
}
|
||||
}
|
||||
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal file
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal 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>
|
||||
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal file
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) {} } })
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* 右侧搜索按钮 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user