Compare commits
20 Commits
v1.0.0
...
b4c27e30c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4c27e30c8 | ||
|
|
6a3aaf3c05 | ||
|
|
0ea3511b90 | ||
|
|
877770aeab | ||
|
|
470c148861 | ||
|
|
522f40a3c2 | ||
|
|
3151d71cdc | ||
|
|
3509caf79d | ||
|
|
59ddbe8d90 | ||
|
|
79ec89b3a9 | ||
|
|
d00d199558 | ||
|
|
a69d7ab1d0 | ||
|
|
cf0b308e22 | ||
|
|
6929faddfc | ||
|
|
3d7aec4cad | ||
|
|
eadeed5e0f | ||
|
|
ef796f6213 | ||
|
|
e7607481e1 | ||
|
|
b67de6e9de | ||
|
|
00ec02eb9e |
@@ -2,7 +2,7 @@
|
||||
|
||||
DG430地感测试工装协议说明
|
||||
|
||||
(V2.0.1-20260522)
|
||||
(V2.0.4-20260601)
|
||||
|
||||
# 1 硬件介绍
|
||||
|
||||
@@ -157,7 +157,7 @@ Flag:是否故障标志
|
||||
|
||||
02-----DLD110系列;
|
||||
|
||||
测试模式:0 为灵敏度测试模式;1为模拟过车测试模式
|
||||
测试模式:0 为灵敏度测试模式;1为波动测试模式
|
||||
|
||||
是否正常完成测试:00-----正常;
|
||||
|
||||
@@ -219,6 +219,40 @@ RFU:保留字节;
|
||||
|
||||
例:7F 81 02 B3 01 31 37
|
||||
|
||||
## 5.4.1 波动测试上报指令,命令:0xB4 (V2.0.3 新增)
|
||||
|
||||
设备主动上报格式:
|
||||
|
||||
| 7Fh | 80h+ADD | 11h | B4h | 状态内容(16字节) | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1B | 1B | 1B | 1B | 16B | 1B | 1B |
|
||||
|
||||
说明:该协议为波动测试过程中 DG430 主动上报,上位机无回复。在以下三种情况触发上报:
|
||||
1. 金属板行驶到波动**最近距离**时
|
||||
2. 金属板行驶到波动**最远距离**时
|
||||
3. 波动的过程中**车检器的输出状态发生变化**时
|
||||
|
||||
状态内容详细说明(16字节,除 RemainCount 外均为小端模式):
|
||||
|
||||
| 字段 | 字节数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| RemainCount | 1B | 当前剩余波动次数 |
|
||||
| Relay | 1B | 继电器输出状态(同 0xB2 继电器输出字段):BIT0=存在继电器信号,BIT1=脉冲继电器信号 |
|
||||
| WorkFreq | 2B | 工作频率,单位 Hz,计算公式:Freq = 10 × X |
|
||||
| CurrDist | 2B | 当前距离/高度(激光检测值,未减皮距),单位 mm |
|
||||
| Speed | 2B | 当前速度,单位 dm/s |
|
||||
| NearDist | 2B | 波动最近距离(皮距 + 最近容差),单位 mm |
|
||||
| FarDist | 2B | 波动最远距离(离开高度 + 皮距 - 最远容差),单位 mm |
|
||||
| EnterDist | 2B | 进入高度(已减皮距),单位 mm |
|
||||
| LeaveDist | 2B | 离开高度(已减皮距),单位 mm |
|
||||
|
||||
例:7F 81 13 B4 03 01 2F 0D 00 C8 01 28 64 00 20 03 A0 0F 28 00 3C 00 XX XX
|
||||
|
||||
解析:
|
||||
RemainCount=3(还剩3次波动),Relay=01(存在继电器有信号),WorkFreq=3375Hz,
|
||||
CurrDist=200mm,Speed=296dm/s,NearDist=800mm,FarDist=4000mm,
|
||||
EnterDist=40mm,LeaveDist=60mm。
|
||||
|
||||
## 5.5 电机前进指令,命令: 0xBA
|
||||
|
||||
上位机发送格式:
|
||||
@@ -319,33 +353,45 @@ eg: 7F 81 08 4A 00 03 03 00 03 04 00 C4 E0, 表示硬件版本号3
|
||||
|
||||
上位机发送格式:
|
||||
|
||||
| 7Fh | 80h+ADD | 10h | 4Bh | Addr | DevType | TestMode | ResetDis | MinusDis | SensMin | SensMax | FreMin | FreMax | PeakMin | PeakMax | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B |
|
||||
| 7Fh | 80h+ADD | 17h | 4Bh | Addr | DevType | TestMode | ResetDis | MinusDis | SensMin | SensMax | FreMin | FreMax | PeakMin | PeakMax | FarTol | NearTol | StepTol | BackForth | NearStay | FarStay | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B | 1B | 1B | 2B | 2B | 1B | 1B |
|
||||
|
||||
说明:该指令为上位机发送给DG430。DG430接收到该指令后返回协议并更新测试参数。
|
||||
|
||||
Addr:1Byte, 设备地址、485地址
|
||||
Addr:1Byte, 设备地址、485地址
|
||||
|
||||
DevType:1Byte, 设备型号
|
||||
DevType:1Byte, 设备型号
|
||||
|
||||
TestMode: 1Byte, 测试模式,0 灵敏度测试模式;1 模拟过车模式
|
||||
TestMode:1Byte, 测试模式,0 灵敏度测试模式;1 波动测试模式
|
||||
|
||||
ResetDis: 复位距离,单位cm
|
||||
ResetDis:复位距离,单位cm
|
||||
|
||||
MinusDis: 皮距,激光到线圈的距离,测算的实际高度要减去这个皮距,单位cm。
|
||||
MinusDis:皮距,激光到线圈的距离,测算的实际高度要减去这个皮距,单位cm
|
||||
|
||||
SensMin,SensMax: 2Byte, 灵敏度最小、最大值
|
||||
SensMin, SensMax:2Byte, 灵敏度最小、最大值(小端模式)
|
||||
|
||||
FreMin, FreMax: 2Byte, 频率最小、最大值
|
||||
FreMin, FreMax:2Byte, 频率最小、最大值(小端模式)
|
||||
|
||||
PeakMin, PeakMax: 2Byte, 峰峰值最小、最大值
|
||||
PeakMin, PeakMax:2Byte, 峰峰值最小、最大值(小端模式)
|
||||
|
||||
测试用例:7F 81 12 4B 01 01 00 30 0D 00 8B 00 E6 07 76 06 D6 09 B0 0C 1C 80 C8
|
||||
FarTol:1Byte, 最远容差,波动测试时,离开高度- 最远容差= 波动最远距离,不包含皮距,单位cm
|
||||
|
||||
NearTol:1Byte, 最近容差,最近容差=波动最近距离,不包含皮距,单位cm
|
||||
|
||||
StepTol:1Byte, 步进容差,默认0。非0时每次波动后最远容差递加该值,最多(波动次数-1)次,单位cm
|
||||
|
||||
BackForth:1Byte, 来回次数,金属板从最远距离→最近距离→最远距离记为1次
|
||||
|
||||
NearStay:2Byte, 最近停留时间,到达波动最近距离后停留的时间,单位ms(小端模式)
|
||||
|
||||
FarStay:2Byte, 最远停留时间,到达波动最远距离后停留的时间,单位ms(小端模式)
|
||||
|
||||
测试用例:7F 81 19 4B 01 01 01 30 0D 00 8B 00 E6 07 76 06 D6 09 B0 0C 1C 03 02 00 05 C8 00 F4 01 2B 9F
|
||||
|
||||
用例说明:
|
||||
|
||||
Addr:0x01, DevType: 0x01, 00: Sensity Test Mode, ResetDis: 48cm, MinusDis: 13cm, SensMin: 139, SensMax: 230, Fre\_Min:1910, FreMax: 1750, PeakMin: 2480, PeakMax: 3100。
|
||||
Addr:0x01, DevType:0x01, TestMode:1(波动测试), ResetDis:48cm, MinusDis:13cm, SensMin:139, SensMax:230, Fre_Min:1910, FreMax:1750, PeakMin:2480, PeakMax:3100, FarTol:3cm, NearTol:2cm, StepTol:0cm, BackForth:5次, NearStay:200ms, FarStay:500ms。
|
||||
|
||||
返回格式:
|
||||
|
||||
@@ -372,19 +418,41 @@ Addr:0x01, DevType: 0x01, 00: Sensity Test Mode, ResetDis: 48cm,
|
||||
|
||||
测试用例:7F 81 01 4C CC CE
|
||||
|
||||
返回格式:
|
||||
| 7Fh | 80h+ADD | 1Bh | 4Ch | Flag | Addr | DevType | TestMode | ResetDis | MinusDis | SensMin | SensMax | FreMin | FreMax | PeakMin | PeakMax | FarTol | NearTol | StepTol | BackForth | NearStay | FarStay | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B | 1B | 1B | 2B | 2B | 1B | 1B |
|
||||
|
||||
| 7Fh | 80h+ADD | 13h | 4Ch | Flag | Addr | DevType | TestMode | ResetDis | MinusDis | SensMin | SensMax | FreMin | FreMax | PeakMin | PeakMax | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B |
|
||||
Flag:是否故障标志,00=正常,01=故障
|
||||
|
||||
Flag:是否故障标志
|
||||
Addr:设备地址
|
||||
|
||||
00----正常
|
||||
DevType:设备型号
|
||||
|
||||
01----故障
|
||||
TestMode:测试模式(0=灵敏度测试, 1=波动测试)
|
||||
|
||||
测试用例:7F 81 13 4C 00 01 01 00 30 0D 00 8B 00 E6 07 76 06 D6 09 B0 0C 1C 86 CA
|
||||
ResetDis:复位距离,单位cm
|
||||
|
||||
MinusDis:皮距,单位cm
|
||||
|
||||
SensMin, SensMax:灵敏度最小、最大值(小端模式)
|
||||
|
||||
FreMin, FreMax:频率最小、最大值(小端模式)
|
||||
|
||||
PeakMin, PeakMax:峰峰值最小、最大值(小端模式)
|
||||
|
||||
FarTol:最远容差,单位cm
|
||||
|
||||
NearTol:最近容差,单位cm
|
||||
|
||||
StepTol:步进容差,单位cm
|
||||
|
||||
BackForth:来回次数
|
||||
|
||||
NearStay:最近停留时间,单位ms(小端模式)
|
||||
|
||||
FarStay:最远停留时间,单位ms(小端模式)
|
||||
|
||||
测试用例:7F 81 1B 4C 00 01 01 01 30 0D 00 8B 00 E6 07 76 06 D6 09 B0 0C 1C 03 02 00 05 C8 00 F4 01 8A 5E
|
||||
|
||||
## 5.11 出厂初始化指令,命令:0x4D
|
||||
|
||||
@@ -413,11 +481,9 @@ eg: 7F 81 01 4D CD CF
|
||||
| 7Fh | 80H +ADD | LEN | 4Eh | XOR | SUM |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
获取控制卡的版本号
|
||||
|
||||
LEN: 0x01
|
||||
|
||||
eg: 7F 81 01 4E CA CC
|
||||
eg: 7F 81 01 4E CE D0
|
||||
|
||||
返回格式:
|
||||
|
||||
@@ -432,6 +498,63 @@ eg: 7F 81 01 4E CA CC
|
||||
|
||||
测试用例:7F 81 02 4E 00 CD D1
|
||||
|
||||
# 6 测试模式流程说明
|
||||
|
||||
DG430 支持两种测试模式,通过 TestMode 参数切换。
|
||||
|
||||
## 6.1 灵敏度测试模式
|
||||
|
||||
TestMode=0 为灵敏度测试模式,用于模拟基本的过车情况。
|
||||
|
||||
**物理布局**
|
||||
|
||||
电机前端有一个金属板,线圈在激光传感器和金属板之间,激光到线圈的距离为 `MinusDis` 皮距。线圈接到车检器上,车检器的输出信号接到测试工装的输入口。
|
||||
|
||||
**测试流程**
|
||||
|
||||
1. **初始阶段** — 开始测试时,金属板从设定的**复位距离**开始向线圈侧前进。
|
||||
2. **进入检测** — 金属板靠近线圈过程中,车检器输出信号(继电器吸合),此时测试工装记录激光检测的距离为**进入距离/进入高度**(需减去皮距),记录此前金属板运行的**进入速度**、**进入频率**等参数。电机立刻停止,等待一个延时后再反向运动(远离线圈)。
|
||||
3. **离开检测** — 车检器输出信号消失(继电器释放),此时测试工装记录激光检测的距离为**离开距离/离开高度**(需减去皮距)。离开到接近复位距离时,测试工装上报测试数据(0xB2),数码管显示进入高度和离开高度。电机回到复位距离位置,等待下一次开始测试指令。
|
||||
|
||||
**上报数据 (0xB2)**
|
||||
|
||||
每次完成一次完整测试后上报 0xB2 状态数据,包含:设备型号、峰峰值、工作频率、进入/离开高度、进入/离开速度、故障信息、继电器状态等。详见 [5.3 上报状态指令](#53-上报状态指令命令-0xb2)。
|
||||
|
||||
## 6.2 波动测试流程
|
||||
|
||||
TestMode=1 为波动测试模式,用于模拟复杂的过车情况。
|
||||
|
||||
电机前端有一个金属板,线圈在激光传感器和金属板之间,激光到线圈的距离为 `MinusDis` 皮距。线圈接到车检器上,车检器的输出信号接到测试工装的输入口。
|
||||
|
||||
1. **初始阶段** — 开始测试时,金属板从设定的**复位距离**开始向线圈侧前进。
|
||||
2. **进入检测** — 金属板靠近线圈过程中,车检器输出信号(继电器吸合),此时测试工装记录激光检测的距离为**进入距离/进入高度**(需减去皮距)。电机立刻停止,然后反向运动(远离线圈)。
|
||||
3. **离开检测** — 车检器输出信号消失(继电器释放),此时测试工装记录激光检测的距离为**离开距离/离开高度**(需减去皮距)。测试工装上报测试数据(0xB2),数码管显示进入高度和离开高度。
|
||||
4. **计算波动范围**:
|
||||
- **波动最远距离** = 离开高度 − 最远容差,实际计算的时候要加上皮距 来比较。
|
||||
- **波动最近距离** = 最近容差,实际计算的时候要加上皮距 来比较。
|
||||
5. **波动测试** — 金属板从波动最远距离开始,前进到波动最近距离,电机停止 → 上报 0xB4 → 等待最近停留时间 → 反向运动到波动最远距离 → 电机停止 → 上报 0xB4 → 等待最远停留时间。记为**一次来回**。
|
||||
6. **步进容差** — 若步进容差 ≠ 0,每次波动后最远容差递加步进容差值,最多递加 (波动次数 − 1) 次。
|
||||
7. **完成阶段** — 最后一次波动结束后,电机继续远离线圈运动,检测到车检器输出信号释放后再次上报 0xB2,电机回到复位距离位置,等待下一次开始测试指令。
|
||||
|
||||
## 6.3 波动测试关键参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 最远容差 (FarTol) | 1B, cm | 离开高度 + 皮距 − 该值 = 波动最远距离 |
|
||||
| 最近容差 (NearTol) | 1B, cm | 皮距 + 该值 = 波动最近距离 |
|
||||
| 步进容差 (StepTol) | 1B, cm | 默认0。非0时每次波动后最远容差递加 |
|
||||
| 来回次数 (BackForth) | 1B | 波动来回次数 |
|
||||
| 最近停留时间 (NearStay) | 2B, ms | 到达最近距离时停留的时间 |
|
||||
| 最远停留时间 (FarStay) | 2B, ms | 到达最远距离时停留的时间 |
|
||||
|
||||
## 6.4 0xB4 上报时机
|
||||
|
||||
0xB4 在以下三种情况下触发:
|
||||
|
||||
1. 金属板行驶到**波动最近距离**时上报
|
||||
2. 金属板行驶到**波动最远距离**时上报
|
||||
3. 波动的过程中**车检器输出状态有变化**时上报
|
||||
|
||||
# 更新记录
|
||||
|
||||
| **版本号** | **更新日期** | **状态** | **更新内容** | **更新人** |
|
||||
@@ -440,4 +563,6 @@ eg: 7F 81 01 4E CA CC
|
||||
| V2.0.0 | 2026-05-21 | | 增加配置指令:电机前进、后退、停止指令,获取版本号、读写测试指令 | 王飞强 |
|
||||
| V2.0.1 | 2026-05-22 | | 增加出厂初始化指令、设备复位指令 | 王飞强 |
|
||||
| V2.0.2 | 2026-05-25 | | 增加测试模式,增加进入速度和离开速度 | 王飞强 |
|
||||
| | | | | |
|
||||
| V2.0.3 | 2026-05-31 | | TestMode=1改为波动测试模式;0x4B/0x4C新增6个波动参数;新增0xB4波动测试上报指令 | 王飞强 |
|
||||
| V2.0.4 | 2026-06-01 | | 第6章重构:拆分为灵敏度测试(6.1)和波动测试(6.2-6.4)流程说明 | 王飞强 |
|
||||
| | | | | |
|
||||
|
||||
572
docs/VD测试工装V1.0培训手册.md
Normal file
572
docs/VD测试工装V1.0培训手册.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# VD 测试工装 V1.0 培训手册
|
||||
|
||||
> **版本**: V1.0
|
||||
> **日期**: 2026-05-31
|
||||
> **作者**: wangfq
|
||||
> **适用对象**: 测试工程师、生产操作员、系统管理员
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#1-项目概述)
|
||||
2. [系统架构](#2-系统架构)
|
||||
3. [硬件环境](#3-硬件环境)
|
||||
4. [EDC 服务](#4-edc-服务-edc_server)
|
||||
5. [EDC 管理系统](#5-edc-管理系统-edc-web)
|
||||
6. [通信协议](#6-通信协议)
|
||||
7. [操作指南](#7-操作指南)
|
||||
8. [常见问题](#8-常见问题)
|
||||
9. [附录](#9-附录)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 项目简介
|
||||
|
||||
**VD 测试工装**(vd_test_fixture)是一套车检器自动化测试系统,用于**批量检测车检器(Vehicle Detector)的核心性能指标**。
|
||||
|
||||
**核心能力**:
|
||||
- **灵敏度测试**: 检测车检器对不同信号强度的响应
|
||||
- **产品一致性测试**: 批量产品之间的性能差异分析
|
||||
- **自动化测试**: 支持设定测试次数,自动循环执行,实时进度反馈
|
||||
- **工装配置管理**: 支持 DG430 V2.0.x 协议的设备参数配置、版本查询、出厂初始化
|
||||
|
||||
### 1.2 术语说明
|
||||
|
||||
| 术语 | 全称 | 说明 |
|
||||
|------|------|------|
|
||||
| **EDC** | Edge Data Center | 边缘数据中心,系统的核心服务 |
|
||||
| **DNT** | Data Network Terminal | 联网终端(PGLC),连接设备与 EDC |
|
||||
| **DG430** | — | 地感测试工装硬件,执行实际测试动作 |
|
||||
| **VD** | Vehicle Detector | 车检器(被测设备) |
|
||||
| **SerialNet** | Serial Network | 串口网络透传,通过 UDP 将指令转发到 DG430 串口 |
|
||||
|
||||
### 1.3 系统组成
|
||||
|
||||
```
|
||||
vd_test_fixture/
|
||||
├── edc_server/ # EDC 边缘数据中心(后端服务)
|
||||
│ ├── src/server.py # UDP/TCP 异步网络服务
|
||||
│ ├── src/handlers.py # 业务处理 + 后台轮询
|
||||
│ ├── src/models.py # 数据库模型 (aiomysql)
|
||||
│ ├── src/dg430.py # DG430 二进制协议解析
|
||||
│ └── src/protocol.py # PGLC JSON 协议解析
|
||||
├── edc-web/ # Flask Web 管理系统(前端)
|
||||
│ ├── app/routes/ # 页面路由: 设备/测试/数据/工装/用户/日志
|
||||
│ ├── app/templates/ # Jinja2 页面模板
|
||||
│ └── app/static/ # CSS + JS
|
||||
├── docs/ # 协议文档 + 培训手册
|
||||
└── MySQL # 共享数据库 (edc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统架构
|
||||
|
||||
### 2.1 整体架构图
|
||||
|
||||
```
|
||||
┌──────────────────┐ 浏览器 HTTPS
|
||||
│ edc-web │ ◄────────────────────── 操作人员
|
||||
│ Flask (Flask-Login) │
|
||||
│ 前端管理界面 │
|
||||
└────────┬─────────┘
|
||||
│ pymysql (同步)
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ MySQL │
|
||||
│ 数据库: edc │
|
||||
│ (共享存储) │
|
||||
└────────┬─────────┘
|
||||
│ aiomysql (异步)
|
||||
▼
|
||||
┌──────────────────┐ UDP :4900 ┌──────────────┐ RS485/TTL ┌────────────┐
|
||||
│ edc_server │ ◄──────────────► │ PGLC 联网终端 │ ◄───────────► │ DG430 工装 │
|
||||
│ Python/uvloop │ SerialNet 透传 │ (DNT) │ 串口协议 │ (测试硬件) │
|
||||
│ │ └──────────────┘ └──────┬─────┘
|
||||
│ UDP :5500/:5505 │ │
|
||||
│ TCP :5550 │ ▼
|
||||
└──────────────────┘ ┌──────────────┐
|
||||
│ 车检器(VD) │
|
||||
│ (被测设备) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 2.2 通信链路
|
||||
|
||||
```
|
||||
操作员浏览器 → edc-web (Flask, port 5000) → MySQL → edc_server (asyncio) → DNT → DG430 → 车检器
|
||||
↕
|
||||
MySQL (同步)
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- edc_server 和 edc-web 共享同一 MySQL 数据库
|
||||
- edc_server 使用 aiomysql (异步),edc-web 使用 pymysql (同步),互不冲突
|
||||
- 前端通过 edc-web 的 REST API 下发指令,实际执行由 edc_server 的轮询任务完成
|
||||
|
||||
### 2.3 端口分配
|
||||
|
||||
| 端口 | 方向 | 协议 | 说明 |
|
||||
|------|------|------|------|
|
||||
| **5500** | 监听 | UDP | EDC 设备发现 / 心跳 |
|
||||
| **5505** | 监听 | UDP | EDC 消息监听 |
|
||||
| **5550** | 监听 | TCP | EDC 时间同步 / 数据上报 / 串口透传 |
|
||||
| **4900** | 发送 | UDP | 向设备发送 SerialNet 透传指令 |
|
||||
| **5550** | 发送 | TCP | 向设备发送 TCP 数据 |
|
||||
| **5000** | 监听 | HTTP | edc-web Flask 管理界面 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 硬件环境
|
||||
|
||||
### 3.1 DG430 地感测试工装
|
||||
|
||||
DG430 是执行测试的核心硬件,负责控制电机驱动模拟车辆经过地感线圈,并采集车检器的响应数据。
|
||||
|
||||
**接口**:
|
||||
| 接口 | 连接 | 说明 |
|
||||
|------|------|------|
|
||||
| IN1/GND | 地感存在信号 | 检测线圈是否有车 |
|
||||
| IN2/GND | 地感脉冲信号 | 检测脉冲继电器 |
|
||||
| IN3/COM | 按钮 | 按下开始测试 |
|
||||
| IN4/COM | 按钮 | 按下复原位置 |
|
||||
| PU+/PU-/DR+/DR-/MF+/MF- | 电机驱动器 | 控制电机前进/后退 |
|
||||
| +5V/GND/NO/NC | 限位开关 | 有信号电机停转 |
|
||||
| 485A/485B | RS485 | 接 PGLC 联网终端 |
|
||||
| GND/LP | 地感线圈 | 模拟车辆通过 |
|
||||
| SW3 | 激光探头 | 检测进入/离开 |
|
||||
|
||||
**拨码开关**:
|
||||
- DIP1=OFF, DIP2=OFF → 测试 132 系列地感
|
||||
- DIP1=ON, DIP2=OFF → 测试 110 系列地感
|
||||
|
||||
**声音提示**:
|
||||
| 声音 | 含义 |
|
||||
|------|------|
|
||||
| 2 声 | 工作频率/峰峰值异常 |
|
||||
| 3 声 | 灵敏度异常 |
|
||||
| 4 声 | 灵敏度提升异常 (132 DIP5) |
|
||||
| 5 声 | 非离开脉冲 |
|
||||
| 6 声 | 脉冲继电器无输入 |
|
||||
|
||||
### 3.2 PGLC 联网终端 (DNT)
|
||||
|
||||
PGLC 终端是连接 EDC 服务和 DG430 工装的**网络桥接设备**:
|
||||
- 通过 **RS485/TTL 串口** 连接 DG430 工装
|
||||
- 通过 **TCP/UDP 网络** 连接 EDC 服务
|
||||
- 负责串口数据与网络数据的双向透传
|
||||
|
||||
---
|
||||
|
||||
## 4. EDC 服务 (edc_server)
|
||||
|
||||
### 4.1 功能概述
|
||||
|
||||
EDC 服务是整个系统的**数据中枢**,负责:
|
||||
|
||||
1. **设备管理**: 发现、注册、心跳检测、在线状态维护
|
||||
2. **数据采集**: 接收设备上报的测试数据、原始传感数据
|
||||
3. **协议解析**: 解析 DG430 二进制协议,提取测试结果
|
||||
4. **指令透传**: SerialNet 透传机制,将前端指令下发给设备
|
||||
5. **后台轮询**: 自动化测试调度、超时检测、状态流转
|
||||
|
||||
### 4.2 启动方式
|
||||
|
||||
```bash
|
||||
cd edc_server
|
||||
source .venv/bin/activate
|
||||
|
||||
# 配置环境变量
|
||||
export EDC_MYSQL_HOST=127.0.0.1
|
||||
export EDC_MYSQL_USER=dg
|
||||
export EDC_MYSQL_PASSWORD=123456
|
||||
export EDC_MYSQL_DB=edc
|
||||
|
||||
# 启动
|
||||
python run.py
|
||||
```
|
||||
|
||||
### 4.3 数据库表结构
|
||||
|
||||
| 表名 | 用途 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| `dnt_info` | 联网终端信息 | serial(唯一), ip, state(在线/离线) |
|
||||
| `tb_state_tst` | 设备测试状态 | dnt_id, pcnum, serialnum, ppvalue, idle_freq, enter_freq, exit_freq |
|
||||
| `tb_serialnet` | 透传发送队列 | dnt_id, send_pkg, rcv_pkg, state(0未发→1已发→2已完成→3超时) |
|
||||
| `tb_fixture_param` | 工装测试参数 | dnt_id(UNIQUE), dev_type, test_mode, sens_min/max, freq_min/max |
|
||||
| `tb_vechicle_base_test` | 车检器基准参数 | type_num(编码), relay_exist, relay_pluse, sens_min/max 等 |
|
||||
| `tb_user` | 用户账号 | username, password_hash, role(admin/operator) |
|
||||
| `tb_log` | 操作日志 | user_id, action_type, target, detail, result, ip |
|
||||
| `tb_collect_{DeviceID}` | 设备原始数据采集表 | dat_type, raw_data, state(0未处理/1已处理) |
|
||||
|
||||
### 4.4 关键流程
|
||||
|
||||
#### 设备注册流程
|
||||
|
||||
```
|
||||
设备上电 → TCP 时间同步 → UDP 上报 Count_Off → EDC 检查 serial
|
||||
├─ 已注册 → 更新 IP/网关 + last_login
|
||||
└─ 未注册 → 插入 dnt_info + 创建 tb_collect_{DeviceID}
|
||||
```
|
||||
|
||||
#### SerialNet 透传流程
|
||||
|
||||
```
|
||||
前端 → edc-web → INSERT tb_serialnet (state=0)
|
||||
→ serialnet_loop 轮询 → UDP 发送到设备 (state=1)
|
||||
→ 设备回复 TSReport(dat_type=8 或 9)
|
||||
→ parse_loop 解析 → UPDATE tb_serialnet (state=2, rcv_pkg=...)
|
||||
→ 前端轮询 api/fixture/serialnet/{id} → 显示结果
|
||||
```
|
||||
|
||||
#### 超时检测
|
||||
|
||||
- serialnet_loop 每轮检查 `state=1` 且 `update_time > 10s` 的记录
|
||||
- 超时记录置为 `state=3`(超时)
|
||||
|
||||
---
|
||||
|
||||
## 5. EDC 管理系统 (edc-web)
|
||||
|
||||
### 5.1 功能概述
|
||||
|
||||
edc-web 是基于 Flask 的 Web 管理系统,提供图形化操作界面。
|
||||
|
||||
**功能模块**:
|
||||
|
||||
| 模块 | URL | 功能 |
|
||||
|------|-----|------|
|
||||
| **登录** | `/login` | 用户认证 (Flask-Login) |
|
||||
| **设备管理** | `/devices` | 联网终端列表、在线状态、名称修改 |
|
||||
| **测试操作** | `/test-op/<dnt_id>` | 单次测试、手动控制、自动化测试 |
|
||||
| **测试数据** | `/test-data` | 历史测试数据查询、分页、导出 |
|
||||
| **工装配置** | `/fixture/<dnt_id>` | DG430 V2.0 配置指令、参数管理 |
|
||||
| **用户管理** | `/users` | 账号管理 (admin only) |
|
||||
| **操作日志** | `/logs` | 操作记录审计 |
|
||||
|
||||
### 5.2 启动方式
|
||||
|
||||
```bash
|
||||
cd edc-web
|
||||
source .venv/bin/activate
|
||||
|
||||
# 配置环境变量(与 edc_server 相同)
|
||||
export EDC_MYSQL_HOST=127.0.0.1
|
||||
export EDC_MYSQL_USER=dg
|
||||
export EDC_MYSQL_PASSWORD=123456
|
||||
export EDC_MYSQL_DB=edc
|
||||
|
||||
# 启动(默认 5000 端口)
|
||||
python run.py
|
||||
```
|
||||
|
||||
**首次启动**: 自动创建默认管理员 `admin / admin123`。
|
||||
|
||||
### 5.3 用户角色
|
||||
|
||||
| 角色 | 权限 |
|
||||
|------|------|
|
||||
| **admin** | 全部功能:设备管理、测试操作、用户管理、日志查看 |
|
||||
| **operator** | 受限功能:设备管理、测试操作 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 通信协议
|
||||
|
||||
### 6.1 DG430 串口协议
|
||||
|
||||
DG430 与 PGLC 终端之间的通信协议,采用**一问一答**方式。
|
||||
|
||||
**数据包格式** (7 字节):
|
||||
|
||||
```
|
||||
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐
|
||||
│ STX │ ADDR │ LEN │ CMD │ DATA │ XOR │ SUM │
|
||||
│ 0x7F │ 1B │ 1B │ 1B │ LEN-1│ 1B │ 1B │
|
||||
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
||||
```
|
||||
|
||||
#### 测试操作指令 (V1.0)
|
||||
|
||||
| 指令 | CMD | Hex (addr=0x01) | 说明 |
|
||||
|------|-----|-----------------|------|
|
||||
| 开始测试 | B0 | `7F 81 01 B0 30 32` | 启动一次测试流程 |
|
||||
| 测试复原 | B1 | `7F 81 01 B1 31 33` | 恢复到初始位置 |
|
||||
| 电机前进 | BA | `7F 81 01 BA 3A 3C` | 电机正转 |
|
||||
| 电机后退 | BB | `7F 81 01 BB 3B 3D` | 电机反转 |
|
||||
| 电机停止 | BC | `7F 81 01 BC 3C 3E` | 电机停止 |
|
||||
|
||||
#### 工装配置指令 (V2.0.x) NEW
|
||||
|
||||
| 指令 | CMD | 说明 |
|
||||
|------|-----|------|
|
||||
| 获取版本号 | 4A | 查询 DG430 固件和硬件版本 |
|
||||
| 配置测试参数 | 4B | 下发工装参数(动态构造) |
|
||||
| 查询测试参数 | 4C | 查询当前全部参数 |
|
||||
| 出厂初始化 | 4D | 恢复出厂设置 |
|
||||
| 设备复位 | 4E | 重启 DG430 设备 |
|
||||
|
||||
> ⚠️ **端序注意**: B2 上报和 0x4B/0x4C 的 2 字节字段**统一用小端** (`_le16`)。协议文档测试用例写的是大端,但实际硬件发送小端。
|
||||
|
||||
#### B2 状态上报
|
||||
|
||||
设备完成测试后自动上报状态数据包,包含:
|
||||
|
||||
| 字段 | 说明 | 范围 |
|
||||
|------|------|------|
|
||||
| State | 设备状态 | 正常/故障 |
|
||||
| Mode | 工作模式 | — |
|
||||
| Sens | 灵敏度 | 实际值 |
|
||||
| PPValue | 峰峰值 | mV |
|
||||
| IdleFreq | 开始工作频率 | kHz |
|
||||
| EnterFreq | 进入工作频率 | kHz |
|
||||
| EnterDist | 进入距离 | mm |
|
||||
| ExitDist | 离开距离 | mm |
|
||||
| EnterSpeed | 进入速度 | dm/s |
|
||||
| ExitSpeed | 离开速度 | dm/s |
|
||||
|
||||
### 6.2 PGLC 网络接口协议
|
||||
|
||||
EDC 服务与 PGLC 终端之间的 JSON 协议。
|
||||
|
||||
#### 主要方法
|
||||
|
||||
| Method | 方向 | 说明 |
|
||||
|--------|------|------|
|
||||
| `TimeStamp` | 设备→EDC | 时间同步请求 |
|
||||
| `Count_Off` | 设备→EDC | 设备注册/发现 |
|
||||
| `TSReport` | 设备→EDC | 子设备数据上报 |
|
||||
| `SerialNet` | EDC→设备 | 串口透传指令下发 |
|
||||
| `Heartbeat` | 设备→EDC | 心跳包 |
|
||||
|
||||
#### SerialNet 透传格式
|
||||
|
||||
```json
|
||||
{
|
||||
"Method": "SerialNet",
|
||||
"Params": {
|
||||
"Addr": 1,
|
||||
"SerialCmd": "7F8101B03032",
|
||||
"SerialDat": "",
|
||||
"WorkMode": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 操作指南
|
||||
|
||||
### 7.1 登录系统
|
||||
|
||||
1. 浏览器访问 `http://<服务器IP>:5000/login`
|
||||
2. 默认账号: `admin` / `admin123`
|
||||
3. 登录后跳转到设备管理页
|
||||
|
||||
### 7.2 设备管理
|
||||
|
||||
**查看设备列表**:
|
||||
- 首页展示所有联网终端
|
||||
- 显示序列号、名称、IP、在线状态
|
||||
|
||||
**修改设备名称**:
|
||||
- 点击设备行的名称编辑图标
|
||||
- 输入新名称后提交保存
|
||||
|
||||
**进入测试**:
|
||||
- 点击设备行右侧的 **「测试」** 按钮
|
||||
|
||||
### 7.3 手动测试操作
|
||||
|
||||
在测试操作页面:
|
||||
|
||||
1. **单次测试**: 点击 **「开始测试」** 按钮,DG430 执行一次完整测试流程
|
||||
2. **手动控制** (仅 admin):
|
||||
- **测试复原**: 恢复初始位置
|
||||
- **电机前进/后退/停止**: 手动控制电机
|
||||
3. **实时数据**: 页面自动显示设备上报的最新测试数据
|
||||
|
||||
### 7.4 自动化测试
|
||||
|
||||
1. 在测试次数输入框中填入测试次数(如 `10`)
|
||||
2. 点击 **「开始」** 按钮
|
||||
- 进度条显示 `已完成/总数`
|
||||
- 成功/失败计数实时更新
|
||||
- 平均值区域持续刷新
|
||||
3. 如需中途停止,点击 **「结束」** 按钮
|
||||
|
||||
**自动化流程**:
|
||||
```
|
||||
开始 → INSERT tb_serialnet (0xB0, state=0)
|
||||
→ serialnet_loop 发送 UDP (state=1)
|
||||
→ 等待设备 B2 上报
|
||||
→ parse_loop 解析存入 tb_state_tst
|
||||
→ 匹配 serialnet 记录 (state=2)
|
||||
→ 前端轮询进度 → 自动发送下一次 0xB0
|
||||
→ ...重复...
|
||||
→ 全部完成 / 超时(fail) / 手动结束
|
||||
```
|
||||
|
||||
**超时处理**: 单次测试超时 10 秒,超时计入失败,自动继续下一次。
|
||||
|
||||
### 7.5 工装配置管理 (V2.0 新增)
|
||||
|
||||
1. 从设备页面进入 **「工装配置」**
|
||||
|
||||
**工装参数设置**:
|
||||
- 选择设备类型、测试模式
|
||||
- 配置灵敏度范围、频率范围、峰峰值范围
|
||||
- 配置 / 查询 / 初始化操作
|
||||
|
||||
**DG430 版本查询**:
|
||||
- 点击 **「获取设备版本号」**,查看硬件/固件版本
|
||||
|
||||
**通信日志**:
|
||||
- 每次指令发送后,下方自动显示通信日志
|
||||
- `→ 发送` (绿色): 发出的指令 hex
|
||||
- `← 收到` (橙色): 设备返回的 hex
|
||||
- `✓ 成功` / `✗ 失败`: 解析结果
|
||||
|
||||
### 7.6 测试数据查询
|
||||
|
||||
1. 点击 **「测试信息」** 菜单
|
||||
2. 功能:
|
||||
- **分页浏览**: 翻页查看历史数据
|
||||
- **关键字搜索**: 按设备编码、批次号搜索
|
||||
- **日期筛选**: 按日期范围过滤
|
||||
- **导出**: 导出测试数据
|
||||
|
||||
### 7.7 用户管理 (admin only)
|
||||
|
||||
1. 点击 **「用户管理」** 菜单
|
||||
2. 功能:
|
||||
- 添加新用户(用户名、密码、角色)
|
||||
- 修改用户角色
|
||||
- 删除用户
|
||||
|
||||
### 7.8 操作日志
|
||||
|
||||
1. 点击 **「操作日志」** 菜单
|
||||
2. 记录类型:
|
||||
- `login` / `logout`: 登录/登出记录
|
||||
- `command`: 指令下发记录
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### 8.1 设备离线
|
||||
|
||||
**现象**: 设备列表显示"离线"
|
||||
|
||||
**排查**:
|
||||
1. 检查设备供电是否正常
|
||||
2. 检查网络连接(ping 设备 IP)
|
||||
3. 查看 edc_server 日志,确认是否收到心跳
|
||||
4. 设备超时阈值: 默认 120 秒(`EDC_DEVICE_TIMEOUT`)
|
||||
|
||||
### 8.2 测试超时
|
||||
|
||||
**现象**: 自动化测试中连续超时
|
||||
|
||||
**排查**:
|
||||
1. 检查 DG430 工装是否正常上电
|
||||
2. 检查 RS485 连接是否正确
|
||||
3. 在工装配置页发送「获取设备版本号」,验证通信链路
|
||||
4. 超时阈值: 10 秒
|
||||
|
||||
### 8.3 速度显示异常
|
||||
|
||||
**现象**: 速度值看起来偏大 10 倍
|
||||
|
||||
**原因**: DG430 协议存储的是 dm/s(分米/秒),系统已自动转换为 m/s(米/秒)。如果发现显示值异常,检查转换逻辑。
|
||||
|
||||
### 8.4 首次启动无管理员
|
||||
|
||||
**现象**: 无法登录
|
||||
|
||||
**解决方案**: edc-web 首次启动时自动创建 `admin / admin123`。如果手动清空过 `tb_user` 表,重启 edc-web 即可重新创建。
|
||||
|
||||
### 8.5 串口通信失败
|
||||
|
||||
**现象**: 工装配置指令无响应
|
||||
|
||||
**排查**:
|
||||
1. 确认 PGLC 终端固件支持 SerialNet 透传
|
||||
2. 确认 RS485 波特率: 19200,TTL 波特率: 115200
|
||||
3. 确认 0x4B/0x4C 指令使用了小端字节序
|
||||
|
||||
---
|
||||
|
||||
## 9. 附录
|
||||
|
||||
### A. DG430 指令速查
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DG430 指令速查表 │
|
||||
├────────┬──────────┬─────────────────────────────────────────┤
|
||||
│ CMD │ 指令 │ Hex (addr=0x01) │
|
||||
├────────┼──────────┼─────────────────────────────────────────┤
|
||||
│ 0xB0 │ 开始测试 │ 7F 81 01 B0 30 32 │
|
||||
│ 0xB1 │ 测试复原 │ 7F 81 01 B1 31 33 │
|
||||
│ 0xBA │ 电机前进 │ 7F 81 01 BA 3A 3C │
|
||||
│ 0xBB │ 电机后退 │ 7F 81 01 BB 3B 3D │
|
||||
│ 0xBC │ 电机停止 │ 7F 81 01 BC 3C 3E │
|
||||
│ 0x4A │ 版本查询 │ 7F 81 01 4A CA CC │
|
||||
│ 0x4B │ 配置参数 │ 动态构造 │
|
||||
│ 0x4C │ 查询参数 │ 7F 81 01 4C CC CE │
|
||||
│ 0x4D │ 出厂初始化 │ 7F 81 01 4D CD CF │
|
||||
│ 0x4E │ 设备复位 │ 7F 81 01 4E CE D0 │
|
||||
└────────┴──────────┴─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### B. 环境变量速查
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `EDC_MYSQL_HOST` | 127.0.0.1 | MySQL 地址 |
|
||||
| `EDC_MYSQL_PORT` | 3306 | MySQL 端口 |
|
||||
| `EDC_MYSQL_USER` | dg | 数据库用户 |
|
||||
| `EDC_MYSQL_PASSWORD` | 123456 | 数据库密码 |
|
||||
| `EDC_MYSQL_DB` | edc | 数据库名 |
|
||||
| `EDC_UDP_PORT` | 5500 | UDP 设备发现 |
|
||||
| `EDC_UDP_MSG_PORT` | 5505 | UDP 消息监听 |
|
||||
| `EDC_TCP_PORT` | 5550 | TCP 数据上报 |
|
||||
| `EDC_DEVICE_UDP_PORT` | 4900 | 设备端 UDP |
|
||||
| `EDC_DEVICE_TCP_PORT` | 5550 | 设备端 TCP |
|
||||
| `EDC_DEVICE_TIMEOUT` | 120 | 设备离线超时(秒) |
|
||||
| `EDC_PARSE_POLL_INTERVAL` | 0.5 | 解析轮询间隔(秒) |
|
||||
| `EDC_LOG_LEVEL` | INFO | 日志级别 |
|
||||
| `EDC_WEB_PORT` | 5000 | Web 管理端口 |
|
||||
|
||||
### C. 数据库快速参考
|
||||
|
||||
```sql
|
||||
-- 查看在线设备
|
||||
SELECT serial, name, ip, state, last_login FROM dnt_info WHERE state=1;
|
||||
|
||||
-- 查看最近测试数据
|
||||
SELECT s.*, d.serial, d.name
|
||||
FROM tb_state_tst s
|
||||
JOIN dnt_info d ON s.dnt_id = d.id
|
||||
ORDER BY s.create_time DESC LIMIT 20;
|
||||
|
||||
-- 查看待发送指令
|
||||
SELECT * FROM tb_serialnet WHERE state=0;
|
||||
|
||||
-- 查看操作日志
|
||||
SELECT l.*, u.username
|
||||
FROM tb_log l
|
||||
JOIN tb_user u ON l.user_id = u.id
|
||||
ORDER BY l.create_time DESC LIMIT 50;
|
||||
```
|
||||
|
||||
### D. 修订历史
|
||||
|
||||
| 版本 | 日期 | 说明 | 作者 |
|
||||
|------|------|------|------|
|
||||
| V1.0 | 2026-05-31 | 初始版本 | wangfq |
|
||||
248
docs/plans/2026-05-31-wave-test-frontend.md
Normal file
248
docs/plans/2026-05-31-wave-test-frontend.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 波动测试模式前端适配 实施计划
|
||||
|
||||
> **For Hermes:** 直接执行各 Task。
|
||||
|
||||
**Goal:** 将波动测试模式(0xB2 的 test_mode + 0xB4 上报数据)接入测试操作页面、查询导出页面,扩展 `tb_state_tst` 表字段。
|
||||
|
||||
**Architecture:** 单表方案 — `tb_state_tst` 增加 `test_mode`、`data_source` 和 B4 专属字段,B2 和 B4 记录共存同表,通过 `data_source` 区分。后端 edc_server 存储、edc-web 查询/导出均扩展。
|
||||
|
||||
**Tech Stack:** Python/aiomysql (edc_server) + Flask/pymysql (edc-web) + vanilla JS 前端
|
||||
|
||||
---
|
||||
|
||||
### 涉及文件
|
||||
|
||||
| 层 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 数据库 | `edc_server/src/models.py` | DDL + ALTER 迁移 + `insert_test_result` 扩展 + 新增 `insert_wave_data` |
|
||||
| 解析 | `edc_server/src/handlers.py` | 0xB2 传 test_mode、0xB4 调 insert_wave_data |
|
||||
| 查询 | `edc-web/app/models.py` | 新增查询函数、导出扩展 |
|
||||
| API | `edc-web/app/routes/test_op.py` | progress 端点返回波动数据 |
|
||||
| API | `edc-web/app/routes/test_data.py` | 查询/导出增加新字段 |
|
||||
| 前端 | `edc-web/app/templates/test_op.html` | 增加波动数据显示区 |
|
||||
| 前端 | `edc-web/app/static/js/test_op.js` | renderLatest/renderRecords 扩展 + B4 轮询 |
|
||||
| 前端 | `edc-web/app/templates/test_data.html` | 表头增加列 + test_mode 筛选 |
|
||||
| 前端 | `edc-web/app/static/js/test_data.js` | renderTable 扩展 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展 tb_state_tst DDL + 迁移 (edc_server)
|
||||
|
||||
**文件:** `edc_server/src/models.py`
|
||||
|
||||
**Step 1:** 修改 CREATE TABLE 增加 9 列
|
||||
|
||||
在现有 `tb_state_tst` DDL 中增加:
|
||||
|
||||
```sql
|
||||
`test_mode` TINYINT DEFAULT 0 COMMENT '0 灵敏度测试, 1 波动测试',
|
||||
`data_source` CHAR(2) DEFAULT 'B2' COMMENT '数据来源 B2/B4',
|
||||
`remain_count` INT DEFAULT 0 COMMENT '剩余波动次数 (B4)',
|
||||
`work_freq` FLOAT DEFAULT 0 COMMENT '工作频率 Hz (B4)',
|
||||
`curr_dist` INT DEFAULT 0 COMMENT '当前距离 mm (B4)',
|
||||
`speed` INT DEFAULT 0 COMMENT '当前速度 dm/s (B4)',
|
||||
`near_dist` INT DEFAULT 0 COMMENT '波动最近距离 mm (B4)',
|
||||
`far_dist` INT DEFAULT 0 COMMENT '波动最远距离 mm (B4)',
|
||||
`b4_enter_dist` INT DEFAULT 0 COMMENT 'B4 进入高度 mm',
|
||||
`b4_leave_dist` INT DEFAULT 0 COMMENT 'B4 离开高度 mm'
|
||||
```
|
||||
|
||||
**Step 2:** 在 `_create_tables` 末尾增加 ALTER TABLE 迁移逻辑(参照已有的 `tb_fixture_param` 迁移模式)
|
||||
|
||||
```python
|
||||
# V2.0.4 迁移:tb_state_tst 增加波动测试字段
|
||||
for col, col_def in [
|
||||
("test_mode", "TINYINT DEFAULT 0 COMMENT '0 灵敏度, 1 波动测试'"),
|
||||
("data_source", "CHAR(2) DEFAULT 'B2' COMMENT 'B2/B4'"),
|
||||
("remain_count", "INT DEFAULT 0 COMMENT '剩余波动次数'"),
|
||||
("work_freq", "FLOAT DEFAULT 0 COMMENT '工作频率 Hz'"),
|
||||
("curr_dist", "INT DEFAULT 0 COMMENT '当前距离 mm'"),
|
||||
("speed", "INT DEFAULT 0 COMMENT '当前速度 dm/s'"),
|
||||
("near_dist", "INT DEFAULT 0 COMMENT '波动最近距离 mm'"),
|
||||
("far_dist", "INT DEFAULT 0 COMMENT '波动最远距离 mm'"),
|
||||
("b4_enter_dist", "INT DEFAULT 0 COMMENT 'B4 进入高度 mm'"),
|
||||
("b4_leave_dist", "INT DEFAULT 0 COMMENT 'B4 离开高度 mm'"),
|
||||
]:
|
||||
try:
|
||||
await cur.execute(f"ALTER TABLE `tb_state_tst` ADD COLUMN `{col}` {col_def}")
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
**Step 3:** 修改 `insert_test_result` 函数签名,增加 `test_mode`、`data_source` 参数
|
||||
|
||||
```python
|
||||
async def insert_test_result(dnt_id, dpg430_addr, pcnum, serialnum,
|
||||
sub_type, str_type, iffinish, fault_info,
|
||||
relay_out, ppvalue, idle_freq, enter_freq,
|
||||
exit_freq, enter_dist, exit_dist,
|
||||
enter_speed, exit_speed,
|
||||
test_mode=0, data_source='B2'):
|
||||
```
|
||||
|
||||
INSERT 语句增加 `test_mode`, `data_source` 列。
|
||||
|
||||
**Step 4:** 新增 `insert_wave_data` 函数
|
||||
|
||||
```python
|
||||
async def insert_wave_data(dnt_id, dpg430_addr, remain_count, relay_out,
|
||||
work_freq, curr_dist, speed, near_dist, far_dist,
|
||||
enter_dist, leave_dist):
|
||||
"""插入 0xB4 波动测试上报数据到 tb_state_tst"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: handlers.py 适配
|
||||
|
||||
**文件:** `edc_server/src/handlers.py`
|
||||
|
||||
**Step 1:** 0xB2 处理 — `insert_test_result` 调用增加 `test_mode=status.test_mode`
|
||||
|
||||
```python
|
||||
await insert_test_result(
|
||||
...
|
||||
test_mode=status.test_mode,
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2:** 0xB4 处理 — 调用 `insert_wave_data` 存库
|
||||
|
||||
```python
|
||||
from src.models import insert_wave_data # 增加导入
|
||||
|
||||
# 在 0xB4 分支末尾
|
||||
await insert_wave_data(
|
||||
dnt_id=dnt_id,
|
||||
dpg430_addr=wave.addr,
|
||||
remain_count=wave.remain_count,
|
||||
relay_out=wave.relay_out,
|
||||
work_freq=wave.work_freq,
|
||||
curr_dist=wave.curr_dist,
|
||||
speed=wave.speed,
|
||||
near_dist=wave.near_dist,
|
||||
far_dist=wave.far_dist,
|
||||
enter_dist=wave.enter_dist,
|
||||
leave_dist=wave.leave_dist,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: edc-web models.py 查询扩展
|
||||
|
||||
**文件:** `edc-web/app/models.py`
|
||||
|
||||
**Step 1:** 新增 `get_latest_wave_data(dnt_id)` — 获取最新一条 B4 数据
|
||||
|
||||
**Step 2:** 新增 `get_wave_records(dnt_id, since)` — 获取本轮 B4 明细
|
||||
|
||||
查询条件: `data_source='B4' AND create_time >= since`
|
||||
|
||||
**Step 3:** 修改 `get_automation_averages` — 不涉及波动字段,保持不变
|
||||
|
||||
---
|
||||
|
||||
### Task 4: edc-web test_op.py 路由扩展
|
||||
|
||||
**文件:** `edc-web/app/routes/test_op.py`
|
||||
|
||||
在 `api_automation_progress` 中增加返回 B4 数据:
|
||||
|
||||
```python
|
||||
latest_wave = get_latest_wave_data(dnt_id)
|
||||
wave_records = get_wave_records(dnt_id, since) if since else []
|
||||
return jsonify({
|
||||
...
|
||||
"latest_wave": latest_wave,
|
||||
"wave_records": wave_records,
|
||||
})
|
||||
```
|
||||
|
||||
导入新增的 `get_latest_wave_data`, `get_wave_records`。
|
||||
|
||||
---
|
||||
|
||||
### Task 5: edc-web test_data.py 路由扩展
|
||||
|
||||
**文件:** `edc-web/app/routes/test_data.py`
|
||||
|
||||
**Step 1:** `api_test_data` — 增加 `test_mode` 筛选参数
|
||||
|
||||
```python
|
||||
test_mode = request.args.get("test_mode", "", type=str) # '' = 全部, '0' = 灵敏度, '1' = 波动
|
||||
```
|
||||
|
||||
传给 `get_test_data`,在 SQL WHERE 中增加 `AND t.test_mode = %s` 条件。
|
||||
|
||||
**Step 2:** 修改 `get_test_data` 函数签名和 SQL(在 models.py 中)
|
||||
|
||||
**Step 3:** CSV 导出同样支持 `test_mode` 筛选
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 前端 test_op.html + test_op.js
|
||||
|
||||
**文件:** `edc-web/app/templates/test_op.html` + `static/js/test_op.js`
|
||||
|
||||
**Step 1:** HTML — 在 `latest-result` 区域增加波动数据显示 div
|
||||
|
||||
```html
|
||||
<h3>波动测试数据</h3>
|
||||
<div id="latest-wave">
|
||||
<p class="placeholder">暂无波动数据...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Step 2:** JS — `renderLatest` 增加 test_mode 显示
|
||||
|
||||
在现有显示中增加:
|
||||
```js
|
||||
<p>测试模式:<strong>${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}</strong></p>
|
||||
```
|
||||
|
||||
**Step 3:** JS — 新增 `renderLatestWave(data)` 函数
|
||||
|
||||
显示 B4 上报数据:剩余次数、当前距离、速度、最近/最远距离、进入/离开高度。
|
||||
|
||||
**Step 4:** JS — `pollProgress` 中调用 `renderLatestWave(data.latest_wave)`
|
||||
|
||||
**Step 5:** JS — 明细表 `renderRecords` 增加测试模式列
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 前端 test_data.html + test_data.js
|
||||
|
||||
**文件:** `edc-web/app/templates/test_data.html` + `static/js/test_data.js`
|
||||
|
||||
**Step 1:** HTML — 搜索栏增加测试模式下拉筛选
|
||||
|
||||
```html
|
||||
<label>
|
||||
测试模式:
|
||||
<select id="search-test-mode">
|
||||
<option value="">全部</option>
|
||||
<option value="0">灵敏度测试</option>
|
||||
<option value="1">波动测试</option>
|
||||
</select>
|
||||
</label>
|
||||
```
|
||||
|
||||
**Step 2:** HTML — 表头增加列:测试模式、数据来源、剩余次数、工作频率、当前距离、速度、最近距离、最远距离
|
||||
|
||||
**Step 3:** JS — `searchData` 传 `test_mode` 参数
|
||||
|
||||
**Step 4:** JS — `renderTable` 增加新列渲染
|
||||
|
||||
**Step 5:** JS — `exportCSV` 传 `test_mode` 参数
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 提交推送
|
||||
|
||||
```bash
|
||||
cd /home/wfq/projects/vd_test_fixture
|
||||
git add -A
|
||||
git commit -m "feat: 波动测试模式前端适配 — tb_state_tst扩展+0xB4存库+页面更新"
|
||||
git push origin main
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
@@ -151,8 +151,13 @@ def get_latest_test_state(dnt_id: int) -> dict | None:
|
||||
|
||||
def get_test_data(page: int = 1, per_page: int = 20,
|
||||
serial: str = "", date_from: str = "",
|
||||
date_to: str = "") -> tuple[list[dict], int]:
|
||||
"""分页查询测试数据(JOIN dnt_info),返回 (records, total)"""
|
||||
date_to: str = "", test_mode: str = "",
|
||||
data_source: str = "") -> tuple[list[dict], int]:
|
||||
"""分页查询测试数据(JOIN dnt_info),返回 (records, total)
|
||||
|
||||
test_mode: ''=全部, '0'=灵敏度, '1'=波动
|
||||
data_source: ''=全部, 'B2', 'B4'
|
||||
"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
@@ -167,6 +172,12 @@ def get_test_data(page: int = 1, per_page: int = 20,
|
||||
if date_to:
|
||||
where.append("t.create_time <= %s")
|
||||
params.append(date_to + " 23:59:59")
|
||||
if test_mode:
|
||||
where.append("t.test_mode = %s")
|
||||
params.append(int(test_mode))
|
||||
if data_source:
|
||||
where.append("t.data_source = %s")
|
||||
params.append(data_source)
|
||||
|
||||
where_clause = " AND ".join(where) if where else "1=1"
|
||||
|
||||
@@ -195,8 +206,13 @@ def get_test_data(page: int = 1, per_page: int = 20,
|
||||
|
||||
|
||||
def get_all_test_data_for_export(serial: str = "", date_from: str = "",
|
||||
date_to: str = "") -> list[dict]:
|
||||
"""导出全部数据"""
|
||||
date_to: str = "", test_mode: str = "",
|
||||
data_source: str = "") -> list[dict]:
|
||||
"""导出全部数据
|
||||
|
||||
test_mode: ''=全部, '0'=灵敏度, '1'=波动
|
||||
data_source: ''=全部, 'B2', 'B4'
|
||||
"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
@@ -211,6 +227,12 @@ def get_all_test_data_for_export(serial: str = "", date_from: str = "",
|
||||
if date_to:
|
||||
where.append("t.create_time <= %s")
|
||||
params.append(date_to + " 23:59:59")
|
||||
if test_mode:
|
||||
where.append("t.test_mode = %s")
|
||||
params.append(int(test_mode))
|
||||
if data_source:
|
||||
where.append("t.data_source = %s")
|
||||
params.append(data_source)
|
||||
|
||||
where_clause = " AND ".join(where) if where else "1=1"
|
||||
cur.execute(
|
||||
@@ -285,6 +307,37 @@ def get_automation_records(dnt_id: int, since: str) -> list[dict]:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_latest_wave_data(dnt_id: int) -> dict | None:
|
||||
"""获取设备最新一条 B4 波动测试数据"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM tb_state_tst WHERE dnt_id=%s AND data_source='B4' "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(dnt_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_wave_records(dnt_id: int, since: str) -> list[dict]:
|
||||
"""获取本轮 B4 波动测试明细"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM tb_state_tst "
|
||||
"WHERE dnt_id=%s AND data_source='B4' AND create_time >= %s "
|
||||
"ORDER BY id ASC",
|
||||
(dnt_id, since),
|
||||
)
|
||||
return cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── 用户管理 ──────────────────────────────────────────────────────
|
||||
|
||||
def get_user_by_username(username: str) -> dict | None:
|
||||
@@ -417,6 +470,7 @@ def upsert_fixture_param(dnt_id: int, **kwargs):
|
||||
fields = [
|
||||
"Addr", "DevType", "TestMode", "RestDis", "MinusDis",
|
||||
"SensMin", "SensMax", "FreMin", "FreMax", "PeakMin", "PeakMax",
|
||||
"FarTol", "NearTol", "StepTol", "BackForth", "NearStay", "FarStay",
|
||||
]
|
||||
if existing:
|
||||
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||
@@ -528,3 +582,48 @@ def delete_vehicle_base_test(test_id: int):
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── 测试数据删除 ──────────────────────────────────────────────
|
||||
|
||||
def delete_test_data(serial: str = "", date_from: str = "",
|
||||
date_to: str = "", data_source: str = "") -> int:
|
||||
"""删除符合条件的测试数据,返回删除行数
|
||||
|
||||
必须至少提供一个条件(serial / date范围 / data_source),不允许无条件全删。
|
||||
"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
where = []
|
||||
params = []
|
||||
if serial:
|
||||
where.append("t.dnt_id IN (SELECT id FROM dnt_info WHERE serial LIKE %s)")
|
||||
params.append(f"%{serial}%")
|
||||
if date_from:
|
||||
where.append("t.create_time >= %s")
|
||||
params.append(date_from)
|
||||
if date_to:
|
||||
where.append("t.create_time <= %s")
|
||||
params.append(date_to + " 23:59:59")
|
||||
if data_source:
|
||||
where.append("t.data_source = %s")
|
||||
params.append(data_source)
|
||||
|
||||
if not where:
|
||||
return 0 # 拒绝无条件全删
|
||||
|
||||
where_clause = " AND ".join(where)
|
||||
cur.execute(
|
||||
f"SELECT COUNT(*) as cnt FROM tb_state_tst t WHERE {where_clause}",
|
||||
params,
|
||||
)
|
||||
cnt = cur.fetchone()["cnt"]
|
||||
|
||||
cur.execute(
|
||||
f"DELETE t FROM tb_state_tst t WHERE {where_clause}", params,
|
||||
)
|
||||
conn.commit()
|
||||
return cnt
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -59,13 +59,18 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
||||
reset_dis: int, minus_dis: int,
|
||||
sens_min: int, sens_max: int,
|
||||
fre_min: int, fre_max: int,
|
||||
peak_min: int, peak_max: int) -> str:
|
||||
"""构造 0x4B 配置指令 hex 字符串
|
||||
peak_min: int, peak_max: int,
|
||||
far_tol: int = 0, near_tol: int = 0,
|
||||
step_tol: int = 0, back_forth: int = 0,
|
||||
near_stay: int = 0, far_stay: int = 0) -> str:
|
||||
"""构造 0x4B 配置指令 hex 字符串 (V2.0.3 扩展)
|
||||
|
||||
格式: 7F | 81 | 12 | 4B | Addr(1) | DevType(1) | TestMode(1) |
|
||||
格式: 7F | 81 | 17 | 4B | Addr(1) | DevType(1) | TestMode(1) |
|
||||
ResetDis(1) | MinusDis(1) |
|
||||
SensMin(2 LE) | SensMax(2 LE) | FreMin(2 LE) | FreMax(2 LE) |
|
||||
PeakMin(2 LE) | PeakMax(2 LE) | XOR | SUM
|
||||
PeakMin(2 LE) | PeakMax(2 LE) |
|
||||
FarTol(1) | NearTol(1) | StepTol(1) | BackForth(1) |
|
||||
NearStay(2 LE) | FarStay(2 LE) | XOR | SUM
|
||||
"""
|
||||
payload = bytes([
|
||||
0x4B, # CMD
|
||||
@@ -78,6 +83,14 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
||||
payload += (_le16(sens_min) + _le16(sens_max) +
|
||||
_le16(fre_min) + _le16(fre_max) +
|
||||
_le16(peak_min) + _le16(peak_max))
|
||||
# V2.0.3 波动参数
|
||||
payload += bytes([
|
||||
far_tol & 0xFF,
|
||||
near_tol & 0xFF,
|
||||
step_tol & 0xFF,
|
||||
back_forth & 0xFF,
|
||||
])
|
||||
payload += _le16(near_stay) + _le16(far_stay)
|
||||
|
||||
pkt = bytes([0x7F, 0x81, len(payload)]) + payload
|
||||
xor, total = _xor_sum(pkt[1:])
|
||||
@@ -118,7 +131,7 @@ def api_fixture_command():
|
||||
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
||||
|
||||
if cmd == "4B":
|
||||
# 动态构造 0x4B 指令
|
||||
# 动态构造 0x4B 指令 (V2.0.3)
|
||||
params = data.get("params", {})
|
||||
send_pkg = build_4b_packet(
|
||||
addr=params.get("addr", 1),
|
||||
@@ -132,6 +145,12 @@ def api_fixture_command():
|
||||
fre_max=params.get("fre_max", 0),
|
||||
peak_min=params.get("peak_min", 0),
|
||||
peak_max=params.get("peak_max", 0),
|
||||
far_tol=params.get("far_tol", 0),
|
||||
near_tol=params.get("near_tol", 0),
|
||||
step_tol=params.get("step_tol", 0),
|
||||
back_forth=params.get("back_forth", 0),
|
||||
near_stay=params.get("near_stay", 0),
|
||||
far_stay=params.get("far_stay", 0),
|
||||
)
|
||||
elif cmd in FIXTURE_COMMANDS:
|
||||
send_pkg = FIXTURE_COMMANDS[cmd]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import csv
|
||||
import io
|
||||
from flask import Blueprint, jsonify, render_template, request, Response
|
||||
from flask_login import login_required
|
||||
from app.models import get_test_data, get_all_test_data_for_export
|
||||
from flask_login import login_required, current_user
|
||||
from app.models import get_test_data, get_all_test_data_for_export, delete_test_data, insert_log
|
||||
|
||||
bp = Blueprint("test_data", __name__)
|
||||
|
||||
@@ -23,8 +23,11 @@ def api_test_data():
|
||||
serial = request.args.get("serial", "", type=str)
|
||||
date_from = request.args.get("date_from", "", type=str)
|
||||
date_to = request.args.get("date_to", "", type=str)
|
||||
test_mode = request.args.get("test_mode", "", type=str)
|
||||
data_source = request.args.get("data_source", "", type=str)
|
||||
|
||||
records, total = get_test_data(page, per_page, serial, date_from, date_to)
|
||||
records, total = get_test_data(page, per_page, serial, date_from, date_to,
|
||||
test_mode, data_source)
|
||||
return jsonify({
|
||||
"records": records,
|
||||
"total": total,
|
||||
@@ -34,14 +37,30 @@ def api_test_data():
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/test-data/chart")
|
||||
def api_chart_data():
|
||||
"""返回图表所需全部数据(不分页)"""
|
||||
serial = request.args.get("serial", "", type=str)
|
||||
date_from = request.args.get("date_from", "", type=str)
|
||||
date_to = request.args.get("date_to", "", type=str)
|
||||
test_mode = request.args.get("test_mode", "", type=str)
|
||||
data_source = request.args.get("data_source", "", type=str)
|
||||
|
||||
records = get_all_test_data_for_export(serial, date_from, date_to,
|
||||
test_mode, data_source)
|
||||
return jsonify({"records": records, "total": len(records)})
|
||||
|
||||
@bp.route("/api/test-data/export")
|
||||
def api_export():
|
||||
"""导出测试数据为 CSV"""
|
||||
serial = request.args.get("serial", "", type=str)
|
||||
date_from = request.args.get("date_from", "", type=str)
|
||||
date_to = request.args.get("date_to", "", type=str)
|
||||
test_mode = request.args.get("test_mode", "", type=str)
|
||||
data_source = request.args.get("data_source", "", type=str)
|
||||
|
||||
records = get_all_test_data_for_export(serial, date_from, date_to)
|
||||
records = get_all_test_data_for_export(serial, date_from, date_to,
|
||||
test_mode, data_source)
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
@@ -59,3 +78,39 @@ def api_export():
|
||||
mimetype="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=test_data.csv"},
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/api/test-data/delete", methods=["POST"])
|
||||
@login_required
|
||||
def api_delete():
|
||||
"""删除测试数据(仅 admin)"""
|
||||
if current_user.role != "admin":
|
||||
return jsonify({"ok": False, "error": "无权限"}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
serial = data.get("serial", "")
|
||||
date_from = data.get("date_from", "")
|
||||
date_to = data.get("date_to", "")
|
||||
data_source = data.get("data_source", "")
|
||||
|
||||
cnt = delete_test_data(serial, date_from, date_to, data_source)
|
||||
|
||||
detail_parts = [f"删除 {cnt} 条测试数据"]
|
||||
if serial:
|
||||
detail_parts.append(f"设备={serial}")
|
||||
if date_from:
|
||||
detail_parts.append(f"从{date_from}")
|
||||
if date_to:
|
||||
detail_parts.append(f"至{date_to}")
|
||||
if data_source:
|
||||
detail_parts.append(f"来源={data_source}")
|
||||
|
||||
insert_log(
|
||||
current_user.id, current_user.username, "delete",
|
||||
target="test_data",
|
||||
detail=", ".join(detail_parts),
|
||||
result="ok",
|
||||
ip=request.remote_addr or "",
|
||||
)
|
||||
|
||||
return jsonify({"ok": True, "deleted": cnt})
|
||||
|
||||
@@ -9,6 +9,8 @@ from app.models import (
|
||||
get_latest_test_state,
|
||||
get_automation_averages,
|
||||
get_automation_records,
|
||||
get_latest_wave_data,
|
||||
get_wave_records,
|
||||
clear_serialnet_records,
|
||||
insert_log,
|
||||
)
|
||||
@@ -118,9 +120,13 @@ def api_automation_progress(dnt_id):
|
||||
latest = get_latest_test_state(dnt_id)
|
||||
averages = get_automation_averages(dnt_id, since if since else None)
|
||||
records = get_automation_records(dnt_id, since) if since else []
|
||||
latest_wave = get_latest_wave_data(dnt_id)
|
||||
wave_records = get_wave_records(dnt_id, since) if since else []
|
||||
return jsonify({
|
||||
"stats": stats,
|
||||
"latest": latest,
|
||||
"averages": averages,
|
||||
"records": records,
|
||||
"latest_wave": latest_wave,
|
||||
"wave_records": wave_records,
|
||||
})
|
||||
|
||||
@@ -144,6 +144,12 @@ function fillFormFromParam(param) {
|
||||
document.getElementById("param-fre-max").value = param.FreMax || 0;
|
||||
document.getElementById("param-peak-min").value = param.PeakMin || 0;
|
||||
document.getElementById("param-peak-max").value = param.PeakMax || 0;
|
||||
document.getElementById("param-far-tol").value = param.FarTol || 0;
|
||||
document.getElementById("param-near-tol").value = param.NearTol || 0;
|
||||
document.getElementById("param-step-tol").value = param.StepTol || 0;
|
||||
document.getElementById("param-back-forth").value = param.BackForth || 0;
|
||||
document.getElementById("param-near-stay").value = param.NearStay || 0;
|
||||
document.getElementById("param-far-stay").value = param.FarStay || 0;
|
||||
const matched = baseTests.find(t => t.type_num === param.DevType);
|
||||
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
|
||||
}
|
||||
@@ -172,6 +178,9 @@ async function saveToDb() {
|
||||
SensMin: data.sens_min, SensMax: data.sens_max,
|
||||
FreMin: data.fre_min, FreMax: data.fre_max,
|
||||
PeakMin: data.peak_min, PeakMax: data.peak_max,
|
||||
FarTol: data.far_tol, NearTol: data.near_tol,
|
||||
StepTol: data.step_tol, BackForth: data.back_forth,
|
||||
NearStay: data.near_stay, FarStay: data.far_stay,
|
||||
}),
|
||||
});
|
||||
const result = await resp.json();
|
||||
@@ -197,6 +206,12 @@ function getFormParams() {
|
||||
fre_max: parseInt(document.getElementById("param-fre-max").value) || 0,
|
||||
peak_min: parseInt(document.getElementById("param-peak-min").value) || 0,
|
||||
peak_max: parseInt(document.getElementById("param-peak-max").value) || 0,
|
||||
far_tol: parseInt(document.getElementById("param-far-tol").value) || 0,
|
||||
near_tol: parseInt(document.getElementById("param-near-tol").value) || 0,
|
||||
step_tol: parseInt(document.getElementById("param-step-tol").value) || 0,
|
||||
back_forth: parseInt(document.getElementById("param-back-forth").value) || 0,
|
||||
near_stay: parseInt(document.getElementById("param-near-stay").value) || 0,
|
||||
far_stay: parseInt(document.getElementById("param-far-stay").value) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -318,6 +333,9 @@ async function sendConfig() {
|
||||
SensMin: params.sens_min, SensMax: params.sens_max,
|
||||
FreMin: params.fre_min, FreMax: params.fre_max,
|
||||
PeakMin: params.peak_min, PeakMax: params.peak_max,
|
||||
FarTol: params.far_tol, NearTol: params.near_tol,
|
||||
StepTol: params.step_tol, BackForth: params.back_forth,
|
||||
NearStay: params.near_stay, FarStay: params.far_stay,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,23 +1,138 @@
|
||||
// 测试信息页
|
||||
// 测试信息页 — 三视图 (全部 / B2 / B4)
|
||||
|
||||
// ─── 视图定义 ───────────────────────────────────
|
||||
|
||||
const VIEWS = {
|
||||
all: {
|
||||
label: '全部数据',
|
||||
data_source: '', // '' = 不过滤
|
||||
cols: [
|
||||
{ key: 'id', title: 'ID' },
|
||||
{ key: 'serial', title: '设备编码' },
|
||||
{ key: 'dpg430_addr', title: '地址' },
|
||||
{ key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
|
||||
{ key: 'str_type', title: '类型' },
|
||||
{ key: 'data_source', title: '来源' },
|
||||
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
|
||||
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
|
||||
{ key: 'idle_freq', title: '开始频率' },
|
||||
{ key: 'enter_dist', title: '进入距离' },
|
||||
{ key: 'exit_dist', title: '离开距离' },
|
||||
{ key: 'remain_count', title: '剩余次数' },
|
||||
{ key: 'curr_dist', title: '当前距离' },
|
||||
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||
],
|
||||
},
|
||||
b2: {
|
||||
label: '灵敏度测试',
|
||||
data_source: 'B2',
|
||||
cols: [
|
||||
{ key: 'id', title: 'ID' },
|
||||
{ key: 'serial', title: '设备编码' },
|
||||
{ key: 'dpg430_addr', title: '地址' },
|
||||
{ key: 'model', title: '型号', render: r => r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-' },
|
||||
{ key: 'str_type', title: '类型' },
|
||||
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
|
||||
{ key: 'iffinish', title: '完成', render: r => r.iffinish === '1' ? '是' : '否' },
|
||||
{ key: 'fault_info', title: '故障信息' },
|
||||
{ key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) },
|
||||
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
|
||||
{ key: 'idle_freq', title: '开始频率' },
|
||||
{ key: 'enter_freq', title: '进入频率' },
|
||||
{ key: 'exit_freq', title: '离开频率' },
|
||||
{ key: 'enter_dist', title: '进入距离' },
|
||||
{ key: 'exit_dist', title: '离开距离' },
|
||||
{ key: 'enter_speed', title: '进入速度', render: r => toSpeed(r.enter_speed) },
|
||||
{ key: 'exit_speed', title: '离开速度', render: r => toSpeed(r.exit_speed) },
|
||||
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||
],
|
||||
},
|
||||
b4: {
|
||||
label: '波动测试',
|
||||
data_source: 'B4',
|
||||
cols: [
|
||||
{ key: 'id', title: 'ID' },
|
||||
{ key: 'serial', title: '设备编码' },
|
||||
{ key: 'dpg430_addr', title: '地址' },
|
||||
{ key: 'remain_count', title: '剩余次数' },
|
||||
{ key: 'work_freq', title: '工作频率(Hz)' },
|
||||
{ key: 'curr_dist', title: '当前距离(mm)' },
|
||||
{ key: 'speed', title: '速度(dm/s)' },
|
||||
{ key: 'near_dist', title: '最近距离(mm)' },
|
||||
{ key: 'far_dist', title: '最远距离(mm)' },
|
||||
{ key: 'b4_enter_dist', title: '进入高度(mm)' },
|
||||
{ key: 'b4_leave_dist', title: '离开高度(mm)' },
|
||||
{ key: 'relay_out', title: '继电器', render: r => decodeRelay(r.relay_code) },
|
||||
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ─── 状态 ───────────────────────────────────────
|
||||
|
||||
let currentView = 'all';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
function toSpeed(v) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
return (parseFloat(v) / 10).toFixed(1);
|
||||
}
|
||||
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
function fmtTime(v) {
|
||||
if (!v) return '-';
|
||||
const d = new Date(v);
|
||||
if (isNaN(d.getTime())) return String(v).substring(0, 19);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const d2 = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const min = String(d.getMinutes()).padStart(2, '0');
|
||||
const s = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${y}-${m}-${d2} ${h}:${min}:${s}`;
|
||||
}
|
||||
|
||||
const RELAY_MAP = {
|
||||
0: '无输出',
|
||||
1: '存在信号',
|
||||
2: '脉冲信号',
|
||||
3: '存在信号; 脉冲信号',
|
||||
};
|
||||
function decodeRelay(v) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
return RELAY_MAP[parseInt(v)] || `0x${parseInt(v).toString(16).toUpperCase().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// ─── 视图切换 ────────────────────────────────────
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('tab-' + view).classList.add('active');
|
||||
// 重置分页
|
||||
currentPage = 1;
|
||||
searchData(1);
|
||||
}
|
||||
|
||||
// ─── 查询 ────────────────────────────────────────
|
||||
|
||||
async function searchData(page = 1) {
|
||||
currentPage = page;
|
||||
const serial = document.getElementById("search-serial").value;
|
||||
const dateFrom = document.getElementById("search-date-from").value;
|
||||
const dateTo = document.getElementById("search-date-to").value;
|
||||
const v = VIEWS[currentView];
|
||||
|
||||
const params = new URLSearchParams({ page, per_page: 20 });
|
||||
const perPage = parseInt(document.getElementById("per-page").value) || 20;
|
||||
|
||||
const params = new URLSearchParams({ page, per_page: perPage });
|
||||
if (serial) params.set("serial", serial);
|
||||
if (dateFrom) params.set("date_from", dateFrom);
|
||||
if (dateTo) params.set("date_to", dateTo);
|
||||
// 按 data_source 过滤(全部不过滤)
|
||||
if (v.data_source) {
|
||||
params.set("data_source", v.data_source);
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/test-data?${params}`);
|
||||
@@ -30,35 +145,39 @@ async function searchData(page = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 渲染表头 ────────────────────────────────────
|
||||
|
||||
function renderHead() {
|
||||
const thead = document.querySelector("#test-data-table thead");
|
||||
const v = VIEWS[currentView];
|
||||
thead.innerHTML = '<tr>' +
|
||||
v.cols.map(c => `<th>${c.title}</th>`).join('') +
|
||||
'</tr>';
|
||||
}
|
||||
|
||||
// ─── 渲染数据行 ──────────────────────────────────
|
||||
|
||||
function renderTable(records) {
|
||||
renderHead();
|
||||
const tbody = document.querySelector("#test-data-table tbody");
|
||||
const v = VIEWS[currentView];
|
||||
const nCols = v.cols.length;
|
||||
|
||||
if (!records.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="17" style="text-align:center;color:#999;">暂无数据</td></tr>';
|
||||
tbody.innerHTML = `<tr><td colspan="${nCols}" style="text-align:center;color:#999;">暂无数据</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = records.map(r => `
|
||||
<tr>
|
||||
<td>${r.id}</td>
|
||||
<td>${r.serial || '-'}</td>
|
||||
<td>${r.dpg430_addr}</td>
|
||||
<td>${r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-'}</td>
|
||||
<td>${r.str_type || '-'}</td>
|
||||
<td>${r.iffinish === '1' ? '是' : '否'}</td>
|
||||
<td>${r.fault_info || '无'}</td>
|
||||
<td>${r.relay_out || '无'}</td>
|
||||
<td>${r.ppvalue?.toFixed(2) || '-'}</td>
|
||||
<td>${r.idle_freq || '-'}</td>
|
||||
<td>${r.enter_freq || '-'}</td>
|
||||
<td>${r.exit_freq || '-'}</td>
|
||||
<td>${r.enter_dist || '-'}</td>
|
||||
<td>${r.exit_dist || '-'}</td>
|
||||
<td>${toSpeed(r.enter_speed)}</td>
|
||||
<td>${toSpeed(r.exit_speed)}</td>
|
||||
<td>${r.create_time || '-'}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
tbody.innerHTML = records.map(r =>
|
||||
'<tr>' + v.cols.map(c => {
|
||||
if (c.render) return `<td>${c.render(r)}</td>`;
|
||||
const val = r[c.key];
|
||||
return `<td>${val !== null && val !== undefined && val !== '' ? val : '-'}</td>`;
|
||||
}).join('') + '</tr>'
|
||||
).join("");
|
||||
}
|
||||
|
||||
// ─── 分页 ────────────────────────────────────────
|
||||
|
||||
function renderPagination() {
|
||||
const div = document.getElementById("pagination");
|
||||
let html = "";
|
||||
@@ -74,18 +193,228 @@ function renderPagination() {
|
||||
div.innerHTML = html;
|
||||
}
|
||||
|
||||
// ─── 导出 ────────────────────────────────────────
|
||||
|
||||
function exportCSV() {
|
||||
const serial = document.getElementById("search-serial").value;
|
||||
const dateFrom = document.getElementById("search-date-from").value;
|
||||
const dateTo = document.getElementById("search-date-to").value;
|
||||
const v = VIEWS[currentView];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (serial) params.set("serial", serial);
|
||||
if (dateFrom) params.set("date_from", dateFrom);
|
||||
if (dateTo) params.set("date_to", dateTo);
|
||||
if (v.data_source) params.set("data_source", v.data_source);
|
||||
|
||||
window.location.href = `/api/test-data/export?${params}`;
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
// ─── 图表 ────────────────────────────────────────
|
||||
|
||||
let chartMode = false;
|
||||
let chartInstance = null;
|
||||
|
||||
// 图表系列定义
|
||||
const CHART_SERIES = {
|
||||
b2: [
|
||||
{ key: 'ppvalue', name: '峰峰值', unit: 'V', yAxisIndex: 0 },
|
||||
{ key: 'idle_freq', name: '开始频率', unit: 'Hz', yAxisIndex: 0 },
|
||||
{ key: 'enter_freq', name: '进入频率', unit: 'Hz', yAxisIndex: 0 },
|
||||
{ key: 'exit_freq', name: '离开频率', unit: 'Hz', yAxisIndex: 0 },
|
||||
{ key: 'enter_dist', name: '进入距离', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'exit_dist', name: '离开距离', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'enter_speed', name: '进入速度', unit: 'dm/s',yAxisIndex: 2 },
|
||||
{ key: 'exit_speed', name: '离开速度', unit: 'dm/s',yAxisIndex: 2 },
|
||||
],
|
||||
b4: [
|
||||
{ key: 'work_freq', name: '工作频率', unit: 'Hz', yAxisIndex: 0 },
|
||||
{ key: 'curr_dist', name: '当前距离', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'speed', name: '速度', unit: 'dm/s',yAxisIndex: 2 },
|
||||
{ key: 'near_dist', name: '最近距离', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'far_dist', name: '最远距离', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'b4_enter_dist', name: '进入高度', unit: 'mm', yAxisIndex: 1 },
|
||||
{ key: 'b4_leave_dist', name: '离开高度', unit: 'mm', yAxisIndex: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
function toggleChart() {
|
||||
const container = document.getElementById('chart-container');
|
||||
const btn = document.getElementById('btn-chart');
|
||||
const table = document.getElementById('test-data-table');
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
chartMode = !chartMode;
|
||||
if (chartMode) {
|
||||
container.style.display = 'block';
|
||||
table.style.display = 'none';
|
||||
pagination.style.display = 'none';
|
||||
btn.textContent = '📋 表格';
|
||||
btn.classList.add('active');
|
||||
// 只对 B2/B4 视图显示图表
|
||||
if (currentView === 'all') switchView('b2');
|
||||
loadChart();
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
table.style.display = '';
|
||||
pagination.style.display = '';
|
||||
btn.textContent = '📈 图表';
|
||||
btn.classList.remove('active');
|
||||
if (chartInstance) { chartInstance.dispose(); chartInstance = null; }
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChart() {
|
||||
const container = document.getElementById('chart-container');
|
||||
if (!container || container.style.display === 'none') return;
|
||||
|
||||
const serial = document.getElementById('search-serial').value;
|
||||
const dateFrom = document.getElementById('search-date-from').value;
|
||||
const dateTo = document.getElementById('search-date-to').value;
|
||||
const v = VIEWS[currentView];
|
||||
|
||||
// 全部视图不适用,用 B2 或 B4
|
||||
const ds = v.data_source || (currentView === 'all' ? 'B2' : v.data_source);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (serial) params.set('serial', serial);
|
||||
if (dateFrom) params.set('date_from', dateFrom);
|
||||
if (dateTo) params.set('date_to', dateTo);
|
||||
if (ds) params.set('data_source', ds);
|
||||
|
||||
let resp, data;
|
||||
try {
|
||||
resp = await fetch(`/api/test-data/chart?${params}`);
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
console.error('加载图表数据失败:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
const records = data.records || [];
|
||||
if (!records.length) {
|
||||
container.innerHTML = '<p style="text-align:center;color:#999;padding:100px;">暂无数据</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 选系列定义
|
||||
const seriesDef = CHART_SERIES[ds === 'B4' ? 'b4' : 'b2'] || CHART_SERIES.b2;
|
||||
|
||||
// 时间轴
|
||||
const times = records.map(r => r.create_time);
|
||||
|
||||
// 构建 series
|
||||
const series = seriesDef.map(def => ({
|
||||
name: `${def.name}(${def.unit})`,
|
||||
type: 'line',
|
||||
yAxisIndex: def.yAxisIndex,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
data: records.map(r => r[def.key] ?? null),
|
||||
connectNulls: false,
|
||||
}));
|
||||
|
||||
// 渲染 ECharts
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(container);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: ds === 'B4' ? '波动测试 (0xB4) 数据趋势' : '灵敏度测试 (0xB2) 数据趋势',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14 },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
bottom: 0,
|
||||
},
|
||||
toolbox: {
|
||||
right: 10,
|
||||
top: 10,
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
title: '保存图片',
|
||||
pixelRatio: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: { left: 60, right: 140, top: 60, bottom: 80 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: times,
|
||||
axisLabel: {
|
||||
formatter: v => fmtTime(v).substring(5, 16), // MM-dd HH:mm
|
||||
rotate: 30,
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{ type: 'value', name: '频率/电压', nameTextStyle: { fontSize: 11 } },
|
||||
{ type: 'value', name: '距离(mm)', nameTextStyle: { fontSize: 11 } },
|
||||
{ type: 'value', name: '速度(dm/s)',nameTextStyle: { fontSize: 11 },
|
||||
offset: 80 },
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'slider', start: 0, end: 100, height: 20, bottom: 30 },
|
||||
{ type: 'inside' },
|
||||
],
|
||||
series: series,
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
|
||||
// 窗口 resize 时自适应
|
||||
window.addEventListener('resize', () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
}, { once: false });
|
||||
}
|
||||
|
||||
// ─── 初始加载 ────────────────────────────────────
|
||||
|
||||
renderHead();
|
||||
searchData(1);
|
||||
|
||||
// ─── 删除(admin)─────────────────────────────────
|
||||
|
||||
function confirmDelete() {
|
||||
const serial = document.getElementById('search-serial').value;
|
||||
const dateFrom = document.getElementById('search-date-from').value;
|
||||
const dateTo = document.getElementById('search-date-to').value;
|
||||
const v = VIEWS[currentView];
|
||||
const ds = v.data_source || '';
|
||||
|
||||
let desc = '';
|
||||
if (serial) desc += `设备: ${serial}\n`;
|
||||
if (dateFrom || dateTo) desc += `日期: ${dateFrom || '不限'} ~ ${dateTo || '不限'}\n`;
|
||||
if (ds) desc += `数据来源: ${ds}\n`;
|
||||
if (!desc) desc = '⚠ 未设置任何筛选条件,不会删除任何数据';
|
||||
|
||||
const msg = `确认删除以下条件的测试数据?\n\n${desc}\n此操作不可撤销!`;
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
doDelete(serial, dateFrom, dateTo, ds);
|
||||
}
|
||||
|
||||
async function doDelete(serial, dateFrom, dateTo, dataSource) {
|
||||
try {
|
||||
const resp = await fetch('/api/test-data/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
serial, date_from: dateFrom,
|
||||
date_to: dateTo, data_source: dataSource,
|
||||
}),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
alert(`已删除 ${data.deleted} 条记录`);
|
||||
searchData(1);
|
||||
} else {
|
||||
alert('删除失败: ' + (data.error || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('删除请求失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 测试操作页
|
||||
// 测试操作页 — 间隔/超时机制
|
||||
|
||||
let autoRunning = false;
|
||||
let autoTotal = 0;
|
||||
@@ -7,9 +7,15 @@ let autoFailed = 0;
|
||||
let autoRemaining = 0;
|
||||
let autoStartTime = "";
|
||||
let localSinceStr = "";
|
||||
|
||||
let pollInterval = null;
|
||||
let timeoutTimers = {}; // record_id → timer
|
||||
const TIMEOUT_MS = 10000;
|
||||
let nextCmdTimer = null; // 间隔等待定时器
|
||||
let timeoutTimer = null; // 超时定时器
|
||||
let timeoutAt = 0; // 超时时刻 (Date.now() + timeoutMs)
|
||||
let lastDoneCount = 0; // 上一轮 done 数,检测新响应
|
||||
let cmdSentAt = 0; // 最近一次发送 0xB0 时间
|
||||
let intervalMs = 10000; // 默认 10s
|
||||
let timeoutMs = 5000; // 默认 5s
|
||||
|
||||
// ─── 手动指令 ─────────────────────────────────
|
||||
|
||||
@@ -43,14 +49,20 @@ async function startAuto() {
|
||||
const count = parseInt(document.getElementById("test-count").value) || 10;
|
||||
if (count < 1) return;
|
||||
|
||||
// 读取参数
|
||||
intervalMs = (parseFloat(document.getElementById("interval-sec").value) || 3) * 1000;
|
||||
timeoutMs = (parseFloat(document.getElementById("timeout-sec").value) || 10) * 1000;
|
||||
if (timeoutMs < 1000) timeoutMs = 1000;
|
||||
|
||||
// 重置
|
||||
autoRunning = true;
|
||||
autoTotal = count;
|
||||
autoDone = 0;
|
||||
autoFailed = 0;
|
||||
autoRemaining = count;
|
||||
autoStartTime = new Date().toISOString(); // 记录开始时间 (UTC)
|
||||
// MySQL 存的是本地时间,需要转本地格式传给后端过滤
|
||||
lastDoneCount = 0;
|
||||
autoStartTime = new Date().toISOString();
|
||||
|
||||
const now = new Date();
|
||||
localSinceStr = now.getFullYear() + "-" +
|
||||
String(now.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
@@ -67,13 +79,14 @@ async function startAuto() {
|
||||
// 清空显示
|
||||
resetAverages();
|
||||
document.getElementById("latest-result").innerHTML = '<p class="placeholder">等待测试...</p>';
|
||||
document.getElementById("latest-wave").innerHTML = '<p class="placeholder">暂无波动数据...</p>';
|
||||
document.getElementById("progress-bar").style.width = "0%";
|
||||
document.getElementById("progress-text").textContent = "0/" + count + " (0 失败)";
|
||||
document.getElementById("stat-done").textContent = "0";
|
||||
document.getElementById("stat-failed").textContent = "0";
|
||||
document.getElementById("stat-remaining").textContent = count;
|
||||
document.getElementById("auto-status").textContent = "";
|
||||
|
||||
// 清空测试明细
|
||||
document.querySelector("#records-table tbody").innerHTML = "";
|
||||
document.getElementById("records-empty").style.display = "block";
|
||||
|
||||
@@ -81,7 +94,7 @@ async function startAuto() {
|
||||
btn.textContent = "结束";
|
||||
btn.className = "btn-stop";
|
||||
|
||||
// 插入第一条 0xB0
|
||||
// 清除旧记录 + 插入第一条
|
||||
try {
|
||||
const resp = await fetch("/api/automation/start", {
|
||||
method: "POST",
|
||||
@@ -90,8 +103,10 @@ async function startAuto() {
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
// 启动超时计时器
|
||||
startTimeout(data.first_record_id);
|
||||
cmdSentAt = Date.now();
|
||||
timeoutAt = cmdSentAt + timeoutMs;
|
||||
armTimeout();
|
||||
setStatus(`已发送,超时 ${timeoutMs/1000}s...`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("启动失败:", e);
|
||||
@@ -99,26 +114,84 @@ async function startAuto() {
|
||||
}
|
||||
|
||||
// 启动轮询
|
||||
pollInterval = setInterval(pollProgress, 1000);
|
||||
pollInterval = setInterval(pollProgress, 500);
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
autoRunning = false;
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
// 记录结束时间
|
||||
clearTimeout(nextCmdTimer);
|
||||
nextCmdTimer = null;
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
document.getElementById("auto-status").textContent = "";
|
||||
document.getElementById("time-end").textContent = new Date().toLocaleString();
|
||||
// 清除所有超时计时器
|
||||
for (const id in timeoutTimers) {
|
||||
clearTimeout(timeoutTimers[id]);
|
||||
}
|
||||
timeoutTimers = {};
|
||||
|
||||
const btn = document.getElementById("btn-auto");
|
||||
btn.textContent = "开始";
|
||||
btn.className = "btn-start";
|
||||
}
|
||||
|
||||
// ─── 发送下一条 0xB0 ────────────────────────────
|
||||
|
||||
async function sendNextCmd() {
|
||||
if (!autoRunning) return;
|
||||
if (autoRemaining <= 0) {
|
||||
// 检查是否还有等待完成的
|
||||
if (autoDone + autoFailed >= autoTotal) {
|
||||
stopAuto();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch("/api/command", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "B0" }),
|
||||
});
|
||||
const rd = await r.json();
|
||||
if (rd.ok) {
|
||||
cmdSentAt = Date.now();
|
||||
timeoutAt = cmdSentAt + timeoutMs;
|
||||
armTimeout();
|
||||
setStatus(`已发送,超时 ${timeoutMs/1000}s...`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("发送下一条失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 超时定时器 ─────────────────────────────────
|
||||
|
||||
function armTimeout() {
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = setTimeout(onTimeout, timeoutMs + 500); // +500ms 容差
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
if (!autoRunning) return;
|
||||
// 超时:即使没收到回复也计数失败,立即发下一条
|
||||
autoFailed++;
|
||||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||
if (autoRemaining < 0) autoRemaining = 0;
|
||||
updateUI();
|
||||
setStatus(`超时!立即发下一条...`);
|
||||
console.log(`超时 (${timeoutMs}ms),失败数: ${autoFailed}`);
|
||||
|
||||
clearTimeout(nextCmdTimer);
|
||||
nextCmdTimer = null;
|
||||
|
||||
if (autoRemaining > 0) {
|
||||
sendNextCmd();
|
||||
} else {
|
||||
stopAuto();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 轮询 ──────────────────────────────────────
|
||||
|
||||
async function pollProgress() {
|
||||
if (!autoRunning) return;
|
||||
|
||||
@@ -128,62 +201,64 @@ async function pollProgress() {
|
||||
const stats = data.stats;
|
||||
|
||||
// 更新计数
|
||||
autoDone = stats.done || 0;
|
||||
autoFailed = stats.failed || 0;
|
||||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||
if (autoRemaining < 0) autoRemaining = 0;
|
||||
const newDone = stats.done || 0;
|
||||
const newFailed = stats.failed || 0;
|
||||
|
||||
updateUI();
|
||||
if (newDone > lastDoneCount) {
|
||||
// 收到新回复 → 清除超时,开始间隔等待
|
||||
const delta = newDone - lastDoneCount;
|
||||
lastDoneCount = newDone;
|
||||
autoDone = newDone;
|
||||
autoFailed = newFailed;
|
||||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||
if (autoRemaining < 0) autoRemaining = 0;
|
||||
|
||||
// 显示最新结果
|
||||
if (data.latest) {
|
||||
renderLatest(data.latest);
|
||||
}
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
clearTimeout(nextCmdTimer);
|
||||
nextCmdTimer = null;
|
||||
|
||||
// 显示平均值
|
||||
if (data.averages) {
|
||||
renderAverages(data.averages);
|
||||
}
|
||||
updateUI();
|
||||
|
||||
// 显示本轮测试明细
|
||||
if (data.records) {
|
||||
renderRecords(data.records);
|
||||
}
|
||||
|
||||
// 自动插入下一条 0xB0
|
||||
if (autoRemaining > 0) {
|
||||
// 检查是否还有 pending 的记录,没有则插入新的
|
||||
if (stats.pending === 0 && stats.sent === 0) {
|
||||
try {
|
||||
const r = await fetch("/api/command", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "B0" }),
|
||||
});
|
||||
const rd = await r.json();
|
||||
if (rd.ok) {
|
||||
startTimeout(rd.record_id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("插入下一条失败:", e);
|
||||
}
|
||||
if (autoRemaining > 0) {
|
||||
const wait = (intervalMs / 1000).toFixed(0);
|
||||
setStatus(`收到 ${delta} 条回复,等待 ${wait}s...`);
|
||||
nextCmdTimer = setTimeout(() => {
|
||||
setStatus("发送中...");
|
||||
sendNextCmd();
|
||||
}, intervalMs);
|
||||
} else {
|
||||
setStatus("全部完成");
|
||||
stopAuto();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 全部完成
|
||||
stopAuto();
|
||||
// 没有新回复,更新超时计数
|
||||
autoFailed = newFailed;
|
||||
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||
if (autoRemaining < 0) autoRemaining = 0;
|
||||
// 检查是否全部完成
|
||||
if (autoRemaining <= 0 && autoDone + autoFailed >= autoTotal) {
|
||||
stopAuto();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示最新结果
|
||||
if (data.latest) renderLatest(data.latest);
|
||||
if (data.averages) renderAverages(data.averages);
|
||||
if (data.latest_wave) renderLatestWave(data.latest_wave);
|
||||
if (data.records) renderRecords(data.records);
|
||||
|
||||
} catch (e) {
|
||||
console.error("轮询失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function startTimeout(recordId) {
|
||||
timeoutTimers[recordId] = setTimeout(async () => {
|
||||
// 超时:检查 record 的状态,如果还是 1 → 视为失败
|
||||
// 后端串行轮询会自动处理超时标记为 state=3
|
||||
// 前端稍后通过 pollProgress 更新计数
|
||||
console.log(`记录 ${recordId} 可能已超时`);
|
||||
}, TIMEOUT_MS);
|
||||
// ─── UI ────────────────────────────────────────
|
||||
|
||||
function setStatus(msg) {
|
||||
document.getElementById("auto-status").textContent = msg;
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
@@ -203,12 +278,37 @@ function toSpeed(v) {
|
||||
return (parseFloat(v) / 10).toFixed(1);
|
||||
}
|
||||
|
||||
function fmtTime(v) {
|
||||
if (!v) return '-';
|
||||
const d = new Date(v);
|
||||
if (isNaN(d.getTime())) return String(v).substring(0, 19);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const d2 = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const min = String(d.getMinutes()).padStart(2, '0');
|
||||
const s = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${y}-${m}-${d2} ${h}:${min}:${s}`;
|
||||
}
|
||||
|
||||
const RELAY_MAP = {
|
||||
0: '无输出',
|
||||
1: '存在信号',
|
||||
2: '脉冲信号',
|
||||
3: '存在信号; 脉冲信号',
|
||||
};
|
||||
function decodeRelay(v) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
return RELAY_MAP[parseInt(v)] || `0x${parseInt(v).toString(16).toUpperCase().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// ─── 显示最新结果 ──────────────────────────────
|
||||
|
||||
function renderLatest(data) {
|
||||
const div = document.getElementById("latest-result");
|
||||
div.innerHTML = `
|
||||
<p>设备型号:<strong>${data.str_type || '-'}</strong></p>
|
||||
<p>测试模式:<strong>${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}</strong></p>
|
||||
<p>峰峰值:${data.ppvalue?.toFixed(2) || '-'} V</p>
|
||||
<p>开始工作频率:${data.idle_freq || '-'} Hz</p>
|
||||
<p>进入工作频率:${data.enter_freq || '-'} Hz</p>
|
||||
@@ -219,7 +319,8 @@ function renderLatest(data) {
|
||||
<p>离开速度:${toSpeed(data.exit_speed)} m/s</p>
|
||||
<p>是否完成:${data.iffinish === '1' ? '是' : '否'}</p>
|
||||
<p>故障信息:${data.fault_info || '无'}</p>
|
||||
<p>时间:${data.create_time || '-'}</p>
|
||||
<p>继电器:${decodeRelay(data.relay_code)}</p>
|
||||
<p>时间:${fmtTime(data.create_time)}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -242,6 +343,28 @@ function resetAverages() {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 显示波动测试数据 ──────────────────────────
|
||||
|
||||
function renderLatestWave(data) {
|
||||
const div = document.getElementById("latest-wave");
|
||||
if (!data || !data.work_freq) {
|
||||
div.innerHTML = '<p class="placeholder">暂无波动数据...</p>';
|
||||
return;
|
||||
}
|
||||
div.innerHTML = `
|
||||
<p>剩余次数:<strong>${data.remain_count || 0}</strong></p>
|
||||
<p>工作频率:${data.work_freq || '-'} Hz</p>
|
||||
<p>当前距离:${data.curr_dist || '-'} mm</p>
|
||||
<p>当前速度:${toSpeed(data.speed)} m/s</p>
|
||||
<p>最近距离:${data.near_dist || '-'} mm</p>
|
||||
<p>最远距离:${data.far_dist || '-'} mm</p>
|
||||
<p>进入高度 (B4):${data.b4_enter_dist || '-'} mm</p>
|
||||
<p>离开高度 (B4):${data.b4_leave_dist || '-'} mm</p>
|
||||
<p>继电器:${decodeRelay(data.relay_code)}</p>
|
||||
<p>时间:${fmtTime(data.create_time)}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── 显示本轮测试明细 ──────────────────────────
|
||||
|
||||
function renderRecords(records) {
|
||||
@@ -260,12 +383,13 @@ function renderRecords(records) {
|
||||
<td style="color:${r.sn_state === 2 ? '#27ae60' : r.sn_state === 3 ? '#e74c3c' : '#888'}">
|
||||
${r.sn_state === 2 ? 'OK' : r.sn_state === 3 ? '超时' : '?'}
|
||||
</td>
|
||||
<td>${r.test_mode === 1 ? '波动' : '灵敏度'}</td>
|
||||
<td>${r.ppvalue?.toFixed(2) || '-'}</td>
|
||||
<td>${r.idle_freq || '-'}</td>
|
||||
<td>${r.enter_dist || '-'}</td>
|
||||
<td>${r.exit_dist || '-'}</td>
|
||||
<td>${toSpeed(r.enter_speed)}</td>
|
||||
<td>${r.create_time || '-'}</td>
|
||||
<td>${fmtTime(r.create_time)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<label>测试模式</label>
|
||||
<select id="param-test-mode">
|
||||
<option value="0">0 - 灵敏度测试模式</option>
|
||||
<option value="1">1 - 模拟过车模式</option>
|
||||
<option value="1">1 - 波动测试模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -68,6 +68,32 @@
|
||||
<label>峰峰值最大值</label>
|
||||
<input type="number" id="param-peak-max" value="0">
|
||||
</div>
|
||||
<div class="form-group"><hr style="border-color:#eee; margin:2px 0;"></div>
|
||||
<h4 style="margin:8px 0 4px 0; color:#e67e22; font-size:13px;">⚡ 波动测试参数 (TestMode=1 时生效)</h4>
|
||||
<div class="form-group">
|
||||
<label>最远容差 (cm)</label>
|
||||
<input type="number" id="param-far-tol" value="0" min="0" max="255">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最近容差 (cm)</label>
|
||||
<input type="number" id="param-near-tol" value="0" min="0" max="255">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>步进容差 (cm)</label>
|
||||
<input type="number" id="param-step-tol" value="0" min="0" max="255">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>来回次数</label>
|
||||
<input type="number" id="param-back-forth" value="0" min="0" max="255">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最近停留时间 (ms)</label>
|
||||
<input type="number" id="param-near-stay" value="0" min="0" max="65535">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最远停留时间 (ms)</label>
|
||||
<input type="number" id="param-far-stay" value="0" min="0" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixture-actions">
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
{% block content %}
|
||||
<h2>测试信息</h2>
|
||||
|
||||
<div class="view-tabs">
|
||||
<button id="tab-all" class="tab-btn active" onclick="switchView('all')">全部数据</button>
|
||||
<button id="tab-b2" class="tab-btn" onclick="switchView('b2')">灵敏度测试 (0xB2)</button>
|
||||
<button id="tab-b4" class="tab-btn" onclick="switchView('b4')">波动测试 (0xB4)</button>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<label>
|
||||
设备编码:
|
||||
@@ -17,30 +23,24 @@
|
||||
</label>
|
||||
<button onclick="searchData(1)" class="btn-search">搜索</button>
|
||||
<button onclick="exportCSV()" class="btn-export">导出 CSV</button>
|
||||
<label style="margin-left:16px;">
|
||||
每页:
|
||||
<select id="per-page" onchange="searchData(1)" style="width:70px;">
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<button id="btn-delete" class="btn-delete" onclick="confirmDelete()">🗑 删除</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="chart-container" style="display:none; width:100%; height:500px; margin-bottom:16px;"></div>
|
||||
|
||||
<table id="test-data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>设备编码</th>
|
||||
<th>DG430地址</th>
|
||||
<th>设备型号</th>
|
||||
<th>类型</th>
|
||||
<th>是否完成</th>
|
||||
<th>故障信息</th>
|
||||
<th>继电器</th>
|
||||
<th>峰峰值(V)</th>
|
||||
<th>开始频率(Hz)</th>
|
||||
<th>进入频率(Hz)</th>
|
||||
<th>离开频率(Hz)</th>
|
||||
<th>进入距离(mm)</th>
|
||||
<th>离开距离(mm)</th>
|
||||
<th>进入速度(m/s)</th>
|
||||
<th>离开速度(m/s)</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
@@ -48,5 +48,46 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
.view-tabs { margin-bottom: 16px; display: flex; gap: 8px; }
|
||||
.tab-btn {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
border-radius: 4px 4px 0 0;
|
||||
font-size: 14px;
|
||||
transition: all .2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #2c3e50;
|
||||
color: #fff;
|
||||
border-color: #2c3e50;
|
||||
}
|
||||
.tab-btn:hover:not(.active) { background: #e0e0e0; }
|
||||
.btn-chart {
|
||||
margin-left: 16px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid #27ae60;
|
||||
background: #fff;
|
||||
color: #27ae60;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.btn-chart.active { background: #27ae60; color: #fff; }
|
||||
.btn-delete {
|
||||
margin-left: 8px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid #e74c3c;
|
||||
background: #fff;
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.btn-delete:hover { background: #e74c3c; color: #fff; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/test_data.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
测试次数:
|
||||
<input type="number" id="test-count" value="10" min="1" max="9999">
|
||||
</label>
|
||||
<label style="margin-left:16px;">
|
||||
间隔时间(秒):
|
||||
<input type="number" id="interval-sec" value="10" min="0" max="300" style="width:60px;">
|
||||
</label>
|
||||
<label style="margin-left:16px;">
|
||||
超时时间(秒):
|
||||
<input type="number" id="timeout-sec" value="5" min="1" max="600" style="width:60px;">
|
||||
</label>
|
||||
<button id="btn-auto" class="btn-start" onclick="toggleAuto()">开始</button>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
@@ -36,6 +44,7 @@
|
||||
<span>失败:<strong id="stat-failed">0</strong></span>
|
||||
<span>剩余:<strong id="stat-remaining">0</strong></span>
|
||||
</div>
|
||||
<div class="auto-status" id="auto-status" style="font-size:12px;color:#888;margin-top:4px;"></div>
|
||||
<div class="auto-time" id="auto-time" style="display:none;margin-top:8px;font-size:12px;color:#888;">
|
||||
开始:<span id="time-start">-</span> 结束:<span id="time-end">-</span>
|
||||
</div>
|
||||
@@ -49,6 +58,11 @@
|
||||
<p class="placeholder">等待设备上报...</p>
|
||||
</div>
|
||||
|
||||
<h3>波动测试数据</h3>
|
||||
<div id="latest-wave">
|
||||
<p class="placeholder">暂无波动数据...</p>
|
||||
</div>
|
||||
|
||||
<h3>自动化平均值</h3>
|
||||
<table id="avg-table">
|
||||
<tr><td>平均峰峰值</td><td id="avg-ppvalue">-</td><td>V</td></tr>
|
||||
@@ -65,7 +79,7 @@
|
||||
<table id="records-table" style="font-size:11px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>串口状态</th><th>峰峰值(V)</th><th>开始频率</th><th>进入距离</th><th>离开距离</th><th>速度(m/s)</th><th>时间</th>
|
||||
<th>#</th><th>串口状态</th><th>模式</th><th>峰峰值(V)</th><th>开始频率</th><th>进入距离</th><th>离开距离</th><th>速度(m/s)</th><th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
flask>=3.0
|
||||
pymysql>=1.1
|
||||
flask>=3.1.0
|
||||
flask-login>=0.6.0
|
||||
pymysql>=1.2.0
|
||||
|
||||
Submodule edc_server updated: e7c20c69d2...dc1d2b8871
Reference in New Issue
Block a user