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