Compare commits
64 Commits
v1.0.0
...
8a6b5c6d07
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a6b5c6d07 | ||
|
|
f0ec79ca2f | ||
|
|
4b082e35df | ||
|
|
521cbe4107 | ||
|
|
4ac6cbb2fe | ||
|
|
0dfb928375 | ||
|
|
366c7f909a | ||
|
|
b4b7387b39 | ||
|
|
aa2815b5cc | ||
|
|
17e1d232e8 | ||
|
|
317c15aff2 | ||
|
|
501e58b65f | ||
|
|
000e4f8d3a | ||
|
|
50451de2df | ||
|
|
2458127cfb | ||
|
|
a26d8807cb | ||
|
|
67da0c9368 | ||
|
|
4c337b60ae | ||
|
|
4b91455485 | ||
|
|
f2e9cc9345 | ||
|
|
d3b6d79a03 | ||
|
|
6b35d07025 | ||
|
|
5df08a26a9 | ||
|
|
0bddb46605 | ||
|
|
ee136cc707 | ||
|
|
60c11fe719 | ||
|
|
8aaa8440d1 | ||
|
|
e863dfbe2f | ||
|
|
69babe9994 | ||
|
|
421d735274 | ||
|
|
3fb51c35f2 | ||
|
|
172af49765 | ||
|
|
c0e77398d4 | ||
|
|
2b71abaec8 | ||
|
|
92c2c2b408 | ||
|
|
78ff0a6c2c | ||
|
|
c5fb4fc9c0 | ||
|
|
431653d033 | ||
|
|
e538efafb5 | ||
|
|
bbfe085140 | ||
|
|
8148aef332 | ||
|
|
87752f12e6 | ||
|
|
aadd498851 | ||
|
|
86c6046fbc | ||
|
|
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地感测试工装协议说明
|
DG430地感测试工装协议说明
|
||||||
|
|
||||||
(V2.0.1-20260522)
|
(V2.0.4-20260601)
|
||||||
|
|
||||||
# 1 硬件介绍
|
# 1 硬件介绍
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ Flag:是否故障标志
|
|||||||
|
|
||||||
02-----DLD110系列;
|
02-----DLD110系列;
|
||||||
|
|
||||||
测试模式:0 为灵敏度测试模式;1为模拟过车测试模式
|
测试模式:0 为灵敏度测试模式;1为波动测试模式
|
||||||
|
|
||||||
是否正常完成测试:00-----正常;
|
是否正常完成测试:00-----正常;
|
||||||
|
|
||||||
@@ -219,6 +219,40 @@ RFU:保留字节;
|
|||||||
|
|
||||||
例:7F 81 02 B3 01 31 37
|
例: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
|
## 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 |
|
| 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 | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B | 1B | 1B | 2B | 2B | 1B | 1B |
|
||||||
|
|
||||||
说明:该指令为上位机发送给DG430。DG430接收到该指令后返回协议并更新测试参数。
|
说明:该指令为上位机发送给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
|
测试用例: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 |
|
Flag:是否故障标志,00=正常,01=故障
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 1B | 2B | 2B | 2B | 2B | 2B | 2B | 1B | 1B |
|
|
||||||
|
|
||||||
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
|
## 5.11 出厂初始化指令,命令:0x4D
|
||||||
|
|
||||||
@@ -413,11 +481,9 @@ eg: 7F 81 01 4D CD CF
|
|||||||
| 7Fh | 80H +ADD | LEN | 4Eh | XOR | SUM |
|
| 7Fh | 80H +ADD | LEN | 4Eh | XOR | SUM |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
|
||||||
获取控制卡的版本号
|
|
||||||
|
|
||||||
LEN: 0x01
|
LEN: 0x01
|
||||||
|
eg: 7F 81 01 4E CE D0
|
||||||
eg: 7F 81 01 4E CA CC
|
|
||||||
|
|
||||||
返回格式:
|
返回格式:
|
||||||
|
|
||||||
@@ -432,6 +498,63 @@ eg: 7F 81 01 4E CA CC
|
|||||||
|
|
||||||
测试用例:7F 81 02 4E 00 CD D1
|
测试用例: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.0 | 2026-05-21 | | 增加配置指令:电机前进、后退、停止指令,获取版本号、读写测试指令 | 王飞强 |
|
||||||
| V2.0.1 | 2026-05-22 | | 增加出厂初始化指令、设备复位指令 | 王飞强 |
|
| V2.0.1 | 2026-05-22 | | 增加出厂初始化指令、设备复位指令 | 王飞强 |
|
||||||
| V2.0.2 | 2026-05-25 | | 增加测试模式,增加进入速度和离开速度 | 王飞强 |
|
| 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 |
|
||||||
834
docs/VD测试工装V2.0培训手册.md
Normal file
834
docs/VD测试工装V2.0培训手册.md
Normal file
@@ -0,0 +1,834 @@
|
|||||||
|
# VD 测试工装 V2.0.0 培训手册
|
||||||
|
|
||||||
|
> **版本**: V2.0.0
|
||||||
|
> **日期**: 2026-06-08
|
||||||
|
> **作者**: wangfq
|
||||||
|
> **适用对象**: 测试工程师、生产操作员、系统管理员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [项目概述](#1-项目概述)
|
||||||
|
2. [V2.0.0 新增功能总览](#2-v200-新增功能总览)
|
||||||
|
3. [系统架构](#3-系统架构)
|
||||||
|
4. [硬件环境](#4-硬件环境)
|
||||||
|
5. [EDC 服务](#5-edc-服务-edc_server)
|
||||||
|
6. [EDC 管理系统](#6-edc-管理系统-edc-web)
|
||||||
|
7. [通信协议](#7-通信协议)
|
||||||
|
8. [操作指南](#8-操作指南)
|
||||||
|
9. [常见问题](#9-常见问题)
|
||||||
|
10. [附录](#10-附录)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 项目简介
|
||||||
|
|
||||||
|
**VD 测试工装**(vd_test_fixture)是一套车检器自动化测试系统,用于**批量检测车检器(Vehicle Detector)的核心性能指标**。
|
||||||
|
|
||||||
|
**核心能力**:
|
||||||
|
- **灵敏度测试(B2)**: 检测车检器对不同信号强度的响应,含峰峰值、频率、距离、速度等指标
|
||||||
|
- **波动测试(B4)** 🆕: 模拟车辆往复运动,动态检测车检器在工作范围内的距离/频率/速度波动
|
||||||
|
- **产品一致性测试**: 批量产品之间的性能差异分析
|
||||||
|
- **自动化测试**: 支持设定间隔时间和超时时间,自动循环执行,实时进度反馈
|
||||||
|
- **工装配置管理**: 支持 DG430 V2.0.x 协议的设备参数配置、版本查询、出厂初始化,可关联线圈参数和模拟车辆参数 🆕
|
||||||
|
- **数据可视化** 🆕: 测试数据图表视图(ECharts),支持多 Y 轴、缩放、图片导出
|
||||||
|
|
||||||
|
### 1.2 术语说明
|
||||||
|
|
||||||
|
| 术语 | 全称 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **EDC** | Edge Data Center | 边缘数据中心,系统的核心服务 |
|
||||||
|
| **DNT** | Data Network Terminal | 联网终端(PGLC),连接设备与 EDC |
|
||||||
|
| **DG430** | — | 地感测试工装硬件,执行实际测试动作 |
|
||||||
|
| **VD** | Vehicle Detector | 车检器(被测设备) |
|
||||||
|
| **SerialNet** | Serial Network | 串口网络透传,通过 UDP 将指令转发到 DG430 串口 |
|
||||||
|
| **B2** | — | 灵敏度测试数据来源标识(0xB2 指令上报) |
|
||||||
|
| **B4** | — | 波动测试数据来源标识(0xB4 指令上报) 🆕 |
|
||||||
|
| **线圈参数** | Coil Info | 地感线圈的物理参数(形状、尺寸、电感量、圈数等) 🆕 |
|
||||||
|
| **模拟车辆** | Simulate Car | 用于模拟车辆通过的金属板参数(形状、尺寸、材质) 🆕 |
|
||||||
|
|
||||||
|
### 1.3 V2.0.0 vs V1.0 对比
|
||||||
|
|
||||||
|
| 功能 | V1.0 | V2.0.0 |
|
||||||
|
|------|:----:|:------:|
|
||||||
|
| 灵敏度测试 | ✓ | ✓ |
|
||||||
|
| 波动测试 | ✗ | ✓ **新增** |
|
||||||
|
| 测试信息查询 | 单表显示 | 三视图标签页 + ECharts 图表 |
|
||||||
|
| 工装配置 | 基本参数 | +波动参数 +线圈/模拟车辆关联 |
|
||||||
|
| 线圈参数管理 | ✗ | ✓ **新增** |
|
||||||
|
| 模拟车辆参数管理 | ✗ | ✓ **新增** |
|
||||||
|
| 时间筛选 | 仅日期范围 | 日期 + 时分秒 |
|
||||||
|
| Admin 数据删除 | ✗ | ✓ **新增** |
|
||||||
|
| 继电器状态 | 文本 | 编码值 + 解码显示 + 图表系列 |
|
||||||
|
| 自动化测试 | 固定间隔 | 可配置间隔/超时 |
|
||||||
|
| 操作日志 | 部分 | 全覆盖(创建/更新/删除) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. V2.0.0 新增功能总览
|
||||||
|
|
||||||
|
### 2.1 波动测试模式
|
||||||
|
|
||||||
|
V2.0.0 最重要的新增功能。在原有灵敏度测试(B2)基础上,增加了**波动测试模式(B4)**。
|
||||||
|
|
||||||
|
**测试原理**: 工装控制电机驱动金属板(模拟车辆)在地感线圈上方往复运动,通过车检器在不同位置的工作频率、距离、速度等指标变化,评估车检器性能的稳定性。
|
||||||
|
|
||||||
|
**与灵敏度测试的区别**:
|
||||||
|
| 对比维度 | 灵敏度测试 (B2) | 波动测试 (B4) |
|
||||||
|
|----------|:-------------:|:------------:|
|
||||||
|
| 运动方式 | 单次进入→离开 | 多次往复运动 |
|
||||||
|
| 测试指标 | 峰峰值、进入/离开频率、距离、速度 | 工作频率、当前距离、速度、剩余次数、最近/最远距离 |
|
||||||
|
| 完成判定 | 一次进入离开 | 达到设定来回次数 |
|
||||||
|
| 继电器 | 存在+脉冲 | 实时变化 |
|
||||||
|
|
||||||
|
### 2.2 线圈参数管理
|
||||||
|
|
||||||
|
新增线圈参数管理功能,用于记录和维护测试环境中的地感线圈信息:
|
||||||
|
|
||||||
|
- 线圈编号、名称
|
||||||
|
- 电感量(μH)
|
||||||
|
- 形状(矩形/圆形),尺寸(长宽/半径 cm)
|
||||||
|
- 圈数、电阻(Ω)
|
||||||
|
- 材质(铜线等)、备注
|
||||||
|
|
||||||
|
线圈参数在工装配置页可关联到具体工装,测试数据中自动记录关联的线圈信息,为后续数据分析提供环境参考。
|
||||||
|
|
||||||
|
### 2.3 模拟车辆参数管理
|
||||||
|
|
||||||
|
管理用于模拟车辆通过的金属板参数:
|
||||||
|
|
||||||
|
- 模拟编号、名称
|
||||||
|
- 形状(矩形/圆形),尺寸(长宽/半径 cm)
|
||||||
|
- 材质(铁板、合金等)、备注
|
||||||
|
|
||||||
|
模拟车辆参数同样可关联到工装配置,测试记录时自动写入。
|
||||||
|
|
||||||
|
### 2.4 测试信息三视图 + 图表
|
||||||
|
|
||||||
|
测试信息查询页面拆分为三个标签页:
|
||||||
|
- **全部数据**: 所有测试记录汇总视图
|
||||||
|
- **灵敏度测试(0xB2)**: B2 专用列布局(含故障信息、继电器、进入/离开频率等)
|
||||||
|
- **波动测试(0xB4)**: B4 专用列布局(含工作频率、当前距离、剩余次数、最近/最远距离等)
|
||||||
|
|
||||||
|
**图表视图**: 点击「📈 图表」按钮可切换为 ECharts 交互式图表,支持:
|
||||||
|
- B2:峰峰值、频率(4条)、距离(2条)、速度(2条)+ 继电器状态
|
||||||
|
- B4:工作频率、当前距离、速度、最近/最远距离、进入/离开高度 + 继电器状态
|
||||||
|
- 四 Y 轴独立刻度
|
||||||
|
- dataZoom 滑块缩放
|
||||||
|
- 图例切换显隐
|
||||||
|
- 2x 高清图片导出
|
||||||
|
|
||||||
|
### 2.5 精确时间筛选
|
||||||
|
|
||||||
|
搜索栏时间筛选从"仅日期"升级为"日期 + 时分秒":
|
||||||
|
- 只填日期不填时间 → 行为不变(date_to 自动取 23:59:59)
|
||||||
|
- 日期 + 时间都填 → 精确到秒过滤
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 系统架构
|
||||||
|
|
||||||
|
### 3.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐ 浏览器 HTTP
|
||||||
|
│ 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) │
|
||||||
|
│ (被测设备) │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.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 的轮询任务完成
|
||||||
|
|
||||||
|
### 3.3 端口分配
|
||||||
|
|
||||||
|
| 端口 | 方向 | 协议 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| **5500** | 监听 | UDP | EDC 设备发现 / 心跳 |
|
||||||
|
| **5505** | 监听 | UDP | EDC 消息监听 |
|
||||||
|
| **5550** | 监听 | TCP | EDC 时间同步 / 数据上报 / 串口透传 |
|
||||||
|
| **4900** | 发送 | UDP | 向设备发送 SerialNet 透传指令 |
|
||||||
|
| **5550** | 发送 | TCP | 向设备发送 TCP 数据 |
|
||||||
|
| **5000** | 监听 | HTTP | edc-web Flask 管理界面 |
|
||||||
|
|
||||||
|
### 3.4 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
vd_test_fixture/
|
||||||
|
├── edc_server/ # EDC 边缘数据中心(后端服务)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── server.py # UDP/TCP 异步网络服务
|
||||||
|
│ ├── handlers.py # 业务处理 + parse_loop/serialnet_loop 轮询
|
||||||
|
│ ├── models.py # 数据库 DDL + aiomysql CRUD
|
||||||
|
│ ├── dg430.py # DG430 二进制协议解析 (B2/B4/4C/4B)
|
||||||
|
│ └── protocol.py # PGLC JSON 协议解析
|
||||||
|
├── edc-web/ # Flask Web 管理系统(前端)
|
||||||
|
│ └── app/
|
||||||
|
│ ├── models.py # pymysql 同步数据库操作
|
||||||
|
│ ├── auth.py # Flask-Login 认证
|
||||||
|
│ ├── routes/ # 页面路由 (fixture/test_data/test_op/devices/users/logs)
|
||||||
|
│ ├── templates/ # Jinja2 HTML 模板
|
||||||
|
│ │ ├── fixture.html # 工装配置页(含线圈/车辆关联)
|
||||||
|
│ │ ├── test_data.html # 测试信息查询页(三视图 + 图表)
|
||||||
|
│ │ ├── test_op.html # 测试操作页(自动化 + 波动数据显示)
|
||||||
|
│ │ ├── coil_info.html # 线圈参数管理页 🆕
|
||||||
|
│ │ ├── simulate_car.html # 模拟车辆管理页 🆕
|
||||||
|
│ │ └── vehicle_base_test.html # 车检器基准管理页
|
||||||
|
│ └── static/
|
||||||
|
│ ├── css/style.css
|
||||||
|
│ └── js/
|
||||||
|
│ ├── fixture.js # 工装配 JS(线圈/车辆选择联动)
|
||||||
|
│ ├── test_data.js # 测试信息 JS(三视图/图表/分页)
|
||||||
|
│ ├── coil_info.js # 线圈管理 JS 🆕
|
||||||
|
│ ├── simulate_car.js # 模拟车辆管理 JS 🆕
|
||||||
|
│ └── ...
|
||||||
|
└── docs/ # 协议文档 + 培训手册
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 硬件环境
|
||||||
|
|
||||||
|
### 4.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 声 | 脉冲继电器无输入 |
|
||||||
|
|
||||||
|
### 4.2 PGLC 联网终端 (DNT)
|
||||||
|
|
||||||
|
PGLC 终端是连接 EDC 服务和 DG430 工装的**网络桥接设备**:
|
||||||
|
- 通过 **RS485/TTL 串口** 连接 DG430 工装
|
||||||
|
- 通过 **TCP/UDP 网络** 连接 EDC 服务
|
||||||
|
- 负责串口数据与网络数据的双向透传
|
||||||
|
|
||||||
|
### 4.3 测试环境准备 🆕
|
||||||
|
|
||||||
|
V2.0.0 新增了测试环境记录能力。在实际测试中,需要准备的硬件包括:
|
||||||
|
|
||||||
|
1. **地感线圈**: 安装在地面的感应线圈,需记录形状/尺寸/电感量/圈数等参数
|
||||||
|
2. **模拟车辆(金属板)**: 安装在工装电机上的金属板,用于模拟车辆通过线圈
|
||||||
|
|
||||||
|
以上两类参数需要在 edc-web 中预先录入,并在工装配置页关联到具体设备。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. EDC 服务 (edc_server)
|
||||||
|
|
||||||
|
### 5.1 功能概述
|
||||||
|
|
||||||
|
EDC 服务是整个系统的**数据中枢**,负责:
|
||||||
|
|
||||||
|
1. **设备管理**: 发现、注册、心跳检测、在线状态维护
|
||||||
|
2. **数据采集**: 接收设备上报的测试数据、原始传感数据
|
||||||
|
3. **协议解析**: 解析 DG430 二进制协议,提取测试结果(B2 + B4)
|
||||||
|
4. **指令透传**: SerialNet 透传机制,将前端指令下发给设备
|
||||||
|
5. **后台轮询**: 自动化测试调度、超时检测、状态流转
|
||||||
|
|
||||||
|
### 5.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
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 启动时自动执行数据库表创建和 ALTER TABLE 迁移,无需手动建表。
|
||||||
|
|
||||||
|
### 5.3 数据库表结构
|
||||||
|
|
||||||
|
| 表名 | 用途 | 关键字段 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `dnt_info` | 联网终端信息 | serial(唯一), ip, state(在线/离线) |
|
||||||
|
| `tb_state_tst` | 设备测试状态 | dnt_id, test_mode, data_source, ppvalue, idle_freq, …, coil_id, simulate_car_id 🆕 |
|
||||||
|
| `tb_serialnet` | 透传发送队列 | dnt_id, send_pkg, rcv_pkg, state(0未发→1已发→2完成→3超时) |
|
||||||
|
| `tb_fixture_param` | 工装测试参数 | dnt_id(UNIQUE), DevType, TestMode, FarTol…, coil_id, simulate_car_id 🆕 |
|
||||||
|
| `tb_coil_info` 🆕 | 线圈参数 | coil_num, name, induct, shape, length/width/radius, turns, resistance, material |
|
||||||
|
| `tb_simulate_car` 🆕 | 模拟车辆参数 | simulate_num, name, shape, length/width/radius, material |
|
||||||
|
| `tb_vechicle_base_test` | 车检器基准参数 | type_num(编码), SensMin/SensMax, FreMin/FreMax, PeakMin/PeakMax |
|
||||||
|
| `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已处理) |
|
||||||
|
|
||||||
|
### 5.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} → 显示结果
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 测试数据写入流程 🆕
|
||||||
|
|
||||||
|
```
|
||||||
|
parse_loop 解析 B2/B4 数据 → 查询 tb_fixture_param
|
||||||
|
├─ B2: 提取 coil_id, simulate_car_id → 写入 tb_state_tst
|
||||||
|
└─ B4: 提取 coil_id, simulate_car_id + DevType → 写入 tb_state_tst (含 sub_type, str_type)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. EDC 管理系统 (edc-web)
|
||||||
|
|
||||||
|
### 6.1 功能概述
|
||||||
|
|
||||||
|
edc-web 是基于 Flask 的 Web 管理系统,提供图形化操作界面。
|
||||||
|
|
||||||
|
**功能模块**:
|
||||||
|
|
||||||
|
| 模块 | URL | 功能 | V2 变化 |
|
||||||
|
|------|-----|------|:------:|
|
||||||
|
| **登录** | `/login` | 用户认证 (Flask-Login) | — |
|
||||||
|
| **设备管理** | `/` | 联网终端列表、在线状态、名称修改 | — |
|
||||||
|
| **测试操作** | `/test-op/<dnt_id>` | 单次测试、手动控制、自动化测试、波动数据显示 | 🆕 新增波动测试区 |
|
||||||
|
| **测试信息** | `/test-data` | 三视图(全部/B2/B4)+ ECharts 图表 + 时间精筛 + Admin 删除 | 🆕 大幅增强 |
|
||||||
|
| **工装配置** | `/fixture/<dnt_id>` | DG430 参数配置 + 波动参数 + 线圈/车辆关联 | 🆕 新增关联 |
|
||||||
|
| **线圈参数** 🆕 | `/coil-info` | 线圈参数增删改查 | 🆕 新增 |
|
||||||
|
| **模拟车辆** 🆕 | `/simulate-car` | 模拟车辆参数增删改查 | 🆕 新增 |
|
||||||
|
| **车检器基准** | `/vehicle-base-test` | 车检器测试基准参数管理 | — |
|
||||||
|
| **用户管理** | `/users` | 账号管理 (admin only) | — |
|
||||||
|
| **操作日志** | `/logs` | 操作记录审计 | 🆕 覆盖更全 |
|
||||||
|
|
||||||
|
### 6.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`。
|
||||||
|
|
||||||
|
### 6.3 用户角色
|
||||||
|
|
||||||
|
| 角色 | 权限 |
|
||||||
|
|------|------|
|
||||||
|
| **admin** | 全部功能:设备管理、测试操作、数据删除、用户管理、日志查看、参数管理 |
|
||||||
|
| **operator** | 受限功能:设备管理、测试操作、数据查看(无删除权限) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 通信协议
|
||||||
|
|
||||||
|
### 7.1 DG430 串口协议
|
||||||
|
|
||||||
|
DG430 与 PGLC 终端之间的通信协议,采用**一问一答**方式。
|
||||||
|
|
||||||
|
**数据包格式** (7 字节):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐
|
||||||
|
│ STX │ ADDR │ LEN │ CMD │ DATA │ XOR │ SUM │
|
||||||
|
│ 0x7F │ 1B │ 1B │ 1B │ LEN-1│ 1B │ 1B │
|
||||||
|
└──────┴──────┴──────┴──────┴──────┴──────┴──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 测试操作指令
|
||||||
|
|
||||||
|
| 指令 | 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)
|
||||||
|
|
||||||
|
| 指令 | 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 |
|
||||||
|
|
||||||
|
#### B4 状态上报(波动测试)🆕
|
||||||
|
|
||||||
|
| 字段 | 说明 | 范围 |
|
||||||
|
|------|------|------|
|
||||||
|
| RemainCount | 剩余波动次数 | — |
|
||||||
|
| WorkFreq | 当前工作频率 | Hz |
|
||||||
|
| CurrDist | 当前距离 | mm |
|
||||||
|
| Speed | 当前速度 | dm/s |
|
||||||
|
| NearDist | 最近距离 | mm |
|
||||||
|
| FarDist | 最远距离 | mm |
|
||||||
|
| EnterDist | 进入高度 | mm |
|
||||||
|
| LeaveDist | 离开高度 | mm |
|
||||||
|
| RelayOut | 继电器输出 | 0x00~0x03 |
|
||||||
|
|
||||||
|
#### 0x4B 配置参数结构 (V2.0.3)
|
||||||
|
|
||||||
|
| 字段 | 字节 | 说明 |
|
||||||
|
|------|:---:|------|
|
||||||
|
| Addr | 1 | 工装设备地址 |
|
||||||
|
| DevType | 1 | 被检设备型号类型编码(1=PD132, 2=DLD110) |
|
||||||
|
| TestMode | 1 | 0=灵敏度测试, 1=波动测试 |
|
||||||
|
| RestDis | 1 | 复位距离 cm |
|
||||||
|
| MinusDis | 1 | 皮距/开始距离 cm |
|
||||||
|
| SensMin/Max | 2+2 LE | 灵敏度范围 |
|
||||||
|
| FreMin/Max | 2+2 LE | 频率范围 Hz |
|
||||||
|
| PeakMin/Max | 2+2 LE | 峰峰值范围 |
|
||||||
|
| FarTol | 1 | 最远容差 cm 🆕 |
|
||||||
|
| NearTol | 1 | 最近容差 cm 🆕 |
|
||||||
|
| StepTol | 1 | 步进容差 cm 🆕 |
|
||||||
|
| BackForth | 1 | 来回次数 🆕 |
|
||||||
|
| NearStay | 2 LE | 最近停留时间 ms 🆕 |
|
||||||
|
| FarStay | 2 LE | 最远停留时间 ms 🆕 |
|
||||||
|
|
||||||
|
### 7.2 PGLC 网络接口协议
|
||||||
|
|
||||||
|
EDC 服务与 PGLC 终端之间的 JSON 协议。
|
||||||
|
|
||||||
|
| Method | 方向 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `TimeStamp` | 设备→EDC | 时间同步请求 |
|
||||||
|
| `Count_Off` | 设备→EDC | 设备注册/发现 |
|
||||||
|
| `TSReport` | 设备→EDC | 子设备数据上报 |
|
||||||
|
| `SerialNet` | EDC→设备 | 串口透传指令下发 |
|
||||||
|
| `Heartbeat` | 设备→EDC | 心跳包 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 操作指南
|
||||||
|
|
||||||
|
### 8.1 登录系统
|
||||||
|
|
||||||
|
1. 浏览器访问 `http://<服务器IP>:5000/login`
|
||||||
|
2. 默认账号: `admin` / `admin123`
|
||||||
|
3. 登录后跳转到设备管理页
|
||||||
|
|
||||||
|
### 8.2 设备管理
|
||||||
|
|
||||||
|
**查看设备列表**:
|
||||||
|
- 首页展示所有联网终端
|
||||||
|
- 显示序列号、名称、IP、在线状态
|
||||||
|
|
||||||
|
**修改设备名称**:
|
||||||
|
- 点击设备行的名称编辑图标
|
||||||
|
- 输入新名称后提交保存
|
||||||
|
|
||||||
|
**进入测试**: 点击设备行右侧的「测试」按钮
|
||||||
|
**进入工装配置**: 点击「工装配」按钮 🆕
|
||||||
|
|
||||||
|
### 8.3 工装配置(重要:新设备必做)🆕
|
||||||
|
|
||||||
|
> ⚠️ **新设备首次使用,必须先完成工装配置并保存到数据库,然后才能进行测试。**
|
||||||
|
|
||||||
|
#### 8.3.1 配置前置准备
|
||||||
|
|
||||||
|
1. 进入「线圈参数管理」页面(`/coil-info`),录入测试使用的线圈参数
|
||||||
|
2. 进入「模拟车辆参数管理」页面(`/simulate-car`),录入金属板参数
|
||||||
|
3. 进入「车检器测试基准参数管理」页面(`/vehicle-base-test`),录入车检器型号基准
|
||||||
|
|
||||||
|
#### 8.3.2 工装参数配置
|
||||||
|
|
||||||
|
从设备页面点击「工装配」进入配置页:
|
||||||
|
|
||||||
|
**左侧 - 工装测试参数**:
|
||||||
|
1. 设置**工装设备地址**(默认 1)
|
||||||
|
2. 选择**测试模式**:灵敏度测试(0) 或 波动测试(1)
|
||||||
|
3. 设置复位距离、皮距
|
||||||
|
4. 从下拉框选择**被检设备型号**(自动填充灵敏度/频率/峰峰值范围)
|
||||||
|
5. 如选择波动测试模式,还需配置 6 个波动参数:
|
||||||
|
- 最远容差 / 最近容差 / 步进容差(cm)
|
||||||
|
- 来回次数
|
||||||
|
- 最近停留时间 / 最远停留时间(ms)
|
||||||
|
|
||||||
|
**右侧 - 关联参数**:
|
||||||
|
6. 在「关联线圈参数」下拉框选择使用的线圈
|
||||||
|
7. 在「关联模拟车辆参数」下拉框选择使用的金属板
|
||||||
|
|
||||||
|
#### 8.3.3 保存配置
|
||||||
|
|
||||||
|
点击 **「💾 保存」** 按钮将参数保存到数据库。
|
||||||
|
|
||||||
|
> 保存会自动记录操作日志。线圈和模拟车辆关联**不需要下发透传到工装硬件**,仅记录在数据库中用于测试环境追溯。
|
||||||
|
|
||||||
|
#### 8.3.4 其他工装操作
|
||||||
|
|
||||||
|
| 按钮 | 指令 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 📋 获取版本号 | 0x4A | 查询 DG430 固件/硬件版本 |
|
||||||
|
| 🔍 查询参数 | 0x4C | 从设备读取当前参数(结果自动更新表单) |
|
||||||
|
| 📤 配置参数 | 0x4B | 下发配置到 DG430 设备(同时自动保存到数据库) |
|
||||||
|
| 🏭 出厂初始化 | 0x4D | 恢复 DG430 出厂设置 |
|
||||||
|
| 🔄 设备复位 | 0x4E | 重启 DG430 设备 |
|
||||||
|
|
||||||
|
**通信日志**: 每次操作后自动显示 hex 收发记录和结果。
|
||||||
|
|
||||||
|
### 8.4 手动测试操作
|
||||||
|
|
||||||
|
在测试操作页面(点击设备行的「测试」按钮):
|
||||||
|
|
||||||
|
1. **单次测试**: 点击「开始测试」按钮,DG430 执行一次完整测试流程
|
||||||
|
2. **手动控制** (仅 admin):
|
||||||
|
- 测试复原: 恢复初始位置
|
||||||
|
- 电机前进/后退/停止: 手动控制电机
|
||||||
|
3. **实时数据**: 页面自动显示设备上报的最新测试数据
|
||||||
|
|
||||||
|
### 8.5 自动化测试 🆕
|
||||||
|
|
||||||
|
V2.0.0 自动化测试支持可配置的间隔和超时:
|
||||||
|
|
||||||
|
1. 设置**间隔时间**(秒,默认 10,0 表示无间隔连续测试)
|
||||||
|
2. 设置**超时时间**(秒,默认 5,单次测试最大等待时间)
|
||||||
|
3. 填入**测试次数**
|
||||||
|
4. 点击「开始」按钮
|
||||||
|
- 进度条显示 `已完成/总数`
|
||||||
|
- 成功/失败计数实时更新
|
||||||
|
- 平均值区域持续刷新
|
||||||
|
- 波动测试数据区实时显示 B4 上报 🆕
|
||||||
|
5. 如需中途停止,点击「结束」按钮
|
||||||
|
|
||||||
|
**自动化流程**:
|
||||||
|
```
|
||||||
|
开始 → INSERT tb_serialnet (0xB0, state=0)
|
||||||
|
→ serialnet_loop 发送 UDP (state=1)
|
||||||
|
→ 等待设备 B2/B4 上报
|
||||||
|
→ parse_loop 解析存入 tb_state_tst(含线圈/车辆关联)
|
||||||
|
→ 匹配 serialnet 记录 (state=2)
|
||||||
|
→ 前端轮询进度 → 等待间隔 → 自动发送下一次 0xB0
|
||||||
|
→ ...重复...
|
||||||
|
→ 全部完成 / 超时(fail) / 手动结束
|
||||||
|
```
|
||||||
|
|
||||||
|
**超时处理**: 单次测试超时计入失败,跳过间隔时间立即发送下一条。连续超时不阻塞。
|
||||||
|
|
||||||
|
### 8.6 测试数据查询 🆕
|
||||||
|
|
||||||
|
#### 8.6.1 三视图切换
|
||||||
|
|
||||||
|
进入「测试信息」页面,顶部三个标签页:
|
||||||
|
|
||||||
|
- **全部数据**: 显示所有测试记录(灵敏度+波动),通用列布局
|
||||||
|
- **灵敏度测试 (0xB2)**: 仅显示 B2 来源数据,含故障信息、继电器、进入/离开频率等专属列
|
||||||
|
- **波动测试 (0xB4)**: 仅显示 B4 来源数据,含工作频率、当前距离、剩余次数等专属列
|
||||||
|
|
||||||
|
#### 8.6.2 搜索筛选
|
||||||
|
|
||||||
|
| 筛选项 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 设备编码 | 模糊搜索 serial |
|
||||||
|
| 时间范围 | 日期 + 时分秒(可选),只填日期不填时间则 date_to 自动取 23:59:59 |
|
||||||
|
| 每页 | 20 / 50 / 100 条 |
|
||||||
|
|
||||||
|
「测试环境」列显示关联的线圈编号和模拟车辆编号。 🆕
|
||||||
|
|
||||||
|
#### 8.6.3 图表视图
|
||||||
|
|
||||||
|
点击「📈 图表」按钮切换为 ECharts 交互式图表:
|
||||||
|
|
||||||
|
- **B2 图表**: 峰峰值(V)、开始/进入/离开频率(Hz)、进入/离开距离(mm)、进入/离开速度(dm/s) + 继电器状态
|
||||||
|
- **B4 图表**: 工作频率(Hz)、当前距离(mm)、速度(dm/s)、最近/最远距离(mm)、进入/离开高度(mm) + 继电器状态
|
||||||
|
- **操作**: dataZoom 缩放、图例点击显隐、右键保存为 2x PNG
|
||||||
|
|
||||||
|
> 图表切换到「全部」视图时自动跳转到 B2 视图。
|
||||||
|
|
||||||
|
#### 8.6.4 导出 CSV
|
||||||
|
|
||||||
|
点击「导出 CSV」按钮下载当前筛选条件下的全部数据为 CSV 文件。
|
||||||
|
|
||||||
|
#### 8.6.5 Admin 数据删除 🆕
|
||||||
|
|
||||||
|
仅 admin 可见「🗑 删除」按钮:
|
||||||
|
1. 设置筛选条件(设备编码 / 时间范围 / 数据来源)
|
||||||
|
2. 点击「🗑 删除」
|
||||||
|
3. 确认框显示筛选条件,确认后执行删除
|
||||||
|
4. 删除操作记录到 tb_log
|
||||||
|
5. ⚠️ 无筛选条件时不删除任何数据
|
||||||
|
|
||||||
|
### 8.7 参数管理 🆕
|
||||||
|
|
||||||
|
#### 线圈参数管理 (`/coil-info`)
|
||||||
|
|
||||||
|
支持线圈参数的全生命周期管理:
|
||||||
|
- **新增**: 填写线圈编号、名称、电感量、形状、尺寸、圈数、电阻、材质、备注
|
||||||
|
- **编辑**: 点击表格行的「编辑」按钮修改
|
||||||
|
- **删除**: 点击「删除」按钮(需确认)
|
||||||
|
- **搜索**: 按编号或名称模糊搜索
|
||||||
|
- 所有操作自动记录到操作日志
|
||||||
|
|
||||||
|
#### 模拟车辆参数管理 (`/simulate-car`)
|
||||||
|
|
||||||
|
支持模拟车辆(金属板)参数的全生命周期管理:
|
||||||
|
- 模拟编号、名称、形状、尺寸、材质、备注
|
||||||
|
- 操作方式和线圈管理一致
|
||||||
|
|
||||||
|
#### 车检器测试基准管理 (`/vehicle-base-test`)
|
||||||
|
|
||||||
|
管理不同型号车检器的测试基准参数:
|
||||||
|
- 类型编码、型号名称
|
||||||
|
- 灵敏度/频率/峰峰值范围
|
||||||
|
- 备注
|
||||||
|
|
||||||
|
### 8.8 用户管理 (admin only)
|
||||||
|
|
||||||
|
1. 点击「用户管理」菜单
|
||||||
|
2. 功能: 添加新用户、修改角色、删除用户
|
||||||
|
|
||||||
|
### 8.9 操作日志
|
||||||
|
|
||||||
|
1. 点击「操作日志」菜单
|
||||||
|
2. V2.0.0 覆盖更全的操作类型:
|
||||||
|
- `login` / `logout`: 登录/登出
|
||||||
|
- `command`: 指令下发(工装配/测试操作)
|
||||||
|
- `create` / `update` / `delete`: 参数管理操作(线圈/车辆/基准/工装)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 常见问题
|
||||||
|
|
||||||
|
### 9.1 新设备如何开始测试?
|
||||||
|
|
||||||
|
1. 确保设备在线(设备列表显示"在线")
|
||||||
|
2. **录入线圈参数** → `/coil-info` 添加测试使用的线圈
|
||||||
|
3. **录入模拟车辆参数** → `/simulate-car` 添加金属板参数
|
||||||
|
4. **录入车检器基准** → `/vehicle-base-test` 添加车检器型号
|
||||||
|
5. **配置工装参数** → 进入工装配页,设置参数、关联线圈和车辆,点击「💾 保存」
|
||||||
|
6. 进入测试操作页开始测试
|
||||||
|
|
||||||
|
### 9.2 波动测试和灵敏度测试有什么区别?
|
||||||
|
|
||||||
|
| 维度 | 灵敏度测试 | 波动测试 |
|
||||||
|
|------|:--------:|:------:|
|
||||||
|
| 目的 | 检测车检器灵敏度、峰峰值等静态指标 | 检测车检器在工作范围内的动态稳定性 |
|
||||||
|
| 运动方式 | 单次进入→离开 | 多次往复运动 |
|
||||||
|
| 数据标识 | data_source=B2 | data_source=B4 |
|
||||||
|
| 配置 | TestMode=0 | TestMode=1(需配波动参数) |
|
||||||
|
| 查询 | 选「灵敏度测试」标签 | 选「波动测试」标签 |
|
||||||
|
|
||||||
|
### 9.3 线圈/模拟车辆关联有什么用?
|
||||||
|
|
||||||
|
线圈和模拟车辆关联**不会下发到硬件**,仅记录在数据库中。当测试记录写入 `tb_state_tst` 时,会自动带上当时的线圈和车辆信息,方便后续数据分析时了解测试环境(例如"某批次测试用了哪种线圈、哪种金属板")。
|
||||||
|
|
||||||
|
### 9.4 设备离线
|
||||||
|
|
||||||
|
**现象**: 设备列表显示"离线"
|
||||||
|
|
||||||
|
**排查**:
|
||||||
|
1. 检查设备供电是否正常
|
||||||
|
2. 检查网络连接(ping 设备 IP)
|
||||||
|
3. 查看 edc_server 日志,确认是否收到心跳
|
||||||
|
4. 设备超时阈值: 默认 120 秒(`EDC_DEVICE_TIMEOUT`)
|
||||||
|
|
||||||
|
### 9.5 测试超时
|
||||||
|
|
||||||
|
**现象**: 自动化测试中连续超时
|
||||||
|
|
||||||
|
**排查**:
|
||||||
|
1. 检查 DG430 工装是否正常上电
|
||||||
|
2. 检查 RS485 连接是否正确
|
||||||
|
3. 在工装配页发送「获取设备版本号」(0x4A),验证通信链路
|
||||||
|
4. 可调整超时时间(默认 5 秒)
|
||||||
|
|
||||||
|
### 9.6 速度显示异常
|
||||||
|
|
||||||
|
**现象**: 速度值看起来偏大 10 倍
|
||||||
|
|
||||||
|
**原因**: DG430 协议存储的是 dm/s(分米/秒),系统已自动转换为 m/s(米/秒)。如果发现显示值异常,检查转换逻辑。
|
||||||
|
|
||||||
|
### 9.7 时间显示偏移 8 小时
|
||||||
|
|
||||||
|
**现象**: 测试数据时间比实际晚 8 小时
|
||||||
|
|
||||||
|
**已修复**: V2.0.0 已修复 Flask jsonify 的 "GMT" 后缀导致的时区偏移问题,所有时间按服务器本地时间(UTC+8)显示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 附录
|
||||||
|
|
||||||
|
### 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. 数据库快速参考
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 查看在线设备
|
||||||
|
SELECT serial, name, ip, state, last_login FROM dnt_info WHERE state=1;
|
||||||
|
|
||||||
|
-- 查看最近灵敏度测试数据
|
||||||
|
SELECT s.*, d.serial
|
||||||
|
FROM tb_state_tst s JOIN dnt_info d ON s.dnt_id = d.id
|
||||||
|
WHERE s.data_source='B2' ORDER BY s.create_time DESC LIMIT 20;
|
||||||
|
|
||||||
|
-- 查看最近波动测试数据
|
||||||
|
SELECT s.*, d.serial
|
||||||
|
FROM tb_state_tst s JOIN dnt_info d ON s.dnt_id = d.id
|
||||||
|
WHERE s.data_source='B4' ORDER BY s.create_time DESC LIMIT 20;
|
||||||
|
|
||||||
|
-- 查看测试环境(线圈+车辆关联)
|
||||||
|
SELECT t.create_time, d.serial, c.coil_num, c.name as coil_name,
|
||||||
|
sc.simulate_num, sc.name as car_name
|
||||||
|
FROM tb_state_tst t
|
||||||
|
JOIN dnt_info d ON t.dnt_id = d.id
|
||||||
|
LEFT JOIN tb_coil_info c ON t.coil_id = c.id
|
||||||
|
LEFT JOIN tb_simulate_car sc ON t.simulate_car_id = sc.id
|
||||||
|
ORDER BY t.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;
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. 环境变量速查
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `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 管理端口 |
|
||||||
|
|
||||||
|
### D. 新设备配置检查清单
|
||||||
|
|
||||||
|
- [ ] 设备上电,确认在线状态
|
||||||
|
- [ ] `/coil-info` 录入线圈参数
|
||||||
|
- [ ] `/simulate-car` 录入模拟车辆参数
|
||||||
|
- [ ] `/vehicle-base-test` 录入车检器基准
|
||||||
|
- [ ] 工装配页:设置测试模式、被检设备型号
|
||||||
|
- [ ] 工装配页:关联线圈和模拟车辆
|
||||||
|
- [ ] 工装配页:点击「💾 保存」
|
||||||
|
- [ ] (可选)「📤 配置参数」下发到 DG430
|
||||||
|
- [ ] 进入测试操作页开始测试
|
||||||
|
|
||||||
|
### E. 修订历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 | 作者 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| V1.0 | 2026-05-31 | 初始版本(灵敏度测试 + 基本工装配置) | wangfq |
|
||||||
|
| V2.0.0 | 2026-06-08 | 新增波动测试、线圈/车辆管理、三视图图表、时间精筛、自动化增强 | 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
|
||||||
|
```
|
||||||
108
docs/reports/weekly-2026-06-05.md
Normal file
108
docs/reports/weekly-2026-06-05.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# V2.0.4 周报 — 车检器测试工装项目
|
||||||
|
|
||||||
|
**报告周期**:2026年6月1日 ~ 6月5日
|
||||||
|
**项目**:vd_test_fixture(车检器自动化测试工装)
|
||||||
|
**作者**:wangfq
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、概述
|
||||||
|
|
||||||
|
本周完成 V2.0.3 波动测试模式全链路实现、继电器状态存储重构、测试操作/信息页面多项增强,以及若干关键 Bug 修复。共提交 **24 个 commits**(主仓库 19 + edc_server 子模块 4 + edc-web 1)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、波动测试模式(V2.0.3)
|
||||||
|
|
||||||
|
### 2.1 协议文档
|
||||||
|
- DG430 串口协议升级至 V2.0.3
|
||||||
|
- 扩展 0x4B/0x4C 字段:新增 FarTol、NearTol、StepTol、BackForth、NearStay、FarStay(6 个波动参数)
|
||||||
|
- 新增 0xB4 波动测试上报指令定义
|
||||||
|
- 第 6 章拆分为 6.1 灵敏度测试流程 + 6.2~6.4 波动测试流程
|
||||||
|
|
||||||
|
### 2.2 后端 edc_server
|
||||||
|
| 模块 | 变更 |
|
||||||
|
|------|------|
|
||||||
|
| `dg430.py` | 新增 `DG430WaveStatus` dataclass、`parse_b4_wave_status()` 解析器;扩展 `DG430FixtureParams` + `parse_4c_params()` 支持 6 个新字段 |
|
||||||
|
| `models.py` | DDL 增加 10 个新列(test_mode、data_source、remain_count、work_freq、b4_enter_dist 等);新增 `insert_wave_data()` 存库函数;ALTER TABLE 自动迁移逻辑 |
|
||||||
|
| `handlers.py` | parse_loop 增加 0xB4 分支,波动数据路由入库;0x4C 处理传递新参数字段 |
|
||||||
|
|
||||||
|
### 2.3 前端 edc-web
|
||||||
|
- **工装参数页**:表单增加 6 个波动参数输入框,JS 适配新字段
|
||||||
|
- **测试操作页**:右侧新增「波动测试数据」显示区,实时展示 B4 上报数据
|
||||||
|
- **测试信息页**:拆为三视图标签页(全部/灵敏度测试 B2/波动测试 B4),按 `data_source` 自动切换列布局
|
||||||
|
- 波动测试数据支持 Excel 导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、继电器输出状态重构
|
||||||
|
|
||||||
|
### 3.1 存储层
|
||||||
|
- `tb_state_tst` 新增 `relay_code TINYINT` 列,存储原始 hex 值
|
||||||
|
- `0x00` = 无输出,`0x01` = 存在信号,`0x02` = 脉冲信号,`0x03` = 存在+脉冲
|
||||||
|
- `relay_out` VARCHAR 列保留,兼容历史数据
|
||||||
|
- B2/B4 解析后直接以 int 值写入 `relay_code`
|
||||||
|
|
||||||
|
### 3.2 前端
|
||||||
|
- 新增 `decodeRelay()` 函数:整数 → 可读文本
|
||||||
|
- 测试操作页、测试信息页(表格视图)统一使用解码显示
|
||||||
|
- **图表视图**:新增第 4 Y 轴(继电器输出),红色三角阶梯线,tooltip 自动解码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、测试操作页面增强
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 自动化间隔/超时 | 新增间隔时间(秒)和超时时间(秒)参数;重写为状态机驱动,收到回复后等待间隔再发下一条,超时立即发下一条 |
|
||||||
|
| 渲染容错 | 每个 render 调用独立 try-catch,避免一处报错级联导致全部数据显示失败 |
|
||||||
|
| 初始数据加载 | 页面打开时自动请求最新测试数据,无需等待启动自动化 |
|
||||||
|
| 时间格式统一 | 所有区域统一显示 `yyyy-MM-dd HH:mm:ss`,修复 Flask jsonify "GMT" 导致的 UTC+8 时区偏移 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、测试信息页面增强
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 三视图标签页 | 全部 / 灵敏度测试 (B2) / 波动测试 (B4),独立列布局 |
|
||||||
|
| ECharts 图表 | 表格/图表一键切换,B2 显示 8 条线(峰峰值/频率/距离/速度),B4 显示 7+1 条线(含继电器),三 Y 轴,dataZoom 缩放,保存为 2x PNG |
|
||||||
|
| 分页条数 | 搜索栏增加「每页:20/50/100」下拉框 |
|
||||||
|
| Admin 删除 | 仅 admin 可见,按设备编码/日期范围/数据来源筛选删除,确认框防误删,`tb_log` 留痕 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Bug 修复
|
||||||
|
|
||||||
|
| 问题 | 根因 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| FarStay 字段长度 | 用户纠正:NearStay 和 FarStay 均为 2 字节 | 全量回退 1 字节改动,恢复协议+代码 2 字节设计 |
|
||||||
|
| 数据库缺列 | 旧表缺少 V2.0.3 新增字段 | 服务启动时 ALTER TABLE ADD COLUMN IF NOT EXISTS 自动迁移 |
|
||||||
|
| 时间显示偏移 8 小时 | Flask jsonify 给本地时间加 "GMT" 后缀,JS 误当 UTC 解析 | `fmtTime()` 先 strip "GMT" 再解析 |
|
||||||
|
| 测试操作三个数据区不显示 | pollProgress 共用一个 try-catch,一处报错跳过后续渲染;全部完成时 return 跳过最终渲染 | 独立 try-catch;渲染代码移到 return 之前 |
|
||||||
|
| 自动化平均值/明细不显示 | 同上(渲染代码在 return 之后) | 同上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、Git 提交统计
|
||||||
|
|
||||||
|
**主仓库**(vd_test_fixture):19 commits
|
||||||
|
**子模块**(edc_server):4 commits
|
||||||
|
**合计**:23 commits
|
||||||
|
|
||||||
|
| 日期 | 主题 |
|
||||||
|
|------|------|
|
||||||
|
| 6/1 | 培训手册 V1.0、精简 requirements.txt |
|
||||||
|
| 6/2 | DG430 V2.0.3 协议文档、后端实现、前端同步、ALTER TABLE 迁移 |
|
||||||
|
| 6/3 | FarStay 字节修正、波动测试前端适配、三视图重构 |
|
||||||
|
| 6/4 | 自动化间隔/超时、协议文档补充灵敏度流程 |
|
||||||
|
| 6/5 | 时间格式化、分页条数、ECharts 图表、图表保存图片、admin 删除、继电器重构、时区修复、渲染容错、图表继电器系列 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、待办事项
|
||||||
|
|
||||||
|
- [ ] 重启 edc_server 使数据库迁移和新字段生效
|
||||||
|
- [ ] edc-web 重启(用户已手动停止)
|
||||||
|
- [ ] 端到端测试完整波动测试流程(参数设置 → 查询 → 执行 → B4 上报 → 前端展示)
|
||||||
|
- [ ] 验证图表功能在不同数据量下的表现
|
||||||
68
docs/reports/weekly-2026-06-12.md
Normal file
68
docs/reports/weekly-2026-06-12.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# 周报 — 2026.06.09 ~ 2026.06.12
|
||||||
|
|
||||||
|
## 一、设备型号动态管理
|
||||||
|
|
||||||
|
**问题**:车检器型号名称(如 DLD110SV)在代码中硬编码,新增型号后测试操作页显示 `Unknown(3)` 或 `0x03`,测试信息页型号列显示 `-`。
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
- **后端** `edc_server`:B2/B4 数据写入时,废弃硬编码 `{1:"PD132",2:"DLD110"}` 映射,改为查询 `tb_vechicle_base_test` 表获取 `type_num → dev_name`,带内存缓存避免高频 DB 查询。
|
||||||
|
- **前端** `edc-web`:
|
||||||
|
- `test_op.js` 工装配置概览面板、最新测试结果区域,从 `/api/vehicle-base-test` 动态获取型号映射。
|
||||||
|
- `test_data.js` 测试信息页型号列,同样改为动态查询。
|
||||||
|
- 每 5 秒自动刷新型号缓存,工装配置页新增型号后无需手动刷新。
|
||||||
|
|
||||||
|
## 二、测试操作页实时数据改进
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
1. 工装本地按键触发的测试数据上报后,网页端无法实时显示,必须依赖网页端"开始"按钮。
|
||||||
|
2. 数据轮询间隔偏长(5 秒),新记录无计数提示。
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
- 新增被动轮询机制,**每 3 秒**自动拉取最新测试数据,覆盖工装本地按键和网页手动指令两种触发方式。
|
||||||
|
- 自动化测试运行期间,被动轮询自动让位给 `pollProgress`(500ms 高频轮询),结束后无缝接回。
|
||||||
|
- 「当前测试数据」标题旁新增 **B2 新记录条数**显示 `(N 条新记录)`,页面加载/自动化开始时自动复位。
|
||||||
|
|
||||||
|
## 三、角色权限体系
|
||||||
|
|
||||||
|
| 角色 | 权限 |
|
||||||
|
|---|---|
|
||||||
|
| `admin` | 全部功能(含用户管理、删除数据) |
|
||||||
|
| `manager` | 管理功能(用户管理除外)— 工装配置、数据删除等 |
|
||||||
|
| `analyst` | 仅测试数据查询/下载 + 修改密码 |
|
||||||
|
| `operator` | 测试操作 + 测试数据查看(不含工装配置) |
|
||||||
|
|
||||||
|
- `analyst` 角色访问受限页面时自动跳转到测试数据页并提示。
|
||||||
|
- 所有用户可自行修改密码。
|
||||||
|
|
||||||
|
## 四、设备日志管理
|
||||||
|
|
||||||
|
- 新增设备日志管理页面,记录设备 TCP 连接/断开、异常事件。
|
||||||
|
- 支持按设备编码、事件类型、时间范围筛选。
|
||||||
|
- 支持 **CSV 导出**,修复时区偏移 8 小时问题。
|
||||||
|
- 设备列表页在线/离线状态每 5 秒实时刷新。
|
||||||
|
- 后端 `device_status_monitor` 增加全表扫描,修正状态不一致问题。
|
||||||
|
|
||||||
|
## 五、UI/UX 优化
|
||||||
|
|
||||||
|
- **继电器输出格式化**:明确区分"✅有输出"/"❌无输出",前端直接显示 DB 字段。
|
||||||
|
- **工装配置概览面板**:测试操作页顶部展示当前配置参数(型号、频率范围、线圈、车辆等),可折叠。
|
||||||
|
- **测试信息页重构**:
|
||||||
|
- 三视图切换(全部数据 / B2 灵敏度 / B4 波动),差异字段自动隐藏。
|
||||||
|
- 表格支持横向滚动,列宽自适应不换行。
|
||||||
|
- 故障信息列限制 12em 宽度,超长截断省略 + hover 显示全文。
|
||||||
|
- 配置页频率/峰峰值前端显示与 DB 原始值双向转换修复。
|
||||||
|
|
||||||
|
## 六、Bug 修复
|
||||||
|
|
||||||
|
| 问题 | 修复 |
|
||||||
|
|---|---|
|
||||||
|
| 浏览器缓存导致工装参数 GET 返回旧数据 | 响应头 `Cache-Control: no-store` |
|
||||||
|
| `renderLatest` 覆盖测试模式,灵敏度/波动显示回退 | 分离测试模式更新与数据渲染逻辑 |
|
||||||
|
| 测试操作页工装配置修改后不同步 | 每 5 秒刷新 + 禁用缓存 |
|
||||||
|
| 设备离线时仍可发送指令 | 在线状态检查,离线/通信不良时弹窗提示并阻止 |
|
||||||
|
| HeartBeat 大小写不匹配,交互未记录 | 统一大小写匹配 |
|
||||||
|
| 后端 `device_status_monitor` 状态不一致 | 增加 `dnt_info` 全表扫描修正 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总计提交**:vd_test_fixture 24 次,edc_server 8 次。
|
||||||
4
edc-web/.gitignore
vendored
4
edc-web/.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
.venv/
|
|
||||||
venv/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.venv/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ def create_app() -> Flask:
|
|||||||
from app.routes.fixture import bp as fixture_bp
|
from app.routes.fixture import bp as fixture_bp
|
||||||
from app.routes.users import bp as users_bp
|
from app.routes.users import bp as users_bp
|
||||||
from app.routes.logs import bp as logs_bp
|
from app.routes.logs import bp as logs_bp
|
||||||
|
from app.routes.device_logs import bp as device_logs_bp
|
||||||
|
|
||||||
app.register_blueprint(devices_bp)
|
app.register_blueprint(devices_bp)
|
||||||
app.register_blueprint(test_op_bp)
|
app.register_blueprint(test_op_bp)
|
||||||
@@ -27,6 +28,7 @@ def create_app() -> Flask:
|
|||||||
app.register_blueprint(fixture_bp)
|
app.register_blueprint(fixture_bp)
|
||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
app.register_blueprint(logs_bp)
|
app.register_blueprint(logs_bp)
|
||||||
|
app.register_blueprint(device_logs_bp)
|
||||||
|
|
||||||
# 初始化默认管理员
|
# 初始化默认管理员
|
||||||
_ensure_admin()
|
_ensure_admin()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -42,11 +42,29 @@ def load_user(user_id):
|
|||||||
def init_auth(app):
|
def init_auth(app):
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
|
|
||||||
|
# analyst 角色:全局路由白名单拦截
|
||||||
|
ANALYST_ALLOWED = {
|
||||||
|
"auth.login", "auth.logout", "auth.change_password",
|
||||||
|
"test_data.test_data_page",
|
||||||
|
"test_data.api_test_data",
|
||||||
|
"test_data.api_chart_data",
|
||||||
|
"test_data.api_export",
|
||||||
|
"test_data.api_delete", # 自身有 inline 角色检查
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _restrict_analyst():
|
||||||
|
if current_user.is_authenticated and current_user.role == "analyst":
|
||||||
|
ep = request.endpoint or ""
|
||||||
|
if ep not in ANALYST_ALLOWED and not ep.startswith("static"):
|
||||||
|
flash("当前角色为 analyst,仅可访问测试数据")
|
||||||
|
return redirect(url_for("test_data.test_data_page"))
|
||||||
|
|
||||||
|
|
||||||
# ─── 装饰器 ────────────────────────────────────────────────────────
|
# ─── 装饰器 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
"""要求 admin 角色"""
|
"""要求 admin 角色(仅 admin,manager 不可通过)"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
@login_required
|
@login_required
|
||||||
@@ -57,6 +75,18 @@ def admin_required(f):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def privileged_required(f):
|
||||||
|
"""要求 admin 或 manager 角色"""
|
||||||
|
from functools import wraps
|
||||||
|
@wraps(f)
|
||||||
|
@login_required
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if current_user.role not in ("admin", "manager"):
|
||||||
|
return "权限不足", 403
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# ─── 登录 / 登出 ────────────────────────────────────────────────────
|
# ─── 登录 / 登出 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
@@ -86,3 +116,53 @@ def logout():
|
|||||||
ip=request.remote_addr or "", result="ok")
|
ip=request.remote_addr or "", result="ok")
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/change-password", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def change_password():
|
||||||
|
"""所有用户自行修改密码"""
|
||||||
|
if request.method == "POST":
|
||||||
|
old_password = request.form.get("old_password", "")
|
||||||
|
new_password = request.form.get("new_password", "").strip()
|
||||||
|
confirm_password = request.form.get("confirm_password", "")
|
||||||
|
|
||||||
|
if not old_password or not new_password:
|
||||||
|
flash("所有字段都不能为空")
|
||||||
|
return render_template("change_password.html")
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
flash("新密码至少6位")
|
||||||
|
return render_template("change_password.html")
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
flash("两次输入的新密码不一致")
|
||||||
|
return render_template("change_password.html")
|
||||||
|
|
||||||
|
# 验证旧密码
|
||||||
|
from app.models import get_conn, get_user_by_username
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
user_dict = get_user_by_username(current_user.username)
|
||||||
|
if not user_dict or not check_password_hash(user_dict["password_hash"], old_password):
|
||||||
|
flash("原密码错误")
|
||||||
|
return render_template("change_password.html")
|
||||||
|
|
||||||
|
# 更新密码
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE tb_user SET password_hash=%s WHERE id=%s",
|
||||||
|
(generate_password_hash(new_password), current_user.id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
insert_log(current_user.id, current_user.username, "update",
|
||||||
|
target="self", detail="修改个人密码",
|
||||||
|
result="ok", ip=request.remote_addr or "")
|
||||||
|
flash("密码修改成功")
|
||||||
|
return redirect(url_for("devices.index"))
|
||||||
|
|
||||||
|
return render_template("change_password.html")
|
||||||
|
|||||||
@@ -149,10 +149,16 @@ def get_latest_test_state(dnt_id: int) -> dict | None:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def get_test_data(page: int = 1, per_page: int = 20,
|
def get_test_data(page: int = 1, per_page: int = 100,
|
||||||
serial: str = "", date_from: str = "",
|
serial: str = "", date_from: str = "",
|
||||||
date_to: str = "") -> tuple[list[dict], int]:
|
date_to: str = "", test_mode: str = "",
|
||||||
"""分页查询测试数据(JOIN dnt_info),返回 (records, total)"""
|
data_source: str = "",
|
||||||
|
detector_serial: str = "") -> tuple[list[dict], int]:
|
||||||
|
"""分页查询测试数据(JOIN dnt_info),最多返回最近 6000 条,返回 (records, total)
|
||||||
|
|
||||||
|
test_mode: ''=全部, '0'=灵敏度, '1'=波动
|
||||||
|
data_source: ''=全部, 'B2', 'B4'
|
||||||
|
"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -161,30 +167,49 @@ def get_test_data(page: int = 1, per_page: int = 20,
|
|||||||
if serial:
|
if serial:
|
||||||
where.append("d.serial LIKE %s")
|
where.append("d.serial LIKE %s")
|
||||||
params.append(f"%{serial}%")
|
params.append(f"%{serial}%")
|
||||||
|
if detector_serial:
|
||||||
|
where.append("t.detector_serial LIKE %s")
|
||||||
|
params.append(f"%{detector_serial}%")
|
||||||
if date_from:
|
if date_from:
|
||||||
where.append("t.create_time >= %s")
|
where.append("t.create_time >= %s")
|
||||||
params.append(date_from)
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
if date_to:
|
if date_to:
|
||||||
where.append("t.create_time <= %s")
|
where.append("t.create_time <= %s")
|
||||||
params.append(date_to + " 23:59:59")
|
params.append(date_to if len(date_to) > 10 else 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"
|
where_clause = " AND ".join(where) if where else "1=1"
|
||||||
|
|
||||||
# count
|
# count — 最多 6000 条
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"SELECT COUNT(*) as total FROM tb_state_tst t "
|
f"SELECT COUNT(*) as total FROM ("
|
||||||
f"JOIN dnt_info d ON t.dnt_id = d.id WHERE {where_clause}",
|
f"SELECT 1 FROM tb_state_tst t "
|
||||||
|
f"JOIN dnt_info d ON t.dnt_id = d.id "
|
||||||
|
f"WHERE {where_clause} ORDER BY t.id DESC LIMIT 6000"
|
||||||
|
f") sub",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
total = cur.fetchone()["total"]
|
total = cur.fetchone()["total"]
|
||||||
|
|
||||||
# data
|
# data — 子查询限 6000 后再分页
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"SELECT t.*, d.serial FROM tb_state_tst t "
|
f"SELECT * FROM ("
|
||||||
|
f"SELECT t.*, d.serial, "
|
||||||
|
f"c.coil_num, c.name as coil_name, "
|
||||||
|
f"sc.simulate_num, sc.name as car_name "
|
||||||
|
f"FROM tb_state_tst t "
|
||||||
f"JOIN dnt_info d ON t.dnt_id = d.id "
|
f"JOIN dnt_info d ON t.dnt_id = d.id "
|
||||||
|
f"LEFT JOIN tb_coil_info c ON t.coil_id = c.id "
|
||||||
|
f"LEFT JOIN tb_simulate_car sc ON t.simulate_car_id = sc.id "
|
||||||
f"WHERE {where_clause} "
|
f"WHERE {where_clause} "
|
||||||
f"ORDER BY t.id DESC LIMIT %s OFFSET %s",
|
f"ORDER BY t.id DESC LIMIT 6000"
|
||||||
|
f") sub ORDER BY id DESC LIMIT %s OFFSET %s",
|
||||||
params + [per_page, offset],
|
params + [per_page, offset],
|
||||||
)
|
)
|
||||||
records = cur.fetchall()
|
records = cur.fetchall()
|
||||||
@@ -195,8 +220,14 @@ def get_test_data(page: int = 1, per_page: int = 20,
|
|||||||
|
|
||||||
|
|
||||||
def get_all_test_data_for_export(serial: str = "", date_from: str = "",
|
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 = "",
|
||||||
|
detector_serial: str = "") -> list[dict]:
|
||||||
|
"""导出全部数据(最多最近 6000 条)
|
||||||
|
|
||||||
|
test_mode: ''=全部, '0'=灵敏度, '1'=波动
|
||||||
|
data_source: ''=全部, 'B2', 'B4'
|
||||||
|
"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -205,18 +236,32 @@ def get_all_test_data_for_export(serial: str = "", date_from: str = "",
|
|||||||
if serial:
|
if serial:
|
||||||
where.append("d.serial LIKE %s")
|
where.append("d.serial LIKE %s")
|
||||||
params.append(f"%{serial}%")
|
params.append(f"%{serial}%")
|
||||||
|
if detector_serial:
|
||||||
|
where.append("t.detector_serial LIKE %s")
|
||||||
|
params.append(f"%{detector_serial}%")
|
||||||
if date_from:
|
if date_from:
|
||||||
where.append("t.create_time >= %s")
|
where.append("t.create_time >= %s")
|
||||||
params.append(date_from)
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
if date_to:
|
if date_to:
|
||||||
where.append("t.create_time <= %s")
|
where.append("t.create_time <= %s")
|
||||||
params.append(date_to + " 23:59:59")
|
params.append(date_to if len(date_to) > 10 else 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"
|
where_clause = " AND ".join(where) if where else "1=1"
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"SELECT t.*, d.serial FROM tb_state_tst t "
|
f"SELECT t.*, d.serial, "
|
||||||
|
f"c.coil_num, c.name as coil_name, "
|
||||||
|
f"sc.simulate_num, sc.name as car_name "
|
||||||
|
f"FROM tb_state_tst t "
|
||||||
f"JOIN dnt_info d ON t.dnt_id = d.id "
|
f"JOIN dnt_info d ON t.dnt_id = d.id "
|
||||||
f"WHERE {where_clause} ORDER BY t.id DESC",
|
f"LEFT JOIN tb_coil_info c ON t.coil_id = c.id "
|
||||||
|
f"LEFT JOIN tb_simulate_car sc ON t.simulate_car_id = sc.id "
|
||||||
|
f"WHERE {where_clause} ORDER BY t.id DESC LIMIT 6000",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
@@ -285,6 +330,37 @@ def get_automation_records(dnt_id: int, since: str) -> list[dict]:
|
|||||||
conn.close()
|
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:
|
def get_user_by_username(username: str) -> dict | None:
|
||||||
@@ -393,12 +469,24 @@ def get_logs(page: int = 1, per_page: int = 30,
|
|||||||
# ─── tb_fixture_param ──────────────────────────────────────────────
|
# ─── tb_fixture_param ──────────────────────────────────────────────
|
||||||
|
|
||||||
def get_fixture_param(dnt_id: int) -> dict | None:
|
def get_fixture_param(dnt_id: int) -> dict | None:
|
||||||
"""获取设备的工装测试参数"""
|
"""获取设备的工装测试参数(含线圈和模拟车辆信息)"""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT * FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,),
|
"SELECT fp.*, "
|
||||||
|
"c.coil_num, c.name as coil_name, c.shape as coil_shape, "
|
||||||
|
"c.length as coil_length, c.width as coil_width, c.radius as coil_radius, "
|
||||||
|
"c.turns as coil_turns, c.resistance as coil_resistance, "
|
||||||
|
"c.material as coil_material, "
|
||||||
|
"sc.simulate_num, sc.name as car_name, sc.shape as car_shape, "
|
||||||
|
"sc.length as car_length, sc.width as car_width, sc.radius as car_radius, "
|
||||||
|
"sc.material as car_material "
|
||||||
|
"FROM tb_fixture_param fp "
|
||||||
|
"LEFT JOIN tb_coil_info c ON fp.coil_id = c.id "
|
||||||
|
"LEFT JOIN tb_simulate_car sc ON fp.simulate_car_id = sc.id "
|
||||||
|
"WHERE fp.dnt_id=%s",
|
||||||
|
(dnt_id,),
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
finally:
|
finally:
|
||||||
@@ -414,9 +502,11 @@ def upsert_fixture_param(dnt_id: int, **kwargs):
|
|||||||
"SELECT id FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,),
|
"SELECT id FROM tb_fixture_param WHERE dnt_id=%s", (dnt_id,),
|
||||||
)
|
)
|
||||||
existing = cur.fetchone()
|
existing = cur.fetchone()
|
||||||
|
# 主线参数字段(不含 coil_id/simulate_car_id,后面单独处理)
|
||||||
fields = [
|
fields = [
|
||||||
"Addr", "DevType", "TestMode", "RestDis", "MinusDis",
|
"Addr", "DevType", "TestMode", "RestDis", "MinusDis",
|
||||||
"SensMin", "SensMax", "FreMin", "FreMax", "PeakMin", "PeakMax",
|
"SensMin", "SensMax", "FreMin", "FreMax", "PeakMin", "PeakMax",
|
||||||
|
"FarTol", "NearTol", "StepTol", "BackForth", "NearStay", "FarStay",
|
||||||
]
|
]
|
||||||
if existing:
|
if existing:
|
||||||
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||||
@@ -434,6 +524,17 @@ def upsert_fixture_param(dnt_id: int, **kwargs):
|
|||||||
f"VALUES (%s, {placeholders})",
|
f"VALUES (%s, {placeholders})",
|
||||||
[dnt_id] + values,
|
[dnt_id] + values,
|
||||||
)
|
)
|
||||||
|
# 单独处理线圈/模拟车辆关联(可选,不覆盖已有值)
|
||||||
|
if "coil_id" in kwargs:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE tb_fixture_param SET coil_id=%s WHERE dnt_id=%s",
|
||||||
|
(kwargs["coil_id"], dnt_id),
|
||||||
|
)
|
||||||
|
if "simulate_car_id" in kwargs:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE tb_fixture_param SET simulate_car_id=%s WHERE dnt_id=%s",
|
||||||
|
(kwargs["simulate_car_id"], dnt_id),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -528,3 +629,347 @@ def delete_vehicle_base_test(test_id: int):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 线圈参数 CRUD ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_coil_info_list(search: str = "") -> list[dict]:
|
||||||
|
"""获取线圈参数列表"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if search:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_coil_info WHERE coil_num LIKE %s OR name LIKE %s "
|
||||||
|
"ORDER BY id DESC",
|
||||||
|
(f"%{search}%", f"%{search}%"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT * FROM tb_coil_info ORDER BY id DESC")
|
||||||
|
return cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_coil_info_by_id(coil_id: int) -> dict | None:
|
||||||
|
"""获取单个线圈参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT * FROM tb_coil_info WHERE id=%s", (coil_id,))
|
||||||
|
return cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_coil_info(**kwargs) -> int:
|
||||||
|
"""创建线圈参数,返回新 ID"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"coil_num", "name", "induct", "shape", "length", "width",
|
||||||
|
"radius", "turns", "resistance", "material", "remark",
|
||||||
|
]
|
||||||
|
col_names = ", ".join(f"`{f}`" for f in fields)
|
||||||
|
placeholders = ", ".join(["%s"] * len(fields))
|
||||||
|
values = [kwargs.get(f, "" if f in ("coil_num", "name", "shape", "material", "remark") else 0) for f in fields]
|
||||||
|
cur.execute(
|
||||||
|
f"INSERT INTO tb_coil_info ({col_names}) VALUES ({placeholders})",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_coil_info(coil_id: int, **kwargs):
|
||||||
|
"""更新线圈参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"coil_num", "name", "induct", "shape", "length", "width",
|
||||||
|
"radius", "turns", "resistance", "material", "remark",
|
||||||
|
]
|
||||||
|
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||||
|
values = [kwargs.get(f, "" if f in ("coil_num", "name", "shape", "material", "remark") else 0) for f in fields] + [coil_id]
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE tb_coil_info SET {sets} WHERE id=%s", values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_coil_info(coil_id: int):
|
||||||
|
"""删除线圈参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("DELETE FROM tb_coil_info WHERE id=%s", (coil_id,))
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 模拟车辆参数 CRUD ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_simulate_car_list(search: str = "") -> list[dict]:
|
||||||
|
"""获取模拟车辆参数列表"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if search:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT * FROM tb_simulate_car WHERE simulate_num LIKE %s OR name LIKE %s "
|
||||||
|
"ORDER BY id DESC",
|
||||||
|
(f"%{search}%", f"%{search}%"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT * FROM tb_simulate_car ORDER BY id DESC")
|
||||||
|
return cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_simulate_car_by_id(car_id: int) -> dict | None:
|
||||||
|
"""获取单个模拟车辆参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT * FROM tb_simulate_car WHERE id=%s", (car_id,))
|
||||||
|
return cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_simulate_car(**kwargs) -> int:
|
||||||
|
"""创建模拟车辆参数,返回新 ID"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"simulate_num", "name", "shape", "length", "width",
|
||||||
|
"radius", "material", "remark",
|
||||||
|
]
|
||||||
|
col_names = ", ".join(f"`{f}`" for f in fields)
|
||||||
|
placeholders = ", ".join(["%s"] * len(fields))
|
||||||
|
values = [kwargs.get(f, "" if f in ("simulate_num", "name", "shape", "material", "remark") else 0) for f in fields]
|
||||||
|
cur.execute(
|
||||||
|
f"INSERT INTO tb_simulate_car ({col_names}) VALUES ({placeholders})",
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_simulate_car(car_id: int, **kwargs):
|
||||||
|
"""更新模拟车辆参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
fields = [
|
||||||
|
"simulate_num", "name", "shape", "length", "width",
|
||||||
|
"radius", "material", "remark",
|
||||||
|
]
|
||||||
|
sets = ", ".join(f"`{f}`=%s" for f in fields)
|
||||||
|
values = [kwargs.get(f, "" if f in ("simulate_num", "name", "shape", "material", "remark") else 0) for f in fields] + [car_id]
|
||||||
|
cur.execute(
|
||||||
|
f"UPDATE tb_simulate_car SET {sets} WHERE id=%s", values,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_simulate_car(car_id: int):
|
||||||
|
"""删除模拟车辆参数"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("DELETE FROM tb_simulate_car WHERE id=%s", (car_id,))
|
||||||
|
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 len(date_from) > 10 else date_from)
|
||||||
|
if date_to:
|
||||||
|
where.append("t.create_time <= %s")
|
||||||
|
params.append(date_to if len(date_to) > 10 else 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()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── tb_device_log ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_device_logs(page: int = 1, per_page: int = 30,
|
||||||
|
serial: str = "", event_type: str = "",
|
||||||
|
date_from: str = "", date_to: str = "") -> tuple[list[dict], int]:
|
||||||
|
"""分页查询设备事件日志,返回 (records, total)"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if serial:
|
||||||
|
where.append("device_serial LIKE %s")
|
||||||
|
params.append(f"%{serial}%")
|
||||||
|
if event_type:
|
||||||
|
where.append("event_type = %s")
|
||||||
|
params.append(event_type)
|
||||||
|
if date_from:
|
||||||
|
where.append("create_time >= %s")
|
||||||
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
|
if date_to:
|
||||||
|
where.append("create_time <= %s")
|
||||||
|
params.append(date_to if len(date_to) > 10 else date_to + " 23:59:59")
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where) if where else "1=1"
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT COUNT(*) as total FROM tb_device_log WHERE {where_clause}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
total = cur.fetchone()["total"]
|
||||||
|
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT * FROM tb_device_log WHERE {where_clause} "
|
||||||
|
f"ORDER BY id DESC LIMIT %s OFFSET %s",
|
||||||
|
params + [per_page, offset],
|
||||||
|
)
|
||||||
|
return cur.fetchall(), total
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def export_device_logs(serial: str = "", event_type: str = "",
|
||||||
|
date_from: str = "", date_to: str = "") -> list[dict]:
|
||||||
|
"""导出全部设备事件日志(不分页)"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if serial:
|
||||||
|
where.append("device_serial LIKE %s")
|
||||||
|
params.append(f"%{serial}%")
|
||||||
|
if event_type:
|
||||||
|
where.append("event_type = %s")
|
||||||
|
params.append(event_type)
|
||||||
|
if date_from:
|
||||||
|
where.append("create_time >= %s")
|
||||||
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
|
if date_to:
|
||||||
|
where.append("create_time <= %s")
|
||||||
|
params.append(date_to if len(date_to) > 10 else date_to + " 23:59:59")
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where) if where else "1=1"
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT * FROM tb_device_log WHERE {where_clause} "
|
||||||
|
f"ORDER BY id DESC",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_device_logs(serial: str = "", event_type: str = "",
|
||||||
|
date_from: str = "", date_to: str = "") -> int:
|
||||||
|
"""删除符合条件的设备日志,返回删除行数。至少需要一个条件。"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if serial:
|
||||||
|
where.append("device_serial LIKE %s")
|
||||||
|
params.append(f"%{serial}%")
|
||||||
|
if event_type:
|
||||||
|
where.append("event_type = %s")
|
||||||
|
params.append(event_type)
|
||||||
|
if date_from:
|
||||||
|
where.append("create_time >= %s")
|
||||||
|
params.append(date_from if len(date_from) > 10 else date_from)
|
||||||
|
if date_to:
|
||||||
|
where.append("create_time <= %s")
|
||||||
|
params.append(date_to if len(date_to) > 10 else date_to + " 23:59:59")
|
||||||
|
|
||||||
|
if not where:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
where_clause = " AND ".join(where)
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT COUNT(*) as cnt FROM tb_device_log WHERE {where_clause}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
cnt = cur.fetchone()["cnt"]
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"DELETE FROM tb_device_log WHERE {where_clause}", params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cnt
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── tb_pending_detector ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def set_pending_detector_serial(dnt_id: int, detector_serial: str):
|
||||||
|
"""设置待插入的车检器序列号(UPSERT)"""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""INSERT INTO tb_pending_detector (dnt_id, detector_serial)
|
||||||
|
VALUES (%s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE detector_serial = VALUES(detector_serial)""",
|
||||||
|
(dnt_id, detector_serial),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
95
edc-web/app/routes/device_logs.py
Normal file
95
edc-web/app/routes/device_logs.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""设备事件日志 API"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, render_template, request, Response
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app.models import get_device_logs, export_device_logs, delete_device_logs, insert_log
|
||||||
|
|
||||||
|
bp = Blueprint("device_logs", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/device-logs")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
"""设备日志页面"""
|
||||||
|
return render_template("device_logs.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/device-logs")
|
||||||
|
@login_required
|
||||||
|
def api_device_logs():
|
||||||
|
"""查询设备事件日志"""
|
||||||
|
page = request.args.get("page", 1, type=int)
|
||||||
|
per_page = request.args.get("per_page", 30, type=int)
|
||||||
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
event_type = request.args.get("event_type", "", type=str)
|
||||||
|
date_from = request.args.get("date_from", "", type=str)
|
||||||
|
date_to = request.args.get("date_to", "", type=str)
|
||||||
|
|
||||||
|
records, total = get_device_logs(
|
||||||
|
page=page, per_page=per_page,
|
||||||
|
serial=serial, event_type=event_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
)
|
||||||
|
pages = max(1, (total + per_page - 1) // per_page)
|
||||||
|
return jsonify({"records": records, "total": total, "pages": pages})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/device-logs/export")
|
||||||
|
@login_required
|
||||||
|
def api_export():
|
||||||
|
"""导出设备事件日志为 CSV"""
|
||||||
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
event_type = request.args.get("event_type", "", type=str)
|
||||||
|
date_from = request.args.get("date_from", "", type=str)
|
||||||
|
date_to = request.args.get("date_to", "", type=str)
|
||||||
|
|
||||||
|
records = export_device_logs(
|
||||||
|
serial=serial, event_type=event_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
if records:
|
||||||
|
headers = list(records[0].keys())
|
||||||
|
writer.writerow(headers)
|
||||||
|
for r in records:
|
||||||
|
writer.writerow(r.values())
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return Response(
|
||||||
|
output.getvalue(),
|
||||||
|
mimetype="text/csv",
|
||||||
|
headers={"Content-Disposition": "attachment; filename=device_logs.csv"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/device-logs/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_device_logs_delete():
|
||||||
|
"""删除设备日志(admin 权限)"""
|
||||||
|
if current_user.role not in ("admin", "manager"):
|
||||||
|
return jsonify({"ok": False, "error": "无权限"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
serial = data.get("serial", "")
|
||||||
|
event_type = data.get("event_type", "")
|
||||||
|
date_from = data.get("date_from", "")
|
||||||
|
date_to = data.get("date_to", "")
|
||||||
|
|
||||||
|
deleted = delete_device_logs(
|
||||||
|
serial=serial, event_type=event_type,
|
||||||
|
date_from=date_from, date_to=date_to,
|
||||||
|
)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "delete",
|
||||||
|
target="device_log",
|
||||||
|
detail=f"删除 {deleted} 条设备日志 serial={serial} type={event_type}",
|
||||||
|
result="ok",
|
||||||
|
ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "deleted": deleted})
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from flask import Blueprint, jsonify, render_template, request
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.models import get_all_devices, update_device_name
|
from app.models import get_all_devices, update_device_name, get_device_by_id
|
||||||
|
|
||||||
bp = Blueprint("devices", __name__)
|
bp = Blueprint("devices", __name__)
|
||||||
|
|
||||||
@@ -21,6 +21,22 @@ def api_devices():
|
|||||||
return jsonify(devices)
|
return jsonify(devices)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/devices/<int:device_id>/status")
|
||||||
|
def api_device_status(device_id):
|
||||||
|
"""获取单个设备的在线状态"""
|
||||||
|
device = get_device_by_id(device_id)
|
||||||
|
if not device:
|
||||||
|
return jsonify({"ok": False, "error": "设备不存在"}), 404
|
||||||
|
state_names = {0: "离线", 1: "在线", 2: "通信不良"}
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"device_id": device_id,
|
||||||
|
"state": device["state"],
|
||||||
|
"state_name": state_names.get(device["state"], "未知"),
|
||||||
|
"serial": device["serial"],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/devices/<int:device_id>/name", methods=["PUT"])
|
@bp.route("/api/devices/<int:device_id>/name", methods=["PUT"])
|
||||||
def api_update_name(device_id):
|
def api_update_name(device_id):
|
||||||
"""修改设备名称"""
|
"""修改设备名称"""
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ from app.models import (
|
|||||||
delete_vehicle_base_test,
|
delete_vehicle_base_test,
|
||||||
get_serialnet_by_id,
|
get_serialnet_by_id,
|
||||||
insert_log,
|
insert_log,
|
||||||
|
get_coil_info_list,
|
||||||
|
get_coil_info_by_id,
|
||||||
|
create_coil_info,
|
||||||
|
update_coil_info,
|
||||||
|
delete_coil_info,
|
||||||
|
get_simulate_car_list,
|
||||||
|
get_simulate_car_by_id,
|
||||||
|
create_simulate_car,
|
||||||
|
update_simulate_car,
|
||||||
|
delete_simulate_car,
|
||||||
)
|
)
|
||||||
|
|
||||||
bp = Blueprint("fixture", __name__)
|
bp = Blueprint("fixture", __name__)
|
||||||
@@ -59,13 +69,18 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
|||||||
reset_dis: int, minus_dis: int,
|
reset_dis: int, minus_dis: int,
|
||||||
sens_min: int, sens_max: int,
|
sens_min: int, sens_max: int,
|
||||||
fre_min: int, fre_max: int,
|
fre_min: int, fre_max: int,
|
||||||
peak_min: int, peak_max: int) -> str:
|
peak_min: int, peak_max: int,
|
||||||
"""构造 0x4B 配置指令 hex 字符串
|
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) |
|
ResetDis(1) | MinusDis(1) |
|
||||||
SensMin(2 LE) | SensMax(2 LE) | FreMin(2 LE) | FreMax(2 LE) |
|
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([
|
payload = bytes([
|
||||||
0x4B, # CMD
|
0x4B, # CMD
|
||||||
@@ -78,6 +93,14 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
|||||||
payload += (_le16(sens_min) + _le16(sens_max) +
|
payload += (_le16(sens_min) + _le16(sens_max) +
|
||||||
_le16(fre_min) + _le16(fre_max) +
|
_le16(fre_min) + _le16(fre_max) +
|
||||||
_le16(peak_min) + _le16(peak_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
|
pkt = bytes([0x7F, 0x81, len(payload)]) + payload
|
||||||
xor, total = _xor_sum(pkt[1:])
|
xor, total = _xor_sum(pkt[1:])
|
||||||
@@ -91,6 +114,8 @@ def build_4b_packet(addr: int, dev_type: int, test_mode: int,
|
|||||||
@login_required
|
@login_required
|
||||||
def fixture_page(dnt_id):
|
def fixture_page(dnt_id):
|
||||||
"""工装配置页面"""
|
"""工装配置页面"""
|
||||||
|
if current_user.role not in ("admin", "manager"):
|
||||||
|
return "无权限:仅管理员可访问工装配置", 403
|
||||||
device = get_device_by_id(dnt_id)
|
device = get_device_by_id(dnt_id)
|
||||||
if not device:
|
if not device:
|
||||||
return "设备不存在", 404
|
return "设备不存在", 404
|
||||||
@@ -110,6 +135,8 @@ def vehicle_base_test_page():
|
|||||||
@login_required
|
@login_required
|
||||||
def api_fixture_command():
|
def api_fixture_command():
|
||||||
"""发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)"""
|
"""发送工装配置指令 (0x4A/0x4B/0x4C/0x4D/0x4E)"""
|
||||||
|
if current_user.role not in ("admin", "manager"):
|
||||||
|
return jsonify({"ok": False, "error": "无权限:仅管理员可执行工装指令"}), 403
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
dnt_id = data.get("dnt_id")
|
dnt_id = data.get("dnt_id")
|
||||||
cmd = data.get("cmd", "").upper()
|
cmd = data.get("cmd", "").upper()
|
||||||
@@ -118,7 +145,7 @@ def api_fixture_command():
|
|||||||
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
||||||
|
|
||||||
if cmd == "4B":
|
if cmd == "4B":
|
||||||
# 动态构造 0x4B 指令
|
# 动态构造 0x4B 指令 (V2.0.3)
|
||||||
params = data.get("params", {})
|
params = data.get("params", {})
|
||||||
send_pkg = build_4b_packet(
|
send_pkg = build_4b_packet(
|
||||||
addr=params.get("addr", 1),
|
addr=params.get("addr", 1),
|
||||||
@@ -132,6 +159,12 @@ def api_fixture_command():
|
|||||||
fre_max=params.get("fre_max", 0),
|
fre_max=params.get("fre_max", 0),
|
||||||
peak_min=params.get("peak_min", 0),
|
peak_min=params.get("peak_min", 0),
|
||||||
peak_max=params.get("peak_max", 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:
|
elif cmd in FIXTURE_COMMANDS:
|
||||||
send_pkg = FIXTURE_COMMANDS[cmd]
|
send_pkg = FIXTURE_COMMANDS[cmd]
|
||||||
@@ -184,17 +217,30 @@ def api_get_serialnet(record_id):
|
|||||||
def api_get_fixture_param(dnt_id):
|
def api_get_fixture_param(dnt_id):
|
||||||
"""获取工装测试参数"""
|
"""获取工装测试参数"""
|
||||||
param = get_fixture_param(dnt_id)
|
param = get_fixture_param(dnt_id)
|
||||||
return jsonify(param or {})
|
resp = jsonify(param or {})
|
||||||
|
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/fixture/param/<int:dnt_id>", methods=["POST"])
|
@bp.route("/api/fixture/param/<int:dnt_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_save_fixture_param(dnt_id):
|
def api_save_fixture_param(dnt_id):
|
||||||
"""保存工装测试参数(仅数据库,不下发设备)"""
|
"""保存工装测试参数(仅数据库,不下发设备)"""
|
||||||
|
if current_user.role not in ("admin", "manager"):
|
||||||
|
return jsonify({"ok": False, "error": "无权限:仅管理员可修改工装参数"}), 403
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({"ok": False, "error": "数据为空"}), 400
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
upsert_fixture_param(dnt_id, **data)
|
upsert_fixture_param(dnt_id, **data)
|
||||||
|
device = get_device_by_id(dnt_id)
|
||||||
|
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "update",
|
||||||
|
target=target,
|
||||||
|
detail="保存工装配置参数",
|
||||||
|
result="ok",
|
||||||
|
ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@@ -256,3 +302,181 @@ def api_delete_vehicle_base_test(test_id):
|
|||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "error": str(e)}), 500
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 线圈参数页面 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/coil-info")
|
||||||
|
@login_required
|
||||||
|
def coil_info_page():
|
||||||
|
"""线圈参数管理页面"""
|
||||||
|
return render_template("coil_info.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/coil-info")
|
||||||
|
@login_required
|
||||||
|
def api_list_coil_info():
|
||||||
|
"""列出线圈参数"""
|
||||||
|
search = request.args.get("search", "")
|
||||||
|
items = get_coil_info_list(search)
|
||||||
|
return jsonify(items)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/coil-info/<int:coil_id>")
|
||||||
|
@login_required
|
||||||
|
def api_get_coil_info(coil_id):
|
||||||
|
"""获取单个线圈参数"""
|
||||||
|
item = get_coil_info_by_id(coil_id)
|
||||||
|
if not item:
|
||||||
|
return jsonify({"error": "不存在"}), 404
|
||||||
|
return jsonify(item)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/coil-info", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_create_coil_info():
|
||||||
|
"""创建线圈参数"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
coil_id = create_coil_info(**data)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "create",
|
||||||
|
target="coil_info",
|
||||||
|
detail=f"创建线圈: {data.get('coil_num','')} {data.get('name','')}",
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "id": coil_id})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/coil-info/<int:coil_id>", methods=["PUT"])
|
||||||
|
@login_required
|
||||||
|
def api_update_coil_info(coil_id):
|
||||||
|
"""更新线圈参数"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
update_coil_info(coil_id, **data)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "update",
|
||||||
|
target="coil_info",
|
||||||
|
detail=f"更新线圈 id={coil_id}: {data.get('coil_num','')} {data.get('name','')}",
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/coil-info/<int:coil_id>", methods=["DELETE"])
|
||||||
|
@login_required
|
||||||
|
def api_delete_coil_info(coil_id):
|
||||||
|
"""删除线圈参数"""
|
||||||
|
try:
|
||||||
|
item = get_coil_info_by_id(coil_id)
|
||||||
|
detail = f"删除线圈 id={coil_id}"
|
||||||
|
if item:
|
||||||
|
detail += f": {item.get('coil_num','')} {item.get('name','')}"
|
||||||
|
delete_coil_info(coil_id)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "delete",
|
||||||
|
target="coil_info",
|
||||||
|
detail=detail,
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 模拟车辆参数页面 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route("/simulate-car")
|
||||||
|
@login_required
|
||||||
|
def simulate_car_page():
|
||||||
|
"""模拟车辆参数管理页面"""
|
||||||
|
return render_template("simulate_car.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/simulate-car")
|
||||||
|
@login_required
|
||||||
|
def api_list_simulate_car():
|
||||||
|
"""列出模拟车辆参数"""
|
||||||
|
search = request.args.get("search", "")
|
||||||
|
items = get_simulate_car_list(search)
|
||||||
|
return jsonify(items)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/simulate-car/<int:car_id>")
|
||||||
|
@login_required
|
||||||
|
def api_get_simulate_car(car_id):
|
||||||
|
"""获取单个模拟车辆参数"""
|
||||||
|
item = get_simulate_car_by_id(car_id)
|
||||||
|
if not item:
|
||||||
|
return jsonify({"error": "不存在"}), 404
|
||||||
|
return jsonify(item)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/simulate-car", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_create_simulate_car():
|
||||||
|
"""创建模拟车辆参数"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
car_id = create_simulate_car(**data)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "create",
|
||||||
|
target="simulate_car",
|
||||||
|
detail=f"创建模拟车辆: {data.get('simulate_num','')} {data.get('name','')}",
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True, "id": car_id})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/simulate-car/<int:car_id>", methods=["PUT"])
|
||||||
|
@login_required
|
||||||
|
def api_update_simulate_car(car_id):
|
||||||
|
"""更新模拟车辆参数"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"ok": False, "error": "数据为空"}), 400
|
||||||
|
try:
|
||||||
|
update_simulate_car(car_id, **data)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "update",
|
||||||
|
target="simulate_car",
|
||||||
|
detail=f"更新模拟车辆 id={car_id}: {data.get('simulate_num','')} {data.get('name','')}",
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/simulate-car/<int:car_id>", methods=["DELETE"])
|
||||||
|
@login_required
|
||||||
|
def api_delete_simulate_car(car_id):
|
||||||
|
"""删除模拟车辆参数"""
|
||||||
|
try:
|
||||||
|
item = get_simulate_car_by_id(car_id)
|
||||||
|
detail = f"删除模拟车辆 id={car_id}"
|
||||||
|
if item:
|
||||||
|
detail += f": {item.get('simulate_num','')} {item.get('name','')}"
|
||||||
|
delete_simulate_car(car_id)
|
||||||
|
insert_log(
|
||||||
|
current_user.id, current_user.username, "delete",
|
||||||
|
target="simulate_car",
|
||||||
|
detail=detail,
|
||||||
|
result="ok", ip=request.remote_addr or "",
|
||||||
|
)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
from flask import Blueprint, jsonify, render_template, request
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.auth import admin_required
|
from app.auth import privileged_required
|
||||||
from app.models import get_logs
|
from app.models import get_logs
|
||||||
|
|
||||||
bp = Blueprint("logs", __name__, url_prefix="/logs")
|
bp = Blueprint("logs", __name__, url_prefix="/logs")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@admin_required
|
@privileged_required
|
||||||
def logs_page():
|
def logs_page():
|
||||||
return render_template("logs.html")
|
return render_template("logs.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/logs")
|
@bp.route("/api/logs")
|
||||||
@admin_required
|
@privileged_required
|
||||||
def api_logs():
|
def api_logs():
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
per_page = request.args.get("per_page", 30, type=int)
|
per_page = request.args.get("per_page", 30, type=int)
|
||||||
|
|||||||
@@ -3,28 +3,34 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from flask import Blueprint, jsonify, render_template, request, Response
|
from flask import Blueprint, jsonify, render_template, request, Response
|
||||||
from flask_login import login_required
|
from flask_login import login_required, current_user
|
||||||
from app.models import get_test_data, get_all_test_data_for_export
|
from app.models import get_test_data, get_all_test_data_for_export, delete_test_data, insert_log
|
||||||
|
|
||||||
bp = Blueprint("test_data", __name__)
|
bp = Blueprint("test_data", __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/test-data")
|
@bp.route("/test-data")
|
||||||
|
@login_required
|
||||||
def test_data_page():
|
def test_data_page():
|
||||||
"""测试信息页"""
|
"""测试信息页"""
|
||||||
return render_template("test_data.html")
|
return render_template("test_data.html")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test-data")
|
@bp.route("/api/test-data")
|
||||||
|
@login_required
|
||||||
def api_test_data():
|
def api_test_data():
|
||||||
"""分页查询测试数据"""
|
"""分页查询测试数据"""
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
per_page = request.args.get("per_page", 20, type=int)
|
per_page = request.args.get("per_page", 100, type=int)
|
||||||
serial = request.args.get("serial", "", type=str)
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
detector_serial = request.args.get("detector_serial", "", type=str)
|
||||||
date_from = request.args.get("date_from", "", type=str)
|
date_from = request.args.get("date_from", "", type=str)
|
||||||
date_to = request.args.get("date_to", "", 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, detector_serial)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"records": records,
|
"records": records,
|
||||||
"total": total,
|
"total": total,
|
||||||
@@ -34,14 +40,36 @@ def api_test_data():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/test-data/export")
|
@bp.route("/api/test-data/chart")
|
||||||
def api_export():
|
@login_required
|
||||||
"""导出测试数据为 CSV"""
|
def api_chart_data():
|
||||||
|
"""返回图表所需全部数据(不分页,最多 6000 条)"""
|
||||||
serial = request.args.get("serial", "", type=str)
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
detector_serial = request.args.get("detector_serial", "", type=str)
|
||||||
date_from = request.args.get("date_from", "", type=str)
|
date_from = request.args.get("date_from", "", type=str)
|
||||||
date_to = request.args.get("date_to", "", 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,
|
||||||
|
detector_serial)
|
||||||
|
return jsonify({"records": records, "total": len(records)})
|
||||||
|
|
||||||
|
@bp.route("/api/test-data/export")
|
||||||
|
@login_required
|
||||||
|
def api_export():
|
||||||
|
"""导出测试数据为 CSV(最多 6000 条)"""
|
||||||
|
serial = request.args.get("serial", "", type=str)
|
||||||
|
detector_serial = request.args.get("detector_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,
|
||||||
|
detector_serial)
|
||||||
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
@@ -59,3 +87,39 @@ def api_export():
|
|||||||
mimetype="text/csv",
|
mimetype="text/csv",
|
||||||
headers={"Content-Disposition": "attachment; filename=test_data.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 not in ("admin", "manager"):
|
||||||
|
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,8 +9,11 @@ from app.models import (
|
|||||||
get_latest_test_state,
|
get_latest_test_state,
|
||||||
get_automation_averages,
|
get_automation_averages,
|
||||||
get_automation_records,
|
get_automation_records,
|
||||||
|
get_latest_wave_data,
|
||||||
|
get_wave_records,
|
||||||
clear_serialnet_records,
|
clear_serialnet_records,
|
||||||
insert_log,
|
insert_log,
|
||||||
|
set_pending_detector_serial,
|
||||||
)
|
)
|
||||||
|
|
||||||
bp = Blueprint("test_op", __name__)
|
bp = Blueprint("test_op", __name__)
|
||||||
@@ -87,10 +90,15 @@ def api_automation_start():
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
dnt_id = data.get("dnt_id")
|
dnt_id = data.get("dnt_id")
|
||||||
count = int(data.get("count", 1))
|
count = int(data.get("count", 1))
|
||||||
|
detector_serial = (data.get("detector_serial") or "").strip()
|
||||||
|
|
||||||
device = get_device_by_id(dnt_id)
|
device = get_device_by_id(dnt_id)
|
||||||
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
target = f"{device['serial']}" if device else f"dnt_id={dnt_id}"
|
||||||
|
|
||||||
|
# 存储待插入的车检器序列号
|
||||||
|
if detector_serial:
|
||||||
|
set_pending_detector_serial(dnt_id, detector_serial)
|
||||||
|
|
||||||
# 清除旧记录,然后插入第一条 0xB0
|
# 清除旧记录,然后插入第一条 0xB0
|
||||||
clear_serialnet_records(dnt_id)
|
clear_serialnet_records(dnt_id)
|
||||||
record_id = insert_serialnet(dnt_id, COMMANDS["B0"])
|
record_id = insert_serialnet(dnt_id, COMMANDS["B0"])
|
||||||
@@ -118,9 +126,13 @@ def api_automation_progress(dnt_id):
|
|||||||
latest = get_latest_test_state(dnt_id)
|
latest = get_latest_test_state(dnt_id)
|
||||||
averages = get_automation_averages(dnt_id, since if since else None)
|
averages = get_automation_averages(dnt_id, since if since else None)
|
||||||
records = get_automation_records(dnt_id, since) if since else []
|
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({
|
return jsonify({
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
"latest": latest,
|
"latest": latest,
|
||||||
"averages": averages,
|
"averages": averages,
|
||||||
"records": records,
|
"records": records,
|
||||||
|
"latest_wave": latest_wave,
|
||||||
|
"wave_records": wave_records,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ tr:hover { background: #f8f9fa; }
|
|||||||
/* === Online Status === */
|
/* === Online Status === */
|
||||||
.status-online { color: #27ae60; font-weight: 600; }
|
.status-online { color: #27ae60; font-weight: 600; }
|
||||||
.status-offline { color: #bdc3c7; }
|
.status-offline { color: #bdc3c7; }
|
||||||
|
.status-poor { color: #f39c12; font-weight: 600; }
|
||||||
|
|
||||||
/* === Editable Name === */
|
/* === Editable Name === */
|
||||||
.editable-name { cursor: pointer; border-bottom: 1px dashed transparent; }
|
.editable-name { cursor: pointer; border-bottom: 1px dashed transparent; }
|
||||||
|
|||||||
173
edc-web/app/static/js/coil_info.js
Normal file
173
edc-web/app/static/js/coil_info.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// 线圈参数管理
|
||||||
|
|
||||||
|
let editId = null; // null=新增, number=编辑
|
||||||
|
|
||||||
|
// ─── Toast ───────────────────────────────────
|
||||||
|
|
||||||
|
function toast(msg, isError = false) {
|
||||||
|
const el = document.getElementById("toast");
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = "msg-toast " + (isError ? "error" : "") + " show";
|
||||||
|
clearTimeout(el._timeout);
|
||||||
|
el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 列表加载 ────────────────────────────────
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const search = document.getElementById("search-input").value;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/coil-info?search=${encodeURIComponent(search)}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
renderTable(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeLabel(item) {
|
||||||
|
if (item.shape === '圆形') return `半径${item.radius || 0}cm`;
|
||||||
|
if (item.shape === '矩形') return `${item.length || 0}×${item.width || 0}cm`;
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.querySelector("#coil-table tbody");
|
||||||
|
if (!data.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="10" style="color:#999;text-align:center;">暂无数据,点右上角「新增」添加</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = data.map(t => `
|
||||||
|
<tr>
|
||||||
|
<td>${esc(t.coil_num)}</td>
|
||||||
|
<td>${esc(t.name)}</td>
|
||||||
|
<td>${t.induct || '-'}</td>
|
||||||
|
<td>${t.shape || '-'}</td>
|
||||||
|
<td>${sizeLabel(t)}</td>
|
||||||
|
<td>${t.turns || '-'}</td>
|
||||||
|
<td>${t.resistance || '-'}</td>
|
||||||
|
<td>${esc(t.material || '-')}</td>
|
||||||
|
<td>${esc(t.remark || '-')}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
|
||||||
|
<button class="btn-del" onclick="deleteRecord(${t.id}, '${esc(t.coil_num || t.name)}')">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 弹窗 ────────────────────────────────────
|
||||||
|
|
||||||
|
function openModal(id = null) {
|
||||||
|
editId = id;
|
||||||
|
document.getElementById("modal-title").textContent = id ? "编辑线圈参数" : "新增线圈参数";
|
||||||
|
document.getElementById("edit-modal").style.display = "flex";
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
fetch(`/api/coil-info/${id}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById("edit-coil-num").value = data.coil_num || "";
|
||||||
|
document.getElementById("edit-name").value = data.name || "";
|
||||||
|
document.getElementById("edit-induct").value = data.induct || 0;
|
||||||
|
document.getElementById("edit-shape").value = data.shape || "";
|
||||||
|
document.getElementById("edit-length").value = data.length || 0;
|
||||||
|
document.getElementById("edit-width").value = data.width || 0;
|
||||||
|
document.getElementById("edit-radius").value = data.radius || 0;
|
||||||
|
document.getElementById("edit-turns").value = data.turns || 0;
|
||||||
|
document.getElementById("edit-resistance").value = data.resistance || 0;
|
||||||
|
document.getElementById("edit-material").value = data.material || "";
|
||||||
|
document.getElementById("edit-remark").value = data.remark || "";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById("edit-coil-num").value = "";
|
||||||
|
document.getElementById("edit-name").value = "";
|
||||||
|
document.getElementById("edit-induct").value = "0";
|
||||||
|
document.getElementById("edit-shape").value = "";
|
||||||
|
document.getElementById("edit-length").value = "0";
|
||||||
|
document.getElementById("edit-width").value = "0";
|
||||||
|
document.getElementById("edit-radius").value = "0";
|
||||||
|
document.getElementById("edit-turns").value = "0";
|
||||||
|
document.getElementById("edit-resistance").value = "0";
|
||||||
|
document.getElementById("edit-material").value = "";
|
||||||
|
document.getElementById("edit-remark").value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById("edit-modal").style.display = "none";
|
||||||
|
editId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 保存 ────────────────────────────────────
|
||||||
|
|
||||||
|
async function saveRecord() {
|
||||||
|
const data = {
|
||||||
|
coil_num: document.getElementById("edit-coil-num").value.trim(),
|
||||||
|
name: document.getElementById("edit-name").value.trim(),
|
||||||
|
induct: parseFloat(document.getElementById("edit-induct").value) || 0,
|
||||||
|
shape: document.getElementById("edit-shape").value,
|
||||||
|
length: parseFloat(document.getElementById("edit-length").value) || 0,
|
||||||
|
width: parseFloat(document.getElementById("edit-width").value) || 0,
|
||||||
|
radius: parseFloat(document.getElementById("edit-radius").value) || 0,
|
||||||
|
turns: parseInt(document.getElementById("edit-turns").value) || 0,
|
||||||
|
resistance: parseFloat(document.getElementById("edit-resistance").value) || 0,
|
||||||
|
material: document.getElementById("edit-material").value.trim(),
|
||||||
|
remark: document.getElementById("edit-remark").value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.coil_num && !data.name) {
|
||||||
|
toast("请输入线圈编号或名称", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resp;
|
||||||
|
if (editId) {
|
||||||
|
resp = await fetch(`/api/coil-info/${editId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resp = await fetch("/api/coil-info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok || resp.ok) {
|
||||||
|
toast(editId ? "更新成功" : "新增成功");
|
||||||
|
closeModal();
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("保存失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("保存失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 删除 ────────────────────────────────────
|
||||||
|
|
||||||
|
async function deleteRecord(id, label) {
|
||||||
|
if (!confirm(`确定要删除「${label}」吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/coil-info/${id}`, { method: "DELETE" });
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok) {
|
||||||
|
toast("删除成功");
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("删除失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("删除失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||||||
|
|
||||||
|
loadList();
|
||||||
@@ -8,24 +8,55 @@ async function loadDevices() {
|
|||||||
|
|
||||||
function renderTable(devices) {
|
function renderTable(devices) {
|
||||||
const tbody = document.querySelector("#device-table tbody");
|
const tbody = document.querySelector("#device-table tbody");
|
||||||
tbody.innerHTML = devices.map(d => `
|
tbody.innerHTML = devices.map(d => {
|
||||||
|
const stateLabel = getStateLabel(d.state);
|
||||||
|
const stateClass = getStateClass(d.state);
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${d.serial}</td>
|
<td>${d.serial}</td>
|
||||||
<td class="editable-name" onclick="editName(${d.id}, '${esc(d.name)}', this)">
|
<td class="editable-name" onclick="editName(${d.id}, '${esc(d.name)}', this)">
|
||||||
${d.name || '(点击编辑)'}
|
${d.name || '(点击编辑)'}
|
||||||
</td>
|
</td>
|
||||||
<td>${d.ip || '-'}</td>
|
<td>${d.ip || '-'}</td>
|
||||||
<td class="${d.state === 1 ? 'status-online' : 'status-offline'}">
|
<td class="${stateClass} status-cell" data-device-id="${d.id}">
|
||||||
${d.state === 1 ? '在线' : '离线'}
|
${stateLabel}
|
||||||
</td>
|
</td>
|
||||||
<td>${d.version || '-'}</td>
|
<td>${d.version || '-'}</td>
|
||||||
<td>${d.last_login || '-'}</td>
|
<td>${d.last_login || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
<button class="btn-test" onclick="location.href='/test/${d.id}'">测试</button>
|
||||||
<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>
|
${USER_ROLE === 'admin' || USER_ROLE === 'manager' ? `<button class="btn-config" onclick="location.href='/fixture/${d.id}'">配置</button>` : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>`;
|
||||||
`).join("");
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStateLabel(state) {
|
||||||
|
return {0: '离线', 1: '在线', 2: '通信不良'}[state] || '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStateClass(state) {
|
||||||
|
return {0: 'status-offline', 1: 'status-online', 2: 'status-poor'}[state] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步刷新所有设备的在线状态
|
||||||
|
async function refreshDeviceStatuses() {
|
||||||
|
const cells = document.querySelectorAll(".status-cell");
|
||||||
|
for (const cell of cells) {
|
||||||
|
const deviceId = cell.dataset.deviceId;
|
||||||
|
if (!deviceId) continue;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/devices/${deviceId}/status`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
cell.textContent = data.state_name;
|
||||||
|
cell.className = getStateClass(data.state) + " status-cell";
|
||||||
|
cell.dataset.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 网络错误静默跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(s) { return s.replace(/'/g, "\\'").replace(/"/g, """); }
|
function esc(s) { return s.replace(/'/g, "\\'").replace(/"/g, """); }
|
||||||
@@ -52,3 +83,5 @@ async function editName(id, currentName, td) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadDevices();
|
loadDevices();
|
||||||
|
// 每 5 秒异步刷新设备在线状态
|
||||||
|
setInterval(refreshDeviceStatuses, 5000);
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
// 工装配置页
|
// 工装配置页
|
||||||
|
|
||||||
|
// ─── 频率/峰峰值转换常量 ─────────────────────
|
||||||
|
// 协议: 工作频率 f(Hz) = 10 * X, X 为 DB/设备中存储和传输的原始值
|
||||||
|
// 协议: 峰峰值 V = ((X * 3.3) / 4095) * 4, X 为 DB/设备中存储和传输的原始值(正整数)
|
||||||
|
const FREQ_SCALE = 10;
|
||||||
|
const PEAK_SCALE = 4095 / (4 * 3.3); // ≈ 310.227
|
||||||
|
|
||||||
|
function rawFreqToHz(x) { return x * FREQ_SCALE; }
|
||||||
|
function hzToRawFreq(hz) { return Math.round(hz / FREQ_SCALE); }
|
||||||
|
function rawPeakToV(x) { return parseFloat(((x * 3.3) / 4095 * 4).toFixed(2)); }
|
||||||
|
function vToRawPeak(v) { return Math.round(v * PEAK_SCALE); }
|
||||||
|
|
||||||
let baseTests = []; // 所有车检器基准参数
|
let baseTests = []; // 所有车检器基准参数
|
||||||
let selectedBaseTest = null;
|
let selectedBaseTest = null;
|
||||||
let pollTimer4C = null; // 0x4C 参数查询轮询
|
let pollTimer4C = null; // 0x4C 参数查询轮询
|
||||||
@@ -9,6 +20,8 @@ let pollTimers = {}; // record_id → timer (指令响应轮询)
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await loadBaseTests();
|
await loadBaseTests();
|
||||||
|
await loadCoilList();
|
||||||
|
await loadCarList();
|
||||||
await loadFixtureParam();
|
await loadFixtureParam();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +93,8 @@ function renderBaseTestTable() {
|
|||||||
<td>${t.type_num}</td>
|
<td>${t.type_num}</td>
|
||||||
<td>${esc(t.dev_name)}</td>
|
<td>${esc(t.dev_name)}</td>
|
||||||
<td>${t.SensMin}~${t.SensMax}</td>
|
<td>${t.SensMin}~${t.SensMax}</td>
|
||||||
<td>${t.FreMin}~${t.FreMax}</td>
|
<td>${rawFreqToHz(t.FreMin)}~${rawFreqToHz(t.FreMax)}</td>
|
||||||
<td>${t.PeakMin}~${t.PeakMax}</td>
|
<td>${rawPeakToV(t.PeakMin)}~${rawPeakToV(t.PeakMax)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
@@ -106,10 +119,10 @@ function fillFromBaseTest(t) {
|
|||||||
document.getElementById("param-dev-type").value = t.type_num;
|
document.getElementById("param-dev-type").value = t.type_num;
|
||||||
document.getElementById("param-sens-min").value = t.SensMin;
|
document.getElementById("param-sens-min").value = t.SensMin;
|
||||||
document.getElementById("param-sens-max").value = t.SensMax;
|
document.getElementById("param-sens-max").value = t.SensMax;
|
||||||
document.getElementById("param-fre-min").value = t.FreMin;
|
document.getElementById("param-fre-min").value = rawFreqToHz(t.FreMin);
|
||||||
document.getElementById("param-fre-max").value = t.FreMax;
|
document.getElementById("param-fre-max").value = rawFreqToHz(t.FreMax);
|
||||||
document.getElementById("param-peak-min").value = t.PeakMin;
|
document.getElementById("param-peak-min").value = rawPeakToV(t.PeakMin);
|
||||||
document.getElementById("param-peak-max").value = t.PeakMax;
|
document.getElementById("param-peak-max").value = rawPeakToV(t.PeakMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDevTypeChange() {
|
function onDevTypeChange() {
|
||||||
@@ -119,11 +132,71 @@ function onDevTypeChange() {
|
|||||||
else { selectedBaseTest = null; renderBaseTestTable(); }
|
else { selectedBaseTest = null; renderBaseTestTable(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 线圈列表 ────────────────────────────────
|
||||||
|
|
||||||
|
let coilList = [];
|
||||||
|
|
||||||
|
async function loadCoilList() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/coil-info");
|
||||||
|
coilList = await resp.json();
|
||||||
|
populateCoilSelect();
|
||||||
|
} catch (e) { console.error("加载线圈列表失败:", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCoilSelect() {
|
||||||
|
const sel = document.getElementById("coil-select");
|
||||||
|
sel.innerHTML = '<option value="">-- 选择线圈 --</option>' +
|
||||||
|
coilList.map(c => `<option value="${c.id}">${esc(c.coil_num || c.name || `#${c.id}`)}</option>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCoilChange() {
|
||||||
|
const id = parseInt(document.getElementById("coil-select").value);
|
||||||
|
const coil = coilList.find(c => c.id === id);
|
||||||
|
const detail = document.getElementById("coil-detail");
|
||||||
|
if (coil) {
|
||||||
|
const sizeText = coil.shape === '圆形' ? `半径${coil.radius}cm` : `${coil.length}×${coil.width}cm`;
|
||||||
|
detail.innerHTML = `${coil.shape || '-'} / ${sizeText} / ${coil.turns || 0}圈 / ${coil.resistance || 0}Ω / ${coil.material || ''}`;
|
||||||
|
} else {
|
||||||
|
detail.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 模拟车辆列表 ────────────────────────────
|
||||||
|
|
||||||
|
let carList = [];
|
||||||
|
|
||||||
|
async function loadCarList() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/simulate-car");
|
||||||
|
carList = await resp.json();
|
||||||
|
populateCarSelect();
|
||||||
|
} catch (e) { console.error("加载模拟车辆列表失败:", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCarSelect() {
|
||||||
|
const sel = document.getElementById("car-select");
|
||||||
|
sel.innerHTML = '<option value="">-- 选择模拟车辆 --</option>' +
|
||||||
|
carList.map(c => `<option value="${c.id}">${esc(c.simulate_num || c.name || `#${c.id}`)}</option>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCarChange() {
|
||||||
|
const id = parseInt(document.getElementById("car-select").value);
|
||||||
|
const car = carList.find(c => c.id === id);
|
||||||
|
const detail = document.getElementById("car-detail");
|
||||||
|
if (car) {
|
||||||
|
const sizeText = car.shape === '圆形' ? `半径${car.radius}cm` : `${car.length}×${car.width}cm`;
|
||||||
|
detail.innerHTML = `${car.shape || '-'} / ${sizeText} / ${car.material || ''}`;
|
||||||
|
} else {
|
||||||
|
detail.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 从 DB 加载/刷新/保存 ────────────────────
|
// ─── 从 DB 加载/刷新/保存 ────────────────────
|
||||||
|
|
||||||
async function loadFixtureParam() {
|
async function loadFixtureParam() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/fixture/param/${DNT_ID}`);
|
const resp = await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`);
|
||||||
const param = await resp.json();
|
const param = await resp.json();
|
||||||
if (param && param.dnt_id) {
|
if (param && param.dnt_id) {
|
||||||
fillFormFromParam(param);
|
fillFormFromParam(param);
|
||||||
@@ -135,21 +208,38 @@ async function loadFixtureParam() {
|
|||||||
function fillFormFromParam(param) {
|
function fillFormFromParam(param) {
|
||||||
document.getElementById("param-addr").value = param.Addr || 1;
|
document.getElementById("param-addr").value = param.Addr || 1;
|
||||||
document.getElementById("param-test-mode").value = param.TestMode || 0;
|
document.getElementById("param-test-mode").value = param.TestMode || 0;
|
||||||
document.getElementById("param-reset-dis").value = param.RestDis || 0;
|
document.getElementById("param-reset-dis").value = (param.RestDis || 0) * 10;
|
||||||
document.getElementById("param-minus-dis").value = param.MinusDis || 0;
|
document.getElementById("param-minus-dis").value = (param.MinusDis || 0) * 10;
|
||||||
document.getElementById("param-dev-type").value = param.DevType || 0;
|
document.getElementById("param-dev-type").value = param.DevType || 0;
|
||||||
document.getElementById("param-sens-min").value = param.SensMin || 0;
|
document.getElementById("param-sens-min").value = param.SensMin || 0;
|
||||||
document.getElementById("param-sens-max").value = param.SensMax || 0;
|
document.getElementById("param-sens-max").value = param.SensMax || 0;
|
||||||
document.getElementById("param-fre-min").value = param.FreMin || 0;
|
document.getElementById("param-fre-min").value = rawFreqToHz(param.FreMin || 0);
|
||||||
document.getElementById("param-fre-max").value = param.FreMax || 0;
|
document.getElementById("param-fre-max").value = rawFreqToHz(param.FreMax || 0);
|
||||||
document.getElementById("param-peak-min").value = param.PeakMin || 0;
|
document.getElementById("param-peak-min").value = rawPeakToV(param.PeakMin || 0);
|
||||||
document.getElementById("param-peak-max").value = param.PeakMax || 0;
|
document.getElementById("param-peak-max").value = rawPeakToV(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);
|
const matched = baseTests.find(t => t.type_num === param.DevType);
|
||||||
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
|
if (matched) { selectedBaseTest = matched; renderBaseTestTable(); }
|
||||||
|
// 设置线圈和模拟车辆选中
|
||||||
|
if (param.coil_id) {
|
||||||
|
document.getElementById("coil-select").value = param.coil_id;
|
||||||
|
onCoilChange();
|
||||||
|
}
|
||||||
|
if (param.simulate_car_id) {
|
||||||
|
document.getElementById("car-select").value = param.simulate_car_id;
|
||||||
|
onCarChange();
|
||||||
|
}
|
||||||
|
// 更新当前关联标签
|
||||||
|
updateCurrentLabels(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshParams() {
|
async function refreshParams() {
|
||||||
const param = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json();
|
const param = await (await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`)).json();
|
||||||
if (param && param.dnt_id) {
|
if (param && param.dnt_id) {
|
||||||
fillFormFromParam(param);
|
fillFormFromParam(param);
|
||||||
commLog('info', null, '已刷新:从数据库加载工装参数');
|
commLog('info', null, '已刷新:从数据库加载工装参数');
|
||||||
@@ -162,41 +252,74 @@ async function refreshParams() {
|
|||||||
|
|
||||||
async function saveToDb() {
|
async function saveToDb() {
|
||||||
const data = getFormParams();
|
const data = getFormParams();
|
||||||
|
const coilId = parseInt(document.getElementById("coil-select").value) || null;
|
||||||
|
const carId = parseInt(document.getElementById("car-select").value) || null;
|
||||||
|
const body = {
|
||||||
|
Addr: data.addr, DevType: data.dev_type, TestMode: data.test_mode,
|
||||||
|
RestDis: data.reset_dis, MinusDis: data.minus_dis,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
if (coilId) body.coil_id = coilId;
|
||||||
|
if (carId) body.simulate_car_id = carId;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/fixture/param/${DNT_ID}`, {
|
const resp = await fetch(`/api/fixture/param/${DNT_ID}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
Addr: data.addr, DevType: data.dev_type, TestMode: data.test_mode,
|
|
||||||
RestDis: data.reset_dis, MinusDis: data.minus_dis,
|
|
||||||
SensMin: data.sens_min, SensMax: data.sens_max,
|
|
||||||
FreMin: data.fre_min, FreMax: data.fre_max,
|
|
||||||
PeakMin: data.peak_min, PeakMax: data.peak_max,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const result = await resp.json();
|
const result = await resp.json();
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
commLog('info', null, '参数已保存到数据库');
|
commLog('info', null, '参数已保存到数据库');
|
||||||
toast("已保存到数据库");
|
toast("已保存到数据库");
|
||||||
|
updateCurrentLabels();
|
||||||
} else {
|
} else {
|
||||||
toast("保存失败: " + (result.error || ""), true);
|
toast("保存失败: " + (result.error || ""), true);
|
||||||
}
|
}
|
||||||
} catch (e) { toast("保存失败: " + e.message, true); }
|
} catch (e) { toast("保存失败: " + e.message, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新页面上的当前线圈/车辆标签 */
|
||||||
|
function updateCurrentLabels(param) {
|
||||||
|
const coilLabel = document.getElementById("current-coil-label");
|
||||||
|
const carLabel = document.getElementById("current-car-label");
|
||||||
|
if (param) {
|
||||||
|
coilLabel.textContent = param.coil_name || param.coil_num || '未设置';
|
||||||
|
carLabel.textContent = param.car_name || param.simulate_num || '未设置';
|
||||||
|
} else {
|
||||||
|
// 从 DOM 读取当前选择
|
||||||
|
const coilId = parseInt(document.getElementById("coil-select").value);
|
||||||
|
const carId = parseInt(document.getElementById("car-select").value);
|
||||||
|
const coil = coilId ? coilList.find(c => c.id === coilId) : null;
|
||||||
|
const car = carId ? carList.find(c => c.id === carId) : null;
|
||||||
|
coilLabel.textContent = coil ? (coil.coil_num || coil.name) : '未设置';
|
||||||
|
carLabel.textContent = car ? (car.simulate_num || car.name) : '未设置';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getFormParams() {
|
function getFormParams() {
|
||||||
return {
|
return {
|
||||||
addr: parseInt(document.getElementById("param-addr").value) || 1,
|
addr: parseInt(document.getElementById("param-addr").value) || 1,
|
||||||
dev_type: parseInt(document.getElementById("param-dev-type").value) || 0,
|
dev_type: parseInt(document.getElementById("param-dev-type").value) || 0,
|
||||||
test_mode: parseInt(document.getElementById("param-test-mode").value) || 0,
|
test_mode: parseInt(document.getElementById("param-test-mode").value) || 0,
|
||||||
reset_dis: parseInt(document.getElementById("param-reset-dis").value) || 0,
|
reset_dis: Math.round((parseInt(document.getElementById("param-reset-dis").value) || 0) / 10),
|
||||||
minus_dis: parseInt(document.getElementById("param-minus-dis").value) || 0,
|
minus_dis: Math.round((parseInt(document.getElementById("param-minus-dis").value) || 0) / 10),
|
||||||
sens_min: parseInt(document.getElementById("param-sens-min").value) || 0,
|
sens_min: parseInt(document.getElementById("param-sens-min").value) || 0,
|
||||||
sens_max: parseInt(document.getElementById("param-sens-max").value) || 0,
|
sens_max: parseInt(document.getElementById("param-sens-max").value) || 0,
|
||||||
fre_min: parseInt(document.getElementById("param-fre-min").value) || 0,
|
fre_min: hzToRawFreq(parseFloat(document.getElementById("param-fre-min").value) || 0),
|
||||||
fre_max: parseInt(document.getElementById("param-fre-max").value) || 0,
|
fre_max: hzToRawFreq(parseFloat(document.getElementById("param-fre-max").value) || 0),
|
||||||
peak_min: parseInt(document.getElementById("param-peak-min").value) || 0,
|
peak_min: vToRawPeak(parseFloat(document.getElementById("param-peak-min").value) || 0),
|
||||||
peak_max: parseInt(document.getElementById("param-peak-max").value) || 0,
|
peak_max: vToRawPeak(parseFloat(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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +360,7 @@ function startRespPolling(recordId, cmd) {
|
|||||||
if (cmd === '4C') {
|
if (cmd === '4C') {
|
||||||
// 参数已在后端 upsert,直接从 DB 加载
|
// 参数已在后端 upsert,直接从 DB 加载
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const p = await (await fetch(`/api/fixture/param/${DNT_ID}`)).json();
|
const p = await (await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`)).json();
|
||||||
if (p && p.dnt_id) fillFormFromParam(p);
|
if (p && p.dnt_id) fillFormFromParam(p);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@@ -309,16 +432,24 @@ async function sendConfig() {
|
|||||||
startRespPolling(data.record_id, "4B");
|
startRespPolling(data.record_id, "4B");
|
||||||
|
|
||||||
// 同时保存到数据库
|
// 同时保存到数据库
|
||||||
|
const coilId = parseInt(document.getElementById("coil-select").value) || null;
|
||||||
|
const carId = parseInt(document.getElementById("car-select").value) || null;
|
||||||
|
const saveBody = {
|
||||||
|
Addr: params.addr, DevType: params.dev_type, TestMode: params.test_mode,
|
||||||
|
RestDis: params.reset_dis, MinusDis: params.minus_dis,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
if (coilId) saveBody.coil_id = coilId;
|
||||||
|
if (carId) saveBody.simulate_car_id = carId;
|
||||||
await fetch(`/api/fixture/param/${DNT_ID}`, {
|
await fetch(`/api/fixture/param/${DNT_ID}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(saveBody),
|
||||||
Addr: params.addr, DevType: params.dev_type, TestMode: params.test_mode,
|
|
||||||
RestDis: params.reset_dis, MinusDis: params.minus_dis,
|
|
||||||
SensMin: params.sens_min, SensMax: params.sens_max,
|
|
||||||
FreMin: params.fre_min, FreMax: params.fre_max,
|
|
||||||
PeakMin: params.peak_min, PeakMax: params.peak_max,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast(`失败: ${data.error}`, true);
|
toast(`失败: ${data.error}`, true);
|
||||||
|
|||||||
151
edc-web/app/static/js/simulate_car.js
Normal file
151
edc-web/app/static/js/simulate_car.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// 模拟车辆参数管理
|
||||||
|
|
||||||
|
let editId = null;
|
||||||
|
|
||||||
|
function toast(msg, isError = false) {
|
||||||
|
const el = document.getElementById("toast");
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = "msg-toast " + (isError ? "error" : "") + " show";
|
||||||
|
clearTimeout(el._timeout);
|
||||||
|
el._timeout = setTimeout(() => { el.className = "msg-toast"; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const search = document.getElementById("search-input").value;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/simulate-car?search=${encodeURIComponent(search)}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
renderTable(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeLabel(item) {
|
||||||
|
if (item.shape === '圆形') return `半径${item.radius || 0}cm`;
|
||||||
|
if (item.shape === '矩形') return `${item.length || 0}×${item.width || 0}cm`;
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.querySelector("#car-table tbody");
|
||||||
|
if (!data.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7" style="color:#999;text-align:center;">暂无数据,点右上角「新增」添加</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = data.map(t => `
|
||||||
|
<tr>
|
||||||
|
<td>${esc(t.simulate_num)}</td>
|
||||||
|
<td>${esc(t.name)}</td>
|
||||||
|
<td>${t.shape || '-'}</td>
|
||||||
|
<td>${sizeLabel(t)}</td>
|
||||||
|
<td>${esc(t.material || '-')}</td>
|
||||||
|
<td>${esc(t.remark || '-')}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
|
||||||
|
<button class="btn-del" onclick="deleteRecord(${t.id}, '${esc(t.simulate_num || t.name)}')">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(id = null) {
|
||||||
|
editId = id;
|
||||||
|
document.getElementById("modal-title").textContent = id ? "编辑模拟车辆参数" : "新增模拟车辆参数";
|
||||||
|
document.getElementById("edit-modal").style.display = "flex";
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
fetch(`/api/simulate-car/${id}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById("edit-simulate-num").value = data.simulate_num || "";
|
||||||
|
document.getElementById("edit-name").value = data.name || "";
|
||||||
|
document.getElementById("edit-shape").value = data.shape || "";
|
||||||
|
document.getElementById("edit-length").value = data.length || 0;
|
||||||
|
document.getElementById("edit-width").value = data.width || 0;
|
||||||
|
document.getElementById("edit-radius").value = data.radius || 0;
|
||||||
|
document.getElementById("edit-material").value = data.material || "";
|
||||||
|
document.getElementById("edit-remark").value = data.remark || "";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementById("edit-simulate-num").value = "";
|
||||||
|
document.getElementById("edit-name").value = "";
|
||||||
|
document.getElementById("edit-shape").value = "";
|
||||||
|
document.getElementById("edit-length").value = "0";
|
||||||
|
document.getElementById("edit-width").value = "0";
|
||||||
|
document.getElementById("edit-radius").value = "0";
|
||||||
|
document.getElementById("edit-material").value = "";
|
||||||
|
document.getElementById("edit-remark").value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById("edit-modal").style.display = "none";
|
||||||
|
editId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRecord() {
|
||||||
|
const data = {
|
||||||
|
simulate_num: document.getElementById("edit-simulate-num").value.trim(),
|
||||||
|
name: document.getElementById("edit-name").value.trim(),
|
||||||
|
shape: document.getElementById("edit-shape").value,
|
||||||
|
length: parseFloat(document.getElementById("edit-length").value) || 0,
|
||||||
|
width: parseFloat(document.getElementById("edit-width").value) || 0,
|
||||||
|
radius: parseFloat(document.getElementById("edit-radius").value) || 0,
|
||||||
|
material: document.getElementById("edit-material").value.trim(),
|
||||||
|
remark: document.getElementById("edit-remark").value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.simulate_num && !data.name) {
|
||||||
|
toast("请输入模拟编号或名称", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resp;
|
||||||
|
if (editId) {
|
||||||
|
resp = await fetch(`/api/simulate-car/${editId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resp = await fetch("/api/simulate-car", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok || resp.ok) {
|
||||||
|
toast(editId ? "更新成功" : "新增成功");
|
||||||
|
closeModal();
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("保存失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("保存失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRecord(id, label) {
|
||||||
|
if (!confirm(`确定要删除「${label}」吗?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/simulate-car/${id}`, { method: "DELETE" });
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.ok) {
|
||||||
|
toast("删除成功");
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
toast("删除失败: " + (result.error || "未知错误"), true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast("删除失败: " + e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
||||||
|
|
||||||
|
loadList();
|
||||||
@@ -1,23 +1,212 @@
|
|||||||
// 测试信息页
|
// 测试信息页 — 三视图 (全部 / B2 / B4)
|
||||||
|
|
||||||
|
// ─── 型号名称缓存 ─────────────────────────────────
|
||||||
|
let devTypeNameCache = {};
|
||||||
|
|
||||||
|
async function initDevTypeNames() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/vehicle-base-test');
|
||||||
|
const tests = await resp.json();
|
||||||
|
devTypeNameCache = {};
|
||||||
|
tests.forEach(t => {
|
||||||
|
if (t.type_num != null && t.dev_name) {
|
||||||
|
devTypeNameCache[t.type_num] = t.dev_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) { console.error('加载型号名称失败:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDevTypeName(subType) {
|
||||||
|
if (subType == null || subType === 0) return '-';
|
||||||
|
return devTypeNameCache[subType] || `Unknown(${subType})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 视图定义 ───────────────────────────────────
|
||||||
|
|
||||||
|
const VIEWS = {
|
||||||
|
all: {
|
||||||
|
label: '全部数据',
|
||||||
|
data_source: '', // '' = 不过滤
|
||||||
|
cols: [
|
||||||
|
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||||
|
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
|
||||||
|
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
|
||||||
|
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
|
||||||
|
{ key: 'data_source', title: '来源' },
|
||||||
|
{ key: 'iffinish', title: '完成', render: r => r.data_source === 'B4' ? '-' : (r.iffinish === '1' ? '是' : '否') },
|
||||||
|
{ key: 'fault_info', title: '故障信息', render: r => r.data_source === 'B4' ? '-' : `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
|
||||||
|
{ key: 'relay_out', title: '继电器', render: r => fmtRelay(r.relay_out) },
|
||||||
|
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.data_source === 'B4' ? '-' : (r.ppvalue != null ? r.ppvalue.toFixed(2) : '-') },
|
||||||
|
{ key: 'idle_freq', title: '开始频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.idle_freq || '-') },
|
||||||
|
{ key: 'enter_freq', title: '触发频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.enter_freq || '-') },
|
||||||
|
{ key: 'exit_freq', title: '释放频率(Hz)', render: r => r.data_source === 'B4' ? '-' : (r.exit_freq || '-') },
|
||||||
|
{ key: 'enter_dist', title: '触发距离(mm)', render: r => {
|
||||||
|
const v = r.data_source === 'B4' ? r.b4_enter_dist : r.enter_dist;
|
||||||
|
return v != null ? v + ' ' : '-';
|
||||||
|
}},
|
||||||
|
{ key: 'exit_dist', title: '释放距离(mm)', render: r => {
|
||||||
|
const v = r.data_source === 'B4' ? r.b4_leave_dist : r.exit_dist;
|
||||||
|
return v != null ? v + ' ' : '-';
|
||||||
|
}},
|
||||||
|
{ key: 'enter_speed', title: '触发速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.enter_speed) },
|
||||||
|
{ key: 'exit_speed', title: '释放速度(dm/s)', render: r => r.data_source === 'B4' ? '-' : toSpeed(r.exit_speed) },
|
||||||
|
{ key: 'remain_count', title: '剩余次数', render: r => r.data_source === 'B2' ? '-' : (r.remain_count ?? '-') },
|
||||||
|
{ key: 'work_freq', title: '工作频率(Hz)', render: r => r.data_source === 'B2' ? '-' : (r.work_freq ?? '-') },
|
||||||
|
{ key: 'curr_dist', title: '当前距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.curr_dist != null ? r.curr_dist + ' ' : '-') },
|
||||||
|
{ key: 'speed', title: '速度(dm/s)', render: r => r.data_source === 'B2' ? '-' : (r.speed ?? '-') },
|
||||||
|
{ key: 'near_dist', title: '最近距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.near_dist != null ? r.near_dist + ' ' : '-') },
|
||||||
|
{ key: 'far_dist', title: '最远距离(mm)', render: r => r.data_source === 'B2' ? '-' : (r.far_dist != null ? r.far_dist + ' ' : '-') },
|
||||||
|
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
|
||||||
|
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
b2: {
|
||||||
|
label: '灵敏度测试',
|
||||||
|
data_source: 'B2',
|
||||||
|
cols: [
|
||||||
|
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||||
|
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
|
||||||
|
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
|
||||||
|
{ key: 'model', title: '型号', render: r => getDevTypeName(r.sub_type) },
|
||||||
|
{ key: 'iffinish', title: '完成', render: r => r.iffinish === '1' ? '是' : '否' },
|
||||||
|
{ key: 'fault_info', title: '故障信息', render: r => `<span style="display:inline-block;max-width:12em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.fault_info || '')}">${escHtml(r.fault_info || '-')}</span>` },
|
||||||
|
{ key: 'relay_out', title: '继电器', render: r => fmtRelay(r.relay_out) },
|
||||||
|
{ key: 'ppvalue', title: '峰峰值(V)', render: r => r.ppvalue?.toFixed(2) || '-' },
|
||||||
|
{ key: 'idle_freq', title: '开始频率(Hz)' },
|
||||||
|
{ key: 'enter_freq', title: '触发频率(Hz)' },
|
||||||
|
{ key: 'exit_freq', title: '释放频率(Hz)' },
|
||||||
|
{ key: 'enter_dist', title: '触发距离(mm)' },
|
||||||
|
{ key: 'exit_dist', title: '释放距离(mm)' },
|
||||||
|
{ key: 'enter_speed', title: '触发速度', render: r => toSpeed(r.enter_speed) },
|
||||||
|
{ key: 'exit_speed', title: '释放速度', render: r => toSpeed(r.exit_speed) },
|
||||||
|
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
|
||||||
|
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
b4: {
|
||||||
|
label: '波动测试',
|
||||||
|
data_source: 'B4',
|
||||||
|
cols: [
|
||||||
|
{ key: 'create_time', title: '时间', render: r => fmtTime(r.create_time) },
|
||||||
|
{ key: 'serial', title: '设备编码', render: r => (r.serial || '').slice(-6) },
|
||||||
|
{ key: 'detector_serial', title: '车检器序列号', render: r => r.detector_serial || '-' },
|
||||||
|
{ 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 => fmtRelay(r.relay_out) },
|
||||||
|
{ key: 'env', title: '测试环境', render: r => envLabel(r) },
|
||||||
|
{ key: 'test_mode', title: '测试模式', render: r => r.test_mode === 1 ? '波动' : '灵敏度' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 状态 ───────────────────────────────────────
|
||||||
|
|
||||||
|
let currentView = 'all';
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
|
||||||
function toSpeed(v) {
|
function toSpeed(v) {
|
||||||
if (v === null || v === undefined || v === '') return '-';
|
if (v === null || v === undefined || v === '') return '-';
|
||||||
return (parseFloat(v) / 10).toFixed(1);
|
return (parseFloat(v) / 10).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPage = 1;
|
function fmtTime(v) {
|
||||||
let totalPages = 1;
|
if (!v) return '-';
|
||||||
|
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀,但实际值是服务器本地时间(UTC+8)
|
||||||
|
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
|
||||||
|
const cleaned = String(v).replace(/ GMT$/, '');
|
||||||
|
const d = new Date(cleaned);
|
||||||
|
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 escHtml(s) {
|
||||||
|
return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建测试环境标签 (线圈 + 模拟车辆) */
|
||||||
|
function envLabel(r) {
|
||||||
|
const parts = [];
|
||||||
|
if (r.coil_num || r.coil_name) {
|
||||||
|
parts.push('🧵' + (r.coil_num || r.coil_name));
|
||||||
|
}
|
||||||
|
if (r.simulate_num || r.car_name) {
|
||||||
|
parts.push('🚗' + (r.simulate_num || r.car_name));
|
||||||
|
}
|
||||||
|
return parts.join(' ') || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtRelay(s) {
|
||||||
|
if (!s) return '-';
|
||||||
|
return s
|
||||||
|
.replace(/继电器有输出/g, '<span style="color:#22c55e;font-weight:600">✅有输出</span>')
|
||||||
|
.replace(/继电器无输出/g, '<span style="color:#ef4444;font-weight:600">❌无输出</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 视图切换 ────────────────────────────────────
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 查询 ────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 合并日期和时间输入框,返回 "YYYY-MM-DD" 或 "YYYY-MM-DD HH:MM:SS" 或 "" */
|
||||||
|
function getDatetime(dateId, timeId) {
|
||||||
|
const d = document.getElementById(dateId).value;
|
||||||
|
const t = document.getElementById(timeId).value;
|
||||||
|
if (!d) return "";
|
||||||
|
if (!t) return d;
|
||||||
|
return d + " " + t;
|
||||||
|
}
|
||||||
|
|
||||||
async function searchData(page = 1) {
|
async function searchData(page = 1) {
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
const serial = document.getElementById("search-serial").value;
|
const serial = document.getElementById("search-serial").value;
|
||||||
const dateFrom = document.getElementById("search-date-from").value;
|
const detectorSerial = document.getElementById("search-detector-serial").value;
|
||||||
const dateTo = document.getElementById("search-date-to").value;
|
const dateFrom = getDatetime("search-date-from", "search-time-from");
|
||||||
|
const dateTo = getDatetime("search-date-to", "search-time-to");
|
||||||
|
const v = VIEWS[currentView];
|
||||||
|
|
||||||
const params = new URLSearchParams({ page, per_page: 20 });
|
const perPage = parseInt(document.getElementById("per-page").value) || 100;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ page, per_page: perPage });
|
||||||
if (serial) params.set("serial", serial);
|
if (serial) params.set("serial", serial);
|
||||||
|
if (detectorSerial) params.set("detector_serial", detectorSerial);
|
||||||
if (dateFrom) params.set("date_from", dateFrom);
|
if (dateFrom) params.set("date_from", dateFrom);
|
||||||
if (dateTo) params.set("date_to", dateTo);
|
if (dateTo) params.set("date_to", dateTo);
|
||||||
|
// 按 data_source 过滤(全部不过滤)
|
||||||
|
if (v.data_source) {
|
||||||
|
params.set("data_source", v.data_source);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/test-data?${params}`);
|
const resp = await fetch(`/api/test-data?${params}`);
|
||||||
@@ -30,35 +219,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) {
|
function renderTable(records) {
|
||||||
|
renderHead();
|
||||||
const tbody = document.querySelector("#test-data-table tbody");
|
const tbody = document.querySelector("#test-data-table tbody");
|
||||||
|
const v = VIEWS[currentView];
|
||||||
|
const nCols = v.cols.length;
|
||||||
|
|
||||||
if (!records.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;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = records.map(r => `
|
tbody.innerHTML = records.map(r =>
|
||||||
<tr>
|
'<tr>' + v.cols.map(c => {
|
||||||
<td>${r.id}</td>
|
if (c.render) return `<td>${c.render(r)}</td>`;
|
||||||
<td>${r.serial || '-'}</td>
|
const val = r[c.key];
|
||||||
<td>${r.dpg430_addr}</td>
|
return `<td>${val !== null && val !== undefined && val !== '' ? val : '-'}</td>`;
|
||||||
<td>${r.sub_type === 1 ? 'PD132' : r.sub_type === 2 ? 'DLD110' : '-'}</td>
|
}).join('') + '</tr>'
|
||||||
<td>${r.str_type || '-'}</td>
|
).join("");
|
||||||
<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("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 分页 ────────────────────────────────────────
|
||||||
|
|
||||||
function renderPagination() {
|
function renderPagination() {
|
||||||
const div = document.getElementById("pagination");
|
const div = document.getElementById("pagination");
|
||||||
let html = "";
|
let html = "";
|
||||||
@@ -74,18 +267,266 @@ function renderPagination() {
|
|||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 导出 ────────────────────────────────────────
|
||||||
|
|
||||||
function exportCSV() {
|
function exportCSV() {
|
||||||
const serial = document.getElementById("search-serial").value;
|
const serial = document.getElementById("search-serial").value;
|
||||||
const dateFrom = document.getElementById("search-date-from").value;
|
const detectorSerial = document.getElementById("search-detector-serial").value;
|
||||||
const dateTo = document.getElementById("search-date-to").value;
|
const dateFrom = getDatetime("search-date-from", "search-time-from");
|
||||||
|
const dateTo = getDatetime("search-date-to", "search-time-to");
|
||||||
|
const v = VIEWS[currentView];
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (serial) params.set("serial", serial);
|
if (serial) params.set("serial", serial);
|
||||||
|
if (detectorSerial) params.set("detector_serial", detectorSerial);
|
||||||
if (dateFrom) params.set("date_from", dateFrom);
|
if (dateFrom) params.set("date_from", dateFrom);
|
||||||
if (dateTo) params.set("date_to", dateTo);
|
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}`;
|
window.location.href = `/api/test-data/export?${params}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始加载
|
// ─── 图表 ────────────────────────────────────────
|
||||||
searchData(1);
|
|
||||||
|
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 buildRelaySeries(records) {
|
||||||
|
return {
|
||||||
|
name: '继电器输出',
|
||||||
|
type: 'line',
|
||||||
|
step: 'end',
|
||||||
|
yAxisIndex: 3,
|
||||||
|
symbol: 'triangle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { type: 'dotted', width: 2, color: '#e74c3c' },
|
||||||
|
itemStyle: { color: '#e74c3c' },
|
||||||
|
data: records.map(r => r.relay_code ?? null),
|
||||||
|
// tooltip 中显示解码后的文本
|
||||||
|
tooltip: {
|
||||||
|
valueFormatter: function (value) {
|
||||||
|
return RELAY_MAP[value] || `未知(${value})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 detectorSerial = document.getElementById('search-detector-serial').value;
|
||||||
|
const dateFrom = getDatetime('search-date-from', 'search-time-from');
|
||||||
|
const dateTo = getDatetime('search-date-to', 'search-time-to');
|
||||||
|
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 (detectorSerial) params.set('detector_serial', detectorSerial);
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 添加继电器状态系列
|
||||||
|
series.push(buildRelaySeries(records));
|
||||||
|
|
||||||
|
// 渲染 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: 200, 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 },
|
||||||
|
{ type: 'value', name: '继电器', nameTextStyle: { fontSize: 11 },
|
||||||
|
min: -0.5, max: 3.5, interval: 1,
|
||||||
|
offset: 160,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: function (v) {
|
||||||
|
return RELAY_MAP[v] || '';
|
||||||
|
},
|
||||||
|
fontSize: 10,
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
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();
|
||||||
|
// 先加载型号名称再查询数据,确保型号列正确渲染
|
||||||
|
initDevTypeNames().then(() => searchData(1));
|
||||||
|
|
||||||
|
// ─── 删除(admin)─────────────────────────────────
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
const serial = document.getElementById('search-serial').value;
|
||||||
|
const dateFrom = getDatetime('search-date-from', 'search-time-from');
|
||||||
|
const dateTo = getDatetime('search-date-to', 'search-time-to');
|
||||||
|
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 autoRunning = false;
|
||||||
let autoTotal = 0;
|
let autoTotal = 0;
|
||||||
@@ -7,13 +7,35 @@ let autoFailed = 0;
|
|||||||
let autoRemaining = 0;
|
let autoRemaining = 0;
|
||||||
let autoStartTime = "";
|
let autoStartTime = "";
|
||||||
let localSinceStr = "";
|
let localSinceStr = "";
|
||||||
|
let currentTestMode = null; // 0=灵敏度, 1=波动, null=未加载
|
||||||
|
let currentDeviceState = null; // 当前设备状态 (0=离线 1=在线 2=通信不良 null=未加载)
|
||||||
|
let devTypeNameCache = {}; // type_num → dev_name 映射(从 tb_vechicle_base_test)
|
||||||
|
|
||||||
let pollInterval = null;
|
let pollInterval = null;
|
||||||
let timeoutTimers = {}; // record_id → timer
|
let nextCmdTimer = null; // 间隔等待定时器
|
||||||
const TIMEOUT_MS = 10000;
|
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
|
||||||
|
|
||||||
|
// ─── 设备在线检查 ──────────────────────────────
|
||||||
|
|
||||||
|
function checkDeviceOnline() {
|
||||||
|
if (currentDeviceState !== 1) {
|
||||||
|
const stateName = currentDeviceState === 2 ? '通信不良' :
|
||||||
|
currentDeviceState === 0 ? '离线' : '未知';
|
||||||
|
alert(`设备当前状态为「${stateName}」,无法发送指令`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 手动指令 ─────────────────────────────────
|
// ─── 手动指令 ─────────────────────────────────
|
||||||
|
|
||||||
async function sendCmd(cmd) {
|
async function sendCmd(cmd) {
|
||||||
|
if (!checkDeviceOnline()) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/command", {
|
const resp = await fetch("/api/command", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -40,17 +62,29 @@ async function toggleAuto() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startAuto() {
|
async function startAuto() {
|
||||||
const count = parseInt(document.getElementById("test-count").value) || 10;
|
if (!checkDeviceOnline()) return;
|
||||||
|
const count = parseInt(document.getElementById("test-count").value) || 1;
|
||||||
if (count < 1) return;
|
if (count < 1) return;
|
||||||
|
|
||||||
|
// 读取车检器序列号
|
||||||
|
const detectorSerial = document.getElementById("detector-serial").value.trim();
|
||||||
|
|
||||||
|
// 读取参数
|
||||||
|
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;
|
autoRunning = true;
|
||||||
autoTotal = count;
|
autoTotal = count;
|
||||||
autoDone = 0;
|
autoDone = 0;
|
||||||
autoFailed = 0;
|
autoFailed = 0;
|
||||||
autoRemaining = count;
|
autoRemaining = count;
|
||||||
autoStartTime = new Date().toISOString(); // 记录开始时间 (UTC)
|
lastDoneCount = 0;
|
||||||
// MySQL 存的是本地时间,需要转本地格式传给后端过滤
|
newB2Count = 0;
|
||||||
|
updateRecordCount();
|
||||||
|
autoStartTime = new Date().toISOString();
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
localSinceStr = now.getFullYear() + "-" +
|
localSinceStr = now.getFullYear() + "-" +
|
||||||
String(now.getMonth() + 1).padStart(2, "0") + "-" +
|
String(now.getMonth() + 1).padStart(2, "0") + "-" +
|
||||||
@@ -67,13 +101,14 @@ async function startAuto() {
|
|||||||
// 清空显示
|
// 清空显示
|
||||||
resetAverages();
|
resetAverages();
|
||||||
document.getElementById("latest-result").innerHTML = '<p class="placeholder">等待测试...</p>';
|
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-bar").style.width = "0%";
|
||||||
document.getElementById("progress-text").textContent = "0/" + count + " (0 失败)";
|
document.getElementById("progress-text").textContent = "0/" + count + " (0 失败)";
|
||||||
document.getElementById("stat-done").textContent = "0";
|
document.getElementById("stat-done").textContent = "0";
|
||||||
document.getElementById("stat-failed").textContent = "0";
|
document.getElementById("stat-failed").textContent = "0";
|
||||||
document.getElementById("stat-remaining").textContent = count;
|
document.getElementById("stat-remaining").textContent = count;
|
||||||
|
document.getElementById("auto-status").textContent = "";
|
||||||
|
|
||||||
// 清空测试明细
|
|
||||||
document.querySelector("#records-table tbody").innerHTML = "";
|
document.querySelector("#records-table tbody").innerHTML = "";
|
||||||
document.getElementById("records-empty").style.display = "block";
|
document.getElementById("records-empty").style.display = "block";
|
||||||
|
|
||||||
@@ -81,17 +116,19 @@ async function startAuto() {
|
|||||||
btn.textContent = "结束";
|
btn.textContent = "结束";
|
||||||
btn.className = "btn-stop";
|
btn.className = "btn-stop";
|
||||||
|
|
||||||
// 插入第一条 0xB0
|
// 清除旧记录 + 插入第一条
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/automation/start", {
|
const resp = await fetch("/api/automation/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ dnt_id: DNT_ID, count }),
|
body: JSON.stringify({ dnt_id: DNT_ID, count, detector_serial: detectorSerial }),
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
// 启动超时计时器
|
cmdSentAt = Date.now();
|
||||||
startTimeout(data.first_record_id);
|
timeoutAt = cmdSentAt + timeoutMs;
|
||||||
|
armTimeout();
|
||||||
|
setStatus(`已发送,超时 ${timeoutMs/1000}s...`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("启动失败:", e);
|
console.error("启动失败:", e);
|
||||||
@@ -99,26 +136,87 @@ async function startAuto() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动轮询
|
// 启动轮询
|
||||||
pollInterval = setInterval(pollProgress, 1000);
|
pollInterval = setInterval(pollProgress, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopAuto() {
|
function stopAuto() {
|
||||||
autoRunning = false;
|
autoRunning = false;
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
pollInterval = null;
|
pollInterval = null;
|
||||||
// 记录结束时间
|
clearTimeout(nextCmdTimer);
|
||||||
|
nextCmdTimer = null;
|
||||||
|
clearTimeout(timeoutTimer);
|
||||||
|
timeoutTimer = null;
|
||||||
|
document.getElementById("auto-status").textContent = "";
|
||||||
document.getElementById("time-end").textContent = new Date().toLocaleString();
|
document.getElementById("time-end").textContent = new Date().toLocaleString();
|
||||||
// 清除所有超时计时器
|
|
||||||
for (const id in timeoutTimers) {
|
|
||||||
clearTimeout(timeoutTimers[id]);
|
|
||||||
}
|
|
||||||
timeoutTimers = {};
|
|
||||||
|
|
||||||
const btn = document.getElementById("btn-auto");
|
const btn = document.getElementById("btn-auto");
|
||||||
btn.textContent = "开始";
|
btn.textContent = "开始";
|
||||||
btn.className = "btn-start";
|
btn.className = "btn-start";
|
||||||
|
|
||||||
|
// 自动化结束 → 焦点回到车检器序列号输入框(全选),方便下一个车检器测试
|
||||||
|
setTimeout(focusDetectorSerial, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 发送下一条 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() {
|
async function pollProgress() {
|
||||||
if (!autoRunning) return;
|
if (!autoRunning) return;
|
||||||
|
|
||||||
@@ -127,63 +225,295 @@ async function pollProgress() {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const stats = data.stats;
|
const stats = data.stats;
|
||||||
|
|
||||||
// 更新计数
|
// ── 先渲染数据(放在所有 return 之前,避免完成时跳过渲染)──
|
||||||
autoDone = stats.done || 0;
|
try { if (data.latest) {
|
||||||
autoFailed = stats.failed || 0;
|
if (data.latest.id !== lastLatestId) {
|
||||||
autoRemaining = autoTotal - autoDone - autoFailed;
|
lastLatestId = data.latest.id;
|
||||||
if (autoRemaining < 0) autoRemaining = 0;
|
if (data.latest.data_source === "B2") { newB2Count++; updateRecordCount(); }
|
||||||
|
}
|
||||||
updateUI();
|
|
||||||
|
|
||||||
// 显示最新结果
|
|
||||||
if (data.latest) {
|
|
||||||
renderLatest(data.latest);
|
renderLatest(data.latest);
|
||||||
}
|
} } catch (e) { console.error("renderLatest:", e); }
|
||||||
|
try { if (data.averages) renderAverages(data.averages); } catch (e) { console.error("renderAverages:", e); }
|
||||||
|
try { if (data.latest_wave) { renderLatestWave(data.latest_wave); lastWaveId = data.latest_wave.id; } } catch (e) { console.error("renderLatestWave:", e); }
|
||||||
|
try { if (data.records && data.records.length) renderRecords(data.records); } catch (e) { console.error("renderRecords:", e); }
|
||||||
|
|
||||||
// 显示平均值
|
// 更新计数
|
||||||
if (data.averages) {
|
const newDone = stats.done || 0;
|
||||||
renderAverages(data.averages);
|
const newFailed = stats.failed || 0;
|
||||||
}
|
|
||||||
|
|
||||||
// 显示本轮测试明细
|
if (newDone > lastDoneCount) {
|
||||||
if (data.records) {
|
// 收到新回复 → 清除超时,开始间隔等待
|
||||||
renderRecords(data.records);
|
const delta = newDone - lastDoneCount;
|
||||||
}
|
lastDoneCount = newDone;
|
||||||
|
autoDone = newDone;
|
||||||
|
autoFailed = newFailed;
|
||||||
|
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||||
|
if (autoRemaining < 0) autoRemaining = 0;
|
||||||
|
|
||||||
// 自动插入下一条 0xB0
|
clearTimeout(timeoutTimer);
|
||||||
if (autoRemaining > 0) {
|
timeoutTimer = null;
|
||||||
// 检查是否还有 pending 的记录,没有则插入新的
|
clearTimeout(nextCmdTimer);
|
||||||
if (stats.pending === 0 && stats.sent === 0) {
|
nextCmdTimer = null;
|
||||||
try {
|
|
||||||
const r = await fetch("/api/command", {
|
updateUI();
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
if (autoRemaining > 0) {
|
||||||
body: JSON.stringify({ dnt_id: DNT_ID, cmd: "B0" }),
|
const wait = (intervalMs / 1000).toFixed(0);
|
||||||
});
|
setStatus(`收到 ${delta} 条回复,等待 ${wait}s...`);
|
||||||
const rd = await r.json();
|
nextCmdTimer = setTimeout(() => {
|
||||||
if (rd.ok) {
|
setStatus("发送中...");
|
||||||
startTimeout(rd.record_id);
|
sendNextCmd();
|
||||||
}
|
}, intervalMs);
|
||||||
} catch (e) {
|
} else {
|
||||||
console.error("插入下一条失败:", e);
|
setStatus("全部完成");
|
||||||
}
|
stopAuto();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 全部完成
|
// 没有新回复,更新超时计数
|
||||||
stopAuto();
|
autoFailed = newFailed;
|
||||||
|
autoRemaining = autoTotal - autoDone - autoFailed;
|
||||||
|
if (autoRemaining < 0) autoRemaining = 0;
|
||||||
|
// 检查是否全部完成
|
||||||
|
if (autoRemaining <= 0 && autoDone + autoFailed >= autoTotal) {
|
||||||
|
stopAuto();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("轮询失败:", e);
|
console.error("轮询失败:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTimeout(recordId) {
|
// ─── 页面加载时获取初始数据 ──────────────────────
|
||||||
timeoutTimers[recordId] = setTimeout(async () => {
|
|
||||||
// 超时:检查 record 的状态,如果还是 1 → 视为失败
|
async function loadDeviceTypeNames() {
|
||||||
// 后端串行轮询会自动处理超时标记为 state=3
|
try {
|
||||||
// 前端稍后通过 pollProgress 更新计数
|
const resp = await fetch(`/api/vehicle-base-test`);
|
||||||
console.log(`记录 ${recordId} 可能已超时`);
|
const tests = await resp.json();
|
||||||
}, TIMEOUT_MS);
|
devTypeNameCache = {};
|
||||||
|
tests.forEach(t => {
|
||||||
|
if (t.type_num != null && t.dev_name) {
|
||||||
|
devTypeNameCache[t.type_num] = t.dev_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) { console.error("加载型号名称失败:", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTestMode() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/fixture/param/${DNT_ID}?_=${Date.now()}`);
|
||||||
|
const param = await resp.json();
|
||||||
|
if (param && param.dnt_id) {
|
||||||
|
updateTestModeUI(param.TestMode);
|
||||||
|
renderConfigOverview(param);
|
||||||
|
} else {
|
||||||
|
// 没有工装参数时,尝试从最新测试数据获取
|
||||||
|
const r2 = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||||||
|
const d2 = await r2.json();
|
||||||
|
if (d2.latest && d2.latest.test_mode !== undefined) {
|
||||||
|
updateTestModeUI(d2.latest.test_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* 静默 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTestModeUI(mode) {
|
||||||
|
currentTestMode = mode;
|
||||||
|
const waveSection = document.getElementById("wave-section");
|
||||||
|
if (mode === 1) {
|
||||||
|
waveSection.style.display = '';
|
||||||
|
} else {
|
||||||
|
waveSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConfigOverview(param) {
|
||||||
|
const panel = document.getElementById("config-overview");
|
||||||
|
if (!panel) return;
|
||||||
|
panel.style.display = '';
|
||||||
|
|
||||||
|
// 测试模式
|
||||||
|
const modeEl = document.getElementById("cfg-test-mode");
|
||||||
|
if (param.TestMode === 1) {
|
||||||
|
modeEl.innerHTML = '<strong style="color:#e67e22;">波动测试</strong>';
|
||||||
|
} else {
|
||||||
|
modeEl.innerHTML = '<strong style="color:#2980b9;">灵敏度测试</strong>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 型号
|
||||||
|
document.getElementById("cfg-dev-type").textContent =
|
||||||
|
devTypeNameCache[param.DevType] || `0x${(param.DevType || 0).toString(16)}`;
|
||||||
|
|
||||||
|
// 距离 (DB cm → 显示 mm)
|
||||||
|
document.getElementById("cfg-reset-dis").textContent = param.RestDis != null ? param.RestDis * 10 : '-';
|
||||||
|
document.getElementById("cfg-minus-dis").textContent = param.MinusDis != null ? param.MinusDis * 10 : '-';
|
||||||
|
|
||||||
|
// 触发和释放范围 (SensMin ~ SensMax)
|
||||||
|
document.getElementById("cfg-sens-range").textContent =
|
||||||
|
(param.SensMin != null && param.SensMax != null) ? `${param.SensMin} ~ ${param.SensMax}` : '-';
|
||||||
|
|
||||||
|
// 频率范围 (配置值 ×10 = 实际 Hz)
|
||||||
|
if (param.FreMin != null && param.FreMax != null) {
|
||||||
|
document.getElementById("cfg-fre-range").textContent = `${param.FreMin * 10} ~ ${param.FreMax * 10}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById("cfg-fre-range").textContent = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 线圈信息
|
||||||
|
const coil = [param.coil_num, param.coil_name].filter(Boolean).join(' ');
|
||||||
|
document.getElementById("cfg-coil").textContent = coil || '-';
|
||||||
|
|
||||||
|
// 模拟车辆信息
|
||||||
|
const car = [param.simulate_num, param.car_name].filter(Boolean).join(' ');
|
||||||
|
document.getElementById("cfg-car").textContent = car || '-';
|
||||||
|
|
||||||
|
// 波动参数
|
||||||
|
const waveParams = document.getElementById("cfg-wave-params");
|
||||||
|
if (param.TestMode === 1) {
|
||||||
|
waveParams.style.display = '';
|
||||||
|
document.getElementById("cfg-near-tol").textContent = param.NearTol ?? '-';
|
||||||
|
document.getElementById("cfg-far-tol").textContent = param.FarTol ?? '-';
|
||||||
|
document.getElementById("cfg-step-tol").textContent = param.StepTol ?? '-';
|
||||||
|
document.getElementById("cfg-back-forth").textContent = param.BackForth ?? '-';
|
||||||
|
document.getElementById("cfg-near-stay").textContent = param.NearStay ?? '-';
|
||||||
|
document.getElementById("cfg-far-stay").textContent = param.FarStay ?? '-';
|
||||||
|
} else {
|
||||||
|
waveParams.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConfig() {
|
||||||
|
const body = document.getElementById("config-body");
|
||||||
|
const toggle = document.getElementById("config-toggle");
|
||||||
|
if (body.style.display === 'none') {
|
||||||
|
body.style.display = '';
|
||||||
|
toggle.textContent = '收起 ▲';
|
||||||
|
} else {
|
||||||
|
body.style.display = 'none';
|
||||||
|
toggle.textContent = '展开 ▼';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInitialData() {
|
||||||
|
await loadDeviceTypeNames();
|
||||||
|
await loadTestMode();
|
||||||
|
refreshDeviceStatus();
|
||||||
|
newB2Count = 0;
|
||||||
|
updateRecordCount();
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.latest) { renderLatest(data.latest); lastLatestId = data.latest.id; }
|
||||||
|
if (data.latest_wave) { renderLatestWave(data.latest_wave); lastWaveId = data.latest_wave.id; }
|
||||||
|
} catch (e) {
|
||||||
|
// 初始加载静默失败
|
||||||
|
}
|
||||||
|
// 页面加载后焦点落到车检器序列号输入框(全选)
|
||||||
|
setTimeout(focusDetectorSerial, 200);
|
||||||
|
}
|
||||||
|
loadInitialData();
|
||||||
|
|
||||||
|
// ─── 回车键触发"开始"按钮 ─────────────────────
|
||||||
|
|
||||||
|
/** 将焦点移到车检器序列号输入框并全选已有文本 */
|
||||||
|
function focusDetectorSerial() {
|
||||||
|
const input = document.getElementById("detector-serial");
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const detectorInput = document.getElementById("detector-serial");
|
||||||
|
if (detectorInput) {
|
||||||
|
detectorInput.addEventListener("keydown", function(e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById("btn-auto");
|
||||||
|
if (btn && !autoRunning) {
|
||||||
|
btn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 设备状态异步刷新 ──────────────────────────
|
||||||
|
|
||||||
|
async function refreshDeviceStatus() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/devices/${DNT_ID}/status`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
currentDeviceState = data.state;
|
||||||
|
const el = document.getElementById("device-status-text");
|
||||||
|
if (el) {
|
||||||
|
el.textContent = data.state_name;
|
||||||
|
if (data.state === 1) {
|
||||||
|
el.className = "status-online";
|
||||||
|
} else if (data.state === 2) {
|
||||||
|
el.className = "status-poor";
|
||||||
|
} else {
|
||||||
|
el.className = "status-offline";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 被动轮询:实时显示上报数据(工装本地按键 / 网页手动指令触发)───
|
||||||
|
|
||||||
|
let lastLatestId = 0; // 最新测试数据 ID,用于判断是否有新数据
|
||||||
|
let lastWaveId = 0; // 最新波动数据 ID
|
||||||
|
let newB2Count = 0; // 本轮新收到的 B2(灵敏度测试) 记录条数
|
||||||
|
|
||||||
|
function updateRecordCount() {
|
||||||
|
const el = document.getElementById("new-record-count");
|
||||||
|
if (el) el.textContent = newB2Count > 0 ? `(${newB2Count} 条新记录)` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLatestData() {
|
||||||
|
// 自动化运行中由 pollProgress 负责渲染,避免冲突
|
||||||
|
if (autoRunning) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/automation/${DNT_ID}/progress`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.latest && data.latest.id !== lastLatestId) {
|
||||||
|
lastLatestId = data.latest.id;
|
||||||
|
renderLatest(data.latest);
|
||||||
|
// 仅 B2(灵敏度测试) 记录计数
|
||||||
|
if (data.latest.data_source === "B2") {
|
||||||
|
newB2Count++;
|
||||||
|
updateRecordCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.latest_wave && data.latest_wave.id !== lastWaveId) {
|
||||||
|
lastWaveId = data.latest_wave.id;
|
||||||
|
renderLatestWave(data.latest_wave);
|
||||||
|
}
|
||||||
|
} catch (e) { /* 静默失败 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最新测试数据每 3 秒轮询
|
||||||
|
setInterval(refreshLatestData, 3000);
|
||||||
|
|
||||||
|
// 每 5 秒刷新设备状态 + 测试模式 + 型号名称缓存(工装页修改后能及时同步)
|
||||||
|
async function refreshAll() {
|
||||||
|
await loadDeviceTypeNames();
|
||||||
|
await loadTestMode();
|
||||||
|
refreshDeviceStatus();
|
||||||
|
}
|
||||||
|
setInterval(refreshAll, 5000);
|
||||||
|
|
||||||
|
// ─── UI ────────────────────────────────────────
|
||||||
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
document.getElementById("auto-status").textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
@@ -203,23 +533,65 @@ function toSpeed(v) {
|
|||||||
return (parseFloat(v) / 10).toFixed(1);
|
return (parseFloat(v) / 10).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtTime(v) {
|
||||||
|
if (!v) return '-';
|
||||||
|
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀,但实际值是服务器本地时间(UTC+8)
|
||||||
|
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
|
||||||
|
const cleaned = String(v).replace(/ GMT$/, '');
|
||||||
|
const d = new Date(cleaned);
|
||||||
|
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 fmtRelay(s) {
|
||||||
|
if (!s) return '-';
|
||||||
|
return s
|
||||||
|
.replace(/继电器有输出/g, '<span style="color:#22c55e;font-weight:600">✅有输出</span>')
|
||||||
|
.replace(/继电器无输出/g, '<span style="color:#ef4444;font-weight:600">❌无输出</span>');
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 显示最新结果 ──────────────────────────────
|
// ─── 显示最新结果 ──────────────────────────────
|
||||||
|
|
||||||
function renderLatest(data) {
|
function renderLatest(data) {
|
||||||
const div = document.getElementById("latest-result");
|
const div = document.getElementById("latest-result");
|
||||||
|
// 优先使用 str_type,为空时从缓存查找
|
||||||
|
let typeName = data.str_type;
|
||||||
|
if (!typeName && data.sub_type != null) {
|
||||||
|
typeName = devTypeNameCache[data.sub_type] || `Unknown(${data.sub_type})`;
|
||||||
|
}
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<p>设备型号:<strong>${data.str_type || '-'}</strong></p>
|
<p>车检器序列号:<strong>${data.detector_serial || '-'}</strong></p>
|
||||||
|
<p>设备型号:<strong>${typeName || '-'}</strong></p>
|
||||||
|
<p>测试模式:<strong>${data.test_mode === 1 ? '波动测试' : '灵敏度测试'}</strong></p>
|
||||||
<p>峰峰值:${data.ppvalue?.toFixed(2) || '-'} V</p>
|
<p>峰峰值:${data.ppvalue?.toFixed(2) || '-'} V</p>
|
||||||
<p>开始工作频率:${data.idle_freq || '-'} Hz</p>
|
<p>开始频率:${data.idle_freq || '-'} Hz</p>
|
||||||
<p>进入工作频率:${data.enter_freq || '-'} Hz</p>
|
<p>触发频率:${data.enter_freq || '-'} Hz</p>
|
||||||
<p>离开工作频率:${data.exit_freq || '-'} Hz</p>
|
<p>释放频率:${data.exit_freq || '-'} Hz</p>
|
||||||
<p>进入距离:${data.enter_dist || '-'} mm</p>
|
<p>触发距离:${data.enter_dist || '-'} mm</p>
|
||||||
<p>离开距离:${data.exit_dist || '-'} mm</p>
|
<p>释放距离:${data.exit_dist || '-'} mm</p>
|
||||||
<p>进入速度:${toSpeed(data.enter_speed)} m/s</p>
|
<p>触发速度:${toSpeed(data.enter_speed)} m/s</p>
|
||||||
<p>离开速度:${toSpeed(data.exit_speed)} m/s</p>
|
<p>释放速度:${toSpeed(data.exit_speed)} m/s</p>
|
||||||
<p>是否完成:${data.iffinish === '1' ? '是' : '否'}</p>
|
<p>是否完成:${data.iffinish === '1' ? '是' : '否'}</p>
|
||||||
<p>故障信息:${data.fault_info || '无'}</p>
|
<p>故障信息:${data.fault_info || '无'}</p>
|
||||||
<p>时间:${data.create_time || '-'}</p>
|
<p>继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}</p>
|
||||||
|
<p>时间:${fmtTime(data.create_time)}</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +614,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>继电器:${fmtRelay(data.relay_out) || decodeRelay(data.relay_code)}</p>
|
||||||
|
<p>时间:${fmtTime(data.create_time)}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 显示本轮测试明细 ──────────────────────────
|
// ─── 显示本轮测试明细 ──────────────────────────
|
||||||
|
|
||||||
function renderRecords(records) {
|
function renderRecords(records) {
|
||||||
@@ -260,12 +654,13 @@ function renderRecords(records) {
|
|||||||
<td style="color:${r.sn_state === 2 ? '#27ae60' : r.sn_state === 3 ? '#e74c3c' : '#888'}">
|
<td style="color:${r.sn_state === 2 ? '#27ae60' : r.sn_state === 3 ? '#e74c3c' : '#888'}">
|
||||||
${r.sn_state === 2 ? 'OK' : r.sn_state === 3 ? '超时' : '?'}
|
${r.sn_state === 2 ? 'OK' : r.sn_state === 3 ? '超时' : '?'}
|
||||||
</td>
|
</td>
|
||||||
|
<td>${r.test_mode === 1 ? '波动' : '灵敏度'}</td>
|
||||||
<td>${r.ppvalue?.toFixed(2) || '-'}</td>
|
<td>${r.ppvalue?.toFixed(2) || '-'}</td>
|
||||||
<td>${r.idle_freq || '-'}</td>
|
<td>${r.idle_freq || '-'}</td>
|
||||||
<td>${r.enter_dist || '-'}</td>
|
<td>${r.enter_dist || '-'}</td>
|
||||||
<td>${r.exit_dist || '-'}</td>
|
<td>${r.exit_dist || '-'}</td>
|
||||||
<td>${toSpeed(r.enter_speed)}</td>
|
<td>${toSpeed(r.enter_speed)}</td>
|
||||||
<td>${r.create_time || '-'}</td>
|
<td>${fmtTime(r.create_time)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
// 车检器测试基准参数管理
|
// 车检器测试基准参数管理
|
||||||
|
|
||||||
|
// ─── 频率/峰峰值转换常量 ─────────────────────
|
||||||
|
// 协议: 工作频率 f(Hz) = 10 * X, X 为 DB/设备中存储和传输的原始值
|
||||||
|
// 协议: 峰峰值 V = ((X * 3.3) / 4095) * 4, X 为 DB/设备中存储和传输的原始值(正整数)
|
||||||
|
const FREQ_SCALE = 10;
|
||||||
|
const PEAK_SCALE = 4095 / (4 * 3.3); // ≈ 310.227
|
||||||
|
|
||||||
|
function rawFreqToHz(x) { return x * FREQ_SCALE; }
|
||||||
|
function hzToRawFreq(hz) { return Math.round(hz / FREQ_SCALE); }
|
||||||
|
function rawPeakToV(x) { return parseFloat(((x * 3.3) / 4095 * 4).toFixed(2)); }
|
||||||
|
function vToRawPeak(v) { return Math.round(v * PEAK_SCALE); }
|
||||||
|
|
||||||
let editId = null; // null=新增, number=编辑
|
let editId = null; // null=新增, number=编辑
|
||||||
|
|
||||||
// ─── Toast ───────────────────────────────────
|
// ─── Toast ───────────────────────────────────
|
||||||
@@ -36,8 +47,8 @@ function renderTable(data) {
|
|||||||
<td>${t.type_num}</td>
|
<td>${t.type_num}</td>
|
||||||
<td>${esc(t.dev_name)}</td>
|
<td>${esc(t.dev_name)}</td>
|
||||||
<td>${t.SensMin} ~ ${t.SensMax}</td>
|
<td>${t.SensMin} ~ ${t.SensMax}</td>
|
||||||
<td>${t.FreMin} ~ ${t.FreMax}</td>
|
<td>${rawFreqToHz(t.FreMin)} ~ ${rawFreqToHz(t.FreMax)}</td>
|
||||||
<td>${t.PeakMin} ~ ${t.PeakMax}</td>
|
<td>${rawPeakToV(t.PeakMin)} ~ ${rawPeakToV(t.PeakMax)}</td>
|
||||||
<td>${esc(t.remark || '-')}</td>
|
<td>${esc(t.remark || '-')}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
|
<button class="btn-edit" onclick="openModal(${t.id})">编辑</button>
|
||||||
@@ -63,10 +74,10 @@ function openModal(id = null) {
|
|||||||
document.getElementById("edit-dev-name").value = data.dev_name;
|
document.getElementById("edit-dev-name").value = data.dev_name;
|
||||||
document.getElementById("edit-sens-min").value = data.SensMin;
|
document.getElementById("edit-sens-min").value = data.SensMin;
|
||||||
document.getElementById("edit-sens-max").value = data.SensMax;
|
document.getElementById("edit-sens-max").value = data.SensMax;
|
||||||
document.getElementById("edit-fre-min").value = data.FreMin;
|
document.getElementById("edit-fre-min").value = rawFreqToHz(data.FreMin);
|
||||||
document.getElementById("edit-fre-max").value = data.FreMax;
|
document.getElementById("edit-fre-max").value = rawFreqToHz(data.FreMax);
|
||||||
document.getElementById("edit-peak-min").value = data.PeakMin;
|
document.getElementById("edit-peak-min").value = rawPeakToV(data.PeakMin);
|
||||||
document.getElementById("edit-peak-max").value = data.PeakMax;
|
document.getElementById("edit-peak-max").value = rawPeakToV(data.PeakMax);
|
||||||
document.getElementById("edit-remark").value = data.remark || "";
|
document.getElementById("edit-remark").value = data.remark || "";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -96,10 +107,10 @@ async function saveRecord() {
|
|||||||
dev_name: document.getElementById("edit-dev-name").value.trim(),
|
dev_name: document.getElementById("edit-dev-name").value.trim(),
|
||||||
SensMin: parseInt(document.getElementById("edit-sens-min").value) || 0,
|
SensMin: parseInt(document.getElementById("edit-sens-min").value) || 0,
|
||||||
SensMax: parseInt(document.getElementById("edit-sens-max").value) || 0,
|
SensMax: parseInt(document.getElementById("edit-sens-max").value) || 0,
|
||||||
FreMin: parseInt(document.getElementById("edit-fre-min").value) || 0,
|
FreMin: hzToRawFreq(parseFloat(document.getElementById("edit-fre-min").value) || 0),
|
||||||
FreMax: parseInt(document.getElementById("edit-fre-max").value) || 0,
|
FreMax: hzToRawFreq(parseFloat(document.getElementById("edit-fre-max").value) || 0),
|
||||||
PeakMin: parseInt(document.getElementById("edit-peak-min").value) || 0,
|
PeakMin: vToRawPeak(parseFloat(document.getElementById("edit-peak-min").value) || 0),
|
||||||
PeakMax: parseInt(document.getElementById("edit-peak-max").value) || 0,
|
PeakMax: vToRawPeak(parseFloat(document.getElementById("edit-peak-max").value) || 0),
|
||||||
remark: document.getElementById("edit-remark").value.trim(),
|
remark: document.getElementById("edit-remark").value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,21 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="top-menu">
|
<nav class="top-menu">
|
||||||
|
{% if current_user.is_authenticated and current_user.role != 'analyst' %}
|
||||||
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
<a href="/" class="{% if request.path == '/' %}active{% endif %}">设备</a>
|
||||||
|
{% endif %}
|
||||||
<a href="/test-data" class="{% if request.path == '/test-data' %}active{% endif %}">测试信息</a>
|
<a href="/test-data" class="{% if request.path == '/test-data' %}active{% endif %}">测试信息</a>
|
||||||
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
{% if current_user.is_authenticated and current_user.role in ('admin', 'manager') %}
|
||||||
|
<a href="/device-logs" class="{% if request.path == '/device-logs' %}active{% endif %}">设备日志</a>
|
||||||
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
<a href="/logs/" class="{% if request.path == '/logs/' %}active{% endif %}">操作日志</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.is_authenticated and current_user.role == 'admin' %}
|
||||||
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
<a href="/users/" class="{% if request.path == '/users/' %}active{% endif %}">用户管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="user-info">
|
<span class="user-info">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ current_user.username }} ({{ current_user.role }})
|
{{ current_user.username }} ({{ current_user.role }})
|
||||||
|
<a href="/change-password">修改密码</a>
|
||||||
<a href="/logout">退出</a>
|
<a href="/logout">退出</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
38
edc-web/app/templates/change_password.html
Normal file
38
edc-web/app/templates/change_password.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}修改密码 - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div style="max-width:400px;margin:40px auto;">
|
||||||
|
<h2>修改密码</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div style="background:#fef3e2;color:#b45309;padding:10px;border-radius:6px;margin-bottom:16px;">
|
||||||
|
{% for msg in messages %}{{ msg }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" style="display:flex;flex-direction:column;gap:16px;">
|
||||||
|
<div>
|
||||||
|
<label>当前密码</label>
|
||||||
|
<input type="password" name="old_password" required
|
||||||
|
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>新密码(至少6位)</label>
|
||||||
|
<input type="password" name="new_password" required minlength="6"
|
||||||
|
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>确认新密码</label>
|
||||||
|
<input type="password" name="confirm_password" required minlength="6"
|
||||||
|
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;">
|
||||||
|
<a href="/" style="line-height:36px;">← 返回</a>
|
||||||
|
<button type="submit" class="btn-search" style="padding:8px 24px;">确认修改</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
105
edc-web/app/templates/coil_info.html
Normal file
105
edc-web/app/templates/coil_info.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}线圈参数管理 - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="test-header">
|
||||||
|
<a href="/">← 返回设备列表</a>
|
||||||
|
<h2>线圈参数管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div class="vbt-header">
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索编号/名称..."
|
||||||
|
style="padding:6px 10px; border:1px solid #ddd; border-radius:4px; font-size:13px; width:200px;"
|
||||||
|
oninput="loadList()">
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" onclick="openModal()">+ 新增</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="coil-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>线圈编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>电感量</th>
|
||||||
|
<th>形状</th>
|
||||||
|
<th>尺寸 (cm)</th>
|
||||||
|
<th>圈数</th>
|
||||||
|
<th>电阻 (Ω)</th>
|
||||||
|
<th>材质</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div id="edit-modal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)closeModal()">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="modal-title">新增线圈参数</h3>
|
||||||
|
<div class="modal-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>线圈编号 *</label>
|
||||||
|
<input type="text" id="edit-coil-num">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名称</label>
|
||||||
|
<input type="text" id="edit-name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>电感量</label>
|
||||||
|
<input type="number" id="edit-induct" step="0.01" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>形状</label>
|
||||||
|
<select id="edit-shape">
|
||||||
|
<option value="">-- 请选择 --</option>
|
||||||
|
<option value="矩形">矩形</option>
|
||||||
|
<option value="圆形">圆形</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>长度 (cm,矩形有效)</label>
|
||||||
|
<input type="number" id="edit-length" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>宽度 (cm,矩形有效)</label>
|
||||||
|
<input type="number" id="edit-width" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>半径 (cm,圆形有效)</label>
|
||||||
|
<input type="number" id="edit-radius" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>圈数</label>
|
||||||
|
<input type="number" id="edit-turns" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>电阻 (Ω)</label>
|
||||||
|
<input type="number" id="edit-resistance" step="0.01" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>材质</label>
|
||||||
|
<input type="text" id="edit-material" placeholder="如铜线">
|
||||||
|
</div>
|
||||||
|
<div class="form-group full">
|
||||||
|
<label>备注</label>
|
||||||
|
<textarea id="edit-remark" rows="2" style="resize:vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">取消</button>
|
||||||
|
<button class="btn-save" onclick="saveRecord()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="msg-toast"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/coil_info.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
191
edc-web/app/templates/device_logs.html
Normal file
191
edc-web/app/templates/device_logs.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}设备日志 - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>设备事件日志</h2>
|
||||||
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<label>设备序列号:<input type="text" id="search-serial" placeholder="筛选设备..."></label>
|
||||||
|
<label>事件类型:
|
||||||
|
<select id="search-event">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="login">登录</option>
|
||||||
|
<option value="online">在线</option>
|
||||||
|
<option value="offline">离线</option>
|
||||||
|
<option value="poor">通信不良</option>
|
||||||
|
<option value="tcp_connect">TCP连接</option>
|
||||||
|
<option value="tcp_disconnect">TCP断开</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
时间范围:
|
||||||
|
<input type="date" id="search-date-from">
|
||||||
|
<input type="time" id="search-time-from" step="1" style="width:110px;" title="起始时间(时:分:秒)">
|
||||||
|
至
|
||||||
|
<input type="date" id="search-date-to">
|
||||||
|
<input type="time" id="search-time-to" step="1" style="width:110px;" title="截止时间(时:分:秒)">
|
||||||
|
</label>
|
||||||
|
<button onclick="searchLogs(1)" class="btn-search">查询</button>
|
||||||
|
<button onclick="exportCSV()" class="btn-export">导出 CSV</button>
|
||||||
|
{% if current_user.role in ('admin', 'manager') %}
|
||||||
|
<button onclick="confirmDeleteLogs()" class="btn-delete">🗑 删除</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>设备序列号</th>
|
||||||
|
<th>设备IP</th>
|
||||||
|
<th>事件类型</th>
|
||||||
|
<th>事件内容</th>
|
||||||
|
<th>时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="log-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="pagination" id="pagination"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let currentPage = 1, totalPages = 1;
|
||||||
|
|
||||||
|
function getDatetime(dateId, timeId) {
|
||||||
|
const d = document.getElementById(dateId).value;
|
||||||
|
const t = document.getElementById(timeId).value;
|
||||||
|
if (!d) return "";
|
||||||
|
if (!t) return d; // 纯日期 → 后端自动补时间
|
||||||
|
return d + " " + t; // 完整 datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchLogs(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
const serial = document.getElementById("search-serial").value;
|
||||||
|
const event_type = document.getElementById("search-event").value;
|
||||||
|
const date_from = getDatetime("search-date-from", "search-time-from");
|
||||||
|
const date_to = getDatetime("search-date-to", "search-time-to");
|
||||||
|
const params = new URLSearchParams({page, per_page: 30});
|
||||||
|
if (serial) params.set("serial", serial);
|
||||||
|
if (event_type) params.set("event_type", event_type);
|
||||||
|
if (date_from) params.set("date_from", date_from);
|
||||||
|
if (date_to) params.set("date_to", date_to);
|
||||||
|
|
||||||
|
const resp = await fetch(`/api/device-logs?${params}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
renderTable(data.records);
|
||||||
|
totalPages = data.pages;
|
||||||
|
renderPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(records) {
|
||||||
|
const tbody = document.getElementById("log-tbody");
|
||||||
|
if (!records.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#999;">暂无记录</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = records.map(r => {
|
||||||
|
let typeStyle = '';
|
||||||
|
if (r.event_type === 'online' || r.event_type === 'login') typeStyle = 'color:#27ae60;font-weight:bold;';
|
||||||
|
else if (r.event_type === 'offline') typeStyle = 'color:#e74c3c;font-weight:bold;';
|
||||||
|
else if (r.event_type === 'poor') typeStyle = 'color:#f39c12;font-weight:bold;';
|
||||||
|
else if (r.event_type === 'tcp_disconnect') typeStyle = 'color:#e74c3c;';
|
||||||
|
else if (r.event_type === 'tcp_connect') typeStyle = 'color:#3498db;';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${r.id}</td>
|
||||||
|
<td>${escHtml(r.device_serial || '-')}</td>
|
||||||
|
<td>${r.device_ip || '-'}</td>
|
||||||
|
<td style="${typeStyle}">${eventLabel(r.event_type)}</td>
|
||||||
|
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(r.event_content || '')}">${r.event_content || '-'}</td>
|
||||||
|
<td>${fmtTime(r.create_time)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventLabel(t) {
|
||||||
|
const m = {login: '登录', online: '在线', offline: '离线', poor: '通信不良',
|
||||||
|
tcp_connect: 'TCP连接', tcp_disconnect: 'TCP断开'};
|
||||||
|
return m[t] || t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(v) {
|
||||||
|
if (!v) return '-';
|
||||||
|
// Flask jsonify 给 MySQL DATETIME 加 "GMT" 后缀,但实际值是服务器本地时间(UTC+8)
|
||||||
|
// 去掉 "GMT" 让 JS 按本地时间解析,避免时区偏移 8 小时
|
||||||
|
const cleaned = String(v).replace(/ GMT$/, '');
|
||||||
|
const d = new Date(cleaned);
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination() {
|
||||||
|
const div = document.getElementById("pagination");
|
||||||
|
let html = `<button onclick="searchLogs(${currentPage-1})" ${currentPage<=1?'disabled':''}>上一页</button>`;
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
html += `<button onclick="searchLogs(${i})" class="${i===currentPage?'active':''}">${i}</button>`;
|
||||||
|
}
|
||||||
|
html += `<button onclick="searchLogs(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}>下一页</button>`;
|
||||||
|
div.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 导出 CSV ────────────────────────────────────
|
||||||
|
|
||||||
|
function exportCSV() {
|
||||||
|
const serial = document.getElementById("search-serial").value;
|
||||||
|
const event_type = document.getElementById("search-event").value;
|
||||||
|
const date_from = getDatetime("search-date-from", "search-time-from");
|
||||||
|
const date_to = getDatetime("search-date-to", "search-time-to");
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (serial) params.set("serial", serial);
|
||||||
|
if (event_type) params.set("event_type", event_type);
|
||||||
|
if (date_from) params.set("date_from", date_from);
|
||||||
|
if (date_to) params.set("date_to", date_to);
|
||||||
|
|
||||||
|
// 直接打开下载链接
|
||||||
|
window.location.href = `/api/device-logs/export?${params}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 删除 ────────────────────────────────────────
|
||||||
|
|
||||||
|
async function confirmDeleteLogs() {
|
||||||
|
const serial = document.getElementById("search-serial").value;
|
||||||
|
const event_type = document.getElementById("search-event").value;
|
||||||
|
const date_from = getDatetime("search-date-from", "search-time-from");
|
||||||
|
const date_to = getDatetime("search-date-to", "search-time-to");
|
||||||
|
if (!serial && !event_type && !date_from && !date_to) {
|
||||||
|
alert("请至少输入设备序列号、选择事件类型或指定时间范围作为删除条件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const msg = `确认删除设备日志?\n条件: serial=${serial || '(无)'} type=${event_type || '(无)'} time=${date_from||'(无)'}~${date_to||'(无)'}\n此操作不可撤销!`;
|
||||||
|
if (!confirm(msg)) return;
|
||||||
|
|
||||||
|
const resp = await fetch("/api/device-logs/delete", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({serial, event_type, date_from, date_to}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
alert(`已删除 ${data.deleted} 条记录`);
|
||||||
|
searchLogs(1);
|
||||||
|
} else {
|
||||||
|
alert("删除失败: " + (data.error || "未知错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchLogs(1);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -20,5 +20,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const USER_ROLE = "{{ current_user.role }}";
|
||||||
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/devices.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/devices.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
<label>测试模式</label>
|
<label>测试模式</label>
|
||||||
<select id="param-test-mode">
|
<select id="param-test-mode">
|
||||||
<option value="0">0 - 灵敏度测试模式</option>
|
<option value="0">0 - 灵敏度测试模式</option>
|
||||||
<option value="1">1 - 模拟过车模式</option>
|
<option value="1">1 - 波动测试模式</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>复位距离 (cm)</label>
|
<label>复位距离 (包含了皮距,mm)</label>
|
||||||
<input type="number" id="param-reset-dis" value="0" min="0">
|
<input type="number" id="param-reset-dis" value="0" min="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>皮距/开始距离 (cm)</label>
|
<label>皮距/开始距离 (mm)</label>
|
||||||
<input type="number" id="param-minus-dis" value="0" min="0">
|
<input type="number" id="param-minus-dis" value="0" min="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>灵敏度最小值</label>
|
<label>触发距离最小值(mm)</label>
|
||||||
<input type="number" id="param-sens-min" value="0">
|
<input type="number" id="param-sens-min" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>灵敏度最大值</label>
|
<label>释放距离最大值(mm)</label>
|
||||||
<input type="number" id="param-sens-max" value="0">
|
<input type="number" id="param-sens-max" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -61,13 +61,39 @@
|
|||||||
<input type="number" id="param-fre-max" value="0">
|
<input type="number" id="param-fre-max" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>峰峰值最小值</label>
|
<label>峰峰值最小值 (V)</label>
|
||||||
<input type="number" id="param-peak-min" value="0">
|
<input type="number" id="param-peak-min" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>峰峰值最大值</label>
|
<label>峰峰值最大值 (V)</label>
|
||||||
<input type="number" id="param-peak-max" value="0">
|
<input type="number" id="param-peak-max" value="0">
|
||||||
</div>
|
</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>最远容差 (mm)</label>
|
||||||
|
<input type="number" id="param-far-tol" value="0" min="0" max="2550">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最近容差 (mm)</label>
|
||||||
|
<input type="number" id="param-near-tol" value="0" min="0" max="2550">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>步进容差 (mm)</label>
|
||||||
|
<input type="number" id="param-step-tol" value="0" min="0" max="2550">
|
||||||
|
</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>
|
||||||
|
|
||||||
<div class="fixture-actions">
|
<div class="fixture-actions">
|
||||||
@@ -107,13 +133,49 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>编码</th><th>名称</th><th>灵敏度</th><th>频率(Hz)</th><th>峰峰值</th>
|
<th>编码</th><th>名称</th><th>触发和释放范围(mm)</th><th>频率(Hz)</th><th>峰峰值(V)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="ref-table-body"></tbody>
|
<tbody id="ref-table-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 线圈参数选择区 -->
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||||
|
<h3 style="margin:0;">关联线圈参数</h3>
|
||||||
|
<button class="btn-config" style="padding:4px 12px; font-size:12px;"
|
||||||
|
onclick="location.href='/coil-info'">管理</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:8px;">
|
||||||
|
<label style="font-size:12px; color:#666;">当前线圈:</label>
|
||||||
|
<span id="current-coil-label" style="font-weight:600; font-size:13px;">未设置</span>
|
||||||
|
</div>
|
||||||
|
<select id="coil-select" style="width:100%; padding:4px; border:1px solid #ddd; border-radius:4px; font-size:12px;"
|
||||||
|
onchange="onCoilChange()">
|
||||||
|
<option value="">-- 选择线圈 --</option>
|
||||||
|
</select>
|
||||||
|
<div id="coil-detail" style="font-size:12px; color:#888; margin-top:4px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模拟车辆参数选择区 -->
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||||
|
<h3 style="margin:0;">关联模拟车辆参数</h3>
|
||||||
|
<button class="btn-config" style="padding:4px 12px; font-size:12px;"
|
||||||
|
onclick="location.href='/simulate-car'">管理</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:8px;">
|
||||||
|
<label style="font-size:12px; color:#666;">当前车辆:</label>
|
||||||
|
<span id="current-car-label" style="font-weight:600; font-size:13px;">未设置</span>
|
||||||
|
</div>
|
||||||
|
<select id="car-select" style="width:100%; padding:4px; border:1px solid #ddd; border-radius:4px; font-size:12px;"
|
||||||
|
onchange="onCarChange()">
|
||||||
|
<option value="">-- 选择模拟车辆 --</option>
|
||||||
|
</select>
|
||||||
|
<div id="car-detail" style="font-size:12px; color:#888; margin-top:4px;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
90
edc-web/app/templates/simulate_car.html
Normal file
90
edc-web/app/templates/simulate_car.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}模拟车辆参数管理 - EDC 工装管理系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="test-header">
|
||||||
|
<a href="/">← 返回设备列表</a>
|
||||||
|
<h2>模拟车辆参数管理</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixture-card">
|
||||||
|
<div class="vbt-header">
|
||||||
|
<div style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索编号/名称..."
|
||||||
|
style="padding:6px 10px; border:1px solid #ddd; border-radius:4px; font-size:13px; width:200px;"
|
||||||
|
oninput="loadList()">
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" onclick="openModal()">+ 新增</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="car-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>模拟编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>形状</th>
|
||||||
|
<th>尺寸 (cm)</th>
|
||||||
|
<th>材质</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div id="edit-modal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)closeModal()">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="modal-title">新增模拟车辆参数</h3>
|
||||||
|
<div class="modal-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>模拟编号 *</label>
|
||||||
|
<input type="text" id="edit-simulate-num">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名称</label>
|
||||||
|
<input type="text" id="edit-name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>形状</label>
|
||||||
|
<select id="edit-shape">
|
||||||
|
<option value="">-- 请选择 --</option>
|
||||||
|
<option value="矩形">矩形</option>
|
||||||
|
<option value="圆形">圆形</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>长度 (cm,矩形有效)</label>
|
||||||
|
<input type="number" id="edit-length" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>宽度 (cm,矩形有效)</label>
|
||||||
|
<input type="number" id="edit-width" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>半径 (cm,圆形有效)</label>
|
||||||
|
<input type="number" id="edit-radius" step="0.1" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>材质</label>
|
||||||
|
<input type="text" id="edit-material" placeholder="如铁板、合金">
|
||||||
|
</div>
|
||||||
|
<div class="form-group full">
|
||||||
|
<label>备注</label>
|
||||||
|
<textarea id="edit-remark" rows="2" style="resize:vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">取消</button>
|
||||||
|
<button class="btn-save" onclick="saveRecord()">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="msg-toast"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/simulate_car.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,49 +4,108 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>测试信息</h2>
|
<h2>测试信息</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div style="background:#fef3e2;color:#b45309;padding:10px;border-radius:6px;margin-bottom:16px;">
|
||||||
|
{% for msg in messages %}{{ msg }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<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">
|
<div class="search-bar">
|
||||||
<label>
|
<label>
|
||||||
设备编码:
|
设备编码:
|
||||||
<input type="text" id="search-serial" placeholder="输入设备编码搜索...">
|
<input type="text" id="search-serial" placeholder="设备编码...">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
日期范围:
|
车检器序列号:
|
||||||
|
<input type="text" id="search-detector-serial" placeholder="车检器序列号...">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
时间范围:
|
||||||
<input type="date" id="search-date-from">
|
<input type="date" id="search-date-from">
|
||||||
|
<input type="time" id="search-time-from" step="1" style="width:110px;" title="起始时间(时:分:秒)">
|
||||||
至
|
至
|
||||||
<input type="date" id="search-date-to">
|
<input type="date" id="search-date-to">
|
||||||
|
<input type="time" id="search-time-to" step="1" style="width:110px;" title="截止时间(时:分:秒)">
|
||||||
</label>
|
</label>
|
||||||
<button onclick="searchData(1)" class="btn-search">搜索</button>
|
<button onclick="searchData(1)" class="btn-search">搜索</button>
|
||||||
<button onclick="exportCSV()" class="btn-export">导出 CSV</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" selected>100</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button id="btn-chart" class="btn-chart" onclick="toggleChart()">📈 图表</button>
|
||||||
|
{% if current_user.role in ('admin', 'manager') %}
|
||||||
|
<button id="btn-delete" class="btn-delete" onclick="confirmDelete()">🗑 删除</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="chart-container" style="display:none; width:100%; height:500px; margin-bottom:16px;"></div>
|
||||||
|
|
||||||
|
<div style="overflow-x:auto; max-width:100%; -webkit-overflow-scrolling:touch;">
|
||||||
<table id="test-data-table">
|
<table id="test-data-table">
|
||||||
<thead>
|
<thead></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>
|
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pagination" id="pagination"></div>
|
<div class="pagination" id="pagination"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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; }
|
||||||
|
#test-data-table th,
|
||||||
|
#test-data-table td { white-space: nowrap; }
|
||||||
|
</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>
|
<script src="{{ url_for('static', filename='js/test_data.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,6 +6,42 @@
|
|||||||
<div class="test-header">
|
<div class="test-header">
|
||||||
<a href="/">← 返回设备列表</a>
|
<a href="/">← 返回设备列表</a>
|
||||||
<h2>测试操作 — {{ device.serial }} ({{ device.name or '未命名' }})</h2>
|
<h2>测试操作 — {{ device.serial }} ({{ device.name or '未命名' }})</h2>
|
||||||
|
<div id="device-status-bar" style="margin-top:4px;font-size:14px;">
|
||||||
|
设备状态:<span id="device-status-text" class="{% if device.state == 1 %}status-online{% elif device.state == 2 %}status-poor{% else %}status-offline{% endif %}">加载中…</span>
|
||||||
|
</div>
|
||||||
|
<div id="test-mode-indicator" style="margin-top:4px;font-size:14px;color:#888;display:none;">加载中…</div>
|
||||||
|
<div id="config-overview" style="margin-top:8px;background:#f8f9fa;border:1px solid #e0e0e0;border-radius:6px;padding:10px 14px;font-size:13px;display:none;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
||||||
|
<strong style="color:#555;">工装配置概览</strong>
|
||||||
|
<span id="config-toggle" style="cursor:pointer;color:#888;font-size:12px;user-select:none;" onclick="toggleConfig()">收起 ▲</span>
|
||||||
|
</div>
|
||||||
|
<div id="config-body">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 24px;">
|
||||||
|
<div>测试模式:<span id="cfg-test-mode">-</span></div>
|
||||||
|
<div>车检器型号:<span id="cfg-dev-type">-</span></div>
|
||||||
|
<div>复位距离:<span id="cfg-reset-dis">-</span> mm</div>
|
||||||
|
<div>皮距:<span id="cfg-minus-dis">-</span> mm</div>
|
||||||
|
<div>触发和释放范围:<span id="cfg-sens-range">-</span> mm</div>
|
||||||
|
<div>频率范围:<span id="cfg-fre-range">-</span> Hz</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:6px;padding-top:6px;border-top:1px dashed #ddd;">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 24px;">
|
||||||
|
<div>线圈:<span id="cfg-coil">-</span></div>
|
||||||
|
<div>模拟车辆:<span id="cfg-car">-</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="cfg-wave-params" style="display:none;margin-top:6px;padding-top:6px;border-top:1px dashed #ddd;">
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px 16px;">
|
||||||
|
<div>最近容差:<span id="cfg-near-tol">-</span> mm</div>
|
||||||
|
<div>最远容差:<span id="cfg-far-tol">-</span> mm</div>
|
||||||
|
<div>步进容差:<span id="cfg-step-tol">-</span> mm</div>
|
||||||
|
<div>来回次数:<span id="cfg-back-forth">-</span></div>
|
||||||
|
<div>最近停留:<span id="cfg-near-stay">-</span> ms</div>
|
||||||
|
<div>最远停留:<span id="cfg-far-stay">-</span> ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="test-layout">
|
<div class="test-layout">
|
||||||
@@ -22,9 +58,22 @@
|
|||||||
|
|
||||||
<h3>自动化测试</h3>
|
<h3>自动化测试</h3>
|
||||||
<div class="automation">
|
<div class="automation">
|
||||||
|
<label>
|
||||||
|
车检器序列号:
|
||||||
|
<input type="text" id="detector-serial" placeholder="选填" style="width:180px;">
|
||||||
|
</label>
|
||||||
|
<br style="margin-bottom:8px;">
|
||||||
<label>
|
<label>
|
||||||
测试次数:
|
测试次数:
|
||||||
<input type="number" id="test-count" value="10" min="1" max="9999">
|
<input type="number" id="test-count" value="1" min="1" max="9999">
|
||||||
|
</label>
|
||||||
|
<label style="margin-left:16px;">
|
||||||
|
间隔时间(秒):
|
||||||
|
<input type="number" id="interval-sec" value="5" min="0" max="300" style="width:60px;">
|
||||||
|
</label>
|
||||||
|
<label style="margin-left:16px;">
|
||||||
|
超时时间(秒):
|
||||||
|
<input type="number" id="timeout-sec" value="4" min="1" max="600" style="width:60px;">
|
||||||
</label>
|
</label>
|
||||||
<button id="btn-auto" class="btn-start" onclick="toggleAuto()">开始</button>
|
<button id="btn-auto" class="btn-start" onclick="toggleAuto()">开始</button>
|
||||||
<div class="progress-container">
|
<div class="progress-container">
|
||||||
@@ -36,6 +85,7 @@
|
|||||||
<span>失败:<strong id="stat-failed">0</strong></span>
|
<span>失败:<strong id="stat-failed">0</strong></span>
|
||||||
<span>剩余:<strong id="stat-remaining">0</strong></span>
|
<span>剩余:<strong id="stat-remaining">0</strong></span>
|
||||||
</div>
|
</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;">
|
<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>
|
开始:<span id="time-start">-</span> 结束:<span id="time-end">-</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,20 +94,27 @@
|
|||||||
|
|
||||||
<!-- 右侧:测试信息显示区 -->
|
<!-- 右侧:测试信息显示区 -->
|
||||||
<div class="test-info">
|
<div class="test-info">
|
||||||
<h3>当前测试数据</h3>
|
<h3>当前测试数据 <span id="new-record-count" style="font-size:12px;color:#888;font-weight:normal;margin-left:8px;"></span></h3>
|
||||||
<div id="latest-result">
|
<div id="latest-result">
|
||||||
<p class="placeholder">等待设备上报...</p>
|
<p class="placeholder">等待设备上报...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="wave-section">
|
||||||
|
<h3>波动测试数据</h3>
|
||||||
|
<div id="latest-wave">
|
||||||
|
<p class="placeholder">暂无波动数据...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>自动化平均值</h3>
|
<h3>自动化平均值</h3>
|
||||||
<table id="avg-table">
|
<table id="avg-table">
|
||||||
<tr><td>平均峰峰值</td><td id="avg-ppvalue">-</td><td>V</td></tr>
|
<tr><td>平均峰峰值</td><td id="avg-ppvalue">-</td><td>V</td></tr>
|
||||||
<tr><td>平均开始工作频率</td><td id="avg-idle-freq">-</td><td>Hz</td></tr>
|
<tr><td>平均开始频率</td><td id="avg-idle-freq">-</td><td>Hz</td></tr>
|
||||||
<tr><td>平均进入工作频率</td><td id="avg-enter-freq">-</td><td>Hz</td></tr>
|
<tr><td>平均触发频率</td><td id="avg-enter-freq">-</td><td>Hz</td></tr>
|
||||||
<tr><td>平均进入距离</td><td id="avg-enter-dist">-</td><td>mm</td></tr>
|
<tr><td>平均触发距离</td><td id="avg-enter-dist">-</td><td>mm</td></tr>
|
||||||
<tr><td>平均离开距离</td><td id="avg-exit-dist">-</td><td>mm</td></tr>
|
<tr><td>平均释放距离</td><td id="avg-exit-dist">-</td><td>mm</td></tr>
|
||||||
<tr><td>平均进入速度</td><td id="avg-enter-speed">-</td><td>m/s</td></tr>
|
<tr><td>平均触发速度</td><td id="avg-enter-speed">-</td><td>m/s</td></tr>
|
||||||
<tr><td>平均离开速度</td><td id="avg-exit-speed">-</td><td>m/s</td></tr>
|
<tr><td>平均释放速度</td><td id="avg-exit-speed">-</td><td>m/s</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3 style="margin-top:20px;">本轮测试明细</h3>
|
<h3 style="margin-top:20px;">本轮测试明细</h3>
|
||||||
@@ -65,7 +122,7 @@
|
|||||||
<table id="records-table" style="font-size:11px;">
|
<table id="records-table" style="font-size:11px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
<label>角色:
|
<label>角色:
|
||||||
<select id="new-role" style="margin:0 8px;">
|
<select id="new-role" style="margin:0 8px;">
|
||||||
<option value="operator">operator</option>
|
<option value="operator">operator</option>
|
||||||
|
<option value="analyst">analyst</option>
|
||||||
|
<option value="manager">manager</option>
|
||||||
<option value="admin">admin</option>
|
<option value="admin">admin</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -51,6 +53,8 @@ async function loadUsers() {
|
|||||||
<td>
|
<td>
|
||||||
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
<select onchange="updateUser(${u.id}, this, 'role')" data-field="role">
|
||||||
<option value="operator" ${u.role==='operator'?'selected':''}>operator</option>
|
<option value="operator" ${u.role==='operator'?'selected':''}>operator</option>
|
||||||
|
<option value="analyst" ${u.role==='analyst'?'selected':''}>analyst</option>
|
||||||
|
<option value="manager" ${u.role==='manager'?'selected':''}>manager</option>
|
||||||
<option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
|
<option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
|
||||||
</select>
|
</select>
|
||||||
<select onchange="updateUser(${u.id}, this, 'is_active')" data-field="is_active">
|
<select onchange="updateUser(${u.id}, this, 'is_active')" data-field="is_active">
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>类型编码</th>
|
<th>类型编码</th>
|
||||||
<th>型号/名称</th>
|
<th>型号/名称</th>
|
||||||
<th>灵敏度范围</th>
|
<th>触发和释放距离范围(mm)</th>
|
||||||
<th>频率范围 (Hz)</th>
|
<th>频率范围 (Hz)</th>
|
||||||
<th>峰峰值范围</th>
|
<th>峰峰值范围(V)</th>
|
||||||
<th>备注</th>
|
<th>备注</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<input type="text" id="edit-dev-name">
|
<input type="text" id="edit-dev-name">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>灵敏度最小值</label>
|
<label>触发距离最小值(mm)</label>
|
||||||
<input type="number" id="edit-sens-min" value="0">
|
<input type="number" id="edit-sens-min" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>灵敏度最大值</label>
|
<label>释放距离最大值(mm)</label>
|
||||||
<input type="number" id="edit-sens-max" value="0">
|
<input type="number" id="edit-sens-max" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
<input type="number" id="edit-fre-max" value="0">
|
<input type="number" id="edit-fre-max" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>峰峰值最小值</label>
|
<label>峰峰值最小值(V)</label>
|
||||||
<input type="number" id="edit-peak-min" value="0">
|
<input type="number" id="edit-peak-min" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>峰峰值最大值</label>
|
<label>峰峰值最大值(V)</label>
|
||||||
<input type="number" id="edit-peak-max" value="0">
|
<input type="number" id="edit-peak-max" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full">
|
<div class="form-group full">
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
flask>=3.0
|
flask>=3.1.0
|
||||||
pymysql>=1.1
|
flask-login>=0.6.0
|
||||||
|
pymysql>=1.2.0
|
||||||
|
|||||||
Submodule edc_server updated: e7c20c69d2...a2f31b3bfe
Reference in New Issue
Block a user