main
parent
ee351733fe
commit
3e4e040afd
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"template": "F:\\PyPro\\PCM_Report\\configs\\600泵\\template.docx",
|
||||
"template": "C:\\PPRO\\PCM_Report\\configs\\600泵\\template.docx",
|
||||
"category_name": "600泵"
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
添加保存状态字段到 experiments 表
|
||||
用于跟踪实验数据自动保存的状态
|
||||
"""
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def add_save_status_columns():
|
||||
"""添加 save_status 和 save_error 字段"""
|
||||
try:
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
|
||||
if not db_path.exists():
|
||||
logger.error(f"数据库文件不存在: {db_path}")
|
||||
return False
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
# 检查字段是否已存在
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns = [row[1] for row in cur.fetchall()]
|
||||
|
||||
logger.info(f"当前 experiments 表字段: {columns}")
|
||||
|
||||
# 添加 save_status 字段
|
||||
if 'save_status' not in columns:
|
||||
logger.info("添加 save_status 字段...")
|
||||
cur.execute("""
|
||||
ALTER TABLE experiments
|
||||
ADD COLUMN save_status TEXT DEFAULT NULL
|
||||
""")
|
||||
logger.info("✅ save_status 字段添加成功")
|
||||
else:
|
||||
logger.info("save_status 字段已存在,跳过")
|
||||
|
||||
# 添加 save_error 字段
|
||||
if 'save_error' not in columns:
|
||||
logger.info("添加 save_error 字段...")
|
||||
cur.execute("""
|
||||
ALTER TABLE experiments
|
||||
ADD COLUMN save_error TEXT DEFAULT NULL
|
||||
""")
|
||||
logger.info("✅ save_error 字段添加成功")
|
||||
else:
|
||||
logger.info("save_error 字段已存在,跳过")
|
||||
|
||||
db.commit()
|
||||
|
||||
# 验证字段已添加
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns_after = [row[1] for row in cur.fetchall()]
|
||||
logger.info(f"更新后 experiments 表字段: {columns_after}")
|
||||
|
||||
db.close()
|
||||
|
||||
logger.info("✅ 数据库迁移完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 添加字段失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("开始添加保存状态字段...")
|
||||
success = add_save_status_columns()
|
||||
|
||||
if success:
|
||||
print("✅ 数据库迁移成功完成")
|
||||
else:
|
||||
print("❌ 数据库迁移失败,请查看日志")
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"influx": {
|
||||
"url": "http://10.0.5.232:8086",
|
||||
"org": "MEASCON",
|
||||
"token": "_jtoxcVDIbol2Uqt_vlhidut-EO0Xo0ZXea2UC5a5Bgotk836F0xPN4NSGY1jYI_WaBKRau4RyZ-g2XSFiNdXw==",
|
||||
"token": "JPZMq2UP5ORhLq8CfsPbawl6k0MSDlJmEwMJ2uvR_TXqW5bUOWIYBQOSXkGNzDqOU3rnuGpIxGxrB_mlAF-EEw==",
|
||||
"username": "PCM",
|
||||
"password": "1842moon",
|
||||
"landingUrl": "http://10.0.5.232:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h",
|
||||
|
|
@ -13,106 +13,223 @@
|
|||
"chart1": {
|
||||
"type": "chart",
|
||||
"label": "chart1",
|
||||
"title": "",
|
||||
"title": "chart1",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart2": {
|
||||
"type": "chart",
|
||||
"label": "chart2",
|
||||
"title": "",
|
||||
"title": "chart2",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart3": {
|
||||
"type": "chart",
|
||||
"label": "chart3",
|
||||
"title": "",
|
||||
"title": "chart3",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart4": {
|
||||
"type": "chart",
|
||||
"label": "chart4",
|
||||
"title": "",
|
||||
"title": "chart4",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart5": {
|
||||
"type": "chart",
|
||||
"label": "chart5",
|
||||
"title": "",
|
||||
"title": "chart5",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart6": {
|
||||
"type": "chart",
|
||||
"label": "chart6",
|
||||
"title": "",
|
||||
"title": "chart6",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart7": {
|
||||
"type": "chart",
|
||||
"label": "chart7",
|
||||
"title": "",
|
||||
"title": "chart7",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart8": {
|
||||
"type": "chart",
|
||||
"label": "chart8",
|
||||
"title": "",
|
||||
"title": "chart8",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart9": {
|
||||
"type": "chart",
|
||||
"label": "chart9",
|
||||
"title": "",
|
||||
"title": "chart9",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart10": {
|
||||
"type": "chart",
|
||||
"label": "chart10",
|
||||
"title": "",
|
||||
"title": "chart10",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart11": {
|
||||
"type": "chart",
|
||||
"label": "chart11",
|
||||
"title": "",
|
||||
"title": "chart11",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"chart12": {
|
||||
"type": "chart",
|
||||
"label": "chart12",
|
||||
"title": "",
|
||||
"title": "chart12",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {}
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
}
|
||||
},
|
||||
"table1": {
|
||||
"type": "table",
|
||||
"label": "table1",
|
||||
"title": "",
|
||||
"title": "table1",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
},
|
||||
"table": {
|
||||
"firstColumn": "time",
|
||||
"firstTitle": "",
|
||||
|
|
@ -122,10 +239,19 @@
|
|||
"table2": {
|
||||
"type": "table",
|
||||
"label": "table2",
|
||||
"title": "",
|
||||
"title": "table2",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
},
|
||||
"table": {
|
||||
"firstColumn": "time",
|
||||
"firstTitle": "",
|
||||
|
|
@ -135,10 +261,19 @@
|
|||
"table3": {
|
||||
"type": "table",
|
||||
"label": "table3",
|
||||
"title": "",
|
||||
"title": "table3",
|
||||
"value": "",
|
||||
"dbQuery": "",
|
||||
"chart": {},
|
||||
"influx": {
|
||||
"bucket": "",
|
||||
"measurement": "",
|
||||
"fields": [],
|
||||
"filters": {},
|
||||
"timeRange": "-1h",
|
||||
"aggregate": "",
|
||||
"windowPeriod": ""
|
||||
},
|
||||
"table": {
|
||||
"firstColumn": "time",
|
||||
"firstTitle": "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"canvas": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"w": 1920.0,
|
||||
"h": 1080.0
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
"widget_type": "label",
|
||||
"x": 173.0,
|
||||
"y": 210.0,
|
||||
"w": 162.0,
|
||||
"h": 62.0,
|
||||
"z": 1.0,
|
||||
"config": {
|
||||
"fieldName": "主轴承#3",
|
||||
"prefix": "主轴承温度",
|
||||
"suffix": " ℃",
|
||||
"fontSize": 16,
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "label",
|
||||
"x": 46.0,
|
||||
"y": 378.0,
|
||||
"w": 162.0,
|
||||
"h": 62.0,
|
||||
"z": 1.0,
|
||||
"config": {
|
||||
"fieldName": "减速箱小轴承2",
|
||||
"prefix": "减速箱小轴承温度",
|
||||
"suffix": "℃",
|
||||
"fontSize": 16,
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "image",
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"w": 649.0,
|
||||
"h": 864.0,
|
||||
"z": 0.0,
|
||||
"config": {
|
||||
"imagePath": "D:/1-2.JPG"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "web",
|
||||
"x": 649.0,
|
||||
"y": 0.0,
|
||||
"w": 886.0,
|
||||
"h": 863.0,
|
||||
"z": 0.0,
|
||||
"config": {
|
||||
"url": "http://127.0.0.1:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h",
|
||||
"locked": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -2,7 +2,7 @@
|
|||
"influx": {
|
||||
"url": "http://10.0.5.232:8086",
|
||||
"org": "MEASCON",
|
||||
"token": "_jtoxcVDIbol2Uqt_vlhidut-EO0Xo0ZXea2UC5a5Bgotk836F0xPN4NSGY1jYI_WaBKRau4RyZ-g2XSFiNdXw==",
|
||||
"token": "JPZMq2UP5ORhLq8CfsPbawl6k0MSDlJmEwMJ2uvR_TXqW5bUOWIYBQOSXkGNzDqOU3rnuGpIxGxrB_mlAF-EEw==",
|
||||
"username": "PCM",
|
||||
"password": "1842moon",
|
||||
"landingUrl": "http://10.0.5.232:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"canvas": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"w": 1920.0,
|
||||
"h": 1080.0
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
"widget_type": "label",
|
||||
"x": 173.0,
|
||||
"y": 210.0,
|
||||
"w": 162.0,
|
||||
"h": 62.0,
|
||||
"z": 1.0,
|
||||
"config": {
|
||||
"fieldName": "主轴承#3",
|
||||
"prefix": "主轴承温度",
|
||||
"suffix": " ℃",
|
||||
"fontSize": 16,
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "label",
|
||||
"x": 46.0,
|
||||
"y": 378.0,
|
||||
"w": 162.0,
|
||||
"h": 62.0,
|
||||
"z": 1.0,
|
||||
"config": {
|
||||
"fieldName": "减速箱小轴承2",
|
||||
"prefix": "减速箱小轴承温度",
|
||||
"suffix": "℃",
|
||||
"fontSize": 16,
|
||||
"color": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "image",
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"w": 649.0,
|
||||
"h": 864.0,
|
||||
"z": 0.0,
|
||||
"config": {
|
||||
"imagePath": "D:/1-2.JPG"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "web",
|
||||
"x": 649.0,
|
||||
"y": 0.0,
|
||||
"w": 886.0,
|
||||
"h": 863.0,
|
||||
"z": 0.0,
|
||||
"config": {
|
||||
"url": "http://127.0.0.1:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h",
|
||||
"locked": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
245
default.json
245
default.json
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
诊断实验列表显示问题
|
||||
"""
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def diagnose():
|
||||
"""诊断实验列表问题"""
|
||||
print("\n" + "="*80)
|
||||
print("诊断实验列表显示问题")
|
||||
print("="*80 + "\n")
|
||||
|
||||
try:
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"❌ 数据库文件不存在: {db_path}")
|
||||
return
|
||||
|
||||
print(f"✅ 数据库文件存在: {db_path}\n")
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
# 1. 检查表结构
|
||||
print("1. 检查 experiments 表结构")
|
||||
print("-" * 80)
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns = cur.fetchall()
|
||||
|
||||
print(f"表字段数量: {len(columns)}")
|
||||
print(f"{'序号':<5} {'字段名':<25} {'类型':<15} {'非空':<5} {'默认值'}")
|
||||
print("-" * 80)
|
||||
for col in columns:
|
||||
cid, name, type_, notnull, default, pk = col
|
||||
print(f"{cid:<5} {name:<25} {type_:<15} {notnull:<5} {str(default)}")
|
||||
|
||||
# 检查关键字段
|
||||
column_names = [col[1] for col in columns]
|
||||
has_save_status = 'save_status' in column_names
|
||||
has_save_error = 'save_error' in column_names
|
||||
|
||||
print(f"\n✅ save_status 字段: {'存在' if has_save_status else '❌ 缺失'}")
|
||||
print(f"✅ save_error 字段: {'存在' if has_save_error else '❌ 缺失'}")
|
||||
|
||||
# 2. 检查实验记录
|
||||
print("\n2. 检查实验记录")
|
||||
print("-" * 80)
|
||||
cur.execute("SELECT COUNT(*) FROM experiments")
|
||||
count = cur.fetchone()[0]
|
||||
print(f"实验记录总数: {count}")
|
||||
|
||||
if count > 0:
|
||||
# 显示最近的记录
|
||||
print("\n最近5条实验记录:")
|
||||
print("-" * 80)
|
||||
|
||||
if has_save_status and has_save_error:
|
||||
cur.execute("""
|
||||
SELECT id, work_order_no, start_ts, end_ts, save_status, save_error
|
||||
FROM experiments
|
||||
ORDER BY id DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT id, work_order_no, start_ts, end_ts
|
||||
FROM experiments
|
||||
ORDER BY id DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
if has_save_status and has_save_error:
|
||||
eid, wo, st, et, save_status, save_error = row
|
||||
print(f"ID: {eid}, 工单: {wo or 'N/A'}, 开始: {st or 'N/A'}, 结束: {et or 'N/A'}")
|
||||
print(f" 保存状态: {save_status or 'NULL'}, 错误: {save_error or 'NULL'}")
|
||||
else:
|
||||
eid, wo, st, et = row
|
||||
print(f"ID: {eid}, 工单: {wo or 'N/A'}, 开始: {st or 'N/A'}, 结束: {et or 'N/A'}")
|
||||
|
||||
# 3. 测试查询语句
|
||||
print("\n3. 测试UI查询语句")
|
||||
print("-" * 80)
|
||||
|
||||
try:
|
||||
if has_save_status and has_save_error:
|
||||
test_sql = """
|
||||
SELECT id, start_ts, end_ts, work_order_no, process_name, part_no,
|
||||
executor, remark, sqlserver_status, is_paused, is_terminated,
|
||||
save_status, save_error
|
||||
FROM experiments
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
else:
|
||||
test_sql = """
|
||||
SELECT id, start_ts, end_ts, work_order_no, process_name, part_no,
|
||||
executor, remark, sqlserver_status, is_paused, is_terminated
|
||||
FROM experiments
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
cur.execute(test_sql)
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
print("✅ 查询成功")
|
||||
print(f"返回字段数: {len(result)}")
|
||||
else:
|
||||
print("⚠️ 查询成功但无数据")
|
||||
except Exception as e:
|
||||
print(f"❌ 查询失败: {e}")
|
||||
|
||||
db.close()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("诊断完成")
|
||||
print("="*80)
|
||||
|
||||
# 给出建议
|
||||
if not has_save_status or not has_save_error:
|
||||
print("\n⚠️ 建议:运行 python add_save_status_columns.py 添加缺失的字段")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 诊断过程出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
diagnose()
|
||||
|
|
@ -563,6 +563,7 @@ class ExperimentStateMonitor:
|
|||
"""实验开始事件处理"""
|
||||
try:
|
||||
self._experiment_started = True
|
||||
# 记录本地时间,执行脚本时会自动转换为UTC
|
||||
self._start_time_recorded = datetime.datetime.now().isoformat(timespec='seconds')
|
||||
|
||||
logger.info(
|
||||
|
|
@ -590,6 +591,7 @@ class ExperimentStateMonitor:
|
|||
return
|
||||
|
||||
self._experiment_ended = True
|
||||
# 记录本地时间,执行脚本时会自动转换为UTC
|
||||
self._end_time_recorded = datetime.datetime.now().isoformat(timespec='seconds')
|
||||
|
||||
logger.info(
|
||||
|
|
@ -786,55 +788,99 @@ class ExperimentStateMonitor:
|
|||
)
|
||||
|
||||
def _execute_and_save_script_data(self) -> None:
|
||||
"""执行动态脚本并保存返回数据到数据库"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
logger.info(f"[脚本执行] 开始为实验{self.experiment_id}执行动态脚本")
|
||||
|
||||
# 获取实验配置
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
cur.execute(
|
||||
"SELECT config_json, work_order_no FROM experiments WHERE id=?",
|
||||
(self.experiment_id,)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
|
||||
if not result:
|
||||
logger.warning(f"[脚本执行] 实验{self.experiment_id}未找到配置")
|
||||
"""执行动态脚本并保存返回数据到数据库(带重试机制)"""
|
||||
max_retries = 3
|
||||
retry_delay = 2 # 秒
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
logger.info(
|
||||
f"[脚本执行] 开始为实验{self.experiment_id}执行动态脚本 "
|
||||
f"(尝试 {attempt + 1}/{max_retries})"
|
||||
)
|
||||
|
||||
# 获取实验配置和时间范围
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
cur.execute(
|
||||
"SELECT config_json, work_order_no, start_ts, end_ts FROM experiments WHERE id=?",
|
||||
(self.experiment_id,)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
|
||||
if not result:
|
||||
logger.warning(f"[脚本执行] 实验{self.experiment_id}未找到配置")
|
||||
db.close()
|
||||
self._mark_save_failed("未找到实验配置")
|
||||
return
|
||||
|
||||
config_json, work_order_no, start_ts, end_ts = result
|
||||
db.close()
|
||||
return
|
||||
|
||||
config_json, work_order_no = result
|
||||
db.close()
|
||||
|
||||
# 解析配置
|
||||
from config_model import AppConfig
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile('w', delete=False, suffix='.json', encoding='utf-8') as tf:
|
||||
tf.write(config_json)
|
||||
snap_path = Path(tf.name)
|
||||
|
||||
config = AppConfig.load(snap_path)
|
||||
|
||||
# 执行脚本
|
||||
from report_generator import _execute_experiment_script
|
||||
script_data = _execute_experiment_script(config)
|
||||
|
||||
if script_data:
|
||||
|
||||
# 解析配置
|
||||
from config_model import AppConfig
|
||||
from tempfile import NamedTemporaryFile
|
||||
import os
|
||||
|
||||
logger.info(f"[脚本执行] 实验{self.experiment_id}正在解析配置...")
|
||||
with NamedTemporaryFile('w', delete=False, suffix='.json', encoding='utf-8') as tf:
|
||||
tf.write(config_json)
|
||||
snap_path = Path(tf.name)
|
||||
|
||||
config = AppConfig.load(snap_path)
|
||||
logger.info(f"[脚本执行] 实验{self.experiment_id}配置解析成功")
|
||||
|
||||
# 设置环境变量(时间范围)
|
||||
prev_start = os.environ.get("EXPERIMENT_START")
|
||||
prev_end = os.environ.get("EXPERIMENT_END")
|
||||
|
||||
try:
|
||||
if start_ts:
|
||||
os.environ["EXPERIMENT_START"] = start_ts
|
||||
logger.info(f"[脚本执行] 设置 EXPERIMENT_START={start_ts}")
|
||||
if end_ts:
|
||||
os.environ["EXPERIMENT_END"] = end_ts
|
||||
logger.info(f"[脚本执行] 设置 EXPERIMENT_END={end_ts}")
|
||||
|
||||
# 执行脚本
|
||||
logger.info(f"[脚本执行] 实验{self.experiment_id}正在执行动态脚本...")
|
||||
from report_generator import _execute_experiment_script
|
||||
script_data = _execute_experiment_script(config)
|
||||
finally:
|
||||
# 恢复环境变量
|
||||
if prev_start is None:
|
||||
os.environ.pop("EXPERIMENT_START", None)
|
||||
else:
|
||||
os.environ["EXPERIMENT_START"] = prev_start
|
||||
if prev_end is None:
|
||||
os.environ.pop("EXPERIMENT_END", None)
|
||||
else:
|
||||
os.environ["EXPERIMENT_END"] = prev_end
|
||||
|
||||
if not script_data:
|
||||
logger.warning(f"[脚本执行] 实验{self.experiment_id}脚本未返回数据")
|
||||
self._mark_save_failed("脚本未返回数据")
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[脚本执行] 实验{self.experiment_id}脚本执行成功,"
|
||||
f"返回数据字段: {list(script_data.keys())}"
|
||||
)
|
||||
|
||||
# 保存脚本数据到 SQLite 数据库
|
||||
logger.info(f"[脚本执行] 实验{self.experiment_id}正在保存数据到 SQLite...")
|
||||
script_data_json = json.dumps(script_data, ensure_ascii=False)
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
cur.execute(
|
||||
"UPDATE experiments SET script_data=? WHERE id=?",
|
||||
"UPDATE experiments SET script_data=?, save_status='success' WHERE id=?",
|
||||
(script_data_json, self.experiment_id)
|
||||
)
|
||||
db.commit()
|
||||
|
|
@ -847,12 +893,57 @@ class ExperimentStateMonitor:
|
|||
|
||||
# 写入 SQL Server(如果配置了)
|
||||
self._write_to_sqlserver(script_data, work_order_no, config)
|
||||
else:
|
||||
logger.warning(f"[脚本执行] 实验{self.experiment_id}脚本未返回数据")
|
||||
|
||||
# 成功保存,退出重试循环
|
||||
logger.info(f"[脚本执行] ✅ 实验{self.experiment_id}数据保存完成")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
is_last_attempt = (attempt == max_retries - 1)
|
||||
|
||||
if is_last_attempt:
|
||||
# 最后一次尝试失败,记录错误并标记失败状态
|
||||
error_msg = f"执行脚本失败(已重试{max_retries}次): {str(e)}"
|
||||
logger.error(
|
||||
f"[脚本执行] ❌ 实验{self.experiment_id}{error_msg}",
|
||||
exc_info=True
|
||||
)
|
||||
self._mark_save_failed(error_msg)
|
||||
else:
|
||||
# 非最后一次尝试,等待后重试
|
||||
logger.warning(
|
||||
f"[脚本执行] ⚠️ 实验{self.experiment_id}执行失败(第{attempt + 1}次尝试),"
|
||||
f"{retry_delay}秒后重试: {e}"
|
||||
)
|
||||
time.sleep(retry_delay)
|
||||
|
||||
def _mark_save_failed(self, error_message: str) -> None:
|
||||
"""标记数据保存失败状态"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
# 更新保存状态为失败,并记录错误信息
|
||||
cur.execute(
|
||||
"""UPDATE experiments
|
||||
SET save_status='failed',
|
||||
save_error=?
|
||||
WHERE id=?""",
|
||||
(error_message, self.experiment_id)
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
logger.error(
|
||||
f"[保存失败] ❌ 实验{self.experiment_id}数据保存失败已标记: {error_message}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[脚本执行] 实验{self.experiment_id}执行脚本失败: {e}",
|
||||
f"[保存失败] 标记失败状态时出错: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
|
|
|||
37
main.py
37
main.py
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import traceback
|
||||
import sys
|
||||
from single_instance import SingleInstance
|
||||
|
||||
# 在导入任何Qt模块之前设置Qt WebEngine环境变量
|
||||
# 这样可以避免PySide6 6.8+版本在Windows上的DirectComposition和GPU崩溃问题
|
||||
|
|
@ -19,18 +20,26 @@ os.environ["QT_OPENGL"] = "software"
|
|||
os.environ["QTWEBENGINE_DISABLE_SANDBOX"] = "1"
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import ui_main
|
||||
ui_main.run_app()
|
||||
except Exception as e:
|
||||
print("=" * 60)
|
||||
print("程序崩溃!错误类型:", type(e).__name__)
|
||||
print("错误信息:", str(e))
|
||||
print("=" * 60)
|
||||
traceback.print_exc()
|
||||
print("=" * 60)
|
||||
input("按Enter键退出...")
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(f"程序异常退出,退出码: {e.code}")
|
||||
with SingleInstance() as can_run:
|
||||
if not can_run:
|
||||
print("=" * 60)
|
||||
print("程序已在运行中,不允许重复启动!")
|
||||
print("=" * 60)
|
||||
input("按Enter键退出...")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import ui_main
|
||||
ui_main.run_app()
|
||||
except Exception as e:
|
||||
print("=" * 60)
|
||||
print("程序崩溃!错误类型:", type(e).__name__)
|
||||
print("错误信息:", str(e))
|
||||
print("=" * 60)
|
||||
traceback.print_exc()
|
||||
print("=" * 60)
|
||||
input("按Enter键退出...")
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(f"程序异常退出,退出码: {e.code}")
|
||||
input("按Enter键退出...")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
class SingleInstance:
|
||||
def __init__(self, app_name="PCM_Report"):
|
||||
self.lockfile = Path(tempfile.gettempdir()) / f"{app_name}.lock"
|
||||
self.fp = None
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
if self.lockfile.exists():
|
||||
# 尝试读取PID,检查进程是否还在运行
|
||||
try:
|
||||
pid = int(self.lockfile.read_text().strip())
|
||||
# 检查进程是否存在
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
PROCESS_QUERY_INFORMATION = 0x0400
|
||||
handle = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
|
||||
if handle:
|
||||
kernel32.CloseHandle(handle)
|
||||
return False # 进程存在,不允许启动
|
||||
else:
|
||||
os.kill(pid, 0) # Unix系统检查
|
||||
return False
|
||||
except (ValueError, ProcessLookupError, OSError):
|
||||
# 进程不存在,删除旧锁文件
|
||||
self.lockfile.unlink(missing_ok=True)
|
||||
|
||||
# 创建锁文件
|
||||
self.lockfile.write_text(str(os.getpid()))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def __exit__(self, *args):
|
||||
try:
|
||||
if self.lockfile.exists():
|
||||
self.lockfile.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试实验结束后自动保存数据的修复方案
|
||||
"""
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def test_save_status_columns():
|
||||
"""测试保存状态字段是否存在"""
|
||||
try:
|
||||
db_path = Path(__file__).parent / "experiments.db"
|
||||
|
||||
if not db_path.exists():
|
||||
logger.error(f"数据库文件不存在: {db_path}")
|
||||
return False
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
# 检查字段
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns = {row[1]: row[2] for row in cur.fetchall()}
|
||||
|
||||
logger.info(f"experiments 表字段: {list(columns.keys())}")
|
||||
|
||||
# 验证新字段
|
||||
has_save_status = 'save_status' in columns
|
||||
has_save_error = 'save_error' in columns
|
||||
|
||||
logger.info(f"save_status 字段存在: {has_save_status}")
|
||||
logger.info(f"save_error 字段存在: {has_save_error}")
|
||||
|
||||
if not has_save_status or not has_save_error:
|
||||
logger.warning("⚠️ 缺少保存状态字段,请运行 add_save_status_columns.py")
|
||||
db.close()
|
||||
return False
|
||||
|
||||
# 查询有结束时间但没有保存状态的实验
|
||||
cur.execute("""
|
||||
SELECT id, work_order_no, end_ts, save_status, save_error
|
||||
FROM experiments
|
||||
WHERE end_ts IS NOT NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
rows = cur.fetchall()
|
||||
logger.info(f"\n最近10个已结束的实验:")
|
||||
logger.info(f"{'ID':<5} {'工单号':<15} {'结束时间':<20} {'保存状态':<10} {'错误信息'}")
|
||||
logger.info("-" * 80)
|
||||
|
||||
for eid, work_order, end_ts, save_status, save_error in rows:
|
||||
status_display = save_status or "未记录"
|
||||
error_display = (save_error[:30] + "...") if save_error and len(save_error) > 30 else (save_error or "")
|
||||
logger.info(f"{eid:<5} {work_order or 'N/A':<15} {end_ts or 'N/A':<20} {status_display:<10} {error_display}")
|
||||
|
||||
db.close()
|
||||
|
||||
logger.info("\n✅ 保存状态字段测试通过")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 测试失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def test_monitor_retry_logic():
|
||||
"""测试监控器的重试逻辑"""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("测试监控器重试逻辑")
|
||||
logger.info("="*80)
|
||||
|
||||
try:
|
||||
from experiment_monitor import ExperimentStateMonitor
|
||||
|
||||
# 检查方法是否存在
|
||||
has_mark_failed = hasattr(ExperimentStateMonitor, '_mark_save_failed')
|
||||
has_execute_save = hasattr(ExperimentStateMonitor, '_execute_and_save_script_data')
|
||||
|
||||
logger.info(f"_mark_save_failed 方法存在: {has_mark_failed}")
|
||||
logger.info(f"_execute_and_save_script_data 方法存在: {has_execute_save}")
|
||||
|
||||
if has_mark_failed and has_execute_save:
|
||||
logger.info("✅ 监控器重试逻辑已实现")
|
||||
return True
|
||||
else:
|
||||
logger.error("❌ 监控器缺少必要的方法")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 测试失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""运行所有测试"""
|
||||
print("\n" + "="*80)
|
||||
print("实验结束自动保存修复方案测试")
|
||||
print("="*80 + "\n")
|
||||
|
||||
results = []
|
||||
|
||||
# 测试1: 数据库字段
|
||||
print("测试1: 检查数据库保存状态字段...")
|
||||
results.append(("数据库字段", test_save_status_columns()))
|
||||
|
||||
# 测试2: 监控器重试逻辑
|
||||
print("\n测试2: 检查监控器重试逻辑...")
|
||||
results.append(("监控器重试", test_monitor_retry_logic()))
|
||||
|
||||
# 汇总结果
|
||||
print("\n" + "="*80)
|
||||
print("测试结果汇总")
|
||||
print("="*80)
|
||||
|
||||
for name, passed in results:
|
||||
status = "✅ 通过" if passed else "❌ 失败"
|
||||
print(f"{name:<20} {status}")
|
||||
|
||||
all_passed = all(passed for _, passed in results)
|
||||
|
||||
print("\n" + "="*80)
|
||||
if all_passed:
|
||||
print("✅ 所有测试通过!修复方案已正确实施")
|
||||
else:
|
||||
print("❌ 部分测试失败,请检查上述错误信息")
|
||||
print("="*80 + "\n")
|
||||
|
||||
return all_passed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试 COT8888 测试工单号功能
|
||||
"""
|
||||
from work_order_query import query_work_order
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def test_cot8888():
|
||||
"""测试 COT8888 工单号"""
|
||||
print("\n" + "="*60)
|
||||
print("测试 COT8888 测试工单号")
|
||||
print("="*60 + "\n")
|
||||
|
||||
# 测试不同的大小写
|
||||
test_cases = ['COT8888', 'cot8888', 'Cot8888']
|
||||
|
||||
for work_order_no in test_cases:
|
||||
print(f"\n测试工单号: {work_order_no}")
|
||||
print("-" * 40)
|
||||
|
||||
result = query_work_order(work_order_no)
|
||||
|
||||
if result:
|
||||
print("✅ 查询成功!")
|
||||
print(f" 工单号: {result.get('work_order_no')}")
|
||||
print(f" 工序号: {result.get('process_no')}")
|
||||
print(f" 工序名称: {result.get('process_name')}")
|
||||
print(f" 零件号: {result.get('part_no')}")
|
||||
print(f" 执行人: {result.get('executor')}")
|
||||
else:
|
||||
print("❌ 查询失败")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("测试完成")
|
||||
print("="*60 + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_cot8888()
|
||||
272
ui_main.py
272
ui_main.py
|
|
@ -1128,6 +1128,9 @@ class MainWindow(QMainWindow):
|
|||
self.logger = get_logger()
|
||||
self.logger.info("MainWindow initialized")
|
||||
|
||||
# 自动检查并更新数据库结构
|
||||
self._auto_migrate_database()
|
||||
|
||||
# 启动界面引用
|
||||
self._splash = None
|
||||
|
||||
|
|
@ -1445,6 +1448,9 @@ class MainWindow(QMainWindow):
|
|||
self._alarm_timer.timeout.connect(self._on_alarm_tick)
|
||||
self._alarm_polling = False
|
||||
self._alarm_timer.start()
|
||||
|
||||
# 启动 PCM_Viewer(延迟执行,确保主窗口完全初始化)
|
||||
QTimer.singleShot(1000, self._auto_start_pcm_viewer)
|
||||
|
||||
def _update_debug_mode_ui(self) -> None:
|
||||
"""根据debug模式更新UI显示"""
|
||||
|
|
@ -3982,6 +3988,45 @@ class MainWindow(QMainWindow):
|
|||
db.close()
|
||||
self._reload_experiments()
|
||||
|
||||
def _auto_migrate_database(self) -> None:
|
||||
"""程序启动时自动检查并更新数据库结构"""
|
||||
try:
|
||||
db_path = APP_DIR / "experiments.db"
|
||||
|
||||
if not db_path.exists():
|
||||
self.logger.info("[数据库迁移] 数据库文件不存在,跳过迁移")
|
||||
return
|
||||
|
||||
db = sqlite3.connect(str(db_path))
|
||||
cur = db.cursor()
|
||||
|
||||
# 检查是否需要添加 save_status 和 save_error 字段
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns = [row[1] for row in cur.fetchall()]
|
||||
|
||||
needs_migration = False
|
||||
|
||||
if 'save_status' not in columns:
|
||||
self.logger.info("[数据库迁移] 添加 save_status 字段...")
|
||||
cur.execute("ALTER TABLE experiments ADD COLUMN save_status TEXT DEFAULT NULL")
|
||||
needs_migration = True
|
||||
|
||||
if 'save_error' not in columns:
|
||||
self.logger.info("[数据库迁移] 添加 save_error 字段...")
|
||||
cur.execute("ALTER TABLE experiments ADD COLUMN save_error TEXT DEFAULT NULL")
|
||||
needs_migration = True
|
||||
|
||||
if needs_migration:
|
||||
db.commit()
|
||||
self.logger.info("[数据库迁移] ✅ 数据库结构更新完成")
|
||||
else:
|
||||
self.logger.info("[数据库迁移] 数据库结构已是最新,无需更新")
|
||||
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[数据库迁移] ❌ 自动迁移失败: {e}", exc_info=True)
|
||||
|
||||
def _reload_experiments(self) -> None:
|
||||
# 保护:如果被后台线程误调用,立即切回主线程执行
|
||||
try:
|
||||
|
|
@ -4024,9 +4069,18 @@ class MainWindow(QMainWindow):
|
|||
try:
|
||||
db = sqlite3.connect(str(APP_DIR / "experiments.db"))
|
||||
cur = db.cursor()
|
||||
|
||||
# 检查是否有新字段(向后兼容)
|
||||
cur.execute("PRAGMA table_info(experiments)")
|
||||
columns = [row[1] for row in cur.fetchall()]
|
||||
has_save_fields = 'save_status' in columns and 'save_error' in columns
|
||||
|
||||
# 根据首页筛选条件拼接查询
|
||||
base_sql = "SELECT id, start_ts, end_ts, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated FROM experiments"
|
||||
# 根据首页筛选条件拼接查询(如果有新字段则包含)
|
||||
if has_save_fields:
|
||||
base_sql = "SELECT id, start_ts, end_ts, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated, save_status, save_error FROM experiments"
|
||||
else:
|
||||
base_sql = "SELECT id, start_ts, end_ts, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated FROM experiments"
|
||||
|
||||
conds: list[str] = []
|
||||
params: list[str] = []
|
||||
|
||||
|
|
@ -4050,14 +4104,31 @@ class MainWindow(QMainWindow):
|
|||
rows = []
|
||||
|
||||
self.exp_history_table.setRowCount(len(rows))
|
||||
for r, (eid, st, et, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated) in enumerate(rows):
|
||||
for r, row_data in enumerate(rows):
|
||||
# 兼容新旧数据库结构
|
||||
if has_save_fields:
|
||||
eid, st, et, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated, save_status, save_error = row_data
|
||||
else:
|
||||
eid, st, et, work_order_no, process_name, part_no, executor, remark, sqlserver_status, is_paused, is_terminated = row_data
|
||||
save_status = None
|
||||
save_error = None
|
||||
|
||||
self.exp_history_table.setItem(r, 0, QTableWidgetItem(str(st or "")))
|
||||
self.exp_history_table.setItem(r, 1, QTableWidgetItem(str(et or "")))
|
||||
self.exp_history_table.setItem(r, 2, QTableWidgetItem(str(work_order_no or "")))
|
||||
self.exp_history_table.setItem(r, 3, QTableWidgetItem(str(process_name or "")))
|
||||
self.exp_history_table.setItem(r, 4, QTableWidgetItem(str(part_no or "")))
|
||||
self.exp_history_table.setItem(r, 5, QTableWidgetItem(str(executor or "")))
|
||||
self.exp_history_table.setItem(r, 6, QTableWidgetItem(str(remark or "")))
|
||||
|
||||
# 备注列:如果保存失败,添加警告标记
|
||||
remark_text = str(remark or "")
|
||||
if save_status == 'failed' and et:
|
||||
remark_text = f"⚠️ 数据保存失败 | {remark_text}" if remark_text else "⚠️ 数据保存失败"
|
||||
remark_item = QTableWidgetItem(remark_text)
|
||||
if save_status == 'failed':
|
||||
remark_item.setForeground(Qt.red)
|
||||
remark_item.setToolTip(f"保存失败原因: {save_error or '未知错误'}")
|
||||
self.exp_history_table.setItem(r, 6, remark_item)
|
||||
|
||||
# 数据库状态列
|
||||
db_status_item = QTableWidgetItem(str(sqlserver_status or ""))
|
||||
|
|
@ -4157,8 +4228,19 @@ class MainWindow(QMainWindow):
|
|||
|
||||
# 保存数据按钮(仅在实验已结束时显示)
|
||||
if et: # 有结束时间
|
||||
btn_save_data = QPushButton("保存数据")
|
||||
btn_save_data.setStyleSheet("QPushButton { background-color: #1976d2; color: white; }")
|
||||
# 根据保存状态设置按钮文本和样式(仅在有新字段时才判断状态)
|
||||
if has_save_fields and save_status == 'failed':
|
||||
btn_save_data = QPushButton("重试保存")
|
||||
btn_save_data.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; }")
|
||||
btn_save_data.setToolTip(f"上次保存失败: {save_error or '未知错误'}\n点击重试")
|
||||
elif has_save_fields and save_status == 'success':
|
||||
btn_save_data = QPushButton("已保存")
|
||||
btn_save_data.setStyleSheet("QPushButton { background-color: #4caf50; color: white; }")
|
||||
btn_save_data.setToolTip("数据已保存,点击可重新保存")
|
||||
else:
|
||||
btn_save_data = QPushButton("保存数据")
|
||||
btn_save_data.setStyleSheet("QPushButton { background-color: #1976d2; color: white; }")
|
||||
|
||||
btn_save_data.clicked.connect(lambda _=False, id=eid: self._execute_script_for_experiment(id))
|
||||
w_save_data = QWidget(); les = QHBoxLayout(); les.setContentsMargins(0,0,0,0); les.addWidget(btn_save_data); les.addStretch(1); w_save_data.setLayout(les)
|
||||
else:
|
||||
|
|
@ -5494,30 +5576,103 @@ class MainWindow(QMainWindow):
|
|||
QMessageBox.critical(self, "错误", f"实验台断电时发生异常: {str(e)}")
|
||||
|
||||
def _open_dashboard_viewer(self):
|
||||
"""打开 PCM_Viewer 全屏展示"""
|
||||
# 选择布局文件
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择看板布局文件",
|
||||
"",
|
||||
"JSON文件 (*.json)"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# 检查文件是否存在
|
||||
from pathlib import Path
|
||||
if not Path(file_path).exists():
|
||||
QMessageBox.warning(self, "警告", "选择的文件不存在!")
|
||||
return
|
||||
|
||||
# 启动 PCM_Viewer 子进程(隐藏模式,通过UDP通信)
|
||||
"""打开 PCM_Viewer 全屏展示(仅发送UDP命令)"""
|
||||
try:
|
||||
self._start_pcm_viewer(file_path)
|
||||
# 第一步:让用户选择配置类型
|
||||
# 传递上次选择的配置类型名称作为默认值
|
||||
default_category_name = self._current_config_category.name if self._current_config_category else None
|
||||
self.logger.info(f"打开数据展示,默认配置类型: {default_category_name}")
|
||||
|
||||
# 使用自定义标题和消息的配置类型选择对话框
|
||||
from config_type_selector import ConfigTypeSelectorDialog
|
||||
category = ConfigTypeSelectorDialog.select_config_type(
|
||||
parent=self,
|
||||
title="数据展示 - 选择展示类型",
|
||||
message="请选择要展示的类型:",
|
||||
default_category_name=default_category_name
|
||||
)
|
||||
|
||||
if category is None:
|
||||
# 用户取消了选择
|
||||
self.logger.info("用户取消了数据展示类型选择")
|
||||
self.statusBar().showMessage("已取消数据展示", 2000)
|
||||
return
|
||||
|
||||
self.logger.info(f"用户为数据展示选择展示类型: {category.name}")
|
||||
|
||||
# 第二步:构建dashboard.json的完整路径
|
||||
# 路径格式: C:\PPRO\PCM_Report\configs\600泵\dashboard.json
|
||||
dashboard_path = category.path / "dashboard.json"
|
||||
|
||||
# 检查文件是否存在
|
||||
if not dashboard_path.exists():
|
||||
QMessageBox.warning(self, "警告", f"展示类型 {category.name} 的dashboard.json文件不存在!\n路径: {dashboard_path}")
|
||||
self.logger.warning(f"dashboard.json不存在: {dashboard_path}")
|
||||
return
|
||||
|
||||
# 第三步:发送UDP命令
|
||||
file_path = str(dashboard_path.resolve()) # 转换为绝对路径字符串
|
||||
self._send_udp_command({
|
||||
'action': 'show_and_fullscreen',
|
||||
'path': file_path
|
||||
})
|
||||
self.logger.info(f"已发送显示命令: {file_path}")
|
||||
self.statusBar().showMessage(f"✓ 已发送看板显示命令: {category.name}", 3000)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"启动看板失败: {str(e)}")
|
||||
self.logger.error(f"启动看板失败: {e}")
|
||||
QMessageBox.critical(self, "错误", f"发送看板命令失败: {str(e)}")
|
||||
self.logger.error(f"发送看板命令失败: {e}")
|
||||
|
||||
def _auto_start_pcm_viewer(self):
|
||||
"""软件启动时自动启动 PCM_Viewer 可执行文件(不发送UDP命令)"""
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
# 获取当前可执行文件所在目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的环境
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 开发环境
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 尝试多个可能的路径
|
||||
possible_paths = [
|
||||
os.path.join(app_dir, "PCM_Viewer.exe"), # 同级目录
|
||||
os.path.join(app_dir, "_internal", "PCM_Viewer.exe"), # _internal 目录
|
||||
r"C:\PPro\PCM_Viewer\dist\PCM_Viewer.exe" # 开发环境备用路径
|
||||
]
|
||||
|
||||
viewer_path = None
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
viewer_path = path
|
||||
break
|
||||
|
||||
if not viewer_path:
|
||||
self.logger.warning(f"未找到 PCM_Viewer.exe,尝试的路径: {possible_paths}")
|
||||
return
|
||||
|
||||
# 检查 PCM_Viewer 是否已在运行
|
||||
viewer_running = self._check_viewer_running()
|
||||
|
||||
if not viewer_running:
|
||||
# 启动 PCM_Viewer(不带参数,正常启动)
|
||||
self.logger.info("自动启动 PCM_Viewer...")
|
||||
subprocess.Popen(
|
||||
[viewer_path],
|
||||
shell=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
self.logger.info("PCM_Viewer 已启动")
|
||||
else:
|
||||
self.logger.info("PCM_Viewer 已在运行,跳过启动")
|
||||
except Exception as e:
|
||||
self.logger.error(f"自动启动 PCM_Viewer 失败: {e}")
|
||||
|
||||
def _start_pcm_viewer(self, layout_path: str, udp_port: int = 9876):
|
||||
"""启动 PCM_Viewer 并通过 UDP 发送显示命令
|
||||
|
|
@ -5531,12 +5686,36 @@ class MainWindow(QMainWindow):
|
|||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
viewer_path = r"F:\PyPro\PCM_Viewer\dist\PCM_Viewer.exe"
|
||||
# 获取当前可执行文件所在目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的环境
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 开发环境
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 尝试多个可能的路径
|
||||
possible_paths = [
|
||||
os.path.join(app_dir, "PCM_Viewer.exe"), # 同级目录
|
||||
os.path.join(app_dir, "_internal", "PCM_Viewer.exe"), # _internal 目录
|
||||
r"C:\PPro\PCM_Viewer\dist\PCM_Viewer.exe" # 开发环境备用路径
|
||||
]
|
||||
|
||||
viewer_path = None
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
viewer_path = path
|
||||
break
|
||||
|
||||
if not viewer_path:
|
||||
self.logger.warning(f"未找到 PCM_Viewer.exe,尝试的路径: {possible_paths}")
|
||||
return
|
||||
|
||||
# 检查 PCM_Viewer 是否已在运行(通过UDP探测)
|
||||
viewer_running = self._check_viewer_running(udp_port)
|
||||
|
||||
print("viewer_running:", viewer_running)
|
||||
if not viewer_running:
|
||||
# 启动 PCM_Viewer(隐藏模式)
|
||||
self.logger.info("启动 PCM_Viewer...")
|
||||
|
|
@ -5559,23 +5738,25 @@ class MainWindow(QMainWindow):
|
|||
self.logger.info(f"已发送显示命令: {layout_path}")
|
||||
|
||||
def _check_viewer_running(self, port: int = 9876) -> bool:
|
||||
"""检查 PCM_Viewer 是否已在运行
|
||||
|
||||
"""检查 PCM_Viewer 是否已在运行(通过进程名检测)
|
||||
|
||||
Args:
|
||||
port: UDP 端口
|
||||
|
||||
port: 保留参数,兼容调用方签名
|
||||
|
||||
Returns:
|
||||
bool: 是否运行中
|
||||
"""
|
||||
import socket
|
||||
import subprocess
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(0.5)
|
||||
# 发送探测命令
|
||||
sock.sendto(b'ping', ('127.0.0.1', port))
|
||||
sock.close()
|
||||
return True
|
||||
except:
|
||||
result = subprocess.run(
|
||||
['tasklist', '/fi', 'imagename eq PCM_Viewer.exe', '/fo', 'csv', '/nh'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
return 'PCM_Viewer.exe' in result.stdout
|
||||
except Exception as e:
|
||||
self.logger.warning(f"进程检测失败,默认视为未运行: {e}")
|
||||
return False
|
||||
|
||||
def _send_udp_command(self, command: dict, port: int = 9876):
|
||||
|
|
@ -5897,10 +6078,11 @@ class MainWindow(QMainWindow):
|
|||
try:
|
||||
db = sqlite3.connect(str(APP_DIR / "experiments.db"))
|
||||
cur = db.cursor()
|
||||
# 设置 end_ts 为当前时间(如果还没有),is_terminated = 0(保持正常状态)
|
||||
# 设置 end_ts 为当前本地时间(如果还没有),is_terminated = 0(保持正常状态)
|
||||
end_time = datetime.datetime.now().isoformat(timespec="seconds")
|
||||
cur.execute(
|
||||
"UPDATE experiments SET end_ts = CASE WHEN end_ts IS NULL THEN datetime('now') ELSE end_ts END, is_terminated = 0 WHERE id = ?",
|
||||
(exp_id,)
|
||||
"UPDATE experiments SET end_ts = CASE WHEN end_ts IS NULL THEN ? ELSE end_ts END, is_terminated = 0 WHERE id = ?",
|
||||
(end_time, exp_id)
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
|
|
|||
|
|
@ -97,6 +97,17 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]:
|
|||
logger.warning("工单号为空")
|
||||
return None
|
||||
|
||||
# 特殊测试工单号:COT8888 - 自动返回假数据用于测试
|
||||
if work_order_no.upper() == 'COT8888':
|
||||
logger.info(f"[测试模式] 检测到测试工单号 {work_order_no},返回假数据")
|
||||
return {
|
||||
'work_order_no': work_order_no,
|
||||
'process_no': 'TEST-8888',
|
||||
'process_name': WORK_ORDER_DB_CONFIG.get('target_process_name', '泵空跑合'),
|
||||
'part_no': 'TEST-PART-8888',
|
||||
'executor': '测试人员'
|
||||
}
|
||||
|
||||
# Debug模式:返回假数据
|
||||
if WORK_ORDER_DB_CONFIG.get('debug_mode', False):
|
||||
logger.info(f"[DEBUG模式] 返回工单号 {work_order_no} 的假数据")
|
||||
|
|
|
|||
Loading…
Reference in New Issue