From da0e41f475f024c70577316d8beb20645c600288 Mon Sep 17 00:00:00 2001 From: risingLee <871066422@qq.com> Date: Wed, 25 Feb 2026 14:03:05 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?=E5=B7=A5=E5=BA=8F=E5=90=8D=E7=A7=B0=E6=9F=A5=E8=AF=A2=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui_main.py | 210 ++++++++++++++++++++++++++++++++++++++++++-- work_order_query.py | 14 +-- 2 files changed, 211 insertions(+), 13 deletions(-) diff --git a/ui_main.py b/ui_main.py index 06131f7..e266c4d 100644 --- a/ui_main.py +++ b/ui_main.py @@ -1196,6 +1196,30 @@ class MainWindow(QMainWindow): self.start_work_order_btn.clicked.connect(self._start_work_order) toolbar_layout.addWidget(self.start_work_order_btn) + # 实验台上电按钮 + self.power_on_btn = QPushButton("实验台上电") + self.power_on_btn.setStyleSheet( + "QPushButton { font-weight:700; font-size:13px; color:white;" + "padding:8px 20px; border-radius:6px; background-color:#4caf50; }" + "QPushButton:hover { background-color:#388e3c; }" + ) + self.power_on_btn.setCursor(Qt.PointingHandCursor) + self.power_on_btn.setFixedHeight(36) + self.power_on_btn.clicked.connect(self._power_on_experiment_table) + toolbar_layout.addWidget(self.power_on_btn) + + # 实验台断电按钮 + self.power_off_btn = QPushButton("实验台断电") + self.power_off_btn.setStyleSheet( + "QPushButton { font-weight:700; font-size:13px; color:white;" + "padding:8px 20px; border-radius:6px; background-color:#f44336; }" + "QPushButton:hover { background-color:#d32f2f; }" + ) + self.power_off_btn.setCursor(Qt.PointingHandCursor) + self.power_off_btn.setFixedHeight(36) + self.power_off_btn.clicked.connect(self._power_off_experiment_table) + toolbar_layout.addWidget(self.power_off_btn) + # 开始记录按钮(仅Debug模式显示) self.start_exp_btn = QPushButton("开始记录") self.start_exp_btn.setStyleSheet( @@ -4441,21 +4465,42 @@ class MainWindow(QMainWindow): self.logger.error(f"Failed to delete abandoned experiment record: {e}") # 只有当最新的记录未结束时才提示 elif end_ts is None: + # 若用户此时关闭程序,视为本次实验“非正常终止”,应标记为作废 reply = QMessageBox.question( self, "实验记录未结束", - f"检测到最新的实验记录未结束(开始时间: {start_ts})\n\n是否结束当前实验记录并关闭程序?", + f"检测到最新的实验记录未结束(开始时间: {start_ts})\n\n" + "关闭程序将把该实验记录标记为【作废】。\n\n" + "是否继续并作废该实验记录?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: - # 用户确认结束实验 + # 用户确认关闭:将该实验标记为作废,而不是已完成 try: - self._end_experiment() - self.logger.info("Experiment ended before closing application") + db = sqlite3.connect(str(APP_DIR / "experiments.db")) + cur = db.cursor() + cur.execute( + """ + UPDATE experiments + SET + end_ts = COALESCE(end_ts, datetime('now')), + is_terminated = 1, + remark = CASE + WHEN remark IS NULL OR remark = '' THEN '作废-手动关闭程序' + WHEN remark LIKE '%作废-手动关闭程序%' THEN remark + ELSE remark || ' [作废-手动关闭程序]' + END + WHERE id = ? + """, + (eid,), + ) + db.commit() + db.close() + self.logger.info(f"[关闭程序] 实验 {eid} 在窗口关闭时被标记为作废") except Exception as e: - self.logger.error(f"Failed to end experiment: {e}") + self.logger.error(f"[关闭程序] 标记实验为作废失败: {e}") event.accept() else: # 用户取消关闭 @@ -5356,12 +5401,94 @@ class MainWindow(QMainWindow): except Exception as e: self.logger.error(f"[UI更新] 处理连接状态变化失败: {e}", exc_info=True) + def _power_on_experiment_table(self) -> None: + """实验台上电按钮点击事件""" + try: + # 确认对话框 + reply = QMessageBox.question( + self, + "确认上电", + "确定要给实验台上电吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + self.logger.info("[实验台上电] 开始执行上电操作") + self.statusBar().showMessage("正在执行实验台上电...", 2000) + + # 写入 Modbus 寄存器:0x5555 = 上电(合闸) + # 参考 pcm-influxdb-debug.py:0x5555 会触发 closeBreaker()(合闸/上电) + success = self._write_modbus_control_register(0x5555) + + if success: + self.logger.info("[实验台上电] ✅ 上电指令发送成功") + self.statusBar().showMessage("✅ 实验台上电成功", 3000) + else: + self.logger.error("[实验台上电] ❌ 上电指令发送失败") + self.statusBar().showMessage("❌ 实验台上电失败,请检查Modbus连接", 5000) + QMessageBox.warning( + self, + "上电失败", + "实验台上电失败,请检查:\n" + "1. Modbus连接配置是否正确\n" + "2. 设备是否在线\n" + "3. 指令值是否正确" + ) + except Exception as e: + self.logger.error(f"[实验台上电] 异常: {e}", exc_info=True) + self.statusBar().showMessage(f"❌ 实验台上电异常: {e}", 5000) + QMessageBox.critical(self, "错误", f"实验台上电时发生异常: {str(e)}") + + def _power_off_experiment_table(self) -> None: + """实验台断电按钮点击事件""" + try: + # 确认对话框 + reply = QMessageBox.question( + self, + "确认断电", + "确定要给实验台断电吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + self.logger.info("[实验台断电] 开始执行断电操作") + self.statusBar().showMessage("正在执行实验台断电...", 2000) + + # 写入 Modbus 寄存器:0x0000 = 断电(开闸) + # 参考 pcm-influxdb-debug.py:0x0000 会触发 openBreaker()(开闸/断电) + success = self._write_modbus_control_register(0x0000) + + if success: + self.logger.info("[实验台断电] ✅ 断电指令发送成功") + self.statusBar().showMessage("✅ 实验台断电成功", 3000) + else: + self.logger.error("[实验台断电] ❌ 断电指令发送失败") + self.statusBar().showMessage("❌ 实验台断电失败,请检查Modbus连接", 5000) + QMessageBox.warning( + self, + "断电失败", + "实验台断电失败,请检查:\n" + "1. Modbus连接配置是否正确\n" + "2. 设备是否在线\n" + "3. 指令值是否正确" + ) + except Exception as e: + self.logger.error(f"[实验台断电] 异常: {e}", exc_info=True) + self.statusBar().showMessage(f"❌ 实验台断电异常: {e}", 5000) + QMessageBox.critical(self, "错误", f"实验台断电时发生异常: {str(e)}") + def _write_modbus_control_register(self, value: int) -> bool: """通过原始Socket直接发送Modbus TCP报文写入控制寄存器1200 完全抛弃pymodbus,使用最底层的socket通信,模拟Modbus Poll的行为 Args: - value: 控制值 (0x5555=继续/开始, 0xAAAA=暂停, 0xFFFF=作废, 1=开始记录, 0=停止记录) + value: 控制值 (0x5555=继续/开始/上电合闸, 0xAAAA=暂停, 0xFFFF=作废, 0x0000=停止/断电开闸, 1=开始记录, 0=停止记录) Returns: bool: 写入是否成功 @@ -6124,6 +6251,74 @@ class MainWindow(QMainWindow): return ts +def _cleanup_incomplete_experiments_on_startup() -> None: + """ + 应用启动时清理上次异常退出遗留的实验记录: + - 对于已经开始(start_ts 不为空)、但没有结束时间(end_ts 为空)、且未标记作废(is_terminated=0)的记录, + 自动标记为作废,并补充 end_ts 和 remark。 + """ + try: + import sqlite3 + from pathlib import Path + from logger import get_logger + + logger = get_logger() + db_path = Path(__file__).parent / "experiments.db" + + if not db_path.exists(): + return + + db = sqlite3.connect(str(db_path)) + cur = db.cursor() + + # 查找未正常结束且未标记作废的实验 + cur.execute( + """ + SELECT id, start_ts, end_ts, is_terminated, remark + FROM experiments + WHERE start_ts IS NOT NULL + AND (end_ts IS NULL OR end_ts = '') + AND (is_terminated IS NULL OR is_terminated = 0) + """ + ) + rows = cur.fetchall() + + if not rows: + db.close() + return + + logger.info("[启动清理] 检测到 %d 条未正常结束的实验记录,将标记为作废", len(rows)) + + for eid, start_ts, end_ts, is_terminated, remark in rows: + cur.execute( + """ + UPDATE experiments + SET + end_ts = COALESCE(end_ts, datetime('now')), + is_terminated = 1, + remark = CASE + WHEN remark IS NULL OR remark = '' THEN '作废-程序异常退出' + WHEN remark LIKE '%作废-程序异常退出%' THEN remark + ELSE remark || ' [作废-程序异常退出]' + END + WHERE id = ? + """, + (eid,), + ) + logger.info("[启动清理] 实验 %s 已标记为作废(程序异常退出)", eid) + + db.commit() + db.close() + + except Exception as e: + try: + from logger import get_logger + logger = get_logger() + logger.error("[启动清理] 清理未完成实验记录失败: %s", e, exc_info=True) + except Exception: + pass + + def run_app() -> None: import sys @@ -6174,6 +6369,9 @@ def run_app() -> None: logger = get_logger() logger.error("数据库初始化失败,程序可能无法正常运行") # 继续运行,但记录错误 + else: + # 数据库结构准备好后,清理上次异常退出遗留的实验记录 + _cleanup_incomplete_experiments_on_startup() except Exception as e: logger = get_logger() logger.error(f"数据库初始化异常: {e}", exc_info=True) diff --git a/work_order_query.py b/work_order_query.py index a323a84..05febc4 100644 --- a/work_order_query.py +++ b/work_order_query.py @@ -89,7 +89,7 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]: 字典包含: - work_order_no: 工单号 - process_no: 工序号 (CSYMBOL) - - process_name: 工序名称 (CNEXTPROCNAME) + - process_name: 工序名称 (CPROCNAME) - part_no: 零件号 (CMATERIALSYMBOL) - executor: 执行人 (CEXCUTOR) """ @@ -169,13 +169,13 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]: sql_query = """ SELECT mte.CSYMBOL, - mte.CNEXTPROCNAME, + mte.CPROCNAME, mpe.CMATERIALSYMBOL, mte.CEXCUTOR FROM MES_TASK_EXTEND mte INNER JOIN MBP_PROJECT_EXTEND mpe ON mte.CPGDID = mpe.CID WHERE mte.CSYMBOL LIKE ? - AND RTRIM(LTRIM(ISNULL(mte.CNEXTPROCNAME, ''))) LIKE ? + AND RTRIM(LTRIM(ISNULL(mte.CPROCNAME, ''))) LIKE ? """ # 添加通配符进行模糊匹配 @@ -193,7 +193,7 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]: work_order_info = { 'work_order_no': work_order_no, 'process_no': row.CSYMBOL if row.CSYMBOL else '', # 工序号 - 'process_name': row.CNEXTPROCNAME if row.CNEXTPROCNAME else '', # 工序名称 + 'process_name': row.CPROCNAME if row.CPROCNAME else '', # 工序名称 'part_no': row.CMATERIALSYMBOL if row.CMATERIALSYMBOL else '', # 零件号 'executor': row.CEXCUTOR if row.CEXCUTOR else '' # 执行人 } @@ -226,13 +226,13 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]: sql_query = """ SELECT mte.CSYMBOL, - mte.CNEXTPROCNAME, + mte.CPROCNAME, mpe.CMATERIALSYMBOL, mte.CEXCUTOR FROM MES_TASK_EXTEND mte INNER JOIN MBP_PROJECT_EXTEND mpe ON mte.CPGDID = mpe.CID WHERE mte.CSYMBOL LIKE %s - AND RTRIM(LTRIM(ISNULL(mte.CNEXTPROCNAME, ''))) LIKE %s + AND RTRIM(LTRIM(ISNULL(mte.CPROCNAME, ''))) LIKE %s """ # 添加通配符进行模糊匹配 @@ -249,7 +249,7 @@ def query_work_order(work_order_no: str) -> Optional[Dict[str, str]]: work_order_info = { 'work_order_no': work_order_no, 'process_no': result.get('CSYMBOL', ''), - 'process_name': result.get('CNEXTPROCNAME', ''), + 'process_name': result.get('CPROCNAME', ''), 'part_no': result.get('CMATERIALSYMBOL', ''), 'executor': result.get('CEXCUTOR', '') }