diff --git a/Document/W13无卡取电设备 - 蓝牙通讯协议.md b/Document/W13无卡取电设备 - 蓝牙通讯协议.md index 05473c3..86872f4 100644 --- a/Document/W13无卡取电设备 - 蓝牙通讯协议.md +++ b/Document/W13无卡取电设备 - 蓝牙通讯协议.md @@ -15,7 +15,7 @@ |---------|------|-------------|---------|------| | B0~B1 | Head | 2 | 0xCC 0xC0 | 固定包头 | | B2~B3 | Len | 2 | 00~548 | 数据的总长度,包括包头和CRC校验,低地址在前 | -| B4~B5 | CRC | 2 | 00~FF | 整包CRC16校验 | +| B4~B5 | CRC | 2 | 00~FF | 整包CRC16Modbus 校验 | | B6~B7 | Frame | 2 | 00~FF | 帧号 | | B8~B9 | FramNum | 2 | 00~FF | 帧总数 | | B10 | Frame_Type | 1 | 00~FF | 帧类型,命令字 | @@ -54,18 +54,90 @@ sequenceDiagram |------|-------|------|------| | PC→MCU | 0x01 | P0: 0x00 | 读取版本号命令 | | MCU→PC | 0x01 | P0: 软件版本号
P1: 硬件版本号 | 返回版本信息 | +CC C0 0D 00 54 08 01 00 01 00 01 04 04 +### 3.2 设置无卡取电条件信息 -### 3.2 设置无卡取电条件信息(命令1) -| 方向 | 命令字 | 参数 | 备注 | -|------|-------|------|------| -| PC→MCU | 0x08 | P0: 有无逻辑标记
P1: 条件组
P2: 条件序号
P3~P4: 条件判定时间
P5: 条件判定时间单位
P6~P9: 端口1~10状态
P10: 触发阈值
P11~P12: 条件超时时间
P13: 条件超时时间单位 | 设置无卡取电条件 | -| MCU→PC | 0x08 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | +PC -> MCU + +命令字: 0x08 + +参数说明 (P0..P13): + +- P0: 有无人逻辑标记 +- P1: 条件组号 +- P2: 同一条件组内的条件序号 +- P3~P4: 条件判定时间(16-bit,低地址在前) +- P5: 条件判定时间单位 +- P6~P10: 端口1~5 状态(共 5 个字节/项,按顺序;第1项为插卡状态,暂未使用) +- P11~P12: 条件组超时时间(16-bit, 低地址在前) +- P13: 条件组超时时间单位(取值:1=秒,2=分,3=时) + +有无人逻辑标志取值: + +- 1: 无人至有人 +- 2: 有人至无人 +- 3: 短暂离开 +- 4: 长时间离开 + +端口状态取值说明(每项): + +- 0: 不判断 +- 1: 触发 +- 2: 释放 +- 3: 关至开 +- 4: 开至关 + +注意:条件组超时时间必须大于或等于条件判定时间(P11~P12 >= P3~P4),否则设备应判定为参数错误。 + +MCU -> PC + +命令字: 0x08(响应) + +- P0: 返回码:0x01 表示参数正确;0x02 表示参数错误 + + +### 3.3 设置无卡取电端口信息 + +PC -> MCU + +命令字: 0x09 + +参数说明 (P0..P10): + +- P0: 端口设备类型 +- P1: 端口设备地址(暂未使用,默认 1) +- P2~P3: 端口设备回路(16-bit,低地址在前,取值 1..5) +- P4: 有人->无人 阈值 +- P5: 虚拟端口号(1..5,对应物理端口含义见下) +- P6: 回路是否启用检测统计(0/1) +- P7~P8: 回路检测统计时间(16-bit,低地址在前) +- P9: 回路检测统计时间单位(1=秒,2=分,3=时) +- P10: 无人->有人 阈值 + +字段说明与取值: + +- 端口设备类型: + - 插卡状态: 10 + - 其他设备类型: 2(默认) +- 端口地址:暂未使用,默认填写 1 +- 端口设备回路(P2~P3):可取 1~5,含义如下: + - 回路1: 插卡状态 + - 回路2: 门磁状态 + - 回路3: 洗手间状态 + - 回路4: 卧室状态 + - 回路5: 门口状态 +- 虚拟端口号(P5):取值 1~5,对应物理端口含义同上(端口1=插卡,2=门磁,3=洗手间,4=卧室,5=门口) +- 回路检测统计最大时间:8 小时(设备端限制) + +注意: +- 请确保统计时间与单位(P7~P9)不会超过设备支持的最大值(通常 8 小时);若超限设备可能返回参数错误。 + +MCU -> PC + +命令字: 0x09(响应) + +- P0: 返回码:0x01 表示参数正确;0x02 表示参数错误 -### 3.3 设置无卡取电条件信息(命令2) -| 方向 | 命令字 | 参数 | 备注 | -|------|-------|------|------| -| PC→MCU | 0x09 | P0: 端口设备类型
P1: 端口设备地址
P2~P3: 端口设备回路
P4: 有人->无人阈值
P5: 虚拟端口号
P6: 回路是否启用检测统计
P7~P8: 回路检测统计时间
P9: 回路检测统计时间单位
P10: 无人->有人阈值 | 设置无卡取电条件 | -| MCU→PC | 0x09 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | ### 3.4 OTA升级开始 | 方向 | 命令字 | 参数 | 备注 | @@ -88,17 +160,35 @@ sequenceDiagram - 端口状态值:0=释放,1=触发。 - 典型端口位含义:bit0=端口1(门磁),bit1=端口2(洗手间),bit2=端口3(卧室),bit3=端口4(门口),后续端口依次类推。 +### 3.7 测试按键功能 ### 3.7 测试按键功能 | 方向 | 命令字 | 参数 | 备注 | |------|-------|------|------| -| PC→MCU | 0x13 | P0: 0x01(按键点按控制)
P1: bit0(按键1触发), bit1(按键2触发), bit2(按键3触发), bit3(按键4触发), bit4(按键5触发), bit5(按键6触发) | 测试按键功能(对应按键仅支持点按,不具备开关状态) | +| PC→MCU | 0x13 | P0: 0x01(按键点按控制)
P1: 按键位掩码(位对应按键见下表) | 请求模拟按键点按;P1 为按键位掩码,可同时置位多个按键以并行触发对应动作 | | MCU→PC | 0x13 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | -### 3.8 设置门磁开关走廊灯、卫浴雷达开关卫浴灯事件 +按键映射说明: + +- 按键1 (bit0): 无人→有人 +- 按键2 (bit1): 有人→无人 +- 按键3 (bit2): 门磁 开 +- 按键4 (bit3): 门磁 关 +- 按键5 (bit4): 洗手间雷达 开 +- 按键6 (bit5): 洗手间雷达 关 + +组包示例与说明: + +- 要模拟“按键3(门磁 开)”点按,PC 发送命令帧 Frame_Type=0x13,参数 P0=0x01,P1=0x04(bit2 = 1)。 +- 要同时触发按键1与按键5,P1 应为 (1<<0) | (1<<4) = 0x11。 +- 设备在收到该命令后按位解析 P1,并对被置位的按键逐一触发对应的内部动作或上报状态变化。若参数格式或范围非法,设备返回 MCU→PC 的 P0=0x02 表示参数错误。 + +注意:这些测试按键仅用于模拟点按触发动作,不会改变设备的持续开/关逻辑(若需设置长期状态,请使用对应的控制命令)。 + +### 3.8 设置门磁开廊灯事件,卫浴雷达开卫浴灯事件触发、释放参数 | 方向 | 命令字 | 参数 | 备注 | |------|-------|------|------| -| PC→MCU | 0x16 | P0: 控制位(bit0=门磁开关走廊灯事件;bit1=卫浴灯开关事件)
门磁开关走廊灯事件:
P1: 事件触发延迟时间数值
P2: 时间单位(1=秒,2=分,3=时)
P3: 事件持续时间数值
P4: 时间单位(1=秒,2=分,3=时)
P5: 事件释放延迟时间数值
P6: 时间单位(1=秒,2=分,3=时)
卫浴灯开关事件:
P7: 事件触发延迟时间数值
P8: 时间单位(1=秒,2=分,3=时)
P9: 事件持续时间数值
P10: 时间单位(1=秒,2=分,3=时)
P11: 事件释放延迟时间数值
P12: 时间单位(1=秒,2=分,3=时) | 设置事件参数(用于控制门磁亮走廊灯、卫浴雷达亮卫浴灯等) | -| MCU→PC | 0x16 | P0: 0x01(参数正确) / 0x02(参数错误) | 返回设置结果 | +| PC→MCU | 0x16 | P0: 控制位
  bit0: 设置门磁开关廊灯事件
  bit1: 设置卫浴灯开关事件

门磁开关廊灯事件:
P1: 事件触发延迟时间数值
P2: 时间单位,1=秒 2=分 3=时
P3: 事件释放延迟时间
P4: 时间单位,1=秒 2=分 3=时

卫浴灯开关事件:
P5: 事件触发延迟时间数值
P6: 时间单位,1=秒 2=分 3=时
P7: 事件释放延迟时间
P8: 时间单位,1=秒 2=分 3=时 | 设置门磁与卫浴雷达的触发/释放时序参数 | +| MCU→PC | 0x16 | P0:
  0x01: 参数正确
  0x02: 参数错误 | 返回设置结果 | ## 4. 命令交互流程图 diff --git a/app.json b/app.json index 6f1c6b8..b547145 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,6 @@ { "pages": [ + "pages/autho/index", "pages/login/login", "pages/index/index", diff --git a/app.wxss b/app.wxss index a458c1f..c42cc75 100644 --- a/app.wxss +++ b/app.wxss @@ -23,7 +23,7 @@ border-radius: 12rpx; width: 47%; margin: 0 1% 40rpx; - background-image: url(https://image.weilanwl.com/color2.0/cardBg.png); + /* background-image: url(https://image.weilanwl.com/color2.0/cardBg.png); */ background-size: cover; background-position: center; position: relative; @@ -34,7 +34,7 @@ border-radius: 12rpx; width: 32%; margin: 0 5rpx 5rpx; - background-image: url(https://image.weilanwl.com/color2.0/cardBg.png); + /* background-image: url(https://image.weilanwl.com/color2.0/cardBg.png); */ background-size: cover; background-position: center; position: relative; diff --git a/icon/vacant.png b/icon/vacant.png deleted file mode 100644 index e3237ff..0000000 Binary files a/icon/vacant.png and /dev/null differ diff --git a/images/BasicsBg.png b/images/BasicsBg.png deleted file mode 100644 index 4c0a18b..0000000 Binary files a/images/BasicsBg.png and /dev/null differ diff --git a/images/cjkz.png b/images/cjkz.png deleted file mode 100644 index ec9eb2a..0000000 Binary files a/images/cjkz.png and /dev/null differ diff --git a/images/componentBg.png b/images/componentBg.png deleted file mode 100644 index 4d6ed3c..0000000 Binary files a/images/componentBg.png and /dev/null differ diff --git a/images/logo.png b/images/logo.png deleted file mode 100644 index 8043b0d..0000000 Binary files a/images/logo.png and /dev/null differ diff --git a/images/share.jpg b/images/share.jpg deleted file mode 100644 index b27658f..0000000 Binary files a/images/share.jpg and /dev/null differ diff --git a/images/tabbar/about.png b/images/tabbar/about.png deleted file mode 100644 index a2e5acc..0000000 Binary files a/images/tabbar/about.png and /dev/null differ diff --git a/images/tabbar/about_cur.png b/images/tabbar/about_cur.png deleted file mode 100644 index 36769f6..0000000 Binary files a/images/tabbar/about_cur.png and /dev/null differ diff --git a/images/tabbar/basics.png b/images/tabbar/basics.png deleted file mode 100644 index 8a5ce76..0000000 Binary files a/images/tabbar/basics.png and /dev/null differ diff --git a/images/tabbar/basics_cur.png b/images/tabbar/basics_cur.png deleted file mode 100644 index 5022f49..0000000 Binary files a/images/tabbar/basics_cur.png and /dev/null differ diff --git a/images/tabbar/chumen.png b/images/tabbar/chumen.png new file mode 100644 index 0000000..8d2cfc3 Binary files /dev/null and b/images/tabbar/chumen.png differ diff --git a/images/tabbar/component.png b/images/tabbar/component.png deleted file mode 100644 index 0a87a81..0000000 Binary files a/images/tabbar/component.png and /dev/null differ diff --git a/images/tabbar/component_cur.png b/images/tabbar/component_cur.png deleted file mode 100644 index 581eca6..0000000 Binary files a/images/tabbar/component_cur.png and /dev/null differ diff --git a/images/tabbar/jinmen.png b/images/tabbar/jinmen.png new file mode 100644 index 0000000..95c5407 Binary files /dev/null and b/images/tabbar/jinmen.png differ diff --git a/images/tabbar/menguanbi.png b/images/tabbar/menguanbi.png new file mode 100644 index 0000000..66822c0 Binary files /dev/null and b/images/tabbar/menguanbi.png differ diff --git a/images/tabbar/menkaiqi.png b/images/tabbar/menkaiqi.png new file mode 100644 index 0000000..a58df44 Binary files /dev/null and b/images/tabbar/menkaiqi.png differ diff --git a/images/tabbar/plugin.png b/images/tabbar/plugin.png deleted file mode 100644 index a7b540b..0000000 Binary files a/images/tabbar/plugin.png and /dev/null differ diff --git a/images/tabbar/plugin_cur.png b/images/tabbar/plugin_cur.png deleted file mode 100644 index f541bd8..0000000 Binary files a/images/tabbar/plugin_cur.png and /dev/null differ diff --git a/images/tabbar/shiyourenkou.png b/images/tabbar/shiyourenkou.png new file mode 100644 index 0000000..6ba9f60 Binary files /dev/null and b/images/tabbar/shiyourenkou.png differ diff --git a/images/tabbar/youren.png b/images/tabbar/youren.png new file mode 100644 index 0000000..e2d94c8 Binary files /dev/null and b/images/tabbar/youren.png differ diff --git a/images/wave.gif b/images/wave.gif deleted file mode 100644 index 5e50b86..0000000 Binary files a/images/wave.gif and /dev/null differ diff --git a/images/zanCode.jpg b/images/zanCode.jpg deleted file mode 100644 index 765e600..0000000 Binary files a/images/zanCode.jpg and /dev/null differ diff --git a/img/back.png b/img/back.png deleted file mode 100644 index b30b8c3..0000000 Binary files a/img/back.png and /dev/null differ diff --git a/img/ble.png b/img/ble.png deleted file mode 100644 index 22d2afd..0000000 Binary files a/img/ble.png and /dev/null differ diff --git a/img/ecble.png b/img/ecble.png deleted file mode 100644 index bc8f4ad..0000000 Binary files a/img/ecble.png and /dev/null differ diff --git a/img/logo.png b/img/logo.png deleted file mode 100644 index 01a53b5..0000000 Binary files a/img/logo.png and /dev/null differ diff --git a/img/s1.png b/img/s1.png deleted file mode 100644 index 95aed59..0000000 Binary files a/img/s1.png and /dev/null differ diff --git a/img/s2.png b/img/s2.png deleted file mode 100644 index 912d282..0000000 Binary files a/img/s2.png and /dev/null differ diff --git a/img/s3.png b/img/s3.png deleted file mode 100644 index f24adc9..0000000 Binary files a/img/s3.png and /dev/null differ diff --git a/img/s4.png b/img/s4.png deleted file mode 100644 index 17a6e1b..0000000 Binary files a/img/s4.png and /dev/null differ diff --git a/img/s5.png b/img/s5.png deleted file mode 100644 index 49d802e..0000000 Binary files a/img/s5.png and /dev/null differ diff --git a/pages/NewHome/NewHome.js b/pages/NewHome/NewHome.js index 75786a1..0bcdc89 100644 --- a/pages/NewHome/NewHome.js +++ b/pages/NewHome/NewHome.js @@ -21,7 +21,7 @@ Page({ icon: 'colorlens' }, { - title: '主机升级', + title: '主机升级绑定', name: 'HostUpgrade', color: 'purple', icon: 'font' diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.js b/pages/basics/BluetoothDebugging/B13page/B13page.js index ae85dce..0f9eb8d 100644 --- a/pages/basics/BluetoothDebugging/B13page/B13page.js +++ b/pages/basics/BluetoothDebugging/B13page/B13page.js @@ -1,5 +1,5 @@ -const { buildCommand, buildReadVersion, COMMANDS } = require('../../../../utils/w13Packet.js') +const { buildCommand, buildReadVersion, buildSetDoorBathEvent, COMMANDS, verifyHexPacket } = require('../../../../utils/w13Packet.js') const FIXED_CONNECT_CMD = new Uint8Array([0xCC, 0xC0, 0x0C, 0x00, 0x4F, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x08]) Page({ data: { @@ -25,28 +25,35 @@ Page({ txCharId: '', rxCharId: '', logList: [], - timeUnits: ['时', '分', '秒'], + timeUnits: ['秒', '分', '时'], hourValues: Array.from({ length: 24 }, (_, i) => i + 1), msValues: Array.from({ length: 60 }, (_, i) => i + 1), ports: [ - { name: '无卡取电 CH1', portLabel: '开门磁', alias: '开门磁', deviceType: 0, deviceAddr: 0, loop: 1, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, - { name: '无卡取电 CH2', portLabel: '门口红外', alias: '门口红外', deviceType: 0, deviceAddr: 0, loop: 2, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, - { name: '无卡取电 CH3', portLabel: '床头红外', alias: '床头红外', deviceType: 0, deviceAddr: 0, loop: 3, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, - { name: '无卡取电 CH4', portLabel: '卫浴红外', alias: '卫浴红外', deviceType: 0, deviceAddr: 0, loop: 4, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 } + { name: '有卡取电', portLabel: '有卡取电', alias: '有卡取电', deviceType: 2, deviceAddr: 1, loop: 1, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '门磁', portLabel: '门磁', alias: '门磁', deviceType: 0, deviceAddr: 0, loop: 2, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '门口', portLabel: '门口', alias: '门口', deviceType: 0, deviceAddr: 0, loop: 3, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '卧室', portLabel: '卧室', alias: '卧室', deviceType: 0, deviceAddr: 0, loop: 4, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 }, + { name: '洗手间', portLabel: '洗手间', alias: '洗手间', deviceType: 0, deviceAddr: 0, loop: 5, thresholdUp: 0, thresholdDown: 0, enabled: false, detectTime: 0, detectUnit: 0 } ], // 条件“有无人标记”选项,参考截图:无人至有人/短暂离开/长时间离开/有人至无人 tagOptions: ['无人至有人', '短暂离开', '长时间离开', '有人至无人'], - stateOptions: ['不判断', '触发', '释放', '开启', '关闭'], - // 默认条件:与截图一致(组1..6,各1条) + stateOptions: ['不判断', '触发', '释放', '关至开', '开至关'], + // 默认条件:由用户指定的条件组与条件 conditions: [ - { group: 1, seq: 1, tag: 0, cardPower: 0, doorMag: 1, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 0, timeoutUnit: 2 }, - { group: 2, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, - { group: 2, seq: 2, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, - { group: 2, seq: 3, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 20, timeoutUnit: 2 }, - { group: 3, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 2, timeout: 0, timeoutUnit: 2 }, - { group: 4, seq: 1, tag: 1, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 0, judgeUnit: 2, timeout: 2, timeoutUnit: 2 }, - { group: 5, seq: 1, tag: 2, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }, - { group: 6, seq: 1, tag: 3, cardPower: 0, doorMag: 4, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 } + // 组1: timeout 2秒,包含1个条件 + { group: 1, seq: 1, tag: 0, cardPower: 0, doorMag: 3, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 2, timeoutUnit: 0 }, + // 组2: timeout 20秒,包含3个条件 + { group: 2, seq: 1, tag: 0, cardPower: 0, doorMag: 0, irHall: 1, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 }, + { group: 2, seq: 2, tag: 0, cardPower: 0, doorMag: 0, irHall: 0, bathRadar: 1, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 }, + { group: 2, seq: 3, tag: 0, cardPower: 0, doorMag: 0, irHall: 0, bathRadar: 0, bathroomRadar: 1, judgeTime: 0, judgeUnit: 0, timeout: 20, timeoutUnit: 0 }, + // 组3: timeout 2秒,包含1个条件 + { group: 3, seq: 1, tag: 1, cardPower: 0, doorMag: 4, irHall: 0, bathRadar: 0, bathroomRadar: 0, judgeTime: 0, judgeUnit: 0, timeout: 2, timeoutUnit: 0 }, + // 组4: timeout 10分,包含1个条件 + { group: 4, seq: 1, tag: 1, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }, + // 组5: timeout 10分,包含1个条件 + { group: 5, seq: 1, tag: 2, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 }, + // 组6: timeout 10分,包含1个条件 + { group: 6, seq: 1, tag: 3, cardPower: 0, doorMag: 3, irHall: 2, bathRadar: 2, bathroomRadar: 2, judgeTime: 2, judgeUnit: 1, timeout: 10, timeoutUnit: 1 } ], // 二级菜单:按组折叠 condGroups: [], @@ -57,6 +64,51 @@ Page({ { key: 'bed', label: '卧室', colorClass: 'gray', triggered: false }, { key: 'hall', label: '走廊', colorClass: 'gray', triggered: false } ] + , + // 卡片颜色类,初始都为灰色 + cardClasses: { + roomHasPeople: 'gray', + roomNoPeople: 'gray', + doorOpen: 'gray', + doorClose: 'gray', + bathHasPeople: 'gray', + bathNoPeople: 'gray' + } + , + // 门磁/卫浴事件控件数据(仅保留 秒 / 分) + eventUnits: ['秒', '分'], + // 雷达读取状态:false=未读取, true=正在读取 + radarReading: false, + // 门磁 + doorTriggerDelay: 0, + doorTriggerUnitIndex: 0, + doorReleaseDelay: 0, + doorReleaseUnitIndex: 0, + doorTriggerOptions: [], + doorReleaseOptions: [], + doorTriggerIndex: 0, + doorReleaseIndex: 0, + // 卫浴 + bathTriggerDelay: 0, + bathTriggerUnitIndex: 0, + bathReleaseDelay: 0, + bathReleaseUnitIndex: 0, + bathTriggerOptions: [], + bathReleaseOptions: [], + bathTriggerIndex: 0, + bathReleaseIndex: 0, + pressedMask: 0, + // 删除模态框状态 + deleteDialogVisible: false, + deleteDialogMode: '', // 'group' | 'condition' + deleteDialogGroups: [], + deleteDialogSeqs: [], + deleteDialogSelectedGroupIndex: 0, + deleteDialogSelectedSeqIndex: 0, + // 添加条件模态状态 + addDialogVisible: false, + addDialogGroups: [], + addDialogSelectedGroupIndex: 0, }, onLoad(options) { @@ -76,6 +128,11 @@ Page({ const txCharId = options && options.txCharId ? decodeURIComponent(options.txCharId) : '' const rxCharId = options && options.rxCharId ? decodeURIComponent(options.rxCharId) : '' this.setData({ DevName: devName, bleName: devName, bleMac, deviceId, serviceId, txCharId, rxCharId }) + // 初始化 picker 选项(触发/释放分别根据各自单位生成) + this.updateDoorTriggerOptions(this.data.doorTriggerUnitIndex || 0) + this.updateDoorReleaseOptions(this.data.doorReleaseUnitIndex || 0) + this.updateBathTriggerOptions(this.data.bathTriggerUnitIndex || 0) + this.updateBathReleaseOptions(this.data.bathReleaseUnitIndex || 0) // 页面进入时打印当前蓝牙连接状态 this.logBleStatus() // 构建条件组 @@ -89,6 +146,92 @@ Page({ }, + unitIndexToProtocolValue(idx) { + const i = Number(idx || 0) + return i === 0 ? 1 : (i === 1 ? 2 : 3) + }, + + // 将协议单位值(1=秒,2=分,3=时)映射为 UI 索引(0=秒,1=分,2=时) + protocolValueToUnitIndex(v) { + const n = Number(v || 0) + if (n === 1) return 0 + if (n === 2) return 1 + if (n === 3) return 2 + return 0 + }, + + // 将存储的单位值规范为 UI 索引(0=秒,1=分,2=时) + // 支持输入为 UI 索引(0..2) 或 协议值(1..3) + ensureUnitIsUIIndex(u) { + const n = Number(u) + if (!isNaN(n)) { + if (n >= 0 && n <= 2) return n + if (n === 1) return 0 + if (n === 2) return 1 + if (n === 3) return 2 + } + return 0 + }, + + // 将 UI 的 tag 索引(0=无人至有人,1=短暂离开,2=长时间离开,3=有人至无人) + // 映射为协议值(1=无人至有人,2=有人至无人,3=短暂离开,4=长时间离开) + // 注意协议文档里定义为 1..4,但 UI 使用 0..3 的索引顺序和语义需对应 + tagIndexToProtocolValue(tagIdx) { + const n = Number(tagIdx) + // UI order: 0=无人至有人,1=短暂离开,2=长时间离开,3=有人至无人 + // Protocol: 1=无人至有人,2=有人至无人,3=短暂离开,4=长时间离开 + if (!isNaN(n)) { + if (n === 0) return 1 + if (n === 1) return 3 + if (n === 2) return 4 + if (n === 3) return 2 + // already a protocol value + if (n === 1 || n === 2 || n === 3 || n === 4) return n + } + return 1 + }, + + // 协议值(1..4) -> UI 索引(0..3),若非法则回退为 0 + protocolValueToTagIndex(v) { + const n = Number(v) + // Protocol: 1=无人至有人,2=有人至无人,3=短暂离开,4=长时间离开 + // UI order: 0=无人至有人,1=短暂离开,2=长时间离开,3=有人至无人 + if (n === 1) return 0 + if (n === 2) return 3 + if (n === 3) return 1 + if (n === 4) return 2 + return 0 + }, + + // 规范化单位值为协议值(1=秒,2=分,3=时) + // 支持两种输入:UI 索引(0/1/2) 或 协议值(1/2/3) + normalizeUnitForPacket(u) { + const n = Number(u || 0) + // 优先把 0/1/2 视为 UI 索引并映射为协议值(0->1,1->2,2->3) + if (n >= 0 && n <= 2) return this.unitIndexToProtocolValue(n) + // 否则若已经是协议值(1/2/3)则直接返回 + if (n === 1 || n === 2 || n === 3) return n + // 保守回退为秒 + return 1 + }, + + // 根据端口信息推导协议中的虚拟端口号(P5) + getVirtualPort(p, idx) { + try { + // 若已明确指定 virtualPort 或 portNo,则优先使用 + if (p && (p.virtualPort != null)) return (p.virtualPort & 0xFF) + if (p && (p.portNo != null)) return (p.portNo & 0xFF) + const label = (p && (p.portLabel || p.name || p.alias || '')) + '' + const txt = label.trim() + if (/有卡|有卡取电|插卡/.test(txt)) return 1 + if (/门磁/.test(txt)) return 2 + if (/门口|开门/.test(txt)) return 3 + if (/床|卧室/.test(txt)) return 4 + if (/卫浴|洗手间|卫/.test(txt)) return 5 + } catch (e) { /* ignore */ } + return (idx + 1) & 0xFF + }, + onShow() { // 页面显示时也打印一次,方便返回/二次进入场景 this.logBleStatus() @@ -102,6 +245,10 @@ Page({ tabSelect(e) { const id = Number(e.currentTarget.dataset.id || 1) this.setData({ TabCur: id }) + // 切换到蓝牙升级标签时,主动读取蓝牙版本号并保存到 bleVersion + if (id === 2) { + this.onSendReadVersion() + } }, onOpenDelayChange(e) { @@ -111,12 +258,192 @@ Page({ this.setData({ bathDelay: e.detail.value }) }, + // === 门磁控件事件处理 === + onDoorFieldInput(e) { + const field = e.currentTarget.dataset.field + let val = Number(e.detail.value || 0) + if (isNaN(val)) val = 0 + // 单位:index 0 => 秒 (max 60), index 1 => 分 (max 30) + const unitIndex = (field === 'triggerDelay') ? (this.data.doorTriggerUnitIndex || 0) : (this.data.doorReleaseUnitIndex || 0) + const maxAllowed = unitIndex === 0 ? 60 : 30 + const minAllowed = 1 + if (val < minAllowed) val = minAllowed + if (val > maxAllowed) val = maxAllowed + const updates = {} + if (field === 'triggerDelay') updates.doorTriggerDelay = val + if (field === 'releaseDelay') updates.doorReleaseDelay = val + this.setData(updates) + }, + onDoorUnitChange(e) { + const field = e.currentTarget.dataset.field + const idx = Number(e.detail.value || 0) + if (field === 'triggerUnit') { + this.setData({ doorTriggerUnitIndex: idx }) + this.updateDoorTriggerOptions(idx) + } + if (field === 'releaseUnit') { + this.setData({ doorReleaseUnitIndex: idx }) + this.updateDoorReleaseOptions(idx) + } + }, + // 发送门磁事件(仅门磁参数,另一侧填0) + sendDoorEvent() { + const d = this.data + // 把索引映射为协议中的单位值:index 0->1(秒),1->2(分),2->3(时) + const mapUnit = (idx) => (Number(idx) === 0 ? 1 : Number(idx) === 1 ? 2 : 3) + // 通过 picker index 推导真实数值(index + 1) + const opts = { + door: { + triggerDelay: Number((d.doorTriggerIndex || 0) + 1), + triggerUnit: mapUnit(d.doorTriggerUnitIndex), + releaseDelay: Number((d.doorReleaseIndex || 0) + 1), + releaseUnit: mapUnit(d.doorReleaseUnitIndex) + }, + bath: null + } + try { + const pkt = buildSetDoorBathEvent(opts) + this.transmitPacket(pkt, '设置门磁事件') + wx.showToast({ title: '门磁事件已发送', icon: 'success' }) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + }, + + // === 卫浴控件事件处理 === + onBathFieldInput(e) { + const field = e.currentTarget.dataset.field + let val = Number(e.detail.value || 0) + if (isNaN(val)) val = 0 + const unitIndex = (field === 'triggerDelay') ? (this.data.bathTriggerUnitIndex || 0) : (this.data.bathReleaseUnitIndex || 0) + const maxAllowed = unitIndex === 0 ? 60 : 30 + const minAllowed = 1 + if (val < minAllowed) val = minAllowed + if (val > maxAllowed) val = maxAllowed + const updates = {} + if (field === 'triggerDelay') updates.bathTriggerDelay = val + if (field === 'releaseDelay') updates.bathReleaseDelay = val + this.setData(updates) + }, + onBathUnitChange(e) { + const field = e.currentTarget.dataset.field + const idx = Number(e.detail.value || 0) + if (field === 'triggerUnit') { + this.setData({ bathTriggerUnitIndex: idx }) + this.updateBathTriggerOptions(idx) + } + if (field === 'releaseUnit') { + this.setData({ bathReleaseUnitIndex: idx }) + this.updateBathReleaseOptions(idx) + } + }, + // 发送卫浴事件(仅卫浴参数,另一侧填0) + sendBathEvent() { + const d = this.data + const mapUnit = (idx) => (Number(idx) === 0 ? 1 : Number(idx) === 1 ? 2 : 3) + const opts = { + door: null, + bath: { + triggerDelay: Number((d.bathTriggerIndex || 0) + 1), + triggerUnit: mapUnit(d.bathTriggerUnitIndex), + releaseDelay: Number((d.bathReleaseIndex || 0) + 1), + releaseUnit: mapUnit(d.bathReleaseUnitIndex) + } + } + try { + const pkt = buildSetDoorBathEvent(opts) + this.transmitPacket(pkt, '设置卫浴事件') + wx.showToast({ title: '卫浴事件已发送', icon: 'success' }) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + }, + + // 切换雷达读取状态:开始/停止 + toggleRadarRead() { + const reading = !!this.data.radarReading + if (!reading) { + // 发送开始读取命令 + this.sendRadarStatusCommand(true) + this.setData({ radarReading: true }) + wx.showToast({ title: '开始读取雷达状态', icon: 'success' }) + } else { + // 发送停止读取命令 + this.sendRadarStatusCommand(false) + this.setData({ radarReading: false }) + wx.showToast({ title: '已停止读取', icon: 'none' }) + } + }, + + // 生成延时选项(单位索引:0=秒,1=分),可选允许包含0 + generateDelayOptions(unitIdx, allowZero = false) { + const isSec = Number(unitIdx) === 0 + const max = isSec ? 60 : 30 + const start = allowZero ? 0 : 1 + const arr = Array.from({ length: max - start + 1 }, (_, i) => `${i + start}`) + return { arr, max } + }, + + updateDoorTriggerOptions(unitIdx) { + // 触发延迟允许 0(通过 allowZero=true) + const gen = this.generateDelayOptions(unitIdx, true) + const curIdx = Number(this.data.doorTriggerIndex || 0) + const maxIdx = gen.arr.length - 1 + const idx = Math.min(Math.max(0, curIdx), maxIdx) + this.setData({ doorTriggerOptions: gen.arr, doorTriggerIndex: idx, doorTriggerDelay: Number(gen.arr[idx]) }) + }, + + updateDoorReleaseOptions(unitIdx) { + const gen = this.generateDelayOptions(unitIdx, false) + const curIdx = Number(this.data.doorReleaseIndex || 0) + const maxIdx = gen.arr.length - 1 + const idx = Math.min(Math.max(0, curIdx), maxIdx) + this.setData({ doorReleaseOptions: gen.arr, doorReleaseIndex: idx, doorReleaseDelay: Number(gen.arr[idx]) }) + }, + + updateBathTriggerOptions(unitIdx) { + const gen = this.generateDelayOptions(unitIdx, true) + const curIdx = Number(this.data.bathTriggerIndex || 0) + const maxIdx = gen.arr.length - 1 + const idx = Math.min(Math.max(0, curIdx), maxIdx) + this.setData({ bathTriggerOptions: gen.arr, bathTriggerIndex: idx, bathTriggerDelay: Number(gen.arr[idx]) }) + }, + + updateBathReleaseOptions(unitIdx) { + const gen = this.generateDelayOptions(unitIdx, false) + const curIdx = Number(this.data.bathReleaseIndex || 0) + const maxIdx = gen.arr.length - 1 + const idx = Math.min(Math.max(0, curIdx), maxIdx) + this.setData({ bathReleaseOptions: gen.arr, bathReleaseIndex: idx, bathReleaseDelay: Number(gen.arr[idx]) }) + }, + + // pickers change handlers + onDoorTriggerPickerChange(e) { + const idx = Number(e.detail.value || 0) + const val = Number((this.data.doorTriggerOptions || [])[idx] || 0) + this.setData({ doorTriggerIndex: idx, doorTriggerDelay: val }) + }, + onDoorReleasePickerChange(e) { + const idx = Number(e.detail.value || 0) + const val = Number((this.data.doorReleaseOptions || [])[idx] || 1) + this.setData({ doorReleaseIndex: idx, doorReleaseDelay: val }) + }, + onBathTriggerPickerChange(e) { + const idx = Number(e.detail.value || 0) + const val = Number((this.data.bathTriggerOptions || [])[idx] || 0) + this.setData({ bathTriggerIndex: idx, bathTriggerDelay: val }) + }, + onBathReleasePickerChange(e) { + const idx = Number(e.detail.value || 0) + const val = Number((this.data.bathReleaseOptions || [])[idx] || 1) + this.setData({ bathReleaseIndex: idx, bathReleaseDelay: val }) + }, + // 便捷示例:读版本号 onSendReadVersion() { try { const pkt = buildReadVersion() - const text = this.data.hexShow ? this.toHex(pkt) : `[${Array.from(pkt).join(', ')}]` - this.appendLog('TX', `读版本号: ${text}`) + this.transmitPacket(pkt, '读版本号') } catch (err) { wx.showToast({ title: '构包失败', icon: 'none' }) } @@ -126,8 +453,9 @@ Page({ onStartOta() { try { const pkt = buildCommand(COMMANDS.OTA_START, [0x01]) - const text = this.toHex(pkt) - this.appendLog('TX', `OTA开始: ${text}`) + this.appendLog('TX', `OTA开始: ${this.toHex(pkt)}`) + // 通过统一发送函数发送包(会检查连接并发现通道) + this.transmitPacket(pkt, 'OTA开始') wx.showToast({ title: '已发送OTA开始', icon: 'success' }) } catch (err) { wx.showToast({ title: '构包失败', icon: 'none' }) @@ -147,6 +475,53 @@ Page({ } }, + // 点击状态卡片 -> 发送测试按键功能命令 (Frame_Type=0x13, P0=0x01, P1=mask) + onTestKeyTap(e) { + const raw = e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.mask + let mask = 0 + try { + // 支持十六进制字符串或数字 + if (typeof raw === 'string' && raw.startsWith('0x')) mask = parseInt(raw, 16) & 0xFF + else mask = Number(raw) || 0 + } catch (err) { mask = 0 } + mask = mask & 0xFF + if (!mask) { + wx.showToast({ title: '无效按键掩码', icon: 'none' }) + return + } + const names = { + 0x01: '房间有人', + 0x02: '房间无人', + 0x04: '门开', + 0x08: '门关', + 0x10: '卫浴有人', + 0x20: '卫浴无人' + } + const keyName = names[mask] || `按键 0x${mask.toString(16).toUpperCase()}` + try { + const payload = [0x01, mask] + const pkt = buildCommand(COMMANDS.TEST_KEYS, payload) + this.transmitPacket(pkt, `测试按键 ${keyName}`) + // this.appendLog('TX', `测试按键 ${keyName} mask=0x${mask.toString(16).toUpperCase()}`) + wx.showToast({ title: `${keyName} 已发送`, icon: 'success' }) + } catch (err) { + wx.showToast({ title: '构包失败', icon: 'none' }) + } + + + }, + + onTestKeyTouchStart(e) { + const raw = e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.masknum + const mask = Number(raw) || 0 + this.setData({ pressedMask: mask }) + }, + + onTestKeyTouchEnd(e) { + // 清除按下样式 + this.setData({ pressedMask: 0 }) + }, + toHex(u8) { return Array.from(u8).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ') }, @@ -342,6 +717,52 @@ Page({ this.ensureBleConnected(doWrite) }, + // 低级写操作,返回 Promise,在写入成功或失败时 resolve/reject + writeRawBytes(u8, label) { + return new Promise((resolve, reject) => { + const { deviceId, serviceId, txCharId } = this.data || {} + if (!deviceId) return reject(new Error('未连接BLE')) + const bytes = (u8 instanceof Uint8Array) ? u8 : new Uint8Array(u8 || []) + const doWrite = () => { + if (!serviceId || !txCharId) { + // 确保通道就绪后重试 + this.ensureBleChannels(() => this.writeRawBytes(u8, label).then(resolve).catch(reject)) + return + } + try { + const hex = this.toHex(bytes) + this.appendLog('TX', `${label}: ${hex}`) + } catch (e) { /* ignore logging errors */ } + wx.writeBLECharacteristicValue({ + deviceId, + serviceId, + characteristicId: txCharId, + value: bytes.buffer, + success: () => resolve(), + fail: (err) => reject(err || new Error('write failed')) + }) + } + this.ensureBleConnected(doWrite) + }) + }, + + // 等待设备对指定命令类型的应答(基于通知),超时后 reject + waitForResponse(expectedType, timeout = 3000) { + return new Promise((resolve, reject) => { + // 仅允许一个挂起的响应等待 + this._pendingResponse = { expectedType, resolve, reject } + const timer = setTimeout(() => { + if (this._pendingResponse && this._pendingResponse.reject === reject) { + this._pendingResponse = null + reject(new Error('等待响应超时')) + } + }, timeout) + // 包装 resolve 以清理定时器 + const origResolve = resolve + this._pendingResponse.resolve = (params) => { clearTimeout(timer); this._pendingResponse = null; origResolve(params) } + }) + }, + ensureBleChannels(done) { const { deviceId, serviceId, txCharId, rxCharId } = this.data || {} // 若未携带 deviceId,尝试从系统已连接设备列表中兜底获取 @@ -430,7 +851,7 @@ Page({ // 开启订阅(若已传入rx特征),保证能接收数据 this.enableNotify() this.setupBleListener() - this.sendRadarStatusCommand(true) + // this.sendRadarStatusCommand(true) }, enableNotify() { @@ -485,25 +906,122 @@ Page({ this.appendLog('TX', `${label}: ${hex}`) // 如果页面接入了BLE连接参数,则尝试写入;未配置则仅记录日志 const { deviceId, serviceId, txCharId } = this.data || {} - if (!deviceId || !serviceId || !txCharId || typeof wx.writeBLECharacteristicValue !== 'function') return - try { - wx.writeBLECharacteristicValue({ - deviceId, - serviceId, - characteristicId: txCharId, - value: (pkt && pkt.buffer) ? pkt.buffer : new Uint8Array(pkt || []).buffer, - fail: (err) => { - const code = err && (err.errCode ?? err.code) - const msg = (err && (err.errMsg || err.message)) || '未知原因' - const detail = code !== undefined ? `code=${code} ${msg}` : msg - this.appendLog('WARN', `写入BLE失败 device=${deviceId} svc=${serviceId} tx=${txCharId} ${detail}`) - wx.showToast({ title: '发送失败', icon: 'none' }) - } - }) - } catch (err) { - const msg = (err && (err.errMsg || err.message)) || '异常' - this.appendLog('WARN', `写入BLE异常 ${msg}`) + if (!deviceId || typeof wx.writeBLECharacteristicValue !== 'function') return + + const doWrite = () => { + const { deviceId: dId, serviceId: sId, txCharId: tId } = this.data || {} + if (!dId || !sId || !tId) { + this.appendLog('WARN', '写入前缺少 service/char,尝试发现通道') + this.ensureBleChannels(() => doWrite()) + return + } + try { + wx.writeBLECharacteristicValue({ + deviceId: dId, + serviceId: sId, + characteristicId: tId, + value: (pkt && pkt.buffer) ? pkt.buffer : new Uint8Array(pkt || []).buffer, + fail: (err) => { + const code = err && (err.errCode ?? err.code) + const msg = (err && (err.errMsg || err.message)) || '未知原因' + const detail = code !== undefined ? `code=${code} ${msg}` : msg + this.appendLog('WARN', `写入BLE失败 device=${dId} svc=${sId} tx=${tId} ${detail}`) + wx.showToast({ title: '发送失败', icon: 'none' }) + } + }) + } catch (err) { + const msg = (err && (err.errMsg || err.message)) || '异常' + this.appendLog('WARN', `写入BLE异常 ${msg}`) + } } + + // 先确保连接并在必要时尝试重连、发现通道;失败则取消发送并提示 + this.ensureBleConnectedAndReconnect(() => { + // 最终确保有 service/char 再写入 + const { serviceId: sId, txCharId: tId } = this.data || {} + if (!sId || !tId) { + this.ensureBleChannels(() => doWrite()) + } else { + doWrite() + } + }) + }, + + // 在发送前检查连接状态;若断开尝试重连并发现通道,重连失败则取消发送并提示 + ensureBleConnectedAndReconnect(next, attempt = 0) { + const { deviceId } = this.data || {} + if (!deviceId) { + wx.showToast({ title: '未连接BLE', icon: 'none' }) + return + } + + const proceed = () => { if (typeof next === 'function') next() } + + // 优先使用 getBLEConnectionState 查询 + if (typeof wx.getBLEConnectionState === 'function') { + try { + wx.getBLEConnectionState({ + deviceId, + success: (res) => { + const connected = !!(res && res.connected) + if (connected) { + proceed() + return + } + // 未连接时尝试主动连接 + if (attempt < 2 && typeof wx.createBLEConnection === 'function') { + this.appendLog('CFG', `尝试建立BLE连接 (attempt=${attempt + 1})`) + wx.createBLEConnection({ + deviceId, + success: () => { + // 连接后重新发现服务/特征 + this.ensureBleChannels(() => proceed()) + }, + fail: () => { + // 重试或最终失败 + if (attempt < 1) { + setTimeout(() => this.ensureBleConnectedAndReconnect(next, attempt + 1), 250) + return + } + this.appendLog('WARN', '尝试重连失败') + wx.showToast({ title: '蓝牙连接异常', icon: 'none' }) + } + }) + return + } + // 无重连接口或超过重试次数,尝试兜底检查 + this._fallbackCheckConnected(deviceId, proceed) + }, + fail: () => { + // 查询失败时尝试直接创建连接 + if (typeof wx.createBLEConnection === 'function' && attempt < 2) { + this.appendLog('CFG', 'getBLEConnectionState失败,尝试createBLEConnection') + wx.createBLEConnection({ + deviceId, + success: () => this.ensureBleChannels(() => proceed()), + fail: () => { + this.appendLog('WARN', 'createBLEConnection失败') + wx.showToast({ title: '蓝牙连接异常', icon: 'none' }) + } + }) + return + } + this._fallbackCheckConnected(deviceId, proceed) + } + }) + } catch (e) { + this.appendLog('WARN', '查询BLE状态异常,尝试重连') + if (typeof wx.createBLEConnection === 'function' && attempt < 2) { + wx.createBLEConnection({ deviceId, success: () => this.ensureBleChannels(() => proceed()), fail: () => wx.showToast({ title: '蓝牙连接异常', icon: 'none' }) }) + return + } + this._fallbackCheckConnected(deviceId, proceed) + } + return + } + + // 不支持 getBLEConnectionState 的平台,使用兜底检查 + this._fallbackCheckConnected(deviceId, proceed) }, /** @@ -575,6 +1093,45 @@ Page({ this.appendLog('PARSE', `雷达状态: 有效端口${parsed.portCount} 有人标记=${parsed.human === 0x01 ? '有人' : '无人'} 位=0b${parsed.bits.toString(2).padStart(8, '0')}`) } } + // 读版本号响应 + if (frameType === (COMMANDS.READ_VERSION & 0xFF)) { + try { + const params = u8.slice(11) || [] + if (params.length === 0) return + // 如果返回错误码 P0=0x02 表示参数/命令错误 + if (params[0] === 0x02) { + this.appendLog('PARSE', '读版本返回:设备返回参数错误') + wx.showToast({ title: '读版本失败:设备返回错误', icon: 'none' }) + return + } + // 常见格式:P0 = 软件版本号, P1 = 硬件版本号 (数值) + if (params.length >= 2 && params.every(b => typeof b === 'number')) { + const sw = params[0] & 0xFF + const hw = params[1] & 0xFF + const verStr = `软件:${sw} 硬件:${hw}` + this.setData({ bleVersion: verStr }) + this.appendLog('PARSE', `蓝牙版本: ${verStr}`) + return + } + // 回退:若是可打印 ASCII 字符,则按字符串显示,否则以 HEX 显示 + let ascii = '' + try { ascii = Array.from(params).map(b => String.fromCharCode(b)).join('').trim() } catch (e) { ascii = '' } + const ver = ascii || Array.from(params).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ') + this.setData({ bleVersion: ver }) + this.appendLog('PARSE', `蓝牙版本: ${ver}`) + } catch (e) { + this.appendLog('WARN', '解析读版本响应失败') + } + } + // 如果存在挂起的等待响应(onOneKeySend 使用),并且类型匹配,则回调并清理 + try { + if (this._pendingResponse && this._pendingResponse.expectedType != null) { + if (frameType === this._pendingResponse.expectedType) { + const params = u8.slice(11) || [] + try { this._pendingResponse.resolve(params) } catch (e) { /* ignore */ } + } + } + } catch (e) { /* ignore */ } }, parseRadarStatus(u8) { @@ -588,6 +1145,7 @@ Page({ }, updateRadarLights(bits) { + // 更新雷达指示灯数组(保持原有展示) const next = (this.data.radarLights || []).map((it, idx) => { const triggered = ((bits >> idx) & 0x01) === 1 return { @@ -596,7 +1154,30 @@ Page({ colorClass: triggered ? 'red' : 'green' } }) - this.setData({ radarLights: next }) + + // 解析位值(约定:bit0=门磁, bit1=卫浴, bit2=卧室, bit3=走廊) + const doorTriggered = ((bits >> 0) & 0x01) === 1 + const bathTriggered = ((bits >> 1) & 0x01) === 1 + const bedTriggered = ((bits >> 2) & 0x01) === 1 + const hallTriggered = ((bits >> 3) & 0x01) === 1 + + // 计算卡片显示规则:默认灰色,按需求将对应卡片设色 + const cardClasses = { + // 房间有人:卧室或走廊任一触发 -> 橙色,否则灰色 + roomHasPeople: (bedTriggered || hallTriggered) ? 'orange' : 'gray', + // 房间无人:卧室和走廊均处于释放 -> 绿色,否则灰色 + roomNoPeople: (!bedTriggered && !hallTriggered) ? 'green' : 'gray', + // 门开:门磁释放 -> 红色,否则灰色 + doorOpen: (!doorTriggered) ? 'red' : 'gray', + // 门关:门磁触发 -> 绿色,否则灰色 + doorClose: (doorTriggered) ? 'green' : 'gray', + // 卫浴有人:卫浴触发 -> 蓝色,否则灰色 + bathHasPeople: (bathTriggered) ? 'blue' : 'gray', + // 卫浴无人:卫浴释放 -> 绿色,否则灰色 + bathNoPeople: (!bathTriggered) ? 'green' : 'gray' + } + + this.setData({ radarLights: next, cardClasses }) }, // 数值约束 @@ -608,8 +1189,8 @@ Page({ return v }, clampDetectByUnit(unit, v) { - // unit: 0=时(1..24) 1=分(1..60) 2=秒(1..60) - const max = unit === 0 ? 24 : 60 + // unit: 0=秒(1..60) 1=分(1..60) 2=时(1..24) + const max = unit === 2 ? 24 : 60 return this.clamp(v, 1, max) }, @@ -619,12 +1200,32 @@ Page({ const list = (this.data.conditions || []).slice().sort((a, b) => (a.group - b.group) || (a.seq - b.seq)) list.forEach(it => { if (!map.has(it.group)) { - map.set(it.group, { group: it.group, timeout: it.timeout, timeoutUnit: it.timeoutUnit, expanded: false, items: [] }) + // 将组级 timeoutUnit 规范为 UI 索引(支持已是 UI 索引或协议值) + const tu = this.ensureUnitIsUIIndex((it && it.timeoutUnit) || 0) + map.set(it.group, { group: it.group, timeout: (it && it.timeout) || 0, timeoutUnit: tu, expanded: false, items: [] }) } - map.get(it.group).items.push({ ...it, expanded: true }) + // 将 item 内的 judgeUnit/timeoutUnit 可能为协议值(1/2/3)时转换为 UI 索引 + const item = { ...it } + item.judgeUnit = this.ensureUnitIsUIIndex(item.judgeUnit) + item.timeoutUnit = this.ensureUnitIsUIIndex(item.timeoutUnit) + map.get(it.group).items.push({ ...item, expanded: true }) }) this.setData({ condGroups: Array.from(map.values()) }) }, + + // 将当前 UI 视图中的 condGroups 同步回 conditions(扁平化) + syncCondGroupsToConditions() { + try { + const flatFromView = [] + ;(this.data.condGroups || []).forEach(grp => { + (grp.items || []).forEach(it => flatFromView.push({ ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit })) + }) + // 仅在有内容时覆盖,避免意外把空覆盖现有数据 + if (flatFromView.length > 0) this.setData({ conditions: flatFromView }) + } catch (e) { + // ignore + } + }, onToggleGroup(e) { const idx = Number(e.currentTarget.dataset.idx) const groups = this.data.condGroups.slice() @@ -634,12 +1235,17 @@ Page({ onGroupNumberInput(e) { const idx = Number(e.currentTarget.dataset.idx) const field = e.currentTarget.dataset.field - let val = Number(e.detail.value || 0) + const raw = e.detail.value + let val = raw === '' ? '' : Number(raw || 0) const groups = this.data.condGroups.slice() if (groups[idx]) { - if (field === 'timeout') { - const unit = Number(groups[idx].timeoutUnit || 0) - val = this.clampDetectByUnit(unit, val || 1) + if (val === '') { + // allow empty while typing + } else { + if (field === 'timeout') { + const unit = Number(groups[idx].timeoutUnit || 0) + val = this.clampDetectByUnit(unit, val || 1) + } } groups[idx][field] = val } @@ -670,13 +1276,18 @@ Page({ const field = e.currentTarget.dataset.field // 序号为只读,不允许通过输入更新 if (field === 'seq') return - let val = Number(e.detail.value || 0) + const raw = e.detail.value + let val = raw === '' ? '' : Number(raw || 0) const groups = this.data.condGroups.slice() const grp = groups[gidx] if (grp && grp.items[iidx]) { - if (field === 'judgeTime') { - const unit = Number(grp.items[iidx].judgeUnit || 0) - val = this.clampDetectByUnit(unit, val || 1) + if (val === '') { + // allow empty while editing + } else { + if (field === 'judgeTime') { + const unit = Number(grp.items[iidx].judgeUnit || 0) + val = this.clampDetectByUnit(unit, val || 1) + } } grp.items[iidx][field] = val } @@ -725,16 +1336,21 @@ Page({ onPortNumberInput(e) { const idx = Number(e.currentTarget.dataset.idx) const field = e.currentTarget.dataset.field - let val = Number(e.detail.value || 0) + const raw = e.detail.value + let val = raw === '' ? '' : Number(raw || 0) const list = this.data.ports.slice() if (list[idx]) { - if (field === 'loop') { - val = this.clamp(val, 1, 12) - } else if (field === 'thresholdUp' || field === 'thresholdDown') { - val = this.clamp(val, 0, 100) - } else if (field === 'detectTime') { - const unit = Number(list[idx].detectUnit || 0) - val = this.clampDetectByUnit(unit, val) + if (val === '') { + // allow empty input while typing — keep empty string in UI + } else { + if (field === 'loop') { + val = this.clamp(val, 1, 12) + } else if (field === 'thresholdUp' || field === 'thresholdDown') { + val = this.clamp(val, 0, 100) + } else if (field === 'detectTime') { + const unit = Number(list[idx].detectUnit || 0) + val = this.clampDetectByUnit(unit, val) + } } list[idx][field] = val } @@ -755,23 +1371,28 @@ Page({ const list = this.data.ports.slice() if (list[idx]) { list[idx][field] = val - // 切换单位时同时校正检测时间范围 - const dt = Number(list[idx].detectTime || 0) - list[idx].detectTime = this.clampDetectByUnit(val, dt || 1) + // 切换单位时只有在 detectTime 非空时才校正检测时间范围(避免把空输入变为最小值) + const dtRaw = list[idx].detectTime + if (dtRaw !== '' && dtRaw != null) { + const dt = Number(dtRaw || 0) + list[idx].detectTime = this.clampDetectByUnit(val, dt || 1) + } } this.setData({ ports: list }) }, onSavePorts() { this.data.ports.forEach((p, i) => { - const P0 = p.deviceType & 0xFF - const P1 = p.deviceAddr & 0xFF - const loopLE = [p.loop & 0xFF, (p.loop >>> 8) & 0xFF] - const P4 = p.thresholdDown & 0xFF - const P5 = (i + 1) & 0xFF + const P0 = (p.deviceType || 0) & 0xFF + const P1 = (p.deviceAddr || 0) & 0xFF + const loopVal = this.getPortNumericValue(p, 'loop', 1) + const loopLE = [loopVal & 0xFF, ((loopVal >>> 8) & 0xFF)] + const P4 = this.getPortNumericValue(p, 'thresholdDown', 0) & 0xFF + const P5 = this.getVirtualPort(p, i) const P6 = p.enabled ? 0x01 : 0x00 - const dtLE = [p.detectTime & 0xFF, (p.detectTime >>> 8) & 0xFF] - const P9 = p.detectUnit & 0xFF - const P10 = p.thresholdUp & 0xFF + const dt = this.getPortNumericValue(p, 'detectTime', 1) + const dtLE = [dt & 0xFF, ((dt >>> 8) & 0xFF)] + const P9 = (this.normalizeUnitForPacket ? this.normalizeUnitForPacket(p.detectUnit || 0) : this.unitIndexToProtocolValue(p.detectUnit || 0)) & 0xFF + const P10 = this.getPortNumericValue(p, 'thresholdUp', 0) & 0xFF const payload = [P0, P1, ...loopLE, P4, P5, P6, ...dtLE, P9, P10] const pkt = buildCommand(COMMANDS.SET_CONDITION_2, payload, { frame: i + 1, framNum: this.data.ports.length }) this.appendLog('TX', `端口配置[${p.name}]: ${this.toHex(pkt)}`) @@ -785,6 +1406,27 @@ Page({ wx.showToast({ title: '已请求读取端口配置', icon: 'none' }) }, + // 获取端口字段的用于组包的数值:如果 UI 输入为空则返回字段的最小允许值 + getPortNumericValue(p, field, min) { + try { + if (!p) return min + const v = p[field] + if (v === '' || v == null) return min + const n = Number(v) + if (isNaN(n)) return min + // 对常见字段做范围约束 + if (field === 'loop') return this.clamp(n, 1, 12) + if (field === 'thresholdUp' || field === 'thresholdDown') return this.clamp(n, 0, 100) + if (field === 'detectTime') { + const unit = Number(p.detectUnit || 0) + return this.clampDetectByUnit(unit, n || min) + } + return n + } catch (e) { + return min + } + }, + // 条件配置交互 onCondNumberInput(e) { const idx = Number(e.currentTarget.dataset.idx) @@ -812,23 +1454,18 @@ Page({ }) this.setData({ conditions: flat }) flat.forEach((c, i) => { - const P0 = c.tag & 0xFF + const P0 = (typeof this.tagIndexToProtocolValue === 'function' ? this.tagIndexToProtocolValue(c.tag) : (((c && c.tag) || 0) + 1)) & 0xFF const P1 = c.group & 0xFF const P2 = c.seq & 0xFF const jtLE = [c.judgeTime & 0xFF, (c.judgeTime >>> 8) & 0xFF] - const P5 = c.judgeUnit & 0xFF - const bit = (v) => (v === 1 ? 1 : 0) - const b0 = bit(c.doorMag) - const b1 = bit(c.irHall) - const b2 = bit(c.bathRadar) - const b3 = bit(c.bathroomRadar) - const P6 = (b0 | (b1 << 1) | (b2 << 2) | (b3 << 3)) & 0xFF - const P7 = 0x00 - const P8 = 0x00 - const P9 = 0x00 - const P10 = 0x00 + const P5 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket(c.judgeUnit || 0) & 0xFF) : (c.judgeUnit & 0xFF) + const P6 = ((c.cardPower) || 0) & 0xFF + const P7 = ((c.doorMag) || 0) & 0xFF + const P8 = ((c.irHall) || 0) & 0xFF + const P9 = ((c.bathRadar) || 0) & 0xFF + const P10 = ((c.bathroomRadar) || 0) & 0xFF const toLE = [c.timeout & 0xFF, (c.timeout >>> 8) & 0xFF] - const P13 = c.timeoutUnit & 0xFF + const P13 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket(c.timeoutUnit || 0) & 0xFF) : (c.timeoutUnit & 0xFF) const payload = [P0, P1, P2, ...jtLE, P5, P6, P7, P8, P9, P10, ...toLE, P13] const pkt = buildCommand(COMMANDS.SET_CONDITION_1, payload, { frame: i + 1, framNum: flat.length }) this.appendLog('TX', `条件配置[组${c.group}/序${c.seq}]: ${this.toHex(pkt)}`) @@ -836,33 +1473,732 @@ Page({ wx.showToast({ title: '条件配置已发送', icon: 'success' }) }, - // 功能栏示例事件 - onDeleteCondGroup() { this.appendLog('UI', '操作: 删除条件组'); wx.showToast({ title: '删除条件组', icon: 'none' }) }, - onDeleteCondition() { this.appendLog('UI', '操作: 删除条件'); wx.showToast({ title: '删除条件', icon: 'none' }) }, - onAddCondGroup() { this.appendLog('UI', '操作: 添加条件组'); wx.showToast({ title: '添加条件组', icon: 'none' }) }, - onAddCondition() { this.appendLog('UI', '操作: 添加条件'); wx.showToast({ title: '添加条件', icon: 'none' }) }, - onExport() { this.appendLog('UI', '操作: 导出'); wx.showToast({ title: '导出', icon: 'none' }) }, - - onImport() { - const now = new Date() - const stamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` - this.setData({ importFileName: `已导入 ${stamp}` }) - this.appendLog('UI', '操作: 导入') - wx.showToast({ title: '导入', icon: 'none' }) + // 功能栏:删除条件组 + onDeleteCondGroup() { + const groups = (this.data.condGroups || []).map(g => String(g.group)) + if (groups.length === 0) { wx.showToast({ title: '无可删除的条件组', icon: 'none' }); return } + // 打开内置模态框选择组 + this.setData({ deleteDialogVisible: true, deleteDialogMode: 'group', deleteDialogGroups: groups, deleteDialogSeqs: [], deleteDialogSelectedGroupIndex: 0, deleteDialogSelectedSeqIndex: 0 }) }, + // 功能栏:删除条件(先选组,再选序号) + onDeleteCondition() { + // 保持 UI 当前编辑内容 + this.syncCondGroupsToConditions() + const groups = (this.data.condGroups || []).map(g => String(g.group)) + if (groups.length === 0) { wx.showToast({ title: '无可删除的条件', icon: 'none' }); return } + // 计算默认选中组的序号列表 + const defaultGroup = Number(groups[0]) + const items = (this.data.conditions || []).filter(c => Number(c.group) === defaultGroup).sort((a,b)=>a.seq-b.seq) + const seqList = items.map(it => `序号 ${it.seq}`) + this.setData({ deleteDialogVisible: true, deleteDialogMode: 'condition', deleteDialogGroups: groups, deleteDialogSeqs: seqList, deleteDialogSelectedGroupIndex: 0, deleteDialogSelectedSeqIndex: 0 }) + }, + + // 功能栏:添加条件组(在 conditions 中添加一条默认条件,并重建视图) + onAddCondGroup() { + // 在添加前同步当前 UI 编辑,避免覆盖未保存修改 + this.syncCondGroupsToConditions() + const conds = (this.data.conditions || []).slice() + // 计算新组号:取现有最大组号 +1,否则为1 + const groups = conds.map(c => Number(c.group)) + const max = groups.length ? Math.max(...groups) : 0 + const newGroup = max + 1 + // 默认条件数据 + const def = { + group: newGroup, + seq: 1, + tag: 0, // 无人至有人 + cardPower: 0, + doorMag: 1, // 触发 + irHall: 0, + bathRadar: 0, + bathroomRadar: 0, + judgeTime: 0, + judgeUnit: 2, + timeout: 1, + timeoutUnit: 2 + } + conds.push(def) + this.setData({ conditions: conds }) + this.buildCondGroups() + this.appendLog('UI', `已添加条件组 ${newGroup}`) + wx.showToast({ title: '已添加条件组', icon: 'success' }) + }, + + // 功能栏:添加条件(选择组后在该组尾部追加一条默认条件) + onAddCondition() { + // 先同步当前 UI 编辑内容 + this.syncCondGroupsToConditions() + const groups = (this.data.condGroups || []).map(g => String(g.group)) + if (groups.length === 0) { wx.showToast({ title: '请先添加条件组', icon: 'none' }); return } + this.setData({ addDialogVisible: true, addDialogGroups: groups, addDialogSelectedGroupIndex: 0 }) + }, + + // 添加模态:取消 + onCancelAddDialog() { + this.setData({ addDialogVisible: false, addDialogGroups: [], addDialogSelectedGroupIndex: 0 }) + }, + + // 添加模态:确认 + onConfirmAddDialog() { + // 在确认添加前同步 UI 编辑内容 + this.syncCondGroupsToConditions() + const groups = this.data.addDialogGroups || [] + const sel = Number(this.data.addDialogSelectedGroupIndex || 0) + const group = Number(groups[sel]) + if (!group) { wx.showToast({ title: '请选择条件组', icon: 'none' }); return } + const conds = (this.data.conditions || []).slice() + const items = conds.filter(c => Number(c.group) === group) + const maxSeq = items.length ? Math.max(...items.map(i=>i.seq)) : 0 + const def = { + group: group, + seq: maxSeq + 1, + tag: 0, + cardPower: 0, + doorMag: 1, + irHall: 0, + bathRadar: 0, + bathroomRadar: 0, + judgeTime: 0, + judgeUnit: 2, + timeout: 0, + timeoutUnit: 2 + } + conds.push(def) + this.setData({ conditions: conds }) + this.buildCondGroups() + this.appendLog('UI', `已在组 ${group} 追加条件 序号 ${def.seq}`) + wx.showToast({ title: '已添加条件', icon: 'success' }) + this.onCancelAddDialog() + }, + + onAddDialogGroupChange(e) { + const idx = Number(e.detail.value || 0) + this.setData({ addDialogSelectedGroupIndex: idx }) + }, + + // 删除模态框:取消 + onCancelDeleteDialog() { + this.setData({ deleteDialogVisible: false, deleteDialogMode: '', deleteDialogGroups: [], deleteDialogSeqs: [], deleteDialogSelectedGroupIndex: 0, deleteDialogSelectedSeqIndex: 0 }) + }, + + // 删除模态框:确认 + onConfirmDeleteDialog() { + // 在执行删除前,先把当前编辑视图中的 condGroups 同步回 conditions, + // 避免用户在 UI(condGroups)中修改后未保存就执行删除/重排,导致改动丢失。 + try { + const flatFromView = [] + ;(this.data.condGroups || []).forEach(grp => { + (grp.items || []).forEach(it => flatFromView.push({ ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit })) + }) + this.setData({ conditions: flatFromView }) + } catch (e) { + // ignore + } + + const mode = this.data.deleteDialogMode + const groups = this.data.deleteDialogGroups || [] + if (mode === 'group') { + const selIdx = Number(this.data.deleteDialogSelectedGroupIndex || 0) + const grp = Number(groups[selIdx]) + // 执行组删除并重排 + let next = (this.data.conditions || []).filter(c => Number(c.group) !== grp) + const groupsLeft = Array.from(new Set(next.map(c => Number(c.group)))).sort((a,b)=>a-b) + const map = {} + groupsLeft.forEach((g, i) => { map[g] = i + 1 }) + const remapped = [] + groupsLeft.forEach(g => { + const items = next.filter(c => Number(c.group) === g).slice().sort((a,b)=>a.seq - b.seq) + items.forEach((it, idx) => { + const copy = { ...it } + copy.group = map[g] + copy.seq = idx + 1 + remapped.push(copy) + }) + }) + this.setData({ conditions: remapped }) + this.buildCondGroups() + this.appendLog('UI', `已删除条件组 ${grp},并重排组/序号`) + wx.showToast({ title: '已删除并重排', icon: 'success' }) + } else if (mode === 'condition') { + const gSel = Number(this.data.deleteDialogSelectedGroupIndex || 0) + const sSel = Number(this.data.deleteDialogSelectedSeqIndex || 0) + const group = Number(groups[gSel]) + const seqItems = (this.data.conditions || []).filter(c => Number(c.group) === group).slice().sort((a,b)=>a.seq - b.seq) + if (!seqItems || seqItems.length === 0) { wx.showToast({ title: '该组无条件', icon: 'none' }); this.onCancelDeleteDialog(); return } + const seq = seqItems[sSel] && seqItems[sSel].seq + if (seq == null) { wx.showToast({ title: '请选择序号', icon: 'none' }); return } + let next = (this.data.conditions || []).filter(c => !(Number(c.group) === group && Number(c.seq) === seq)) + const same = next.filter(c => Number(c.group) === group).slice().sort((a,b)=>a.seq - b.seq) + const others = next.filter(c => Number(c.group) !== group) + const rebuilt = [] + same.forEach((it, idx) => { const copy = { ...it }; copy.seq = idx + 1; rebuilt.push(copy) }) + let merged = [...others, ...rebuilt] + merged.sort((a,b)=> (Number(a.group)-Number(b.group)) || (a.seq - b.seq)) + // 重新映射组号为连续序号并在组内重排 seq + const groupsLeft = Array.from(new Set(merged.map(c => Number(c.group)))).sort((a,b)=>a-b) + const map = {} + groupsLeft.forEach((g, i) => { map[g] = i + 1 }) + const remapped = [] + groupsLeft.forEach(g => { + const items = merged.filter(c => Number(c.group) === g).slice().sort((a,b)=>a.seq - b.seq) + items.forEach((it, idx) => { + const copy = { ...it } + copy.group = map[g] + copy.seq = idx + 1 + remapped.push(copy) + }) + }) + this.setData({ conditions: remapped }) + this.buildCondGroups() + this.appendLog('UI', `已删除 条件组 ${group} 序号 ${seq},并重排组/序号`) + wx.showToast({ title: '已删除并重排', icon: 'success' }) + } + this.onCancelDeleteDialog() + }, + + // 模态框中选择组后刷新序号列表(仅用于删除条件模态) + onDeleteDialogGroupChange(e) { + const idx = Number(e.detail.value || 0) + const groups = this.data.deleteDialogGroups || [] + const group = Number(groups[idx]) + const items = (this.data.conditions || []).filter(c => Number(c.group) === group).slice().sort((a,b)=>a.seq - b.seq) + const seqList = items.map(it => `序号 ${it.seq}`) + this.setData({ deleteDialogSelectedGroupIndex: idx, deleteDialogSeqs: seqList, deleteDialogSelectedSeqIndex: 0 }) + }, + + onDeleteDialogSeqChange(e) { + const idx = Number(e.detail.value || 0) + this.setData({ deleteDialogSelectedSeqIndex: idx }) + }, + /* + * 导出当前端口与条件配置到本地文本文件并提供分享/预览入口 + * + * 目的: + * - 将当前页面内的 `ports` 与 `condGroups` 配置序列化为一组 W13 报文(按行文本), + * 并写入应用的用户数据目录,以便用户可在小程序外部保存或分享给他人。 + * + * 输入 (从页面状态读取): + * - this.data.ports: 端口配置数组(每个元素包含 deviceType, deviceAddr, loop, thresholdDown, enabled, detectTime, detectUnit, thresholdUp 等) + * - this.data.condGroups: 条件组数组(每组包含 group, timeout, timeoutUnit, items[],items 中包含 tag/seq/judgeTime/timeout 等) + * + * 输出: + * - 在 `wx.env.USER_DATA_PATH` 下生成一个 `w13_export_YYYYMMDD_HHMMSS.txt` 文本文件, + * 每行格式为: `CMD=XX FRAME=N HEX=....`,便于其它工具或同事复用。 + * - 写入成功后尝试通过小程序分享接口(如可用)直接分享文件给微信好友;若不可用或分享失败,回退到文档预览 `wx.openDocument` 供用户手动分享。 + * + * 错误处理与回退策略: + * - 先做能力检测(`wx.getFileSystemManager`、`wx.env.USER_DATA_PATH`),若缺失则立即失败并提示。 + * - 对单行生成做 try/catch,避免单个条目异常中断整个导出流程,并在日志中记录失败行号与错误信息。 + * - 写文件后对分享/打开接口均做类型检测与 try/catch,记录异常与设备端返回的错误信息(序列化保存),避免出现未捕获异常。 + * + * 使用说明(复现测试): + * 1. 在目标设备/开发者工具打开该页面,确保有可导出的 `ports` 与 `condGroups` 数据。 + * 2. 点击导出控件,观察页面顶部/底部的日志(appendLog)以及 toast 提示。 + * 3. 若分享失败,请将 appendLog 中关于 share/open 的 WARN/DEBUG 日志贴回以便定位。 + */ + onExport() { + // debugger + this.appendLog('UI', '操作: 导出') + wx.showLoading({ title: '正在生成导出文件...', mask: true }) + try { + // 环境检查:确保文件系统与打开文档接口可用 + if (typeof wx.getFileSystemManager !== 'function') { + wx.hideLoading() + wx.showToast({ title: '当前环境不支持文件写入', icon: 'none' }) + this.appendLog('WARN', '导出失败:wx.getFileSystemManager 不可用') + return + } + if (!wx.env || !wx.env.USER_DATA_PATH) { + // 某些运行环境下 wx.env 可能不可用,提前提示 + wx.hideLoading() + wx.showToast({ title: '无法确定用户数据目录', icon: 'none' }) + this.appendLog('WARN', '导出失败:wx.env.USER_DATA_PATH 不存在') + return + } + const lines = [] + // 1) 端口配置(命令 0x09) + const ports = Array.isArray(this.data.ports) ? this.data.ports : [] + if (typeof buildCommand !== 'function' || typeof this.toHex !== 'function') { + this.appendLog('WARN', '导出: 必需函数 buildCommand 或 toHex 不可用') + } + for (let i = 0; i < ports.length; i++) { + const p = ports[i] + try { + // 默认按照协议:deviceType 默认为 2,deviceAddr 默认为 1 + const P0 = (p && (p.deviceType != null)) ? (p.deviceType & 0xFF) : 2 + const P1 = (p && (p.deviceAddr != null)) ? (p.deviceAddr & 0xFF) : 1 + const loopLE = [((p && p.loop) || 0) & 0xFF, (((p && p.loop) || 0) >>> 8) & 0xFF] + const P4 = ((p && p.thresholdDown) || 0) & 0xFF + const P5 = this.getVirtualPort(p, i) + const P6 = (p && p.enabled) ? 0x01 : 0x00 + const dtLE = [((p && p.detectTime) || 0) & 0xFF, ((((p && p.detectTime) || 0) >>> 8) & 0xFF)] + const P9 = (this.normalizeUnitForPacket ? this.normalizeUnitForPacket((p && p.detectUnit) || 0) : this.unitIndexToProtocolValue((p && p.detectUnit) || 0)) & 0xFF + const P10 = ((p && p.thresholdUp) || 0) & 0xFF + const payload = [P0, P1, ...loopLE, P4, P5, P6, ...dtLE, P9, P10] + const pkt = (typeof buildCommand === 'function') ? buildCommand(COMMANDS.SET_CONDITION_2, payload, { frame: i + 1, framNum: ports.length }) : new Uint8Array([]) + const hex = (typeof this.toHex === 'function') ? this.toHex(pkt) : '' + // 导出只保留数据包的十六进制字符串(每行一个包) + lines.push(hex) + } catch (err) { + this.appendLog('WARN', `导出端口第${i + 1}行失败: ${err && (err.message || err)}`) + } + } + + // 2) 条件配置(命令 0x08) - 扁平化 condGroups + const flat = [] + ;(this.data.condGroups || []).forEach(grp => { + (grp.items || []).forEach(it => flat.push({ ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit })) + }) + for (let i = 0; i < flat.length; i++) { + const c = flat[i] + try { + const P0 = (typeof this.tagIndexToProtocolValue === 'function' ? this.tagIndexToProtocolValue((c && c.tag) || 0) : (((c && c.tag) || 0) + 1)) & 0xFF + const P1 = ((c && c.group) || 0) & 0xFF + const P2 = ((c && c.seq) || 0) & 0xFF + const jtLE = [((c && c.judgeTime) || 0) & 0xFF, (((c && c.judgeTime) || 0) >>> 8) & 0xFF] + const P5 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.judgeUnit) || 0) & 0xFF) : (((c && c.judgeUnit) || 0) & 0xFF) + // 新协议:P6~P10 为 5 字节端口状态(P6: 插卡状态,P7..P10 对应 doorMag, irHall, bathRadar, bathroomRadar) + const S0 = ((c && c.cardPower) || 0) & 0xFF // 插卡状态 + const S1 = ((c && c.doorMag) || 0) & 0xFF + const S2 = ((c && c.irHall) || 0) & 0xFF + const S3 = ((c && c.bathRadar) || 0) & 0xFF + const S4 = ((c && c.bathroomRadar) || 0) & 0xFF + const toLE = [((c && c.timeout) || 0) & 0xFF, ((((c && c.timeout) || 0) >>> 8) & 0xFF)] + const P13 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.timeoutUnit) || 0) & 0xFF) : (((c && c.timeoutUnit) || 0) & 0xFF) + const payload = [P0, P1, P2, ...jtLE, P5, S0, S1, S2, S3, S4, ...toLE, P13] + const pkt = (typeof buildCommand === 'function') ? buildCommand(COMMANDS.SET_CONDITION_1, payload, { frame: i + 1, framNum: flat.length }) : new Uint8Array([]) + const hex = (typeof this.toHex === 'function') ? this.toHex(pkt) : '' + // 导出只保留数据包的十六进制字符串(每行一个包) + lines.push(hex) + } catch (err) { + this.appendLog('WARN', `导出条件第${i + 1}行失败: ${err && (err.message || err)}`) + } + } + + // 3) 写文件到用户数据目录 + const fsm = wx.getFileSystemManager() + const now = new Date() + // 生成文件名格式: W13_Export_YYMMDDHHMMSS.txt (例如 260115114708 表示 2026-01-15 11:47:08) + const yy = String(now.getFullYear()).slice(-2) + const mm = (now.getMonth() + 1).toString().padStart(2, '0') + const dd = now.getDate().toString().padStart(2, '0') + const HH = now.getHours().toString().padStart(2, '0') + const MI = now.getMinutes().toString().padStart(2, '0') + const SS = now.getSeconds().toString().padStart(2, '0') + const stamp = `${yy}${mm}${dd}${HH}${MI}${SS}` + const fileName = `W13_Export_${stamp}.txt` + const filePath = `${wx.env.USER_DATA_PATH}/${fileName}` + const content = lines.join('\n') + fsm.writeFile({ + filePath, + data: content, + encoding: 'utf8', + success: () => { + wx.hideLoading() + this.appendLog('UI', `导出文件: ${filePath}`) + // 将导出文件名显示在功能栏 + try { this.setData({ importFileName: fileName }) } catch (e) { /* ignore */ } + wx.showToast({ title: '导出完成,正在打开', icon: 'success' }) + const stringifyErr = (e) => { + try { + if (e === undefined) return 'undefined' + if (e === null) return 'null' + if (typeof e === 'string') return e + return JSON.stringify(e) + } catch (xx) { + try { return String(e) } catch (yy) { return '[unstringifiable error]' } + } + } + + + // 优先尝试直接通过小程序分享接口分享文件给微信用户 + const tryShare = () => { + try { + this.appendLog('DEBUG', `shareFileMessage type=${typeof wx.shareFileMessage}, shareFile type=${typeof wx.shareFile}, openDocument type=${typeof wx.openDocument}`) + if (typeof wx.shareFileMessage === 'function') { + try { + wx.shareFileMessage({ filePath, success: () => { this.appendLog('UI', '已通过 shareFileMessage 分享') }, fail: (e) => { this.appendLog('WARN', `shareFileMessage 失败 ${stringifyErr(e)}`); fallbackOpen() } }) + } catch (ex) { + this.appendLog('WARN', `调用 shareFileMessage 抛出异常: ${stringifyErr(ex)}`) + fallbackOpen() + } + return true + } + if (typeof wx.shareFile === 'function') { + try { + wx.shareFile({ filePath, success: () => { this.appendLog('UI', '已通过 shareFile 分享') }, fail: (e) => { this.appendLog('WARN', `shareFile 失败 ${stringifyErr(e)}`); fallbackOpen() } }) + } catch (ex) { + this.appendLog('WARN', `调用 shareFile 抛出异常: ${stringifyErr(ex)}`) + fallbackOpen() + } + return true + } + return false + } catch (err) { + this.appendLog('WARN', `tryShare 内部错误: ${stringifyErr(err)}`) + return false + } + } + + const fallbackOpen = () => { + if (typeof wx.openDocument === 'function') { + try { + wx.openDocument({ filePath, fileType: 'txt', success: () => { this.appendLog('UI', '已打开导出文件供预览') }, fail: (e) => { wx.showToast({ title: '打开失败', icon: 'none' }); this.appendLog('WARN', `打开导出文件失败 ${stringifyErr(e)}`) } }) + } catch (ex) { + wx.showToast({ title: '打开失败', icon: 'none' }) + this.appendLog('WARN', `调用 openDocument 抛出异常: ${stringifyErr(ex)}`) + } + } else { + this.appendLog('WARN', '无法直接打开或分享导出文件,文件已保存') + wx.showToast({ title: '导出完成,文件已保存', icon: 'none' }) + } + } + + // 注意:小程序的分享接口通常要求由用户手势(点击)触发, + // 直接在异步回调中调用可能被平台拒绝(如 'can only be invoked by user TAP gesture')。 + // 因此这里弹出确认对话,用户点击“分享”后再尝试调用分享接口;否则回退到预览或仅提示已保存。 + wx.showModal({ + title: '导出完成', + content: '文件已导出,是否现在分享给微信联系人?', + confirmText: '分享', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + // 用户确认分享 —— 视为用户手势,可尝试调用分享接口 + try { + const shared = tryShare() + if (!shared) { + // 分享接口不可用或尝试失败,回退到打开预览 + fallbackOpen() + } + } catch (ex) { + this.appendLog('WARN', `用户触发分享时出错: ${stringifyErr(ex)}`) + fallbackOpen() + } + } else { + // 用户取消:仅提示文件已保存 + this.appendLog('UI', '用户取消分享,文件已保存') + wx.showToast({ title: '文件已保存', icon: 'none' }) + } + }, + fail: (e) => { + // 若弹窗本身失败(极少见),记录并回退打开预览 + this.appendLog('WARN', `显示分享确认对话框失败: ${stringifyErr(e)}`) + fallbackOpen() + } + }) + }, + fail: (e) => { + wx.hideLoading() + wx.showToast({ title: '写入文件失败', icon: 'none' }) + this.appendLog('WARN', `写入导出文件失败 ${e && e.errMsg}`) + } + }) + } catch (err) { + wx.hideLoading() + wx.showToast({ title: '导出失败', icon: 'none' }) + this.appendLog('WARN', `导出异常 ${err && err.message}`) + } + }, + + // 从微信聊天选择文件并导入配置 + // 要求:文件每行是数据包的十六进制字符串,所有端口包(0x09)必须在前,随后是条件包(0x08),且不允许其它命令或交错顺序 + onImport() { + + this.appendLog('UI', '操作: 导入') + // debugger + // 允许用户从聊天中选择文件(share 场景) + if (typeof wx.chooseMessageFile !== 'function') { + wx.showToast({ title: '当前环境不支持从聊天选择文件', icon: 'none' }) + this.appendLog('WARN', '导入失败:wx.chooseMessageFile 不可用') + return + } + wx.chooseMessageFile({ + count: 1, + type: 'file', + success: (res) => { + const tf = (res && res.tempFiles && res.tempFiles[0]) || null + if (!tf) { + wx.showToast({ title: '未选择文件', icon: 'none' }) + return + } + const filePath = tf.path || tf.tempFilePath || tf.url + const fileName = tf.name || (filePath ? filePath.split('/').pop() : '') + const fsm = wx.getFileSystemManager() + try { + fsm.readFile({ filePath, encoding: 'utf8', success: (r) => { + const content = (r && r.data) || '' + const rawLines = content.split(/\r?\n/).map(l => l.trim()).filter(Boolean) + if (rawLines.length === 0) { + wx.showToast({ title: '文件为空', icon: 'none' }) + this.appendLog('WARN', '导入失败:文件为空') + return + } + + const types = [] + const parsedPorts = [] + const parsedConditions = [] + try { + for (let i = 0; i < rawLines.length; i++) { + const line = rawLines[i] + // 使用 verifyHexPacket 校验并得到完整包字节数组 + const v = verifyHexPacket(line) + const pkt = v && v.packet + if (!pkt || pkt.length < 11) throw new Error(`第${i+1}行包格式不正确`) + const cmd = pkt[10] & 0xFF + types.push(cmd) + const params = pkt.slice(11) + if (cmd === COMMANDS.SET_CONDITION_2) { + // 解析端口配置(0x09) + if (params.length < 11) throw new Error(`第${i+1}行端口包长度不正确`) + const P0 = params[0] & 0xFF + const P1 = params[1] & 0xFF + const loop = (params[2] & 0xFF) | ((params[3] & 0xFF) << 8) + const P4 = params[4] & 0xFF + const P5 = params[5] & 0xFF + const P6 = params[6] & 0xFF + const dt = (params[7] & 0xFF) | ((params[8] & 0xFF) << 8) + const P9 = params[9] & 0xFF + const P10 = params[10] & 0xFF + parsedPorts.push({ portNo: P5, deviceType: P0, deviceAddr: P1, loop, thresholdDown: P4, enabled: (P6 === 1), detectTime: dt, detectUnit: P9, thresholdUp: P10 }) + } else if (cmd === COMMANDS.SET_CONDITION_1) { + // 解析条件配置(0x08) - 新协议:P6~P10 为 5 字节的端口状态(第1字节为插卡状态) + if (params.length < 14) throw new Error(`第${i+1}行条件包长度不正确`) + const P0 = params[0] & 0xFF + const P1 = params[1] & 0xFF + const P2 = params[2] & 0xFF + const jt = (params[3] & 0xFF) | ((params[4] & 0xFF) << 8) + const P5 = params[5] & 0xFF + const P6 = params[6] & 0xFF // 插卡状态 + const P7 = params[7] & 0xFF // 门磁状态 + const P8 = params[8] & 0xFF // 门口红外状态 + const P9 = params[9] & 0xFF // 洗手间状态 + const P10 = params[10] & 0xFF // 卧室状态 + const to = (params[11] & 0xFF) | ((params[12] & 0xFF) << 8) + const P13 = params[13] & 0xFF + // 将协议中的 tag 值(1..4) 转为 UI 索引(0..3) + const tagIdx = (typeof this.protocolValueToTagIndex === 'function') ? this.protocolValueToTagIndex(P0) : (P0 > 0 ? (P0 - 1) : 0) + parsedConditions.push({ tag: tagIdx, group: P1, seq: P2, judgeTime: jt, judgeUnit: P5, cardPower: P6, doorMag: P7, irHall: P8, bathRadar: P9, bathroomRadar: P10, timeout: to, timeoutUnit: P13 }) + } else { + throw new Error(`第${i+1}行包含不支持的命令 0x${cmd.toString(16)}`) + } + } + } catch (parseErr) { + wx.showToast({ title: '导入失败:文件内容格式错误', icon: 'none' }) + this.appendLog('WARN', `导入失败: ${parseErr && (parseErr.message || parseErr)}`) + return + } + + // 验证顺序:所有 0x09 在前,随后全部为 0x08,不允许其它交错 + let seen08 = false + for (let t of types) { + if (t === COMMANDS.SET_CONDITION_2) { + if (seen08) { + wx.showToast({ title: '导入失败:端口与条件顺序不正确', icon: 'none' }) + this.appendLog('WARN', '导入失败:端口包必须在前,条件包在后,发现交错') + return + } + } else if (t === COMMANDS.SET_CONDITION_1) { + seen08 = true + } + } + + // 将解析的端口数据写入当前 ports(按 portNo-1 索引) + const ports = (this.data.ports || []).slice() + for (let p of parsedPorts) { + const idx = (p.portNo || 1) - 1 + if (idx < 0 || idx >= ports.length) { + wx.showToast({ title: '导入失败:端口编号超出范围', icon: 'none' }) + this.appendLog('WARN', `导入失败:端口编号 ${p.portNo} 超出范围`) + return + } + ports[idx] = { ...ports[idx], deviceType: p.deviceType, deviceAddr: p.deviceAddr, loop: p.loop, thresholdDown: p.thresholdDown, enabled: !!p.enabled, detectTime: p.detectTime, detectUnit: p.detectUnit, thresholdUp: p.thresholdUp } + } + + // 将解析的条件写入 conditions 并构建 condGroups + const conditions = parsedConditions.map(c => ({ tag: c.tag, group: c.group, seq: c.seq, judgeTime: c.judgeTime, judgeUnit: c.judgeUnit, doorMag: c.doorMag, irHall: c.irHall, bathRadar: c.bathRadar, bathroomRadar: c.bathroomRadar, timeout: c.timeout, timeoutUnit: c.timeoutUnit })) + + // 最终更新数据并记录导入文件名 + this.setData({ ports, conditions, importFileName: fileName }) + // 根据新的 conditions 构建分组视图 + this.buildCondGroups() + wx.showToast({ title: '导入成功', icon: 'success' }) + this.appendLog('UI', `导入完成: ${fileName} (端口 ${parsedPorts.length} 条, 条件 ${parsedConditions.length} 条)`) + }, fail: (e) => { + wx.showToast({ title: '读取文件失败', icon: 'none' }) + this.appendLog('WARN', `读取导入文件失败 ${e && e.errMsg}`) + } }) + } catch (ex) { + wx.showToast({ title: '导入失败', icon: 'none' }) + this.appendLog('WARN', `导入异常 ${ex && (ex.message || ex)}`) + } + }, + fail: (e) => { + wx.showToast({ title: '选择文件失败', icon: 'none' }) + this.appendLog('WARN', `chooseMessageFile 失败 ${e && e.errMsg}`) + } + }) + }, + + + // 一键下发:同时下发端口配置与条件配置 onOneKeySend() { - try { - // 先发送端口配置 - this.onSavePorts() - // 再发送条件配置 - this.onSaveConditions() - wx.showToast({ title: '已一键下发', icon: 'success' }) - this.appendLog('UI', '操作: 一键下发') - } catch (err) { - wx.showToast({ title: '下发失败', icon: 'none' }) + // 按序下发:先端口(0x09)再条件(0x08)。每条发送间隔500ms;重发3次,重发间隔200ms;每次发送后等待回复且回复 P0===0x01 + const sendAll = async () => { + try { + // debugger + this.appendLog('UI', '操作: 一键下发(开始)') + // 标准化 ports:缺失 deviceType/deviceAddr 时写入默认值,确保后续导出/下发一致 + try { + const rawPorts = Array.isArray(this.data.ports) ? this.data.ports : [] + let changed = false + const normalized = rawPorts.map((p) => { + const np = { ...p } + if (np.deviceType == null || np.deviceType === 0) { + if (np.deviceType !== 2) { np.deviceType = 2; changed = true } + } + if (np.deviceAddr == null || np.deviceAddr === 0) { + if (np.deviceAddr !== 1) { np.deviceAddr = 1; changed = true } + } + return np + }) + if (changed) { + try { this.setData({ ports: normalized }) } catch (e) { /* ignore */ } + } + } catch (e) { /* ignore normalization error */ } + // 构造端口包列表 + const ports = Array.isArray(this.data.ports) ? this.data.ports : [] + const portPkts = [] + for (let i = 0; i < ports.length; i++) { + const p = ports[i] + const P0 = (p && p.deviceType) ? (p.deviceType & 0xFF) : 0 + const P1 = (p && p.deviceAddr) ? (p.deviceAddr & 0xFF) : 0 + const loopLE = [((p && p.loop) || 0) & 0xFF, (((p && p.loop) || 0) >>> 8) & 0xFF] + const P4 = ((p && p.thresholdDown) || 0) & 0xFF + const P5 = this.getVirtualPort(p, i) + const P6 = (p && p.enabled) ? 0x01 : 0x00 + const dtLE = [((p && p.detectTime) || 0) & 0xFF, ((((p && p.detectTime) || 0) >>> 8) & 0xFF)] + const P9 = (this.normalizeUnitForPacket ? this.normalizeUnitForPacket((p && p.detectUnit) || 0) : this.unitIndexToProtocolValue((p && p.detectUnit) || 0)) & 0xFF + const P10 = ((p && p.thresholdUp) || 0) & 0xFF + const payload = [P0, P1, ...loopLE, P4, P5, P6, ...dtLE, P9, P10] + const pkt = buildCommand(COMMANDS.SET_CONDITION_2, payload, { frame: i + 1, framNum: ports.length }) + portPkts.push({ pkt, label: `端口 ${p && p.name || (i+1)}` }) + const porhex = this.toHex(pkt) + console.log(porhex) + } + + // 构造条件包列表(扁平化 condGroups),并兼容旧字段名 + const flat = [] + ;(this.data.condGroups || []).forEach(grp => { + (grp.items || []).forEach(it => { + const item = { ...it, group: grp.group, timeout: grp.timeout, timeoutUnit: grp.timeoutUnit } + // 若旧字段 cardPower 存在,则在 tag 真的缺失时映射到 tag + if (item.tag == null && (item.cardPower != null)) item.tag = item.cardPower + // 保证 judgeUnit/timeoutUnit 有默认值 + if (item.judgeUnit == null) item.judgeUnit = (item.judgeUnit === 0 ? 0 : 2) + if (item.timeoutUnit == null) item.timeoutUnit = (item.timeoutUnit === 0 ? 0 : 2) + flat.push(item) + }) + }) + const condPkts = [] + for (let i = 0; i < flat.length; i++) { + const c = flat[i] + const P0 = (typeof this.tagIndexToProtocolValue === 'function' ? this.tagIndexToProtocolValue((c && c.tag) || 0) : (((c && c.tag) || 0) + 1)) & 0xFF + const P1 = ((c && c.group) || 0) & 0xFF + const P2 = ((c && c.seq) || 0) & 0xFF + const jtLE = [((c && c.judgeTime) || 0) & 0xFF, (((c && c.judgeTime) || 0) >>> 8) & 0xFF] + const P5 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.judgeUnit) || 0) & 0xFF) : (((c && c.judgeUnit) || 0) & 0xFF) + const P6 = ((c && c.cardPower) || 0) & 0xFF + const P7 = ((c && c.doorMag) || 0) & 0xFF + const P8 = ((c && c.bathRadar) || 0) & 0xFF + const P9 = ((c && c.bathroomRadar) || 0) & 0xFF + const P10 = ((c && c.irHall) || 0) & 0xFF + const toLE = [((c && c.timeout) || 0) & 0xFF, ((((c && c.timeout) || 0) >>> 8) & 0xFF)] + const P13 = (typeof this.normalizeUnitForPacket === 'function') ? (this.normalizeUnitForPacket((c && c.timeoutUnit) || 0) & 0xFF) : (((c && c.timeoutUnit) || 0) & 0xFF) + const payload = [P0, P1, P2, ...jtLE, P5, P6, P7, P8, P9, P10, ...toLE, P13] + const pkt = buildCommand(COMMANDS.SET_CONDITION_1, payload, { frame: i + 1, framNum: flat.length }) + condPkts.push({ pkt, label: `条件 ${c && c.group || ''}-${c && c.seq || i+1}` }) + const cohex = this.toHex(pkt) + console.log(cohex) + } + + // 确保已启用通知监听 + try { this.enableNotify() } catch (e) { /* ignore */ } + + // 发送辅助:对每个包进行最多 3 次重发,重发间隔200ms,发送成功后等待回复且回复 P0===0x01,成功后等待 500ms 再继续 + /** + * 带重试机制的蓝牙数据包发送函数 + * @param {Object} pktObj - 数据包对象,包含pkt和label属性 + * @param {string} cmdType - 命令类型,用于等待特定响应 + * @returns {Promise} 发送成功返回true,失败抛出错误 + * @description + * 1. 最大重试3次,每次重试间隔200ms + * 2. 先建立响应等待,再写入数据 + * 3. 成功响应需满足params[0] === 0x01 + * 4. 成功发送后等待100ms间隔 + * 5. 失败会记录日志并自动重试 + */ + const sendWithRetry = async (pktObj, cmdType) => { + const maxRetries = 3 + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + // 先建立等待响应的挂起,确保在写入过程中收到的通知不会丢失 + const waitPromise = this.waitForResponse(cmdType, 3000) + try { + await this.writeRawBytes(pktObj.pkt, pktObj.label) + } catch (writeErr) { + // 写入失败,取消挂起并抛出错误以触发重试/终止 + if (this._pendingResponse && typeof this._pendingResponse.reject === 'function') { + try { this._pendingResponse.reject(new Error('写入失败')) } catch (e) { /* ignore */ } + this._pendingResponse = null + } + throw writeErr + } + // 等待回复(期望类型为 cmdType),超时则抛出以触发重试 + const params = await waitPromise + // 期望 params[0] === 0x01 表示参数正确 + if (params && params.length > 0 && (params[0] & 0xFF) === 0x01) { + this.appendLog('UI', `${pktObj.label} 下发成功 (attempt ${attempt})`) + // 等待每条发送间隔 100ms + await new Promise(r => setTimeout(r, 100)) + return true + } else { + throw new Error('设备返回失败或参数不正确') + } + } catch (err) { + this.appendLog('WARN', `${pktObj.label} 发送 attempt ${attempt} 失败: ${err && (err.message || err)}`) + if (attempt < maxRetries) { + // 重发间隔 200ms + await new Promise(r => setTimeout(r, 200)) + continue + } + // 达到最大重试仍失败,抛出以停止整个流程 + throw new Error(`${pktObj.label} 下发失败`) + } + } + } + + // 先发送端口包 + for (let i = 0; i < portPkts.length; i++) { + const obj = portPkts[i] + await sendWithRetry(obj, COMMANDS.SET_CONDITION_2 & 0xFF) + } + // 再发送条件包 + for (let i = 0; i < condPkts.length; i++) { + const obj = condPkts[i] + await sendWithRetry(obj, COMMANDS.SET_CONDITION_1 & 0xFF) + } + + wx.showToast({ title: '下发完成', icon: 'success' }) + this.appendLog('UI', '一键下发完成') + } catch (err) { + wx.showToast({ title: '下发失败', icon: 'none' }) + this.appendLog('WARN', `一键下发失败: ${err && (err.message || err)}`) + } } + sendAll() }, onCheckboxChange(e) { @@ -938,8 +2274,14 @@ Page({ const now = new Date() const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` const finalText = this.data.withTimestamp ? `[${timeStr}] ${direction}: ${text}` : `${direction}: ${text}` + try { + // 同步输出到控制台,方便开发者在调试时查看 + console.log(finalText) + } catch (e) { /* ignore console errors */ } + const id = `${Date.now()}-${Math.random().toString(16).slice(2)}` const next = [{ id, time: timeStr, text: finalText }, ...this.data.logList] + this.setData({ logList: next }) } }) diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.wxml b/pages/basics/BluetoothDebugging/B13page/B13page.wxml index 6d70f3a..d904555 100644 --- a/pages/basics/BluetoothDebugging/B13page/B13page.wxml +++ b/pages/basics/BluetoothDebugging/B13page/B13page.wxml @@ -4,6 +4,8 @@ {{DevName || 'B13设备'}} + + @@ -14,6 +16,17 @@ + + + + + + + + + + + @@ -22,50 +35,98 @@ - + - - + + + + 房间有人 - - + + + + 房间无人 - - + + + + 门开 - - + + + + 门关 - - + + + + 卫浴有人 - - + + + + 卫浴无人 - - - - 开门延时 - {{openDelay}}s + + + + 门磁开廊灯事件设置 + + + + 触发延迟 + + {{doorTriggerOptions[doorTriggerIndex]}} + + 单位 + + {{eventUnits[doorTriggerUnitIndex]}} + + 释放延迟 + + {{doorReleaseOptions[doorReleaseIndex]}} + + 单位 + + {{eventUnits[doorReleaseUnitIndex]}} + + - - - - 卫浴延时 - {{bathDelay}}s + + + 卫浴雷达开卫浴灯事件设置 + + + + 触发延迟 + + {{bathTriggerOptions[bathTriggerIndex]}} + + 单位 + + {{eventUnits[bathTriggerUnitIndex]}} + + 释放延迟 + + {{bathReleaseOptions[bathReleaseIndex]}} + + 单位 + + {{eventUnits[bathReleaseUnitIndex]}} + + - @@ -122,13 +183,15 @@ {{bleVersion || '-'}} - 读取蓝牙信息 + 读取蓝牙信息 + {{importFileName || '未选择文件'}} + @@ -142,8 +205,8 @@ 端口配置 - - + + @@ -189,10 +252,10 @@ 条件配置 - - - - + + + + @@ -204,9 +267,7 @@ {{grp.group}} 超时: - - {{grp.timeout || 1}} - + 单位: {{timeUnits[grp.timeoutUnit]}} @@ -230,9 +291,7 @@ 持续判定时间: - - {{it.judgeTime || 1}} - + 单位: @@ -245,10 +304,10 @@ 有无人标记 有卡取电 - 开门磁 - 门口红外 - 卫红外 - 浴红外 + 门磁 + 门口 + 洗手间 + 卧室 @@ -291,6 +350,52 @@ - + + + + {{deleteDialogMode === 'group' ? '删除条件组' : '删除条件'}} + + + 选择条件组: + + {{deleteDialogGroups[deleteDialogSelectedGroupIndex]}} + + + + 选择条件组: + + {{deleteDialogGroups[deleteDialogSelectedGroupIndex]}} + + 选择条件序号: + + {{deleteDialogSeqs[deleteDialogSelectedSeqIndex] || '无'}} + + + + + + + + + + + + + + 添加条件 + + 选择条件组: + + {{addDialogGroups[addDialogSelectedGroupIndex]}} + + + + + + + + + + - + diff --git a/pages/basics/BluetoothDebugging/B13page/B13page.wxss b/pages/basics/BluetoothDebugging/B13page/B13page.wxss index ff526a0..388629d 100644 --- a/pages/basics/BluetoothDebugging/B13page/B13page.wxss +++ b/pages/basics/BluetoothDebugging/B13page/B13page.wxss @@ -17,15 +17,20 @@ .label { font-size: 24rpx; color: #606266; } .cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8rpx; margin-bottom: 10rpx; } -.card { background: #fff; border-radius: 14rpx; padding: 14rpx 6rpx; display: flex; flex-direction: column; align-items: center; } -.icon { width: 30rpx; height: 30rpx; border-radius: 8rpx; margin-bottom: 4rpx; } +.card { background: #fff; border-radius: 14rpx; padding: 24rpx 8rpx; display: flex; flex-direction: column; align-items: center; min-height: 140rpx; box-sizing: border-box; transition: transform 120ms ease, filter 120ms ease; } +.icon { width: 48rpx; height: 48rpx; border-radius: 10rpx; margin-bottom: 8rpx; } .icon.orange { background: #ff8f00; } .icon.red { background: #ff3b30; } .icon.green { background: #21c161; } .icon.blue { background: #0ea5e9; } .icon.gray { background: #9aa0a6; } +.icon { overflow: hidden; display:flex; align-items:center; justify-content:center; } +.icon-img { width: 100%; height: 100%; display: block; } .card-title { font-size: 24rpx; color: #555; } +/* 按下效果 */ +.card.pressed { transform: translateY(4rpx) scale(0.985); filter: brightness(0.94); box-shadow: none; } + .slider-card { background: #fff; border-radius: 14rpx; padding: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); margin-bottom: 10rpx; } .slider-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6rpx; } .slider-head .title { font-size: 26rpx; color: #333; } @@ -56,6 +61,26 @@ .dr-btn-view { background: #0ea5e9; color: #ffffff; border-radius: 12rpx; box-shadow: 0 2rpx 6rpx rgba(14,165,233,0.35); } .dr-btn-view:active { opacity: 0.85; } +/* 顶部操作按钮:紧凑内间距,适配工具栏顶部的多个小按钮 */ +.top-actions button { + padding: 0 10rpx; + height: 58rpx; + line-height: 58rpx; + border-radius: 10rpx; + font-size: 24rpx; +} + +/* 确保 OTA 按钮为蓝色(优先覆盖可能的主题色) */ +.top-actions .ota-btn { + background: #0ea5e9 !important; + color: #ffffff !important; + border: 1rpx solid rgba(14,165,233,0.9) !important; + box-shadow: 0 2rpx 6rpx rgba(14,165,233,0.25); +} + +/* 小尺寸按钮,适用于工具栏中次级操作,如读取蓝牙信息 */ +.dr-btn-small { padding: 0 12rpx; min-width: 120rpx; height: 64rpx; line-height: 64rpx; } + /* 配置表样式 */ .cfg-card { background:#fff; border-radius: 14rpx; padding: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); } .cfg-head { font-size: 26rpx; color:#333; margin-bottom: 10rpx; } @@ -63,10 +88,41 @@ .head-actions { display:flex; gap: 10rpx; } .cond-card .head-actions { gap: 8rpx; } .cond-card .head-actions button { padding: 0 14rpx; height: 48rpx; line-height: 48rpx; } -.toolbar { display:flex; flex-wrap: wrap; gap: 8rpx; align-items: center; justify-content: space-between; } -.toolbar .import-box { min-width: 260rpx; padding: 6rpx 10rpx; border: 1rpx solid #e5e9f2; border-radius: 10rpx; background:#f7f8fa; font-size: 22rpx; color:#606266; } -.toolbar .toolbar-actions { display:flex; align-items:center; gap: 6rpx; } -.toolbar .toolbar-actions button { padding: 0 12rpx; } +.toolbar { + /* 单行布局:文件名在左,三个按钮在右 */ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8rpx; +} +.toolbar .import-box { + /* 文件名靠左,占用剩余空间 */ + flex: 1 1 auto; + margin-right: 8rpx; + padding: 6rpx 10rpx; + border: 1rpx solid #e5e9f2; + border-radius: 10rpx; + background: #f7f8fa; + font-size: 22rpx; + color: #606266; + text-align: left; + /* 限制不换行、不扩展容器,溢出时截断并显示省略号 */ + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.toolbar .toolbar-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10rpx; +} +.toolbar .toolbar-actions button { padding: 0 12rpx; height: 54rpx; line-height: 54rpx; margin: 0; } + +/* 隐藏特定按钮(通过在 WXML 上添加 class="hide-btn" 使用) */ +.hide-btn { display: none !important; } .table.header, .table.body { display:flex; flex-direction: column; gap: 6rpx; } .table.port .tr { display:grid; grid-template-columns: 1.4fr 1.2fr 1.4fr 1.4fr 0.8fr 1.1fr 1fr; gap: 6rpx; padding: 6rpx 0; } .table.body.port { gap: 2rpx; } @@ -102,6 +158,26 @@ .cond-scroll { max-height: 680rpx; border: 1rpx solid #e5e9f2; border-radius: 12rpx; padding: 6rpx; box-sizing: border-box; background: #fff; } .group-card { background:#fafafa; border: 1rpx solid #edf0f5; border-radius: 12rpx; padding: 10rpx; } .group-head .group-info { display:flex; align-items:center; gap: 12rpx; } + +.dialog-mask { + position: fixed; + left: 0; right: 0; top: 0; bottom: 0; + background: rgba(0,0,0,0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; +} +.dialog { + width: 680rpx; + background: #fff; + border-radius: 12rpx; + padding: 24rpx; +} +.dialog-title { font-size: 32rpx; margin-bottom: 12rpx; } +.dialog-body { margin-bottom: 16rpx; } +.dialog-actions { display: flex; justify-content: flex-end; gap: 12rpx } +.dialog .picker-text { padding: 10rpx 6rpx; border: 1rpx solid #eee; border-radius: 6rpx } .group-title { font-size: 24rpx; color:#1e3a8a; font-weight: 600; } .group-seq { font-size: 24rpx; color:#1e3a8a; font-weight: 600; } .group-time { display:flex; align-items:center; gap: 8rpx; } @@ -115,3 +191,17 @@ .log-time { display: block; font-size: 22rpx; color: #9aa0a6; margin-bottom: 4rpx; } .log-text { display: block; font-size: 24rpx; color: #333; word-break: break-all; } .log-empty { font-size: 24rpx; color: #9aa0a6; text-align: center; padding: 20rpx 0; } + +/* Inline form for trigger/release delay - keep controls on one row on mobile */ +.form-row { display:block; } +.form-inline { display:flex; align-items:center; gap: 10rpx; flex-wrap: nowrap; } +.form-inline .label { font-size: 22rpx; color:#606266; white-space: nowrap; } +.inline-input { width: 110rpx; height: 44rpx; padding: 0 8rpx; border: 1rpx solid #e5e9f2; border-radius: 8rpx; font-size: 24rpx; box-sizing: border-box; } +.inline-picker .picker-text { width: 90rpx; height: 44rpx; padding: 0 8rpx; border-radius: 8rpx; font-size: 22rpx; } + +/* If screen too narrow, allow wrapping but keep label + control groups together */ +@media (max-width: 360px) { + .form-inline { flex-wrap: wrap; } + .inline-input { width: 90rpx; } + .inline-picker .picker-text { width: 80rpx; } +} diff --git a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.js b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.js index 24c0a37..989bf80 100644 --- a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.js +++ b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.js @@ -405,7 +405,7 @@ wx.createSelectorQuery() that.setData({ faceSNCode: code }); - debugger + // debugger that.GetFaceSNOK(); }, fail: (err) => { diff --git a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxml b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxml index f836003..d81c526 100644 --- a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxml +++ b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxml @@ -35,11 +35,13 @@ - + - {{item.RoomNumber}} - {{item.FaceSN || '未绑定'}} + data-status="{{item.FaceStatus}}" data-faceSN="{{item.FaceSN}}" hover-class="navigator-hover" class="nav-li4 {{item.FaceStatus===true? 'online':'offline'}}" wx:for="{{HostsDataFilters}}" id="msg-{{index}}" wx:key="index"> + {{item.RoomNumber}} + + {{ item.FaceSN || (item.FaceStatus !== true ? '点击扫码绑定人脸机' : '') }} + diff --git a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxss b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxss index 33628d0..9a16dc6 100644 --- a/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxss +++ b/pages/basics/FacialDeviceBinding/FacialDeviceBinding.wxss @@ -11,6 +11,7 @@ display: flex; flex-wrap: wrap; padding: 0 10rpx; + background-color: #F0F0F0; /* 房间卡片的大框背景色 */ } /* 房间项样式 */ @@ -28,22 +29,14 @@ transform: scale(0.95); } -/* 在线状态样式 */ -.bg-green { - background-color: #4CD964; - color: white; +/* 在线/离线样式(改为 online/offline 以便跨页面复用) */ +.nav-li4.online { + background-color: #FFFFFF; /* 在线高亮白 */ + color: #111111; } - -/* 青色在线状态样式 */ -.bg-cyan { - background-color: #00BCD4; - color: white; -} - -/* 离线状态样式 */ -.bg-gray { - background-color: #E5E5EA; - color: #8E8E93; +.nav-li4.offline { + background-color: #C9CBCA; /* 设备离线浅灰色 */ + color: #111111; } /* 搜索框样式 */ diff --git a/pages/basics/HostUpgrade/EquipmentCaontrol/EquipmentCaontrol.wxml b/pages/basics/HostUpgrade/EquipmentCaontrol/EquipmentCaontrol.wxml index 7567b3b..d494505 100644 --- a/pages/basics/HostUpgrade/EquipmentCaontrol/EquipmentCaontrol.wxml +++ b/pages/basics/HostUpgrade/EquipmentCaontrol/EquipmentCaontrol.wxml @@ -24,6 +24,7 @@ 固件版本:{{devNode.Version}} + {{CurrentUpgradeDevStart? CurrentUpgradeDevStart:""}} @@ -33,17 +34,17 @@ - - {{Gfilename}} + + {{Gfilename?Gfilename:'\u00A0'}} - - {{Pfilename}} + + {{Pfilename?Pfilename:'\u00A0'}} - - + + diff --git a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.js b/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.js deleted file mode 100644 index b3f9b2b..0000000 --- a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.js +++ /dev/null @@ -1,2020 +0,0 @@ -// pages/Hosts/Hosts.js -const app = getApp() -//导入酒店信息请求发方法 -import { - GetHostsInfo, - GetMAC, - GetFaceSN, - ErrorInfo, - CheckFaceSN, - OpenDoorTest, - GetRoomType, - GetRoomTypeAndModalsListLog, - WebChatUpgrade, - QueryUpdateHostStatus, - ForwardQueryUpdateHostProgressBar, - GetRoomTypeNode, - SetRCULight, - SetRCUAir, - SetRCUCurtain, -} from '../../lib/RequestingCenter.js' -import md5 from '../../utils/md5.min.js' - -Page({ - /** - * 页面的初始数据 - */ - data: { - islogs: false, - //手动输入 - FaceSNinput: 0, - //手动输入 - input: 0, - //显示 0 全部 1 主机 2 人脸机 - showinfo:0, - //帮助 - Help: false, - //弹窗 - modal: 0, - //房间数据 - HostsData: null, - //过滤后房间数据 - HostsDataFilters: null, - //酒店信息 - Hotelinfo: null, - //是否返回 只有一个酒店不能返回 - isback: true, - //已经绑定是数量 - YMac: 0, - //未绑定是数量 - NMac: 0, - //筛选按钮的值 - sel: 0, - //权限信息 - autho: null, - //选择的房间信息 - selHosts: null, - //扫描的条形码 - code: null, - //swview高度 - top_height: 0, - top_Nheight: 0, - //本次操作的状态数据 - statusdata: [0, 0, 0, 0], - //mac绑定的酒店 服务器获取 - bdHosts:[], - //房型信息 - roomtype:[], - //升级状态 - Upgradestatus:"", - UpgradestatusTimeout:"", - UpgradestatusTimeout2:"", - UpgradestIstrue:true, - Upgradenode:[], - //房间信息 - roomtypeInfo:[], - //房间回路信息 - roomtypeInfoNode:[], - roomtypeInfoNodeinfo:[], - - ISLoopDebugging:0, - // 播放音量 - VolumeLevel:30, - - roomGfilename:"", - roomPfilename:"", - }, - // 监听单个字段 - - - observersUpgradestatus: function() { - - let Upgradestatus=this.data.Upgradestatus - let UpgradestatusTimeout2=this.data.UpgradestatusTimeout2 - if (Upgradestatus=="升级就绪"||Upgradestatus[0]=='.'||Upgradestatus[Upgradestatus.length-1]=="%") { - - if (UpgradestatusTimeout2>0) { - this.setData({ - UpgradestatusTimeout:UpgradestatusTimeout2 - }) - this.observersUpgradestatusTimeout() - } - - } - - }, - observersUpgradestatusTimeout:async function () { - let _this =this - let Upgradestatus=this.data.Upgradestatus - var timestampSec = Math.floor(Date.now() / 1000); - var UpgradestatusTimeout=this.data.UpgradestatusTimeout - if ((timestampSec-UpgradestatusTimeout) >120) { - _this.setData({ - Upgradestatus:"升级超时!", - UpgradestatusTimeout2:0 - }) - - }else{ - - - setTimeout(async function () { - let UpgradestIstrue =_this.data.UpgradestIstrue - let selHosts =_this.data.selHosts - let ID =selHosts.HotelID - let bID =selHosts.Id - let Upgradenode=_this.data.Upgradenode - let RoomTypeID = selHosts.RoomTypeID - let dastr =_this.data.Upgradestatus - let Version =Upgradenode.CFG_CURR_VER + ".0.0" - let str= Upgradenode.App_Cfg_For_L4 - var getnode - //let gVersion = str.split('_')[0]+"_"+ str.split('_')[1]+"_"+ str.split('_')[2]+"_"+ str.split('_')[3]+"_"+str.split('_')[4] - if (dastr=="升级就绪") { - dastr='' - } - if (dastr[dastr.length-1]=="%") { - dastr='' - } - if (dastr.length>6) { - dastr='.' - }else{ - dastr=dastr+'.' - } - - if (UpgradestIstrue ==true){ - await QueryUpdateHostStatus({ - hotelid:ID, - roomTypeID:RoomTypeID - }).then(res => { - if (res.total ==1) { - - if (res.rows.length>0) { - getnode=res.rows[0] - //配置 - if (getnode.ConfigVersion==Version) { - _this.setData({ - Upgradestatus:'升级完成!', - UpgradestatusTimeout2:0 - }) - - } else{ - _this.setData({ - Upgradestatus:dastr, - UpgradestatusTimeout2:timestampSec - }) - } - - - - }else - { - _this.setData({ - Upgradestatus:dastr - }) - } - - } else{ - _this.setData({ - Upgradestatus:"服务器反馈设备升级失败!", - UpgradestatusTimeout2:0 - }) - } - - }, err => { - _this.setData({ - Upgradestatus:"服务器接口调用异常,设备升级失败!", - UpgradestatusTimeout2:0 - }) - }).catch(err => { - _this.setData({ - Upgradestatus:"服务器接口调用异常,设备升级失败!", - UpgradestatusTimeout2:0 - }) - }); - }else{ - - await ForwardQueryUpdateHostProgressBar({ - hostIDList:'['+bID+']' - - }).then(res => { - if (res.IsSuccess ==true) { - - if (res.Response.length>0) { - getnode=res.Response[0] - - if (getnode.BaiFenBi=="100%") { - _this.setData({ - Upgradestatus:'升级完成!', - UpgradestatusTimeout2:0 - }) - - } else{ - if (getnode.BaiFenBi.length>0) { - _this.setData({ - Upgradestatus:getnode.BaiFenBi, - UpgradestatusTimeout2:timestampSec - }) - } else{ - _this.setData({ - Upgradestatus:dastr, - UpgradestatusTimeout2:timestampSec - }) - } - - } - - - - }else - { - _this.setData({ - Upgradestatus:dastr - }) - } - - } else{ - _this.setData({ - Upgradestatus:"服务器反馈设备升级失败!", - UpgradestatusTimeout2:0 - }) - } - - }, err => { - _this.setData({ - Upgradestatus:"服务器接口调用异常,设备升级失败!", - UpgradestatusTimeout2:0 - }) - }).catch(err => { - _this.setData({ - Upgradestatus:"服务器接口调用异常,设备升级失败!", - UpgradestatusTimeout2:0 - }) - }); - } - _this.observersUpgradestatus() - - }, 1000) - - //定时询问 - - } - }, - changeVolumeLevel(e) - { - - let VolumeLevel =e.detail.value - console.log(e) - this.setData({ - VolumeLevel:VolumeLevel - }) - }, - - GetFaceSN_long:function (params) { - if (!this.CheckFaceSNAutho()) { - app.toast(2, "无绑定权限~") - return; - } - let index = params.currentTarget.dataset['index'] - // console.log(index) - let selHosts = this.data.HostsDataFilters[index]; - selHosts.index = index; - this.setData({ - selHosts: selHosts, - FaceSNinput: 100 - }) - if (this.data.HostsDataFilters[index].FaceSN != "" && this.data.HostsDataFilters[index].FaceSN != null) { - this.setData({ - modal: -1, - message: [ this.data.HostsDataFilters[index].FaceSN,selHosts.RoomNumber] - }) - } else { - this.ShowInputsn(); - } - }, - /// 解绑sn - JbSn:function(params) { - let jbjd = null; - let faceSN = null; - let that = this; - let index = -1; - //debugger - if( typeof params.currentTarget.dataset['index'] != 'undefined'){ - faceSN = this.data.code; - index = params.currentTarget.dataset['index'] - //解绑的房间 - jbjd = this.data.bdHosts[index]; - // 0 表示绑定成功~ - // 1 表示绑定失败~ - // 2 表示取消绑定~ - // xg - }else{ - jbjd = this.data.selHosts; - faceSN = this.data.selHosts.FaceSN; - } - console.log(jbjd) - - GetFaceSN({faceSN:faceSN,roomID:jbjd.Id,roomNumber:jbjd.RoomNumber,faceAddress:this.data.address,HotelID:jbjd.HotelID,isjb:true}).then( - res => { - if (res.Status == 200) { - switch (res.Data) { - case 0: - try { - app.toast(1, "解绑成功~"); - let data = that.data.HostsData; - let OLD = -1; - try { - data.forEach( - (x, INDEX) => { - if (x.RoomNumber === jbjd.RoomNumber) { - OLD = INDEX; - throw new Error(); - } - } - )} catch (error) { - } - if (OLD >= 0) { - // 0 表示绑定成功~ - // 1 表示绑定失败~ - // 2 表示取消绑定~ - data[OLD].xg = 2; - data[OLD].FaceSN = ""; - } - let res = that.GetFilters(that.data.sel, data); - let databdHosts = that.data.bdHosts; - let selHosts = that.data.selHosts; - // 表示 是查询到 sn被绑定 index>-1 ==-1 表示是长安解绑 - if(index>-1){ - databdHosts.splice(index,1); - }else{ - selHosts.FaceSN = null - } - that.setData({ - selHosts:selHosts, - HostsData: data, - bdHosts:databdHosts, - HostsDataFilters: res[0], - statusdata: res[1], - input: 0 - }) - } catch (error) { - console.log(error) - } - break; - case 1: - app.toast(2, "SN已经绑定酒店~") - break; - case 2: - app.toast(2, "SN绑定酒店失败~") - break; - case 3: - app.toast(2, "SN注册绑定酒店失败~") - break; - case 4: - app.toast(2, "未知错误~") - break; - case 5: - app.toast(2, "数据不符合~") - break; - case 6: - app.toast(2, "解绑失败~") - break; - default: - app.toast(2, "其他错误~") - break; - } - /// 0 成功 - /// 1 已经注册已经绑定酒店 - /// 2 已经注册更新失败 - /// 3 未注册为分配酒店 添加注册 添加酒店是啊比 - /// 4 未能预计的结果-- - /// 5 数据不符合 - /// 6 解绑失败 - console.log(0) - - } - else{ - app.toast(2, "网络繁忙") - } - }, - err => { - app.toast(2, "网络繁忙") - } - ).catch(err => { - app.toast(2, "网络繁忙") - }); - }, - CheckFaceMAC:function(vlues){ - return true; - console.log(vlues.length != 16) - return vlues.length == 16; - }, - SetDeviceSwitchStatus(e){ - let devarr=e.target.dataset.index - let devval=parseInt(e.target.dataset.value) - let ID=parseInt(e.target.id) - let type =e.target.dataset.type - debugger - this.SetDevStartToHostMachine(ID,devarr,devval,type) - console.log(e) - - }, - SetDeviceSwitchStatusslider(e){ - let devarr=e.target.dataset.index - let devval=parseInt(e.detail.value) - let ID=parseInt(e.target.id) - - this.SetDevStartToHostMachine(ID,devarr,devval,0) - console.log(e) - - }, - SetDevStartToHostMachine: async function (id,devarr,Handletype,type){ - let roomtypeInfoNodeinfo=this.data.roomtypeInfoNodeinfo - let narry=[] - for (let index = 0; index < roomtypeInfoNodeinfo.length; index++) { - const element = roomtypeInfoNodeinfo[index]; - if (element[0]===id) { - for (let nindex = 0; nindex < element[1].length; nindex++) { - const nelement = element[1][nindex]; - if (nelement[0]===devarr) { - narry =this.SetloopDataChange(id,nelement[2] ,Handletype,type) - let iset =await this.ControlStatusMainCircuit(id,narry,devarr) - if (iset) { - roomtypeInfoNodeinfo[index][1][nindex][2]=narry - this.setData({ - roomtypeInfoNodeinfo:roomtypeInfoNodeinfo - }) - } - - - return - } - } - return - } - } - }, - SetloopDataChange(id, dataarry,Handletype,type){ - - let result =[] - switch (id) { - case 1: - if (dataarry[0][2]==1) { - dataarry[0][2]=0 - }else - { - dataarry[0][2]=1 - } - result=dataarry - break; - case 23: - case 5: - dataarry[0][2]=Handletype - result=dataarry - break; - case 52: - let devval=parseInt(type) - if (devval==1) { - if ( dataarry[devval-1][2]==1) { - dataarry[devval-1][2]=0 - result=dataarry - }else{ - dataarry[devval-1][2]=1 - result=dataarry - } - - }else{ - dataarry[devval-1][2]=Handletype - result=dataarry - } - - break; - case 7: - let bevval=parseInt(type) - switch (bevval) { - case 1: - if ( dataarry[bevval-1][2]==1) { - dataarry[bevval-1][2]=0 - result=dataarry - }else{ - dataarry[bevval-1][2]=1 - result=dataarry - } - break; - case 3: - case 2: - if (dataarry[bevval-1][2]==4) { //制冷 制热 送风 自动 - dataarry[bevval-1][2]=1 - }else{ - dataarry[bevval-1][2]= dataarry[bevval-1][2]+1 - } - - break; - - case 4: //- - if (dataarry[bevval-1][2]==16) { - break; - }else{ - dataarry[bevval-1][2]=dataarry[bevval-1][2]-1 - } - break - case 5: //+ - - if (dataarry[bevval-2][2]==32) { - break; - }else{ - dataarry[bevval-2][2]=dataarry[bevval-2][2]+1 - } - - break; - - default: - break; - } - - // bint.push([7,[[1,'开关',0],[2,'模式',0],[3,'风速',0],[4,'温度',25],[5,'阀开',0]]]) - result=dataarry - break; - default: - break; - } - return result - }, - - ControlStatusMainCircuit:async function (id, dataarry,addr){ - let HostsDataFilters =this.data.HostsDataFilters - let selHosts =this.data.selHosts - let Hotelinfo=this.data.Hotelinfo - let timestr = HostsDataFilters[0].CreateTime - console.log(timestr) - - let timestamp = parseInt(timestr.match(/\d+/)[0]); - let date = new Date(timestamp); - - // 格式化输出(示例:YYYY-MM-DD HH:mm:ss) - let year = date.getFullYear(); - let month = (date.getMonth() + 1).toString().padStart(2, '0'); - let day = date.getDate().toString().padStart(2, '0'); - let CreateTime = `${year}-${month}-${day}` - let res=[] - let sw =dataarry[0][2] - - switch (id) { - case 1: - sw= sw==1 ? 1:2 - res = await SetRCULight({ - roomNumber:selHosts.RoomNumber, - code:Hotelinfo.Code, - creatDate:CreateTime, - status: sw, - modalAddress:addr, - brightness:0 - }) - break; - - - case 5: - res = await SetRCUCurtain({ - roomNumber:selHosts.RoomNumber, - code:Hotelinfo.Code, - creatDate:CreateTime, - modalAddress:addr, - status:sw - }) - break - case 7: - - res = await SetRCUAir({ - roomNumber:selHosts.RoomNumber, - code:Hotelinfo.Code, - creatDate:CreateTime, - modalAddress:addr, - onOff:sw, - temperature:dataarry[3][2], - fanSpeed:dataarry[2][2]==4? 0:dataarry[2][2], - mode:dataarry[1][2]==4? 0:dataarry[1][2], - valve:1 - }) - break - case 23: - res = await SetRCULight({ - roomNumber:selHosts.RoomNumber, - code:Hotelinfo.Code, - creatDate:CreateTime, - status: sw==0 ? 2:1, - modalAddress:addr, - brightness:sw - }) - break; - default: - return false - break; - } - - if (res.IsSuccess==true) { - return true - } - return false - - }, - GetFaceSNend:function(){ - this.GetHide(); - let that = this; - let xg = 1; - GetFaceSN({faceSN:this.data.code, roomID:this.data.selHosts.Id,roomNumber:this.data.selHosts.RoomNumber,faceAddress:this.data.address,HotelID:this.data.Hotelinfo.HotelId}).then( - res => { - if (res.Status == 200) { - switch (res.Data) { - case 0: - app.toast(1, "绑定成功~"); - xg = 0; - break; - case 1: - app.toast(2, "SN已经绑定酒店~"); - break; - case 2: - xg = 1; - app.toast(2, "SN绑定酒店失败~"); - break; - case 3: - app.toast(2, "SN注册绑定酒店失败~"); - break; - case 4: - app.toast(2, "未知错误~"); - break; - case 5: - app.toast(2, "数据不符合~"); - break; - case 6: - app.toast(2, "解绑失败~"); - break; - default: - app.toast(2, "其他错误~"); - break; - } - - - /// 0 成功 - /// 1 已经注册已经绑定酒店 - /// 2 已经注册更新失败 - /// 3 未注册为分配酒店 添加注册 添加酒店是啊比 - /// 4 未能预计的结果-- - /// 5 数据不符合 - /// 6 解绑失败 - try { - - - that.data.selHosts.xg = xg; - if(xg==0){ - that.data.selHosts.FaceSN = this.data.code; - } - let data = that.data.HostsData; - data[that.data.selHosts.index] = that.data.selHosts; - let res = that.GetFilters(that.data.sel, that.data.HostsData); - that.setData({ - HostsData: data, - HostsDataFilters: res[0], - statusdata: res[1], - code: null, - input: 0 - }) - } catch (error) { - console.log(error) - } - } - else{ - app.toast(2, "网络繁忙") - } - }, - err => { - app.toast(2, "网络繁忙") - } - ).catch(err => { - app.toast(2, "网络繁忙") - }); - }, - CheckFaceMACWL(){ - let that = this; - CheckFaceSN({faceSN:this.data.code}).then( - res => { - if (res.Status == 200) { - if(res.Data<=0){ - that.setData({ - modal:-521, - }) - }else{ - let remove = []; - let bdHosts = res.Data; - try { - //记录已经分配酒店但是未分配房间 且酒店是当前酒店 那就判断为没有绑定 - that.data.autho.forEach((element, index) => { - element.Hotels.forEach((elements, indexs) => { - for (let index = 0; index < bdHosts.length; index++) { - if (elements.HotelId == bdHosts[index].HotelID) { - if(bdHosts[index].HotelID == that.data.Hotelinfo.HotelId && bdHosts[index].Id==0){ - remove.push(index); - } - elements.Auth.forEach (Auth=>{ - if(Auth.AuthorityId == 21 && Auth.AuthotypeId == 3){ - //有权限 - bdHosts[index].qx = 0; - } - }) - } - } - }) - }); - } catch (error) { - console.log(error) - } - remove.forEach (x=>{ - bdHosts.splice(x, 1); - }) - let modalval = -200; - if(bdHosts.length<=0){ - modalval = -521; - } - that.setData({ - modal:modalval, - bdHosts:bdHosts - }) - // app.toast(2, "已经被绑定") - } - }else{ - app.toast(2, "网络繁忙") - } - }, - err => { - console.log(err) - app.toast(2, "网络繁忙") - }).catch(err => { - console.log(err) - app.toast(2, "网络繁忙") - }); - }, - GetFaceSNOK:function(){ - let that = this; - if (!this.CheckFaceMAC(that.data.code)) { - this.setData({ - modal: 520 - }) - return; - }; - if (that.data.selHosts.FaceSN == that.data.code) { - app.toast(2, "条码一致,无需更改~") - return; - } - this.CheckFaceMACWL() - }, - //GetFaceCode 人脸机扫码 - GetFaceCode:function(){ - var that = this; - setTimeout(function () { - that.GetHide() - }, 100); - wx.scanCode({ - // onlyFromCamera: true,// 只允许从相机扫码 - success(res) { - // 扫码成功后 在此处理接下来的逻辑 - that.setData({ - code: res.result - }) - that.GetFaceSNOK(); - }, - fail(err) { - app.toast(2, "未识别到有效条形码") - } - }) - }, - //检查人脸机房间权限 - CheckFaceSNAutho: function () { - let res = false; - this.data.Hotelinfo.Auth.forEach(x=>{ - if(x.AuthorityId==21 && x.AuthotypeId == 3){ - res = true; - } - }); - return res; - }, -// 点击人脸机 - GetFaceSN:function (params) { - //长按事件 - if (this.endTime - this.startTime > 350) { - return; - } - if (!this.CheckFaceSNAutho()) { - app.toast(2, "无绑定权限~") - return; - } - let index = params.currentTarget.dataset['index'] - // console.log(index) - let selHosts = this.data.HostsDataFilters[index]; - selHosts.index = index; - this.setData({ - selHosts: selHosts, - FaceSNinput: 0 - }) - if (this.data.HostsDataFilters[index].FaceSN != "" && this.data.HostsDataFilters[index].FaceSN != null) { - this.setData({ - modal:-1, - message: [this.data.HostsDataFilters[index].FaceSN, selHosts.RoomNumber] - }) - } else { - this.GetFaceCode() - } - - }, -//人脸机测试 -testinfo:function(e){ -// console.log(e.currentTarget.id) -var jbjd=this.data.selHosts -// console.log(jbjd) -wx.navigateTo({ - url: '/pages/test/test?Hotelinfo=' + e.currentTarget.id+'&RoomID='+jbjd.Id+'&faceadd='+this.data.address -}) -}, - -//开门测试 -OpenDoor:function(e){ - var that=this; - console.log(e.currentTarget.id) - var sn=e.currentTarget.id.split("_") - console.log(sn) - OpenDoorTest({faceSN:sn[2],isjb:true}).then( - res => { - if (res.Status == 200) { - app.toast(2, res.Message) - } - else{ - app.toast(2, res.Message) - } - }, - err => { - app.toast(2, "网络繁忙") - } - ).catch(err => { - app.toast(2, "网络繁忙") - }); - }, - - - - //检查房间权限 - CheckAutho: function () { - let res = false; - this.data.Hotelinfo.Auth.forEach(x=>{ - if(x.AuthorityId==16 && x.AuthotypeId == 3){ - res = true; - } - }); - return res; - }, - //touch start 开始触摸房间区域 - handleTouchStart: function (e) { - this.startTime = e.timeStamp; - //console.log(" startTime = " + e.timeStamp); - }, - //touch end结束触摸房间区域 - handleTouchEnd: function (e) { - this.endTime = e.timeStamp; - //console.log(" endTime = " + e.timeStamp); - }, - // 帮助 - HelpClick: function (params) { - this.setData({ - islogs: false, - Help: !this.data.Help - }) - }, - //日志信息 - islogsClick: function (params) { - this.setData({ - islogs: !this.data.islogs, - Help: false - }) - }, - // 长按房间 - GetMAC_long: function (params) { - if (!this.CheckAutho()) { - app.toast(2, "无绑定权限~") - return; - } - let index = params.currentTarget.dataset['index'] - // console.log(index) - let selHosts = this.data.HostsDataFilters[index]; - selHosts.index = index; - this.setData({ - selHosts: selHosts, - input: 100 - }) - if (this.data.HostsDataFilters[index].MAC != "" && this.data.HostsDataFilters[index].MAC != null) { - this.setData({ - modal: 1, - message: [selHosts.RoomNumber, this.data.HostsDataFilters[index].MAC] - }) - } else { - this.ShowInput(); - } - }, - // 手动输入mac - ShowInput: function (params) { - this.setData({ - modal: 1000, - code:"" - }) - }, - // 手动输入sn - ShowInputsn: function (params) { - this.setData({ - modal: -1000, - code:"" - }) - }, - //解绑MAC - Jb:async function(params) { - let index = params.currentTarget.dataset['index'] - //解绑的酒店 - let jbjd = this.data.bdHosts[index]; - // this.GetMacOKend(false); - let that = this; - await GetMAC({ - roomNumber: jbjd.RoomNumber, - roomID: jbjd.Id, - HotelID: jbjd.HotelID, - MAC: "", - NoCheck: false, - loc: that.data.address - }).then( - res => { - if (res.Status == 1000) { - app.toast(2, "无权限") - } - if (res.Status == 200) { - app.toast(1, "解绑成功") - var databdHosts = that.data.bdHosts; - databdHosts.splice(index,1); - that.setData({ - bdHosts:databdHosts - }) - //如果酒店是当前酒店 就把当前酒店的房间 标记为解绑 - if(jbjd.HotelID != that.data.HotelId) - { - // console.log(jbjd.HotelID) - // console.log(that.data.HotelId) - return; - } - let data = that.data.HostsData; - let OLD = -1; - try { - data.forEach( - (x, INDEX) => { - if (x.RoomNumber === jbjd.RoomNumber) { - OLD = INDEX; - throw new Error(); - } - } - )} catch (error) { - } - if (OLD >= 0) { - // 0 表示绑定成功~ - // 1 表示绑定失败~ - // 2 表示取消绑定~ - data[OLD].xg = 2; - data[OLD].MAC = ""; - } - let res = that.GetFilters(that.data.sel, data); - that.setData({ - HostsData: data, - HostsDataFilters: res[0], - statusdata: res[1] - }) - console.log(data[OLD]) - } else { - app.toast(2, "解绑失败") - } - }, - err => { - app.toast(2, "网络繁忙") - }).catch(err => { - app.toast(2, "网络繁忙") - }); - }, - - FuzzyMatch(astr,bstr ){ - if (astr.includes(bstr)) { - return true - }else{ - return false - } - }, - - - //检查mac地址 - GetMACOK: function () { - let that = this; - if (!this.CheckMAC(that.data.code)) { - this.setData({ - modal: 520 - }) - return; - }; - var newmac = ""; - for (var i = 0; i < that.data.code.length; i += 2) { - if (i + 2 >= that.data.code.length) { - newmac += that.data.code[i] + that.data.code[i + 1]; - } else { - newmac += that.data.code[i] + that.data.code[i + 1] + "-"; - } - } - if (that.data.selHosts.MAC == newmac) { - app.toast(2, "条码一致,无需更改~") - return; - } - this.setData({ - modal: 521, - }) - }, - //最终绑定MAC - GetMacOKend: async function (type) { - let that = this; - if (type != true && type != false) { - type = type.currentTarget.dataset['type'] - } - this.GetHide(); - let xg = null; - await GetMAC({ - roomNumber: that.data.selHosts.RoomNumber, - HotelID: that.data.selHosts.HotelID, - MAC: that.data.code, - NoCheck: type, - loc: that.data.address - }).then( - res => { - if (res.Status == 1000) { - xg = 100; - app.toast(2, "无权限") - } - if (res.Status == 200) { - xg = 0; - app.toast(1, "绑定成功") - } else { - if (!type) { - xg = 1; - app.toast(2, "绑定失败") - } else { - try { - if (res.Status == 100) { - xg = 100; - let modal = 3; - let mesg = [that.data.code]; - //mac绑定的房间 - let bdHosts = res.Data.Hosts; - try { - that.data.autho.forEach((element, index) => { - element.Hotels.forEach((elements, indexs) => { - for (let index = 0; index < res.Data.Hosts.length; index++) { - if (elements.HotelId == res.Data.Hosts[index].HotelID) { - elements.Auth.forEach (Auth=>{ - if(Auth.AuthorityId == 16 && Auth.AuthotypeId == 3){ - //有权限 - bdHosts[index].qx = 0; - } - }) - } - } - }) - }); - } catch (error) { - console.log(error) - } - that.setData({ - modal: modal, - bdHosts:bdHosts, - message: mesg - }) - } else { - if (res.Status == 150) { - xg = 1; - app.toast(2, "绑定失败") - } else { - xg = 100; - app.toast(2, "网络繁忙") - } - } - } catch (error) { - xg = 100; - console.log(error) - } - } - } - }, - err => { - xg = 100; - app.toast(2, "网络繁忙") - }).catch(err => { - xg = 100; - app.toast(2, "网络繁忙") - }); - try { - // 100表示检查MAC并没有执行绑定操作 或者 请求过程出错 不执行下面的操作 - if (xg == 100) { - return; - } - //标记修改 - that.data.selHosts.xg = xg; - - // return; - var newmac = ""; - for (var i = 0; i < that.data.code.length; i += 2) { - if (i + 2 >= that.data.code.length) { - newmac += that.data.code[i] + that.data.code[i + 1]; - } else { - newmac += that.data.code[i] + that.data.code[i + 1] + "-"; - } - } - that.data.selHosts.MAC = newmac; - let data = that.data.HostsData; - // 本房间有相同的 mac的房间标记为取消绑定 已经弃用了 - // let OLD = -1; - // try { - // data.forEach( - // (x, INDEX) => { - // if (x.MAC == newmac && that.data.selHosts.Id != x.Id) { - // OLD = INDEX; - // throw new Error(); - // } - // } - // ) - // } catch (error) { - // console.log("房间 已经找到,无需循环~") - // } - // if (OLD >= 0) { - // // 0 表示绑定成功~ - // // 1 表示绑定失败~ - // // 2 表示取消绑定~ - // data[OLD].xg = 2; - // data[OLD].MAC = ""; - // } - data[that.data.selHosts.index] = that.data.selHosts; - let res = that.GetFilters(that.data.sel, that.data.HostsData); - that.setData({ - HostsData: data, - HostsDataFilters: res[0], - statusdata: res[1], - code: null, - input: 0 - }) - } catch (error) { - console.log(error) - } - }, - //检查Mac - CheckMAC: function (vlues) { - return (vlues.indexOf("34D0B8") == 0 && vlues.length == 12 && vlues.indexOf(" ") < 0); - }, - //扫码~ - GetCode: async function () { - var that = this; - setTimeout(function () { - that.GetHide() - }, 100); - wx.scanCode({ - // onlyFromCamera: true,// 只允许从相机扫码 - success(res) { - // 扫码成功后 在此处理接下来的逻辑 - that.setData({ - code: res.result - }) - that.GetMACOK(); - }, - fail(err) { - app.toast(2, "未识别到有效条形码") - } - }) - }, - //隐藏提示~ - GetHide() { - this.setData({ - modal: 0, - input: 0, - FaceSNinput:0 , - Upgradestatus:"", - UpgradestatusTimeout2:0, - ISLoopDebugging:0, - }) - }, - GetReturnSet() { - this.setData({ - - Upgradestatus:"", - UpgradestatusTimeout2:0, - ISLoopDebugging:0, - }) - }, - // 房间点击事件 - GetMAC:async function (e) { - let roomtypeInfo=this.data.roomtypeInfo - let index = e.currentTarget.dataset['index'] - // console.log(index) - let selHosts = this.data.HostsDataFilters[index]; - - for (let index = 0; index < roomtypeInfo.length; index++) { - const element = roomtypeInfo[index]; - if (selHosts.RoomTypeID==element.ID) { - this.setData({ - roomtypeInfoNode: element - }) - break - } - } - - let Gfilename="未知固件" - let Pfilename="未知配置" - await QueryUpdateHostStatus({ - hotelid:selHosts.HotelID, - roomTypeID:selHosts.RoomTypeID - }).then(res => { - if (res.total ==1) { - - if (res.rows.length>0) { - Gfilename=res.rows[0].Version - Pfilename=res.rows[0].ConfigVersion - } - } - }) - debugger - //长按事件 - if (this.endTime - this.startTime > 350) { - return; - } - if (!this.CheckAutho()) { - app.toast(2, "无绑定权限~") - return; - } - // let index = e.currentTarget.dataset['index'] - // // console.log(index) - // let selHosts = this.data.HostsDataFilters[index]; - selHosts.index = index; - this.setData({ - selHosts: selHosts, - input: 0 , - roomGfilename:Gfilename, - roomPfilename:Pfilename - - }) - if (this.data.HostsDataFilters[index].MAC != "" && this.data.HostsDataFilters[index].MAC != null) { - this.setData({ - modal: 1, - message: [this.data.HostsDataFilters[index].MAC, selHosts.RoomNumber] - }) - } else { - this.GetCode() - } - }, - // 未分配max - GetMaxby(type = 0, arry = null) { - let y = 0; - let n = 0; - if (arry == null) { - arry = this.data.HostsData; - } - arry.forEach(x => { - if (x.MAC == "" || x.MAC == null) { - n++; - } else { - y++; - } - }) - if (type == 0) { - return y; - } - return n; - }, - //分类过滤方法 - GetFilters(type = -1, arry = null) { - if (type == -1) { - type = this.data.sel - } - if (arry == null && this.data.HostsData != null) { - arry = this.data.HostsData; - } - if (arry == null) { - return; - } - let ok = 0; - let err = 0; - let no = 0; - let df = 0; - let res = []; - arry.forEach(x => { - switch (x.xg) { - case 0: - ok++; - break; - case 1: - err++; - break; - case 2: - no++; - break; - default: - df++; - break; - } - if (type == 0) { - res.push(x) - } else { - if (x.MAC == "" || x.MAC == null) { - if (type == 2) { - res.push(x) - } - } else { - if (type == 1) { - res.push(x) - } - } - } - }) - return [res, [ok, err, no, df]]; - }, - // 分类点击事件 - GetshowinfoClick(e) { - - - - let showinfo = e.currentTarget.dataset['index'] - this.setData({ - showinfo:showinfo - }) - - }, -GetRoomTypeList(){ - //debugger - GetHostsInfo({ - HotelID: that.data.Hotelinfo.HotelId - }).then(res =>{ - if (res.Status == 200) { - console.log('获取房型成功') - } - else{ - console.log('获取房型失败:'+that.data.Hotelinfo.HotelId) - } - }).catch(err => { - console.log('网络访问错误') - console.log(err) - }); -}, - - GetshowinfoClick1(e){ - let showinfo = e.currentTarget.dataset['index'] -var that=this; -that.setData({ - showinfo:showinfo - }) - - GetHostsInfo({ - HotelID: that.data.Hotelinfo.HotelId - }).then(res => { - - if (res.Status == 200) { - that.setData({ - HostsData: res.Data, - HostsDataFilters: res.Data, - isback: (app.globalData.autho.length > 1 || app.globalData.autho[0].Hotels.length > 1), - HotelId: that.data.Hotelinfo.HotelId, - statusdata: [0, 0, 0, 0], - YMac: this.GetMaxby(0, res.Data), - NMac: this.GetMaxby(1, res.Data) - }) - that.GetLOC(); - } else { - - app.toast(2, res.Message || "网络繁忙") - } - }, err => { - console.log(err) - app.toast(2, "网络繁忙") - }).catch(err => { - console.log(err) - - app.toast(2, "网络繁忙") - }); - - - - }, - - - - - // 绑定筛选分类点击事件 - GetClick(e) { - this.setData({ - islogs: false, - Help: false - }) - let index = e.currentTarget.dataset['index'] - let res = this.GetFilters(index, this.data.HostsData); - this.setData({ - sel: index, - HostsDataFilters: res[0], - statusdata: res[1] - }) - }, - /** - * 生命周期函数--监听页面加载 - */ - onLoad: async function (options) { - if (!options.HotelId || app.globalData.autho == null) { - app.toast(2, "无酒店信息~") - return; - } - - - this.setData({ - autho: app.globalData.autho - }) - // console.log(this.data.autho) - - try { - this.data.autho.forEach((element, index) => { - element.Hotels.forEach((elements, indexs) => { - if (elements.HotelId == options.HotelId) { - this.setData({ - Hotelinfo: elements - }) - throw new Error(); - } - }) - }); - } catch (error) { - console.log("已经找到,无需循环~") - } - let that = this; - setTimeout(function () { - // 1.使用wx.createSelectorQuery()查询到需要滚动到的元素位置 - wx.createSelectorQuery().select('#scroll').boundingClientRect(res => { - // 2.使用wx.getSysTemInfo()获取设备及页面高度windowHeight(px) - wx.getSystemInfo({ - success(ress) { - that.setData({ - top_height: ress.windowHeight - res.top , - top_Nheight:ress.windowHeight - res.top-80 - }) - } - }) - }).exec(function (params) { - console.log(params) - }) - }, 100) - - await GetHostsInfo({ - HotelID: this.data.Hotelinfo.HotelId - }).then(res => { - - if (res.Status == 200) { - that.setData({ - HostsData: res.Data, - HostsDataFilters: res.Data, - isback: (app.globalData.autho.length > 1 || app.globalData.autho[0].Hotels.length > 1), - HotelId: options.HotelId, - statusdata: [0, 0, 0, res.Data.length], - YMac: this.GetMaxby(0, res.Data), - NMac: this.GetMaxby(1, res.Data) - }) - app.globalData.HotelId=options.HotelId - - - that.GetLOC(); - } else { - - app.toast(2, res.Message || "网络繁忙") - } - }, err => { - console.log(err) - app.toast(2, "网络繁忙") - }).catch(err => { - console.log(err) - - app.toast(2, "网络繁忙") - }); - console.log(app.globalData.HotelId) - - await GetRoomType({ - HotelID: this.data.Hotelinfo.HotelId - }).then(res => { - app.SetroontypeListindex(res) - that.setData({ - roomtype:app.globalData.roomIDName - }) - }, err => { - console.log('GetRoomType error') - }).catch(err => { - - }); - - await GetRoomTypeAndModalsListLog({ - code:this.data.Hotelinfo.Code - }).then(res =>{ - if (res.IsSuccess==true) { - - that.setData({ - roomtypeInfo:res.Result - }) - } - }) - - - }, - //页面卸载 - onUnload: function name(params) { - let data = wx.getStorageSync("oldHotelinfo") || [] - let ok = false; - try { - for (let index = 0; index < data.length; index++) { - const element = data[index]; - if (element.key == this.data.Hotelinfo.HotelId) { - ok = true; - data[index].data = this.data.HostsData; - throw new Error(); - } - } - } catch (error) { - console.log('已经找到了') - } - if (!ok) { - data.push({ - key: this.data.Hotelinfo.HotelId, - data: this.data.HostsData - }) - } - try { - wx.setStorageSync('oldHotelinfo', data) - } catch (error) { - console.log(error) - } - - // 获取页面栈 - var pages = getCurrentPages(); - var currPage = pages[pages.length - 1]; // 当前页 - var prevPage = pages[pages.length - 2]; // 上一个页面 - prevPage.setData({ - issel: this.data.Hotelinfo.HotelId - }); - }, - LoopDebugging:async function (e) - { - // - let roomtypeInfoNode =this.data.roomtypeInfoNode - let roomtypeInfoNodeinfo=this.data.roomtypeInfoNode.Modals - let roomstart =false - let addrtype - const myMap =[] - let li =[] - let addrtypelist =true - - let message =this.data.message - for (let index = 0; index < roomtypeInfoNodeinfo.length; index++) { - const element = roomtypeInfoNodeinfo[index]; - addrtype=this.GetloopType(element.ModalAddress) - if (addrtype.length==0) { - continue - } - addrtypelist=true - for (let index2 = 0; index2 < myMap.length; index2++) { - const element2 = myMap[index2]; - - if ( element2[0] == addrtype[0][0]) { - li= myMap[index2][1] - li.push([element.ModalAddress,element.Name,addrtype[0][1]]) - myMap[index2][1]=li - addrtypelist=false - break - } - } - - if (addrtypelist) { - li=[] - li.push([element.ModalAddress,element.Name,addrtype[0][1]]) - myMap.push([addrtype[0][0],li]) - } - - - } - - roomtypeInfoNodeinfo=myMap.sort((a, b) => a[0] - b[0]) - //获取房间在线状态 roomstart - console.log(roomtypeInfoNodeinfo) - - await GetRoomTypeNode({ - hotelid: this.data.Hotelinfo.HotelId , - roomTypeID:roomtypeInfoNode.ID - }).then(res => { - if (res.IsSuccess== true) { - for (let index = 0; index < res.HostData.length; index++) { - const element = res.HostData[index]; - if (element.RoomNumber == message[1]) { - roomstart=element.Status - break - } - } - } - }) - - roomtypeInfoNode.roomstart=roomstart - - this.setData({ - ISLoopDebugging:1, - roomtypeInfoNode:roomtypeInfoNode, - roomtypeInfoNodeinfo:roomtypeInfoNodeinfo - }) - console.log(this.data.roomtypeInfoNode) - }, - Mathceil(number,tval){ - return Math.ceil(number,tval) - }, - GetloopType(loopaddr){ - let astr =loopaddr[0]+loopaddr[1]+loopaddr[2] - let aint =parseInt(astr,10) - let bint =[] - - switch (aint) { - case 1 : - // case 4 : - // case 54: - bint.push([1,[[1,'开关',0]]])//开关 - break; - case 5: //开关停 - bint.push([5,[[1,'开关',0]]]) - break; - case 7: //开关停 - - bint.push([7,[[1,'开关',0],[2,'模式',1],[3,'风速',1],[4,'温度',25],[5,'阀开',0]]]) - break; - case 23 : - case 24: - bint.push([23,[[1,'开关',10]]]) - break; - case 52: - bint.push([52,[[1,'开关',0],[2,'亮度',10],[3,'色温',10]]]) - break; - default: - - break; - } - - return bint - }, - -//固件升级 - ConfigurationUpgrade: async function(e){ - let _this=this - wx.showModal({ - title: "提示", - content: "是否进行固件升级!", - success (res) { - if (res.confirm) { - console.log('固件升级') - _this.setData({ - ISLoopDebugging:2, - }) - let selHosts =_this.data.selHosts - _this.Carryoutupgrade(false) - } else if (res.cancel) { - return - } - } - }) - - - }, - -//配置升级 -FirmwareUpgrade: async function (e){ - console.log('配置升级') - - - - let _this=this - wx.showModal({ - title: "提示", - content: "是否进行配置升级!", - success (res) { - if (res.confirm) { - console.log('固件升级') - _this.setData({ - ISLoopDebugging:2, - }) - let selHosts =_this.data.selHosts - _this.Carryoutupgrade(true) - } else if (res.cancel) { - return - } - } - }) - - - -}, -// 进行升级 -Carryoutupgrade:async function (params) { - - let selHosts =this.data.selHosts - let ID =selHosts.Id - let RoomTypeID = selHosts.RoomTypeID - let roontypeList =app.globalData.roontypeList - let Gfilename ="" - let Pfilename ="" - let Upgradenode =[] - var timestampSec = Math.floor(Date.now() / 1000); - for (let index = 0; index < roontypeList.length; index++) { - const element = roontypeList[index]; - if (RoomTypeID==element.ROOM_TYPE_OLD_ID) { - Upgradenode=element - if (element.App_Cfg_For_L2.length>0) { - Gfilename=element.App_Cfg_For_L2 - } - if (element.App_Cfg_For_L4.length>0) { - Gfilename=element.App_Cfg_For_L4 - } - if (element.APPTYPE=="App_Cfg") { - Pfilename=element.CONFIG_BIN - }else{ - Pfilename="" - } - break - } - } - Upgradenode.Gfilename=this.data.roomGfilename - Upgradenode.Pfilename=this.data.roomPfilename - // debugger - // await QueryUpdateHostStatus({ - // hotelid:selHosts.HotelID, - // roomTypeID:RoomTypeID - // }).then(res => { - // if (res.total ==1) { - - // if (res.rows.length>0) { - - // Upgradenode.Gfilename=res.rows[0].Version - // Upgradenode.Pfilename="配置版本:"+res.rows[0].ConfigVersion - // } - // } - // }) - - - - Upgradenode.App_Cfg_For_L4=Gfilename - Gfilename=Gfilename.replace('.hex','.bin') - if (params) { - if (Pfilename.length>0) { - await WebChatUpgrade({ - roomTypeID:RoomTypeID, - hostidLists:'['+ID+']', - upgradefileName:Pfilename - }).then(res => { - if (res.IsSuccess ==true) { - this.setData({ - Upgradestatus:'升级就绪', - UpgradestIstrue:params, - UpgradestatusTimeout2:timestampSec, - Upgradenode:Upgradenode - }) - this.observersUpgradestatus() - } else{ - this.setData({ - Upgradestatus:'服务器升级配置失败!' - }) - this.DisplayPrompt('服务器升级配置失败!',1000) - } - - }, err => { - this.setData({ - Upgradestatus:'升级配置失败,服务器响应异常' - }) - this.DisplayPrompt('升级配置失败,服务器响应异常',1000) - }).catch(err => { - this.setData({ - Upgradestatus:'升级配置失败,服务器响应异常' - }) - this.DisplayPrompt('升级配置失败,服务器响应异常',1000) - }); - - }else{ - this.setData({ - Upgradestatus:'升级配置失败!服务器未获取到配置!' - }) - this.DisplayPrompt('升级配置失败!服务器未获取到配置!',1000) - } - }else{ - if (Gfilename.length>0) { - await WebChatUpgrade({ - roomTypeID:RoomTypeID, - hostidLists:'['+ID+']', - upgradefileName:Gfilename - }).then(res => { - if (res.IsSuccess ==true) { - this.setData({ - Upgradestatus:'升级就绪', - UpgradestIstrue:params, - UpgradestatusTimeout2:timestampSec, - Upgradenode:Upgradenode - }) - this.observersUpgradestatus() - } else{ - this.setData({ - Upgradestatus:'服务器升级固件失败!' - }) - this.DisplayPrompt('服务器升级固件失败!',1000) - } - - }, err => { - this.setData({ - Upgradestatus:'升级固件失败,服务器响应异常' - }) - - this.DisplayPrompt('升级固件失败,服务器响应异常',1000) - }).catch(err => { - this.setData({ - Upgradestatus:'升级固件失败,服务器响应异常' - }) - - this.DisplayPrompt('升级固件失败,服务器响应异常',1000) - }); - }else - { - this.setData({ - Upgradestatus:"升级固件失败!!" - }) - - this.DisplayPrompt('升级固件失败!!',1000) - } - } - -}, -DisplayPrompt(tipstr, showtime) -{ - wx.showLoading({ - title: tipstr, - }) - setTimeout(function () { - wx.hideLoading() - }, showtime) -}, - - // 反馈异常 - ErrorInfo: async function (e) { - let type = e.currentTarget.dataset['type'] - let that = this; - await ErrorInfo({ - type: type, - MAC: this.data.selHosts.MAC, - roomNumber: this.data.selHosts.RoomNumber, - HotelID: this.data.selHosts.HotelID, - }).then(res => { - if (res.Status == 200) { - that.GetHide(); - app.globalData.userinfo.error.push({ - HostsId: that.data.selHosts.Id, - MAC: that.data.selHosts.MAC, - type: type - }); - app.toast(1, "反馈成功~") - } else { - app.toast(2, "反馈失败~") - } - }, err => { - app.toast(2, "反馈失败~") - }).catch((e) => { - app.toast(2, "反馈失败~") - }) - }, - // 微信api,获取经纬度 - getFuzzyLocations() { - wx.getFuzzyLocation({ - type: 'wgs84', - success: this.updateLocation, - fail: (e) => { - console.log(e) - } - }) - }, - // 根据经纬度,设置数据 - updateLocation(res) { - let { - latitude: lat, - longitude: lon - } = res - let data = { - lat, - lon - } - // this.setData(data) - this.getAddress(lat, lon) - }, - // 根据经纬度,逆地址解析 - getAddress(lat, lon) { - // 在wx.request中,this指向wx.request,故无法setData,此处将this指向that - var that = this - let SIG = md5("/ws/geocoder/v1?key=" + app.globalData.MapKey + "&location=" + String(lat) + "," + String(lon) + app.globalData.MapSk) - wx.request({ - url: 'https://apis.map.qq.com/ws/geocoder/v1', - data: { - key: app.globalData.MapKey, - location: `${lat},${lon}`, - sig: SIG - }, - success(res) { - let result = res.data.result - // console.log(result) - // formatted_addresses.recommend是经过腾讯地图优化过的描述方式,更具人性化特点 - let formatted_addresses = result.formatted_addresses.recommend - // 此处的that指向app - that.setData({ - address: formatted_addresses - }) - }, - fail: (e) => { - console.log(e) - } - }) - }, - //每次展示获取一次定位 - onShow() { - let that = this; - wx.getSetting({ - success: (res) => { - console.log(JSON.stringify(res)) - // res.authSetting['scope.userFuzzyLocation'] == undefined 表示 初始化进入该页面 - // res.authSetting['scope.userFuzzyLocation'] == false 表示 非初始化进入该页面,且未授权 - // res.authSetting['scope.userFuzzyLocation'] == true 表示 地理位置授权 - if (res.authSetting['scope.userFuzzyLocation'] != undefined && res.authSetting['scope.userFuzzyLocation'] != true) { - //以前被拒绝授权指引用户授权 - wx.showModal({ - title: '请求授权当前位置', - content: '需要获取您的地理位置,请确认授权', - success: function (res) { - if (res.cancel) { - app.toast(2, "拒绝授权") - } else if (res.confirm) { - wx.openSetting({ - success: function (dataAu) { - if (dataAu.authSetting["scope.userFuzzyLocation"] == true) { - app.toast(1, "授权成功") - that.getFuzzyLocations(); - } else { - app.toast(2, "授权失败") - } - } - }) - } - } - }) - - } else if (res.authSetting['scope.userFuzzyLocation'] == undefined) { - //调用wx.getFuzzyLocation - that.getFuzzyLocations(); - } else { - that.getFuzzyLocations(); - } - } - }) - }, - - Loopswitch(e){ - let addr =0 - //debugger - - }, - - - //读取本地历史缓存 并且 修改房间信息 - GetLOC() { - let hc = null; - let data = wx.getStorageSync("oldHotelinfo") || [] - try { - for (let index = 0; index < data.length; index++) { - const element = data[index]; - if (element.key == this.data.Hotelinfo.HotelId) { - hc = data[index].data; - throw new Error(); - } - } - } catch (error) { - console.log('已经找到') - } - if (hc != null) { - let data = this.data.HostsData; - hc.forEach(x => { - for (let index = 0; index < data.length; index++) { - if (data[index].Id == x.Id) { - data[index].xg = x.xg; - } - } - }) - this.setData({ - HostsData: data, - HostsDataFilters: data - }) - } - } -}) \ No newline at end of file diff --git a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.json b/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.json deleted file mode 100644 index 9c6da14..0000000 --- a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "usingComponents": { - "loscom": "/components/logscom/logscom", - "HostUpgrade":"/pages/HostUpgrade/HostUpgrade", - "Upgrade":"/pages/Upgrade/Upgrade" - - } - -} \ No newline at end of file diff --git a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxml b/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxml deleted file mode 100644 index d852668..0000000 --- a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxml +++ /dev/null @@ -1,668 +0,0 @@ - - - 返回 - {{Hotelinfo.HotelName}}({{Hotelinfo.Code}}) - - - - - - - - - - - - - - - - - - - - - - - - - - - {{Help?'关闭':'帮助'}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{item.RoomNumber}} - - - - - -M:{{item.MAC[0]}}{{item.MAC[1]}}{{item.MAC[2]}}{{item.MAC[3]}}{{item.MAC[4]}}{{item.MAC[5]}}{{item.MAC[6]}}{{item.MAC[7]}}{{item.MAC[8]}} - -{{item.MAC[9]}}{{item.MAC[10]}}{{item.MAC[11]}}{{item.MAC[12]}}{{item.MAC[13]}}{{item.MAC[14]}}{{item.MAC[15]}}{{item.MAC[16]}} - - - - - - RCU - - - 绑定 - - - - - - - - - - - - - s:{{item.FaceSN[0]}}{{item.FaceSN[1]}}{{item.FaceSN[2]}}{{item.FaceSN[3]}}{{item.FaceSN[4]}}{{item.FaceSN[5]}}{{item.FaceSN[6]}}{{item.FaceSN[7]}} - -{{item.FaceSN[8]}}{{item.FaceSN[9]}}{{item.FaceSN[10]}}{{item.FaceSN[11]}}{{item.FaceSN[12]}}{{item.FaceSN[13]}}{{item.FaceSN[14]}}{{item.FaceSN[15]}} - - - - - - - s:{{item.FaceSN[0]}}{{item.FaceSN[1]}}{{item.FaceSN[2]}}{{item.FaceSN[3]}}{{item.FaceSN[4]}}{{item.FaceSN[5]}}{{item.FaceSN[6]}}{{item.FaceSN[7]}} - -{{item.FaceSN[8]}}{{item.FaceSN[9]}}{{item.FaceSN[10]}}{{item.FaceSN[11]}}{{item.FaceSN[12]}}{{item.FaceSN[13]}}{{item.FaceSN[14]}}{{item.FaceSN[15]}} - - - - - - 人脸机 - - - - 绑定 - - - - - - - - - - - - - -暂无数据~ - - - - - - - - - - - - - - - - - 颜色指示块 - - - - 指示块,仅针对本次进入页面有效,不代表历史操作记录,刷新或者重新进入页面都会执行重置操作。 - - - - - - - - MAC绑定操作 - - - - 点击 - 点击房间,如果已经绑定MAC会弹出提示语句,点击继续,继续执行扫码绑定,扫码后,会有普通的MAC矫正,如果MAC已经被绑定,会再次弹出提示语句, - 如果MAC绑定的酒店有权限就会有提示语句,然后可以点击继续执行绑定,这将解除原有绑定,绑定当前MAC;如果MAC绑定的酒店没有权限,就仅有提示语,而没有继续绑定按钮。 - 长按 - 长按会执行手动输入MAC,后续与点击扫码绑定一致~ - - - - - - - - - - - - - - - - - - - - - - - - - - {{selHosts.RoomNumber}}已经绑定MAC({{selHosts.Status == 1 ? '在线':'离线'}}) - - - - - - - - - - - - 酒店:{{Hotelinfo.HotelName}} - 房间:{{message[1]}} - MAC地址:{{message[0]}} - 当前固件:{{roomGfilename}} - 当前配置:{{roomPfilename}} - 房间已经绑定MAC,点击继续将执行扫描绑定;点击取消,将不会执行任何操作~ - - - - - - - 固件升级 - 配置升级 - 回路调试 - - - 反馈错误 - 取消 - 继续 - - - - - - - - - - - - - - {{VolumeLevel}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{row[1][DimIndex][1]}} - - {{row[1][DimIndex][2][0][2]}} - - - - - - - - 开关 - 色温 - 亮度 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{row[1][coloIndex][1]}} - - - - - - - - - - - - - - - - - - - - - - - - - - - {{row[1][coloIndex][2][3][2]}} - - - - - - - - - - - - - - - - - 设备当前版本: - {{UpgradestIstrue == 0? Upgradenode.Gfilename:Upgradenode.Pfilename}} - - - 准备升级版本: - {{UpgradestIstrue == 0? Upgradenode.App_Cfg_For_L4:Upgradenode.CONFIG_BIN}} - - - 升级状态: - {{Upgradestatus}} - - - - - - - - - - {{code}}已经被绑定 - - - - - - -酒店:{{item.HotelName}} -房间:{{item.RoomNumber}} - - - - MAC地址:{{code}} - MAC已经被其他房间绑定,点击继续将取消原有绑定,执行新的绑定;点击取消,将不会执行任何操作~ - - - 反馈错误 - 取消 - 继续 - - - - - - - - 为{{selHosts.RoomNumber}}绑定MAC - - - - - - 房间:{{selHosts.RoomNumber}} - MAC地址: - - - - - - 取消 - 确认 - - - - - - - - 绑定确认 - - - - - - 酒店:{{Hotelinfo.HotelName}} - 房间:{{selHosts.RoomNumber}} - MAC地址:{{code}} - 确定为房间绑定MAC,点击继续将执行扫描绑定;点击取消,将不会执行任何操作~ - - - 取消 - 继续 - - - - - - - - 无效条码 - - - - - - 无效条码:{{code}} - 扫描的条码为无效条码~ - - - 确定 - - - - - - - - - 绑定确认 - - - - - - 酒店:{{Hotelinfo.HotelName}} - 房间:{{selHosts.RoomNumber}} - 人脸机SN:{{code}} - 确定为房间绑定SN,点击继续将执行扫描绑定;点击取消,将不会执行任何操作~ - - - 取消 - 继续 - - - - - - - - {{selHosts.RoomNumber}}已经绑定人脸机 - - - - - - - 酒店:{{Hotelinfo.HotelName}} - 房间:{{message[1]}} - 人脸机SN:{{message[0]}} - 房间已经绑定人脸机,点击继续将执行扫描绑定;点击取消,将不会执行任何操作~ - - - - - - - - - 设置 - 开门 - - - - - - - - - - - - - - - - {{code}}已经被绑定 - - - - - - - - - -酒店:{{item.HotelName}} -房间:{{item.RoomNumber}} - - - - - - - - SN:{{code}} - SN已经被其他房间绑定,点击继续将取消原有绑定,执行新的绑定;点击取消,将不会执行任何操作~ - - - - 取消 - - 继续 - - - - - - - - - 为{{selHosts.RoomNumber}}绑定人脸机SN - - - - - - 房间:{{selHosts.RoomNumber}} - 人脸机SN: - - - - - - 取消 - 确认 - - - diff --git a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxss b/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxss deleted file mode 100644 index bf3cb0b..0000000 --- a/pages/basics/HostUpgrade/EquipmentCaontrol/Hosts/Hosts.wxss +++ /dev/null @@ -1,42 +0,0 @@ -.Ncu-bar { - display: flex; - position: relative; - align-items: center; - min-height: 70rpx ; - justify-content: space-between; -} -.Ncu-bar1 { - display: flex; - position: relative; - align-items: center; - min-height: 70rpx ; - -} -.Ncubar1 { - display: flex; - position: relative; - align-items:flex-start; - min-height: 70rpx ; - -} -.ControlLine{ - background-color: gray; - height: 1rpx; - width: 100%; -} -.ControlLine_h{ - background-color: lightgray; - height: 80rpx; - width: 1rpx; -} -.textvlg { - display: inline-flex; - align-items: center; - justify-content: center; - text-align:center; - padding: 0 5rpx; - font-size:28rpx; - height: 80rpx; - width: 70rpx; - text-decoration: none; -} \ No newline at end of file diff --git a/pages/basics/HostUpgrade/HostUpgrade.js b/pages/basics/HostUpgrade/HostUpgrade.js index 8049f24..c870245 100644 --- a/pages/basics/HostUpgrade/HostUpgrade.js +++ b/pages/basics/HostUpgrade/HostUpgrade.js @@ -62,6 +62,10 @@ Page({ bdHosts: [], address: "", UpgradeType:"", + checkedCount: 0, + totalCount: 0, + successCount: 0, + failCount: 0, }, /** @@ -145,13 +149,33 @@ Page({ upgrader.on('progress', p => { - console.log(p) - this.setData({ CurrentUpgradeDevStart: p }) // 实时刷新 UI + console.log('upgrader progress:', p) + try { + // 支持数字或对象 {percent:xx} 或字符串数字 + let prog = p && typeof p === 'object' && p.percent != null ? p.percent : (typeof p === 'number' ? p : (typeof p === 'string' && !isNaN(Number(p)) ? Number(p) : p)); + const cur = this.data.CurrentUpgradeDev; + if (cur && this.data.devlist && Array.isArray(this.data.devlist)) { + const devlist = this.data.devlist.slice(); + const idx = devlist.findIndex(d => String(d.RoomNumber) === String(cur) || String(d.ID) === String(cur)); + if (idx >= 0) { + devlist[idx].UpgradeProgress = prog; + if (!devlist[idx].UpgradeStatus || devlist[idx].UpgradeStatus === 0) devlist[idx].UpgradeStatus = 1; // 进行中 + this.setData({ devlist }); + } + } + this.setData({ CurrentUpgradeDevStart: p }); + } catch (e) { + console.log(e) + this.setData({ CurrentUpgradeDevStart: p }); + } }) upgrader.on('error', e => { wx.showToast({ title: '升级失败', icon: 'none' }) }) + // 初始化计数 + try { this.updateCounts && this.updateCounts(); } catch (e) { console.log('init counts error', e); } + }, /** @@ -575,6 +599,7 @@ Page({ for (let i = 0; i < this.data.devlist.length; i++) { devlist[i].UpgradeStatus = 0 + devlist[i].UpgradeProgress = ""; if (devlist[i].checkbox) { //勾选 if (devlist[i].Status) { //离在线 @@ -617,31 +642,64 @@ Page({ upgradefileName: filename, hotelid:HotelId }) - .then(res => { - _this.setData({ - UpgradeCount:_this.data.UpgradeCount+1, //升级计数 - }) - console.log('升级完成', res) + .then(res => { + // 标记对应设备为已完成,并更新进度,取消勾选 + try { + const devlist = _this.data.devlist.slice(); + const idx = devlist.findIndex(d => String(d.ID) === String(element.ID) || String(d.RoomNumber) === String(element.RoomNumber)); + if (idx >= 0) { + devlist[idx].UpgradeProgress = 100; + devlist[idx].UpgradeStatus = 2; // 完成 + devlist[idx].checkbox = false; + _this.setData({ devlist, UpgradeCount: _this.data.UpgradeCount + 1 }, () => { _this.updateCounts && _this.updateCounts(); }); + } else { + _this.setData({ UpgradeCount: _this.data.UpgradeCount + 1 }, () => { _this.updateCounts && _this.updateCounts(); }); + } + } catch (err) { + console.log('update dev progress error', err); + _this.setData({ UpgradeCount: _this.data.UpgradeCount + 1 }); + } + console.log('升级完成', res) }) .catch(err =>{ - - + // 标记对应设备为失败,取消勾选,并继续下一个 + try { + const devlist = _this.data.devlist.slice(); + const idx = devlist.findIndex(d => String(d.ID) === String(element.ID) || String(d.RoomNumber) === String(element.RoomNumber)); + if (idx >= 0) { + devlist[idx].UpgradeProgress = ""; + devlist[idx].UpgradeStatus = 3; // 失败 + // 保持勾选状态(不取消),以便用户查看/重试 + _this.setData({ devlist }, () => { _this.updateCounts && _this.updateCounts(); }); + } + } catch (err2) { + console.log('update dev fail state error', err2); + } console.log('升级失败', err) }); }catch (error) { - _this.setData({ - //升级计数 - CurrentUpgradeDev:element.RoomNumber, //当前升级设备 - CurrentUpgradeDevStart:errstr+',服务器响应异常',//当前升级设备状态 - Completed:false - }) - //this.DisplayPrompt(errstr+',服务器响应异常',1000) - return + // 如果 startUpgrade 抛出异常,标记该设备为失败并继续下一个 + try { + const devlist2 = _this.data.devlist.slice(); + const idx2 = devlist2.findIndex(d => String(d.ID) === String(element.ID) || String(d.RoomNumber) === String(element.RoomNumber)); + if (idx2 >= 0) { + devlist2[idx2].UpgradeProgress = ""; + devlist2[idx2].UpgradeStatus = 3; // 失败 + // 保持勾选状态(不取消),以便用户查看/重试 + _this.setData({ devlist: devlist2 }, () => { _this.updateCounts && _this.updateCounts(); }); + } + } catch (err2) { + console.log('handle thrown upgrade error', err2); + } + console.log('升级异常', error); + // 不 return,继续下一个设备 } } this.DisplayPrompt2("升级完成!", 1000) + // 所有设备处理完毕,取消批量升级标记 + _this.setData({ Completed:false }); }, DisplayPrompt2(tipstr, showtime) @@ -665,7 +723,7 @@ Page({ this.setData({ devlist:devlist, Allcheckbox:values -}); + }, () => { this.updateCounts && this.updateCounts(); }); }, checkboxSub(e){ let devlist =this.data.devlist @@ -691,7 +749,7 @@ Page({ devlist:devlist, Allcheckbox:values, checksun:checksun -}); + }, () => { this.updateCounts && this.updateCounts(); }); }, SearchForHotels(e){ //debugger @@ -889,6 +947,7 @@ Page({ devlist[nindex].show=1 devlist[nindex].checkbox=0 devlist[nindex].UpgradeStatus=0 + devlist[nindex].UpgradeProgress = ""; devlist[nindex].Model =_this.removeNullCharsAndInvisibleChars(devlist[nindex].Model) } } @@ -926,7 +985,7 @@ if (Nres.Status==200) { Pfilename:Pfilename, Gfilename:Gfilename, Upgradenode:Upgradenode - }); + }, () => { this.updateCounts && this.updateCounts(); }); console.log(devlist) } catch (err) { console.log('GetRoomTypeNode error', err); @@ -941,6 +1000,18 @@ if (Nres.Status==200) { } }, + updateCounts() { + try { + const devlist = Array.isArray(this.data.devlist) ? this.data.devlist : []; + const total = devlist.length; + const checked = devlist.filter(d => d && d.checkbox).length; + const success = devlist.filter(d => d && Number(d.UpgradeStatus) === 2).length; + const fail = devlist.filter(d => d && Number(d.UpgradeStatus) === 3).length; + this.setData({ checkedCount: checked, totalCount: total, successCount: success, failCount: fail }); + } catch (e) { + console.log('updateCounts error', e); + } + }, tabSelect(e) { this.setData({ TabCur: e.currentTarget.dataset.id, diff --git a/pages/basics/HostUpgrade/HostUpgrade.wxml b/pages/basics/HostUpgrade/HostUpgrade.wxml index c878395..996d28c 100644 --- a/pages/basics/HostUpgrade/HostUpgrade.wxml +++ b/pages/basics/HostUpgrade/HostUpgrade.wxml @@ -48,13 +48,17 @@ - - + + - {{item.RoomNumber}} - {{item.MAC}} + data-status="{{item.Status}}" data-HotelCode="{{Hotelinfo.Code}}" data-roomtypeid="{{item.RoomTypeID}}" hover-class="navigator-hover" class="nav-li4 {{item.Status===1? 'online':'offline'}}" wx:for="{{HostsDataFilters}}" id="msg-{{index}}" wx:key> + {{item.RoomNumber}} + + {{item.MAC || (item.Status !== 1 ? '点击扫码绑定主机' : '')}} + + + 升级: {{CurrentUpgradeDevStart}} @@ -103,30 +107,47 @@ bindtouchend="handleTouchEnd" style="margin-top: 20rpx;"> + - - + 全选 -正在升级房号:{{CurrentUpgradeDev}}:{{CurrentUpgradeDevStart}} -已升级设备: {{UpgradeCount}} - - + + 已勾选: {{checkedCount}}/{{totalCount}} + 成功: {{successCount}} + 失败: {{failCount}} + + + + + + + 房号排序 + + + + 固件版本排序 + + + + 配置版本排序 + + - + - - + + - + 房号:{{devlist[index].RoomNumber}} @@ -147,6 +168,20 @@ bindtouchend="handleTouchEnd" style="margin-top: 20rpx;"> 固件版本:{{devlist[index].Version}} + + + 升级完成 + + + 升级失败 + + + 升级中{{devlist[index].UpgradeProgress}}{{devlist[index].UpgradeProgress? '%':''}} + + + + + diff --git a/pages/basics/HostUpgrade/HostUpgrade.wxss b/pages/basics/HostUpgrade/HostUpgrade.wxss index e613993..de3e682 100644 --- a/pages/basics/HostUpgrade/HostUpgrade.wxss +++ b/pages/basics/HostUpgrade/HostUpgrade.wxss @@ -109,4 +109,33 @@ align-items: center; min-height: 50rpx ; justify-content: space-between; -} \ No newline at end of file +} + +/* 房间列表项在线/离线样式(与查房页面保持一致) */ +.nav-li4.online { + background-color: #FFFFFF; /* 在线高亮白 */ + color: #111111; +} +.nav-li4.offline { + background-color: #C9CBCA; /* 设备离线浅灰色 */ + color: #111111; +} + +/* 房间卡片的大框背景色与响应式布局 */ +.nav-list1 { + background-color: #F0F0F0; + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: 10rpx; +} + +/* 单列卡片(默认,手机) +.nav-list1 .nav-li4 { + width: 98%; + margin: 10rpx 0; + box-sizing: border-box; +} */ + + + diff --git a/pages/basics/MakingRounds/MakingRounds.wxml b/pages/basics/MakingRounds/MakingRounds.wxml index c203a78..dbc582b 100644 --- a/pages/basics/MakingRounds/MakingRounds.wxml +++ b/pages/basics/MakingRounds/MakingRounds.wxml @@ -35,9 +35,9 @@ - + + data-status="{{item.Status}}" hover-class="navigator-hover" class="nav-li4 {{item.Status===1? 'online':'offline'}}" wx:for="{{HostsDataFilters}}" id="msg-{{index}}" wx:key> {{item.RoomNumber}} {{item.MAC}} diff --git a/pages/basics/MakingRounds/MakingRounds.wxss b/pages/basics/MakingRounds/MakingRounds.wxss index e9b4548..fa90fe0 100644 --- a/pages/basics/MakingRounds/MakingRounds.wxss +++ b/pages/basics/MakingRounds/MakingRounds.wxss @@ -99,3 +99,18 @@ transform: translate(0rpx, 0rpx); margin-right: initial; } + +/* 查房列表项在线/离线样式 */ +.nav-li4.online { + background-color: #FFFFFF; /* 在线高亮白 */ + color: #111111; +} +.nav-li4.offline { + background-color: #C9CBCA; /* 设备离线浅灰色 */ + color: #111111; +} + +/* 房间卡片的大框背景色 */ +.nav-list1 { + background-color: #F0F0F0; +} diff --git a/pages/device/device.js b/pages/device/device.js deleted file mode 100644 index f59b5a7..0000000 --- a/pages/device/device.js +++ /dev/null @@ -1,149 +0,0 @@ -// pages/device.js -const ecUI = require('../../utils/ecUI.js') -const ecBLE = require('../../utils/ecBLE.js') - -let ctx -let isCheckScroll = true -let isCheckRevHex = false -let isCheckSendHex = false -let sendData = '' - -Page({ - /** - * 页面的初始数据 - */ - data: { - textRevData: '', - scrollIntoView: 'scroll-view-bottom', - }, - /** - * 生命周期函数--监听页面加载 - */ - onLoad() { - ctx = this - isCheckScroll = true - isCheckRevHex = false - isCheckSendHex = false - sendData = '' - ecBLE.setChineseType(ecBLE.ECBLEChineseTypeGBK) - - //on disconnect - ecBLE.onBLEConnectionStateChange(() => { - ecUI.showModal('提示', '设备断开连接') - }) - //receive data - ecBLE.onBLECharacteristicValueChange((str, strHex) => { - let data = - ctx.data.textRevData + - ctx.dateFormat('[hh:mm:ss,S]:', new Date()) + - (isCheckRevHex ? strHex.replace(/[0-9a-fA-F]{2}/g, ' $&') : str) + - '\r\n' - // console.log(data) - ctx.setData({ textRevData: data }) - if (isCheckScroll) { - if (ctx.data.scrollIntoView === "scroll-view-bottom") { - ctx.setData({ scrollIntoView: "scroll-view-bottom2" }) - } else { - ctx.setData({ scrollIntoView: "scroll-view-bottom" }) - } - } - }) - }, - /** - * 生命周期函数--监听页面卸载 - */ - onUnload() { - ecBLE.onBLEConnectionStateChange(() => { }) - ecBLE.onBLECharacteristicValueChange(() => { }) - ecBLE.closeBLEConnection() - }, - checkScroll(e) { - if (e.detail.value.length) isCheckScroll = true - else isCheckScroll = false - }, - checkRevHex(e) { - if (e.detail.value.length) isCheckRevHex = true - else isCheckRevHex = false - }, - checkSendHex(e) { - if (e.detail.value.length) isCheckSendHex = true - else isCheckSendHex = false - }, - inputSendData(e) { - sendData = e.detail.value - }, - btClearTap() { - this.setData({ textRevData: '' }) - }, - btSendTap() { - if (isCheckSendHex) { - let data = sendData - .replace(/\s*/g, '') - .replace(/\n/g, '') - .replace(/\r/g, '') - if (data.length === 0) { - ecUI.showModal('提示', '请输入要发送的数据') - return - } - if (data.length % 2 != 0) { - ecUI.showModal('提示', '数据长度只能是双数') - return - } - if (data.length > 488) { - ecUI.showModal('提示', '最多只能发送244字节') - return - } - if (!new RegExp('^[0-9a-fA-F]*$').test(data)) { - ecUI.showModal('提示', '数据格式错误,只能是0-9,a-f,A-F') - return - } - ecBLE.writeBLECharacteristicValue(data, true) - } else { - if (sendData.length === 0) { - ecUI.showModal('提示', '请输入要发送的数据') - return - } - let tempSendData = sendData.replace(/\n/g, '\r\n') - if (tempSendData.length > 244) { - ecUI.showModal('提示', '最多只能发送244字节') - return - } - ecBLE.writeBLECharacteristicValue(tempSendData, false) - } - }, - dateFormat(fmt, date) { - let o = { - 'M+': date.getMonth() + 1, //月份 - 'd+': date.getDate(), //日 - 'h+': date.getHours(), //小时 - 'm+': date.getMinutes(), //分 - 's+': date.getSeconds(), //秒 - 'q+': Math.floor((date.getMonth() + 3) / 3), //季度 - S: date.getMilliseconds(), //毫秒 - } - if (/(y+)/.test(fmt)) - fmt = fmt.replace( - RegExp.$1, - (date.getFullYear() + '').substr(4 - RegExp.$1.length) - ) - for (var k in o) - if (new RegExp('(' + k + ')').test(fmt)) { - // console.log(RegExp.$1.length) - // console.log(o[k]) - fmt = fmt.replace( - RegExp.$1, - RegExp.$1.length == 1 - ? (o[k] + '').padStart(3, '0') - : ('00' + o[k]).substr(('' + o[k]).length) - ) - } - return fmt - }, - checkChinese(e){ - if(e.detail.value==='gbk'){ - ecBLE.setChineseType(ecBLE.ECBLEChineseTypeGBK) - }else{ - ecBLE.setChineseType(ecBLE.ECBLEChineseTypeUTF8) - } - } -}) diff --git a/pages/device/device.json b/pages/device/device.json deleted file mode 100644 index 348b77c..0000000 --- a/pages/device/device.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "usingComponents": {}, - "enablePullDownRefresh": false, - "disableScroll": true -} diff --git a/pages/device/device.wxml b/pages/device/device.wxml deleted file mode 100644 index 6f1bd0f..0000000 --- a/pages/device/device.wxml +++ /dev/null @@ -1,47 +0,0 @@ - - - - 数据接收 : - - - - 滚动 - - - - Hex - - - - - - {{textRevData}} - - - - - - - 数据发送 : - - - Hex - - - -