From aa532ef4ecb2005b3db422cf9bce0e8cd4820dc5 Mon Sep 17 00:00:00 2001 From: zhu-mengmeng <15588200382@163.com> Date: Sat, 19 Jul 2025 16:51:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/jtDB.db | Bin 192512 -> 192512 bytes from pymodbus.py | 10 +- test_register_monitor.py | 1 + utils/modbus_monitor.py | 2 +- utils/register_handlers.py | 15 +- widgets/main_window.py | 196 +- widgets/main_window.py.bak | 4019 ------------------------------------ 7 files changed, 137 insertions(+), 4106 deletions(-) create mode 100644 test_register_monitor.py delete mode 100644 widgets/main_window.py.bak diff --git a/db/jtDB.db b/db/jtDB.db index 54892585e8e8022173e8f9003da83efee9817053..f4c7d20d93c73dc0a1f938b8a3da925df0141684 100644 GIT binary patch delta 1345 zcmb7E%}>){94;LhZnFl&(JVkVf&^i9ydQfz+Jgi~gbhL<^njQcZ^nxUFCHY>K+q^6 zP=ZZJQ2a_v_|nXT1stGf4*m(4Y_KI7kDM^R@8*!P9iU0u-`l6n^E|)j_cpv^3-8z_ zD~nuHRsJH^r&|YCisb2=EsxBaLogA3deVF$@~6c-f4;;L;LiONwgv0i4%?vhjwKK& zE5#PiaP?((S+(2F)e7uzR@&jwiuA^D!cdLd_&UZr4A*_$){Bt@ zgBl{h2Qc#Y00d+-&;>v|7D?u*9*{Q!Vwf`JEMD0cx5B8iRB#PTcz5D^JN zh$Q+3WPtJG9xsNxK33zPMxUd000$HAX5-QD?${H0O@+h1zdYMD=HqM2duyASIvHEp zd%YNs&e3IZWhOB`Gnm*I$-qRCJKLL*vCqf&hD+UrT~o_h+xDz&OWO9u5Os`BD0c=Rbtx1t>? z%p4#c*QEZ|>yp-)PWWRivAHlf)Y6b6uEu^u+~FkFGI54$im$Iw4y7R_sP+4)I3Ppg z!7JWT?g6q)Ckf)zoSxF(uqv&<50T6k-hT@G-2gHTl>O3a*|WoNgdOFuV~}OXAU)k8 zvyTt$ACgD5e>L-_8|H7*CEr)+zAQF}L|rcC@EHW?HUT~fDlRCi7^zqhby67din_M| z0#!^z9T!G`sH1!Yk&sHTsKY{#>ZKAuJ_3;nDN)mLgs2Fr0E)WjIO0`F29ffRhgQD5s@520THoa{XPLLlU_fD22&Xg12qDZkOma9{yzqw2m%ev z01oU8%(D>?zzws^?KlDhQU|P)2hgRX5fBH1toOI9_W@X_0}l%2mj|E%AF~k 0: - self.fill_input_button_style() - logging.info(f"D0寄存器有值({value}),填充上料按钮样式") + # # 当D0寄存器有值时,填充上料按钮样式 + # if address == 0 and value > 0: + # self.fill_input_button_style() + # logging.info(f"D0寄存器有值({value}),填充上料按钮样式") - # 当D4寄存器有值时,填充下料按钮样式 - elif address == 4 and value > 0: - self.fill_output_button_style() - logging.info(f"D4寄存器有值({value}),填充下料按钮样式") + # # 当D4寄存器有值时,填充下料按钮样式 + # elif address == 4 and value > 0: + # self.fill_output_button_style() + # logging.info(f"D4寄存器有值({value}),填充下料按钮样式") - # 当D0寄存器为 0 时,恢复上料按钮样式 - if address == 0 and value == 0: - self.restore_input_button_style() - logging.info(f"D0寄存器为 0 ,恢复上料按钮样式") + # # 当D0寄存器为 0 时,恢复上料按钮样式 + # if address == 0 and value == 0: + # self.restore_input_button_style() + # logging.info(f"D0寄存器为 0 ,恢复上料按钮样式") - # 当D4寄存器为 0 时,恢复下料按钮样式 - elif address == 4 and value == 0: - self.restore_output_button_style() - logging.info(f"D4寄存器为 0 ,恢复下料按钮样式") - elif address == 2 and value == 0: + # # 当D4寄存器为 0 时,恢复下料按钮样式 + # elif address == 4 and value == 0: + # self.restore_output_button_style() + # logging.info(f"D4寄存器为 0 ,恢复下料按钮样式") + # D2寄存器控制上料按钮样式 + if address == 2 and value == 0: + self.restore_input_button_style() + logging.info(f"D2寄存器为 0 ,恢复上料按钮样式") + elif address == 2 and value == 1: + self.fill_input_button_style() + self.fill_start_button_style() + logging.info(f"D2寄存器为 1 ,填充上料按钮样式") + elif address == 2 and value == 0 and address == 3 and value == 0: self.restore_start_button_style() - logging.info(f"D2寄存器为 0 ,恢复开始按钮样式") + logging.info(f"D2寄存器为 0 ,D3寄存器为 0 ,恢复开始按钮样式") + # D3寄存器控制下料按钮样式 elif address == 3 and value == 0: - self.restore_start_button_style() - logging.info(f"D3寄存器为 0 ,恢复开始按钮样式") - elif address ==2 and value == 1: - self.fill_start_button_style() - logging.info(f"D2寄存器为 1 ,填充开始按钮样式") + self.restore_output_button_style() + logging.info(f"D3寄存器为 0 ,恢复下料按钮样式") elif address == 3 and value == 1: + self.fill_output_button_style() self.fill_start_button_style() - logging.info(f"D3寄存器为 1 ,填充开始按钮样式") + logging.info(f"D3寄存器为 1 ,填充下料按钮样式") # 当D11寄存器变为0时,复位D10寄存器为0 elif address == 11 and value == 0: try: diff --git a/widgets/main_window.py.bak b/widgets/main_window.py.bak deleted file mode 100644 index 4000ee8..0000000 --- a/widgets/main_window.py.bak +++ /dev/null @@ -1,4019 +0,0 @@ -import os -import sys -import logging -import json -from datetime import datetime -from pathlib import Path -from utils.modbus_utils import ModbusUtils -from utils.modbus_monitor import get_instance as get_modbus_monitor -from utils.app_mode import AppMode -from apis.gc_api import GcApi -from utils.register_handlers import ( - NGHandler, - WeightDataHandler, - LabelSignalHandler, - MachineStatusHandlers, - LoadingFeedbackHandler, - UnloadingFeedbackHandler, - Error1Handler, - Error2Handler, - Error3Handler, - UnloadingLevelHandler, - UnloadingPositionHandler, - EmergencyStopHandler -) -from utils.electricity_monitor import ElectricityHandler -# 导入PySide6 -from PySide6.QtWidgets import ( - QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel, - QTableWidget, QMenu, QComboBox, QFormLayout, QDialog, QVBoxLayout, - QFrame, QHBoxLayout, QSplitter, QPushButton -) -from PySide6.QtCore import Qt, QTimer, Slot, Signal -from PySide6.QtGui import QBrush, QColor -import time - -# 导入UI -from ui.main_window_ui import MainWindowUI -# 导入相机显示组件 -from widgets.camera_display_widget import CameraDisplayWidget - -# 导入检验配置管理器 -from utils.inspection_config_manager import InspectionConfigManager -# 导入托盘类型管理器 -from utils.pallet_type_manager import PalletTypeManager -# 导入串口管理 -from utils.serial_manager import SerialManager -from widgets.report_dialog import ReportDialog -from widgets.unloading_dialog_widget import UnloadingDialog - -class MainWindow(MainWindowUI): - """主窗口""" - - # 定义信号作为类变量 - loading_feedback_signal = Signal(str, str) # 参数:status_type, desc - unloading_feedback_signal = Signal(str, str) # 参数:status_type, desc - unloading_level_ui_signal = Signal(int) # 用于在主线程中更新下料层数UI - unloading_position_ui_signal = Signal(int) # 用于在主线程中更新下料位置UI - emergency_stop_signal = Signal(int, str) # 用于在主线程中处理急停信号 - - def __init__(self, user_id=None, user_name=None, corp_name=None, corp_id=None): - """初始化主窗口""" - super().__init__(user_id) - - # 初始化用户信息 - self.user_id = user_id - self.user_name = user_name - self.corp_name = corp_name - self.corp_id = corp_id - - # 初始化焦点跟踪器 - from utils.focus_tracker import FocusTracker - self.focus_tracker = FocusTracker.get_instance() - self.focus_tracker.initialize() - - # 连接焦点变化信号 - self.focus_tracker.focus_changed.connect(self._log_focus_widget_info) - - # 初始化系统变量 - self._current_weight = 0.0 # 当前重量 - self._last_weight_time = 0 # 上次称重时间 - self._stability_check_timer = None # 稳定性检查定时器 - self._weight_stable_threshold = 2 # 重量稳定阈值(秒) - self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理 - self._last_processed_weight = 0.0 # 新增:记录上次处理的重量 - - # 线径数据处理相关属性 - self._last_diameter_value = 0 # 最后一次有效的线径值 - self._diameter_stable = False # 保留此属性以避免引用错误 - - # 初始化数据加载状态标志 - self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用 - self._current_order_code = None # 存储当前订单号 - self.init_seq = {} # 初始化轴包装的序号 - - # 初始化拆垛和下料相关的属性 - self._current_stow_num = 0 # 当前拆垛层数 - self._current_unload_num = 0 # 当前下料层数 - self._total_unload_num = 0 - self._current_unload_info = None # 存储当前下料信息 - self._loading_info = None # 存储上料对话框的信息 - self._is_loading_active = False # 标识上料任务是否正在进行 - self._current_gc_qd = 0 # 当前工程号的强度数据 - - # 信号的连接在connect_signals方法中统一处理,不在这里连接 - - # 称重相关变量 - self._current_weight = None # 当前称重值(千克) - self._last_weight_time = None # 最后一次称重时间 - self._weight_stable_threshold = 2 # 重量稳定阈值(秒) - self._stability_check_timer = None # 用于检查重量稳定性的定时器 - - # 设置窗口标题 - if user_name and corp_name: - self.setWindowTitle(f"腾智微丝产线包装系统 ({corp_name})") - - # 加载配置文件 - self.config = self.load_config() - - # 初始化检验配置管理器 - self.inspection_manager = InspectionConfigManager.get_instance() - - # 初始化托盘类型管理器 - self.pallet_type_manager = PalletTypeManager.get_instance() - - # 创建表单布局,用于添加托盘类型选择控件 - self.material_form_layout = QFormLayout() - self.material_content_layout.addLayout(self.material_form_layout) - - self.output_form_layout = QFormLayout() - self.output_content_layout.addLayout(self.output_form_layout) - - # 为下料区添加占位标签,确保它保持为空 - self.output_placeholder = QWidget() - self.output_placeholder.setStyleSheet("background-color: #f0f0f0;") - placeholder_layout = QVBoxLayout(self.output_placeholder) - placeholder_layout.setAlignment(Qt.AlignCenter) - - # 添加标题标签 - title_label = QLabel("下料区") - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("color: #888888;") - title_label.setFont(self.second_title_font) - placeholder_layout.addWidget(title_label) - - self.output_content_layout.addWidget(self.output_placeholder) - - # 添加下料信息标签 - self.unloading_level_label = QLabel("下料层数:--") - self.unloading_position_label = QLabel("下料位置:--") - placeholder_layout.addWidget(self.unloading_level_label) - placeholder_layout.addWidget(self.unloading_position_label) - self.unloading_level_label.setStyleSheet("color: #888888; font-weight: bold;") - self.unloading_position_label.setStyleSheet("color: #888888; font-weight: bold;") - self.unloading_level_label.setFont(self.normal_font) - self.unloading_position_label.setFont(self.normal_font) - - # 创建堆叠部件 - self.stacked_widget = QStackedWidget() - self.stacked_widget.addWidget(self.central_widget) # 主页面 - - # 设置中央部件为堆叠部件 - self.setCentralWidget(self.stacked_widget) - - # # 添加托盘类型选择下拉框 - # self.add_pallet_type_selectors() - - # 连接信号和槽 - self.connect_signals() - - # 默认显示主页面 - self.stacked_widget.setCurrentIndex(0) - - # 配置检验列 - 使用检验配置管理器获取启用的列数和标题 - self.update_inspection_columns() - - # 设置表格上下文菜单 - self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) - - # 加载未完成的检验数据 - self._safe_load_data() - - # 加载已完成检验数据 - self.show_pack_item() - # 创建状态处理器实例 - self.machine_handlers = MachineStatusHandlers() - - # 添加状态显示到状态栏 - self.modbus_status_label = QLabel("Modbus: 未连接") - self.weight_label = QLabel("重量: --") - self.label_status_label = QLabel("贴标: 无贴标") - self.error_status_label = QLabel("故障: 无") - - # 设置样式 - self.error_status_label.setStyleSheet("color: green; font-weight: bold;") - - # 添加到状态栏 - self.statusBar().addPermanentWidget(self.modbus_status_label) - self.statusBar().addPermanentWidget(self.weight_label) - self.statusBar().addPermanentWidget(self.label_status_label) - self.statusBar().addPermanentWidget(self.error_status_label) - self.statusBar().addPermanentWidget(QLabel(" ")) - logging.info(f"主窗口已创建,用户: {user_name}") - - # 初始化串口管理器 - self.serial_manager = SerialManager() - - # 注册串口数据回调函数 - self.register_serial_callbacks() - - # 加载托盘号列表 - self.load_pallet_codes() - - # 恢复开始按钮原始样式 - self.restore_start_button_style() - - # 恢复上料和下料按钮原始样式 - if hasattr(self, 'restore_input_button_style'): - self.restore_input_button_style() - if hasattr(self, 'restore_output_button_style'): - self.restore_output_button_style() - - # 启动Modbus监控,确保电力消耗数据在应用启动时就能显示 - self.setup_modbus_monitor() - - # 更新订单数量和产量统计数据 - self.update_order_statistics() - - logging.info("主窗口初始化时已启动Modbus监控系统") - - def get_axios_num(self,tray_id): - """获取托盘号对应的轴号""" - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - axios_num = inspection_dao.get_axios_num(tray_id) - return axios_num - def get_axios_num_by_order_id(self, order_id): - """获取订单号对应的轴号""" - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - axios_num = inspection_dao.get_axios_num_by_order_id(order_id) - return axios_num - def load_config(self): - """加载配置文件""" - config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json") - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - logging.info(f"已加载配置文件: {config_path}") - return config - except Exception as e: - logging.error(f"加载配置文件失败: {e}") - return {} - - def connect_signals(self): - """连接信号槽""" - # 连接微丝产线表格单元格变更信号槽 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - # 连接菜单动作 - self.main_action.triggered.connect(self.show_main_page) - self.settings_action.triggered.connect(self.show_settings_page) - - # 工程号输入框回车事件 - self.order_edit.returnPressed.connect(self.handle_order_enter) - - # 托盘号输入框回车和切换事件,触发未加载数据查询 - # QComboBox没有returnPressed信号,只有currentTextChanged和activated信号 - self.tray_edit.currentTextChanged.connect(self.handle_tray_changed) - self.tray_edit.activated.connect(self.handle_tray_changed) # 当用户选择一项时触发 - - # 连接按钮事件 - self.input_button.clicked.connect(self.handle_input) - self.output_button.clicked.connect(self.handle_output) - self.start_button.clicked.connect(self.handle_start) - self.stop_button.clicked.connect(self.handle_stop) - self.delete_row_button.clicked.connect(self.handle_delete_row) - - # 连接托盘完成按钮事件 - self.tray_complete_button.clicked.connect(self.handle_tray_complete) - - # 设置表格上下文菜单 - self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) - self.process_table.customContextMenuRequested.connect(self.show_table_context_menu) - - # 不再需要连接相机信号 - - # 连接报表按钮点击事件 - self.report_button.clicked.connect(self.on_report) - - # 连接加载反馈信号 - self.loading_feedback_signal.connect(self._handle_loading_feedback_ui) - - # 连接下料反馈信号 - self.unloading_feedback_signal.connect(self._handle_unloading_feedback_ui) - - # 连接下料层数和位置UI更新信号 - self.unloading_level_ui_signal.connect(self.handle_unloading_level_ui) - self.unloading_position_ui_signal.connect(self.handle_unloading_position_ui) - - # 连接急停信号 - self.emergency_stop_signal.connect(self._handle_emergency_stop_ui) - - def update_inspection_columns(self): - """更新检验列配置 - 使用检验配置管理器获取启用的列数和标题""" - try: - # 获取已启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 获取启用的列数 - column_count = len(enabled_configs) - if column_count == 0: - # 如果没有启用的列,至少显示一列 - column_count = 1 - headers = ["检验项"] - else: - # 如果有启用的列,使用配置的标题 - headers = [config['display_name'] for config in enabled_configs] - - # 设置检验列 - self.set_inspection_columns(column_count, headers) - logging.info(f"已更新检验列配置:{column_count}列, 标题: {headers}") - except Exception as e: - logging.error(f"更新检验列配置失败: {str(e)}") - # 如果更新失败,使用默认配置 - self.set_inspection_columns(1, ["检验项"]) - - def show_main_page(self): - self.stacked_widget.setCurrentWidget(self.central_widget) - - # 更新检验列配置 - self.update_inspection_columns() - - # 加载未完成的检验数据 - self._safe_load_data() - - # 加载托盘号列表 - self.load_pallet_codes() - - logging.info("显示主页面") - - def load_pallet_codes(self): - """从托盘类型管理器加载托盘号并更新到tray_edit""" - try: - # 获取当前文本,以便保留用户选择 - current_text = self.tray_edit.currentText() - - # 清空当前项目 - self.tray_edit.clear() - - # 获取托盘号 - pallet_codes = self.pallet_type_manager.get_pallet_code() - - if pallet_codes and len(pallet_codes) > 0: - # 添加托盘号到下拉框 - self.tray_edit.addItems(pallet_codes) - - # 如果有之前的选择,尝试恢复它 - index = self.tray_edit.findText(current_text) - if index != -1: - self.tray_edit.setCurrentIndex(index) - else: - self.tray_edit.setCurrentIndex(-1) - self.tray_edit.setCurrentText("") - - logging.info(f"已加载托盘号,共 {len(pallet_codes)} 个") - else: - # 如果没有托盘号,则不添加任何项目,保持为空 - logging.warning("未找到托盘号,托盘号列表将为空") - self.tray_edit.setCurrentText("") - - - except Exception as e: - logging.error(f"加载托盘号失败: {str(e)}") - # 如果加载失败,确保下拉框为空 - self.tray_edit.clear() - self.tray_edit.setCurrentText("") - - def show_settings_page(self): - """显示设置页面""" - # 创建设置窗口 - if not hasattr(self, 'settings_window'): - from widgets.settings_window import SettingsWindow - self.settings_window = SettingsWindow(self) - # 连接设置改变信号 - self.settings_window.settings_changed.connect(self.on_settings_changed) - - # 显示设置窗口 - self.settings_window.show() - logging.info("显示设置窗口") - - def on_settings_changed(self): - """设置改变时触发""" - # 重新加载配置 - from utils.config_loader import ConfigLoader - config_loader = ConfigLoader.get_instance() - config_loader.load_config() - self.config = self.load_config() # 重新加载配置到 self.config - - # 更新串口管理器配置 - self.serial_manager.reload_config() - - # 重新打开已配置的串口 - self.serial_manager.auto_open_configured_ports() - - # 重新注册串口回调函数 - self.register_serial_callbacks() - - # 重新加载托盘号 - self.load_pallet_codes() - - logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册米电阻、线径和扫码器回调") - - def handle_input(self): - """处理上料按钮点击事件""" - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 启动监听(不论后续是否确认上料) - # 启动Modbus监控 - if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running(): - self.setup_modbus_monitor() - logging.info("已在上料操作前启动Modbus监控") - - # 启动串口监听 - self.serial_manager.auto_open_configured_ports() - - # 启动键盘监听器 - self.serial_manager.start_keyboard_listener() - logging.info("已在上料操作前启动键盘监听器") - - # 创建上料对话框 - from widgets.loading_dialog_widget import LoadingDialog - dialog = LoadingDialog(parent=self,user_id=self.user_id,user_name=self.user_name,corp_id=self.corp_id) - - # 如果已有上料信息,作为参考显示在对话框中,但允许用户修改 - if self._loading_info and self._current_stow_num > 0: - dialog.order_input.setText(self._loading_info.get('order_code', '')) - dialog.tray_input.setText(self._loading_info.get('tray_code', '')) - dialog.axis_value.setText(self._loading_info.get('axis_value', '--')) - dialog.quantity_value.setText(self._loading_info.get('quantity_value', '--')) - dialog.weight_value.setText(self._loading_info.get('weight_value', '--')) - dialog.pallet_tier_value.setText(str(self._current_stow_num)) - # 不禁用输入框,允许用户修改 - - # 连接订单号信号 - dialog.order_code_signal.connect(self.handle_order_code_received) - - # 显示对话框 - result = dialog.exec() - - # 如果用户确认,则执行上料操作 - if result == QDialog.Accepted: - # 从对话框中获取订单号和托盘号,并更新到主窗口 - order_code = dialog.order_input.text() - tray_code = dialog.tray_input.text() - self._current_order_code = order_code - self.tray_edit.setCurrentText(tray_code) - - # 获取托盘料值作为拆垛层数 - stow_num = dialog.pallet_tier_value.text() - if stow_num == "--" or not stow_num: - QMessageBox.warning(self, "错误", "未获取到托盘料信息,请重试") - return - - # 始终使用用户最新输入的信息 - self._current_stow_num = int(stow_num) - # 保存上料信息 - self._loading_info = { - 'order_code': dialog.order_input.text(), - 'tray_code': dialog.tray_input.text(), - 'axis_value': dialog.axis_value.text(), - 'quantity_value': dialog.quantity_value.text(), - 'weight_value': dialog.weight_value.text(), - } - - # 执行Modbus操作 - modbus = ModbusUtils() - client = modbus.get_client() - try: - # 上料 D0 给到层数,等待点击开始后,进行上料 - success0 = modbus.write_register_until_success(client, 0, self._current_stow_num) - # 读取D0寄存器值 - current_stow_num = modbus.read_holding_register(client, 0) - logging.info(f"上料初始化成功:层数 {current_stow_num} 已写入寄存器0") - if success0: - # 创建状态标签并显示在右上角 - self.show_operation_status("拆垛层数", "input", str(current_stow_num if current_stow_num else self._current_stow_num)) - else: - QMessageBox.information(self, "操作提示", "上料失败") - except Exception as e: - logging.error(f"上料操作失败: {str(e)}") - QMessageBox.critical(self, "错误", f"上料操作失败: {str(e)}") - finally: - modbus.close_client(client) - - def handle_output(self): - """处理下料按钮点击事件""" - try: - # 启动监听(不论后续是否确认下料) - if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running(): - self.setup_modbus_monitor() - self.serial_manager.auto_open_configured_ports() - self.serial_manager.start_keyboard_listener() - - dialog = UnloadingDialog(self, self.user_id) - - # 如果有之前的下料信息,作为参考显示在对话框中,但允许用户修改 - if self._current_unload_info: - dialog.set_unloading_info(self._current_unload_info) - logging.info(f"显示之前的下料信息作为参考") - - if dialog.exec_() == QDialog.Accepted: - # 获取用户最新输入的下料信息 - unloading_info = dialog.get_unloading_info() - - # 始终使用用户最新输入的信息 - self._total_unload_num = int(unloading_info.get('tier', '3')) - self._current_unload_num = int(unloading_info.get('tier', '3')) # 直接使用用户输入的层数,而不是从1开始 - self._current_unload_info = unloading_info - logging.info(f"下料任务设置:总层数={self._total_unload_num}, 当前层数={self._current_unload_num}") - - # 将用户输入的层数写入寄存器 - modbus = ModbusUtils() - client = modbus.get_client() - try: - modbus.write_register_until_success(client, 4, self._current_unload_num) - logging.info(f"下料初始化成功:层数 {self._current_unload_num} 已写入寄存器4") - finally: - modbus.close_client(client) - - # 读取D4寄存器值 - current_unload_num = modbus.read_holding_register(client, 4) - logging.info(f"下料初始化成功:层数 {current_unload_num} 已写入寄存器4") - - # 统一更新UI显示 - tray_code = self._current_unload_info.get('tray_code', '') - self.show_operation_status("码垛层数", "output", f"{current_unload_num if current_unload_num else self._current_unload_num}/{current_unload_num if current_unload_num else self._total_unload_num} ") - else: - logging.info("下料对话框已取消") - except Exception as e: - logging.error(f"处理下料操作失败: {str(e)}") - QMessageBox.critical(self, "错误", f"处理下料操作失败: {str(e)}") - - def restore_start_button_style(self): - """恢复开始按钮的原始样式""" - try: - # 使用与main_window_ui.py中初始化时相同的样式,只恢复背景色 - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - border: 1px solid #4caf50; - } - QPushButton:hover { - background-color: #d7eeda; - } - """ - self.start_button.setStyleSheet(button_style) - logging.info("已恢复开始按钮原始样式") - except Exception as e: - logging.error(f"恢复开始按钮样式失败: {str(e)}") - - def fill_start_button_style(self): - """填充开始按钮样式 - 绿色背景,白色字体""" - try: - # 使用与main_window_ui.py中初始化时相同的样式,只改变背景色和文字颜色 - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - background-color: #4caf50; - color: white; - border: 1px solid #4caf50; - } - QPushButton:hover { - background-color: #45a049; - color: white; - } - """ - self.start_button.setStyleSheet(button_style) - logging.info("已填充开始按钮样式") - except Exception as e: - logging.error(f"填充开始按钮样式失败: {str(e)}") - - def handle_start(self): - """ - 处理开始按钮点击事件 - 根据当前操作类型(上料/下料)写入相应的寄存器 - - 上料: 将当前层数写入D0寄存器,并将D2寄存器设置为1 - - 下料: 确保D3寄存器设置为1,D4寄存器已包含当前下料层数 - """ - modbus = ModbusUtils() - client = modbus.get_client() - try: - # 判断当前操作类型(通过检查当前下料信息是否存在) - if self._current_unload_info and self._current_unload_num > 0: - # 下料模式 - 开始下料操作 - # 确保寄存器3(下料启动)设为1,寄存器4已在handle_output中设置了当前层数 - success2 = modbus.write_register_until_success(client, 2, 1) - success3 = modbus.write_register_until_success(client, 3, 1) - - if success2 and success3: - logging.info(f"开始下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") - QMessageBox.information(self, "操作提示", f"开始下料操作:当前第{self._current_unload_num}层") - # 填充按钮样式 - self.fill_start_button_style() - else: - QMessageBox.warning(self, "错误", "开始下料操作失败") - else: - # 上料模式 - 默认操作 - # 写入当前层数到D0寄存器 - success0 = modbus.write_register_until_success(client, 0, self._current_stow_num) - success2 = modbus.write_register_until_success(client, 2, 1) - - if success0 and success2: - self._is_loading_active = True # 标记上料任务已开始 - logging.info(f"开始上料操作:当前层数 {self._current_stow_num}") - # 填充按钮样式 - self.fill_start_button_style() - else: - QMessageBox.warning(self, "错误", "开始上料操作失败") - - except Exception as e: - logging.error(f"开始操作失败: {str(e)}") - QMessageBox.critical(self, "错误", f"开始操作失败: {str(e)}") - finally: - modbus.close_client(client) - - def handle_stop(self): - """处理停止按钮点击事件,根据当前操作类型(上料/下料)停止相应的操作并关闭modbus监控""" - modbus = ModbusUtils() - client = modbus.get_client() - try: - # 判断当前操作类型(通过检查当前下料信息是否存在) - if self._current_unload_info and self._current_unload_num > 0: - # 下料模式 - 停止下料操作 - success3 = modbus.write_register_until_success(client, 3, 0) - - if success3: - logging.info(f"停止下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") - QMessageBox.information(self, "操作提示", "已停止下料操作") - # 恢复开始按钮和下料按钮原始样式 - self.restore_start_button_style() - self.restore_output_button_style() - else: - QMessageBox.warning(self, "错误", "停止下料操作失败") - else: - # 上料模式 - 停止上料操作 - success2 = modbus.write_register_until_success(client, 2, 0) - - if success2: - self._is_loading_active = False # 标记上料任务已停止 - logging.info("停止上料操作") - QMessageBox.information(self, "操作提示", "已停止上料操作") - # 恢复开始按钮和上料按钮原始样式 - self.restore_start_button_style() - self.restore_input_button_style() - else: - QMessageBox.warning(self, "错误", "停止上料操作失败") - - except Exception as e: - logging.error(f"停止操作失败: {str(e)}") - QMessageBox.critical(self, "错误", f"停止操作失败: {str(e)}") - finally: - modbus.close_client(client) - # 停止Modbus监控 - if hasattr(self, 'modbus_monitor'): - logging.info("停止Modbus监控") - self.modbus_monitor.stop() - # 停止串口监听 - self.serial_manager.stop_keyboard_listener() - self.serial_manager.close_all_ports() - - def clear_operation_status(self, operation_type): - """清除右上角的操作状态显示。""" - status_label_name = f"{operation_type}_status_label" - if hasattr(self, status_label_name): - try: - getattr(self, status_label_name).deleteLater() - delattr(self, status_label_name) - logging.info(f"已清除 '{operation_type}' 状态标签。") - except AttributeError: - pass # Failsafe - - def handle_camera_status(self, is_connected, message): - """相机状态处理的空方法,保留是为了兼容性""" - pass - - def handle_camera_connection(self, is_connected, message): - """相机连接处理的空方法,保留是为了兼容性""" - pass - - def handle_camera_params_changed(self, exposure_time, gain, frame_rate): - """相机参数变更处理的空方法,保留是为了兼容性""" - pass - - def handle_camera_error(self, error_msg): - """相机错误处理的空方法,保留是为了兼容性""" - pass - - def closeEvent(self, event): - """窗口关闭事件""" - # 停止Modbus监控 - if hasattr(self, 'modbus_monitor'): - logging.info("停止Modbus监控") - self.modbus_monitor.stop() - - # 停止串口监听 - self.serial_manager.stop_keyboard_listener() - self.serial_manager.close_all_ports() - - # 接受关闭事件 - event.accept() - - - def handle_order_enter(self): - """处理工程号输入框按下回车事件""" - logging.info("工程号输入框按下回车事件") - # 获取当前输入的工程号 - gc_note = self.order_edit.text().strip() - if gc_note: - logging.info(f"输入的工程号: {gc_note}") - #判断是否是接口,如果不是接口直接添加如果是则走接口 - # 如果开启接口模式,则需要调用接口同步到业务库 - if AppMode.is_api(): - from dao.inspection_dao import InspectionDAO - from apis.gc_api import GcApi - inspection_dao = InspectionDAO() - # 调用接口 - gc_api = GcApi() - response = gc_api.get_gc_info(gc_note) - if response.get("status", False): - gc_info = response.get("data", {}) - self._current_gc_qd = gc_info.get("qd",0) - # 先获取当前 info_table 已有的数据 - order_info = {} - for field_name, label in self.info_values.items(): - order_info_key = self.FIELD_MAPPING.get(field_name) - if order_info_key: - order_info[order_info_key] = label.text() - # 更新/补充 qd 字段 - order_info["qd"] = self._current_gc_qd - # 再调用 update_info_table - self.update_info_table(order_info) - self.add_new_inspection_row(gc_note, self._current_order_code) - else: - self.add_new_inspection_row(gc_note, self._current_order_code) - - else: - logging.warning("工程号为空") - QMessageBox.warning(self, "输入提示", "请输入有效的工程号") - - def add_new_inspection_row(self, gc_note, order_code): - """在微丝产线表格中添加一条新记录,添加到表格末尾 - - Args: - gc_note: 工程号 - order_info: 从接口获取的工程号信息 - """ - try: - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 断开单元格变更信号,避免加载过程中触发保存 - try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass - - # 计算新行的行索引(添加到末尾) - data_start_row = self.process_table.rowCount() - - # 在末尾添加新行 - self.process_table.insertRow(data_start_row) - - # 计算新行的序号(最后一个序号+1) - new_seq = 1 # 默认为1 - if data_start_row > 2: # 如果有其他数据行 - prev_seq_item = self.process_table.item(data_start_row - 1, 0) - if prev_seq_item: - try: - prev_seq = int(prev_seq_item.text()) - new_seq = prev_seq + 1 - except ValueError: - new_seq = data_start_row - 1 # 备选方案:使用行索引作为序号 - - # 添加工程号到表格的第二列 - item = QTableWidgetItem(gc_note) - item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(data_start_row, 1, item) - - # 添加序号到表格的第一列 - item = QTableWidgetItem(str(new_seq)) - item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(data_start_row, 0, item) - - # 获取订单信息 - order_info = self.inspection_manager.get_order_info(order_code) - - # 检验列设置为可编辑状态 - for i, config in enumerate(enabled_configs): - col_index = 2 + i # 检验列从第3列开始 - - # 创建单元格 - item = QTableWidgetItem("") - item.setTextAlignment(Qt.AlignCenter) - - # 如果有order_info数据,尝试匹配字段并设置值 - # if order_info: - # config_name = config.get('name') - # # 检查order_info中是否有与config_name匹配的键 - # if config_name in order_info: - # value = str(order_info[config_name]) - # item = QTableWidgetItem(value) - # item.setTextAlignment(Qt.AlignCenter) - # # 设置单元格背景为浅绿色,表示自动填充 - # item.setBackground(QBrush(QColor("#c8e6c9"))) - - # # 保存到数据库 - # from dao.inspection_dao import InspectionDAO - # inspection_dao = InspectionDAO() - # tray_id = self.tray_edit.currentText() - # data = [{ - # 'position': config.get('position'), - # 'config_id': config.get('id'), - # 'value': value, - # 'status': 'pass', # 默认设置为通过状态 - # 'remark': '', - # 'tray_id': tray_id - # }] - # inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) - # logging.info(f"自动填充字段 {config_name} 值为 {value}") - - # 设置单元格属性以标识其关联的检验项 - item.setData(Qt.UserRole, config.get('id')) - self.process_table.setItem(data_start_row, col_index, item) - - # 包装列设置为可编辑状态 - packaging_start_col = 2 + len(enabled_configs) - for i in range(2): # 贴标和称重 - col_index = packaging_start_col + i - item = QTableWidgetItem("") - item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(data_start_row, col_index, item) - - # 设置表格为可编辑状态 - self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) - - # 重新连接单元格内容变更信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - # 选中新添加的行 - self.process_table.selectRow(data_start_row) - - # 限制最大行数 - self.limit_table_rows(10) # 最多保留10行数据 - - # 将工程号和托盘号保存到数据库,确保能够正确关联 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - tray_id = self.tray_edit.currentText() - - # 为每个检验位置创建一个空记录,确保工程号在数据库中存在 - # 只为没有自动填充值的配置创建空记录 - for config in enabled_configs: - config_name = config.get('name') - # 如果order_info中没有对应的键,或者order_info为None - if not order_info or config_name not in order_info: - data = [{ - 'position': config.get('position'), - 'config_id': config.get('id'), - 'value': '', - 'status': 'init', # 设置初始状态 - 'remark': '', - 'tray_id': tray_id - }] - inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) - - # 为贴标和称重也创建空记录 - for position in [11, 12, 13]: # 11是贴标,12是毛重,13是净重 - data = [{ - 'position': position, - 'config_id': position, - 'value': '', - 'status': 'init', # 设置初始状态 - 'remark': '', - 'tray_id': tray_id - }] - inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) - - # 初始化产品状态为init - inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'init') - logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条,初始状态为init") - - except Exception as e: - logging.error(f"添加新记录失败: {str(e)}") - QMessageBox.warning(self, "添加失败", f"添加新记录失败: {str(e)}") - finally: - # 重新加载数据,确保UI显示正确 - self._safe_load_data() - - def limit_table_rows(self, max_rows): - """限制表格最大行数 - - Args: - max_rows: 最大行数(不包括表头行) - """ - try: - # 计算数据总行数 - data_rows = self.process_table.rowCount() - 2 # 减去表头行 - - # 如果超过最大行数,删除多余的行 - if data_rows > max_rows: - # 要删除的行数 - rows_to_remove = data_rows - max_rows - - # 从最后一行开始删除 - for i in range(rows_to_remove): - self.process_table.removeRow(self.process_table.rowCount() - 1) - - logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove} 行") - - except Exception as e: - logging.error(f"限制表格行数失败: {str(e)}") - - def handle_inspection_cell_changed(self, row, column): - """处理微丝包装单元格内容变更 - - Args: - row: 行索引 - column: 列索引 - """ - try: - # 只处理数据行的检验列变更 - if row < 2: # 忽略表头行 - return - - # 忽略首尾两列(序号和工程号) - if column < 2: - return - - # 获取工程号 - order_item = self.process_table.item(row, 1) - if not order_item: - return - - gc_note = order_item.text().strip() - if not gc_note: - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 判断是否是检验列(非包装列) - packaging_start_col = 2 + len(enabled_configs) - - # 获取单元格内容 - cell_item = self.process_table.item(row, column) - if not cell_item: - return - - value = cell_item.text().strip() - - # 默认设置为通过状态 - status = 'pass' - - # 记录当前正在处理的数据类型,用于日志输出 - data_type = "检验" - - if column >= 2 and column < packaging_start_col: - # 是检验列 - config_index = column - 2 - if config_index < len(enabled_configs): - config = enabled_configs[config_index] - data_type = config['display_name'] - - # 显示临时状态消息 - self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) - - # 设置单元格颜色为浅绿色,表示已填写 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) - - # 保持当前状态不变,由状态管理逻辑处理 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) - - # 保存到数据库 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) - - # 判断是否是包装列 - elif column == packaging_start_col: - # 贴标列 - data_type = "贴标" - self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存贴标数据,position和config_id都是11 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status) - - elif column == packaging_start_col + 1: - # 毛重列 - data_type = "毛重" - self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存毛重数据,position和config_id都是12 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status) - elif column == packaging_start_col + 2: - # 净重列 - data_type = "净重" - self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存净重数据,position和config_id都是13 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status) - - # 记录详细日志 - logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") - - # 检查是否完成检验并更新状态 - self.check_inspection_completed(row) - - except Exception as e: - logging.error(f"处理检验单元格变更失败: {str(e)}") - self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) - finally: - # 延迟一段时间后再触发查询,避免频繁刷新UI - # 但要避免在加载过程中触发新的加载 - if not self._loading_data_in_progress: - QTimer.singleShot(1000, self._safe_load_data) - - def validate_inspection_value(self, config, value): - """验证检验值是否有效 - - Args: - config: 检验配置 - value: 检验值 - - Returns: - bool: 是否有效 - """ - try: - # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 - if config.get('position') in [11, 12]: # 11是贴标,12是称重 - return True - - # 检查值是否为空 - if not value and config.get('required', False): - return False - - # 根据数据类型验证 - data_type = config.get('data_type') - - if data_type == 'number': - # 数值类型验证 - try: - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - - num_value = float(value) - min_value = config.get('min_value') - max_value = config.get('max_value') - - if min_value is not None and num_value < min_value: - return False - - if max_value is not None and num_value > max_value: - return False - - return True - except ValueError: - return False - - elif data_type == 'enum': - # 枚举类型验证 - enum_values = config.get('enum_values') - if enum_values and isinstance(enum_values, list): - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - return value in enum_values - return False - - # 文本类型不做特殊验证 - return True - - except Exception as e: - logging.error(f"验证检验值失败: {str(e)}") - return False - - def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status): - """保存检验数据到数据库 - - Args: - order_id: 订单号 - gc_note: 工程号 - position: 位置序号 - config_id: 配置ID - value: 检验值 - status: 状态 - """ - try: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - modbus = ModbusUtils() - client = modbus.get_client() - - # 获取当前产品状态,优先使用产品状态管理中的状态 - current_status = inspection_dao.get_product_status(order_id, gc_note, tray_id) - - # 如果当前状态不是初始状态,则使用当前状态而不是传入的status - if current_status not in ['', 'init']: - status = current_status - - # 记录保存前的详细日志 - logging.info(f"正在保存检验数据: 工程号={gc_note}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}") - - # 构建数据 - data = [{ - 'position': position, - 'config_id': config_id, - 'value': value, - 'status': status, - 'remark': '', - 'tray_id': tray_id - }] - - # 保存到数据库 - inspection_dao.save_inspection_data(order_id, gc_note, data) - 'value': value, - 'status': status, - 'remark': '', - 'tray_id': tray_id - }] - - # 保存到数据库 - inspection_dao.save_inspection_data(order_id, gc_note, data) - except Exception as e: - logging.error(f"保存检验数据失败: {str(e)}") - # 显示错误消息 - QMessageBox.warning(self, "保存失败", f"保存检验数据错误: {str(e)[:50]}...") - self.statusBar().showMessage(f"保存检验数据错误: {str(e)[:50]}...", 3000) - - def _safe_load_data(self): - """安全地加载数据,避免循环调用""" - # 获取当前托盘号,用于日志记录 - tray_id = self.tray_edit.currentText() - - if self._loading_data_in_progress: - # 如果已经在加载数据,不要再次触发 - logging.debug(f"已有数据加载正在进行,忽略此次请求 (托盘号: {tray_id})") - return - - try: - self._loading_data_in_progress = True - self.load_finished_inspection_data() - logging.info(f"数据加载完成,托盘号: {tray_id}") - except Exception as e: - logging.error(f"安全加载数据失败: {str(e)}, 托盘号: {tray_id}") - # 即使加载失败,也尝试显示包装记录 - try: - self.show_pack_item() - logging.info(f"加载失败后尝试显示包装记录, 托盘号: {tray_id}") - except Exception as ex: - logging.error(f"加载失败后显示包装记录失败: {str(ex)}, 托盘号: {tray_id}") - finally: - self._loading_data_in_progress = False - - def load_finished_inspection_data(self): - """加载未完成的检验数据并显示在表格中""" - # 注意:此方法通常应通过_safe_load_data调用,以防止循环 - try: - # 使用InspectionDAO获取未完成的检验数据 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 使用get_inspection_data_unfinished获取未完成的数据 - unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id) - - # 断开单元格变更信号,避免加载过程中触发保存 - try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass - - # 清空表格现有数据行,只保留表头 - while self.process_table.rowCount() > 2: - self.process_table.removeRow(2) - - if not unfinished_data: - logging.info(f"托盘号 {tray_id} 没有未完成的检验数据") - # 确保表格完全清空,只保留表头行 - self.process_table.setRowCount(2) # 只保留表头的两行 - - # 重新连接单元格变更信号 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass - - # 加载包装记录 - return - - logging.info(f"已加载未完成的检验数据,共 {len(unfinished_data)} 条记录") - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 按工程号分组 - orders_data = {} - for data in unfinished_data: - gc_note = data['gc_note'] - if gc_note not in orders_data: - orders_data[gc_note] = [] - orders_data[gc_note].append(data) - - # 添加数据到表格 - 从第3行开始添加数据 - row_idx = 2 - - # 使用DAO方法按创建时间排序工程号,确保FIFO顺序(最早创建的在最前面) - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - sorted_gc_notes = inspection_dao.get_orders_by_create_time(list(orders_data.keys())) - - for gc_note in sorted_gc_notes: - items = orders_data[gc_note] - - # 添加新行 - self.process_table.insertRow(row_idx) - - # 添加序号到第一列 - seq_item = QTableWidgetItem(str(row_idx - 1)) - seq_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(row_idx, 0, seq_item) - - # 添加工程号到第二列 - order_item = QTableWidgetItem(gc_note) - order_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(row_idx, 1, order_item) - - # 添加检验数据 - for item in items: - position = item['position'] - value = item['value'] if item['value'] else "" - status = item['status'] - config_id = item['config_id'] - - - # 找到对应的列索引 - col_index = None - for i, config in enumerate(enabled_configs): - if config.get('position') == position: - col_index = 2 + i # 检验列从第3列开始 - break - - if col_index is not None: - # 创建单元格并设置值 - cell_item = QTableWidgetItem(str(value)) - cell_item.setTextAlignment(Qt.AlignCenter) - # 存储配置ID,用于保存时确定是哪个检验项 - cell_item.setData(Qt.UserRole, config_id) - # 设置单元格 - self.process_table.setItem(row_idx, col_index, cell_item) - # 添加贴标(11)和称重数据(12) - if position == 11: # 贴标 - # 贴标列索引 = 2(序号和工程号) + 检验列数 - label_col = 2 + len(enabled_configs) - self.process_table.setItem(row_idx, label_col, QTableWidgetItem(str(value))) - elif position == 12: # 称重 - # 称重列索引 = 2(序号和工程号) + 检验列数 + 1(贴标) - weight_col = 2 + len(enabled_configs) + 1 - self.process_table.setItem(row_idx, weight_col, QTableWidgetItem(str(value))) - elif position == 13: # 净重 - # 净重列索引 = 2(序号和工程号) + 检验列数 + 2(贴标和称重) - net_weight_col = 2 + len(enabled_configs) + 2 - self.process_table.setItem(row_idx, net_weight_col, QTableWidgetItem(str(value))) - row_idx += 1 - - # 设置表格为可编辑状态 - self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) - - # 重新连接单元格变更信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - - except Exception as e: - logging.error(f"加载未完成的检验数据失败: {str(e)}") - QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}") - - finally: - # 加载包装记录,但要避免循环调用 - # 设置一个标志,防止 show_pack_item 触发更多的数据加载 - # 只有在_safe_load_data调用此方法,且没有明确设置加载状态的情况下才调用 - has_loading_flag = hasattr(self, '_loading_data_in_progress') - is_loading = getattr(self, '_loading_data_in_progress', False) - - # 如果是被_safe_load_data调用(即已经设置了_loading_data_in_progress),则无需额外设置 - if has_loading_flag and is_loading: - # 直接调用show_pack_item,不改变加载状态 - try: - self.show_pack_item() - logging.info("在load_finished_inspection_data中调用show_pack_item") - except Exception as e: - logging.error(f"在load_finished_inspection_data中调用show_pack_item失败: {str(e)}") - # 否则,这是直接调用此方法(非_safe_load_data),需要设置加载状态 - elif not is_loading: - self._loading_data_in_progress = True - try: - self.show_pack_item() - logging.info("在load_finished_inspection_data中直接调用show_pack_item") - finally: - self._loading_data_in_progress = False - - def load_finished_record_to_package_record(self, order_id, gc_note, tray_id): - """加载已完成检验数据到包装记录 - - Args: - order_id: 工程号 - tray_id: 托盘号 - """ - try: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 获取该工程号的所有检验数据 - inspection_data = inspection_dao.get_inspection_data_by_order(order_id, gc_note, tray_id) - - if not inspection_data: - logging.warning(f"未找到工程号 {gc_note} 托盘号 {tray_id} 的检验数据") - return - - # 获取轴号并保存 - label_value = self.get_axios_num_by_order_id(self._current_order_code) - - # 从检验数据中获取贴标和称重数据 - weight_value = "" - net_weight_value = "" - for item in inspection_data: - if item['position'] == 12: # 称重 - weight_value = item['value'] - elif item['position'] == 13: # 净重 - net_weight_value = item['value'] - - # 只要贴标字段有值,就可以写入包装记录 - if label_value == None or label_value == "": - logging.warning(f"工程号 {order_id} 托盘号 {tray_id} 的贴标字段为空,不添加到包装记录") - return - - - # 获取当前时间作为完成时间 - finish_time = datetime.now() - - # 将数据写入到数据库表 inspection_pack_data - inspection_dao.save_package_record(order_id, tray_id, str(label_value+1), weight_value,net_weight_value, finish_time,gc_note) - - # 回显数据,但避免循环调用 - if not getattr(self, '_loading_data_in_progress'): - self._loading_data_in_progress = True - try: - self.show_pack_item() - finally: - self._loading_data_in_progress = False - - logging.info(f"已将工程号 {order_id} 托盘号 {tray_id} 的检验数据添加到包装记录并回显") - - except Exception as e: - logging.error(f"加载已完成检验数据到包装记录失败: {str(e)}") - QMessageBox.warning(self, "加载失败", f"加载已完成检验数据到包装记录失败: {str(e)}") - def show_pack_item(self): - """显示包装记录""" - try: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - logging.info(f"显示包装记录,当前托盘号: {tray_id}") - - if not tray_id: - logging.warning("托盘号为空,无法显示包装记录") - # 清空表格 - self.record_table.setRowCount(0) - self.update_package_statistics() - return - - # 读取已包装的记录信息 - package_record = inspection_dao.get_package_record(tray_id) - - # 记录获取的数据情况 - if package_record: - logging.info(f"成功获取包装记录,托盘号={tray_id},记录数量={len(package_record)}") - else: - logging.info(f"包装记录为空,托盘号={tray_id}") - - # 清空表格内容 - self.record_table.setRowCount(0) - - # 断开包装记录表的信号连接(如果有) - try: - self.record_table.blockSignals(True) # 使用blockSignals替代手动断开信号 - except Exception as e: - logging.warning(f"阻止信号失败: {str(e)}") - - # 如果没有包装记录,直接返回 - if not package_record: - logging.info(f"托盘号 {tray_id} 没有包装记录数据") - self.update_package_statistics() - self.record_table.blockSignals(False) # 恢复信号 - return - - logging.info(f"托盘号 {tray_id} 已加载包装记录,共 {len(package_record)} 条记录") - - # 添加所有包装记录到表格 - for index, item in enumerate(package_record): - try: - row_index = self.record_table.rowCount() - self.record_table.insertRow(row_index) - - # 设置单元格数据,使用安全的方式访问数据 - cell_data = [ - str(index + 1), # 序号 - str(item[0]) if len(item) > 0 else "", # 订单 - str(item[1]) if len(item) > 1 else "", # 工程号 - str(item[2]) if len(item) > 2 else "", # 品名 - str(item[3]) if len(item) > 3 else "", # 规格 - str(item[4]) if len(item) > 4 else "", # 托号 - str(item[5]) if len(item) > 5 else "", # 轴包装号 - str(item[6]) if len(item) > 6 else "", # 毛重 - str(item[7]) if len(item) > 7 else "", # 净重 - str(item[8]) if len(item) > 8 else "" # 完成时间 - ] - - # 批量设置单元格 - for col, data in enumerate(cell_data): - cell_item = QTableWidgetItem(data) - cell_item.setTextAlignment(Qt.AlignCenter) - self.record_table.setItem(row_index, col, cell_item) - except Exception as e: - logging.error(f"设置第 {index} 行数据时出错: {str(e)}, 数据: {item}") - continue # 继续处理下一行 - - # 恢复信号 - self.record_table.blockSignals(False) - - # 更新包装记录统计数据 - self.update_package_statistics() - logging.info(f"包装记录显示完成,托盘号={tray_id},总记录数={self.record_table.rowCount()}") - - except Exception as e: - logging.error(f"显示包装记录失败: {str(e)}") - self.record_table.blockSignals(False) # 确保信号被恢复 - QMessageBox.warning(self, "显示失败", f"显示包装记录失败: {str(e)}") - def update_package_statistics(self): - """更新包装记录统计数据""" - try: - # 获取包装记录表的行数 - package_count = self.record_table.rowCount() - - # 更新任务表格中的已完成数量 - completed_item = QTableWidgetItem(str(package_count)) - completed_item.setTextAlignment(Qt.AlignCenter) - self.task_table.setItem(2, 2, completed_item) - - # 计算已完成公斤数(如果称重列有数值) - completed_kg = 0 - for row in range(self.record_table.rowCount()): - weight_item = self.record_table.item(row, 6) # 称重列 - if weight_item and weight_item.text(): - try: - completed_kg += float(weight_item.text()) - except ValueError: - pass - - # 更新任务表格中的已完成公斤 - completed_kg_item = QTableWidgetItem(str(completed_kg)) - completed_kg_item.setTextAlignment(Qt.AlignCenter) - self.task_table.setItem(2, 3, completed_kg_item) - - logging.info(f"已更新包装记录统计数据: 完成数量={package_count}, 完成公斤={completed_kg}") - - except Exception as e: - logging.error(f"更新包装记录统计数据失败: {str(e)}") - - def show_table_context_menu(self, pos): - """显示表格上下文菜单 - - Args: - pos: 鼠标位置 - """ - try: - # 获取当前单元格 - cell_index = self.process_table.indexAt(pos) - if not cell_index.isValid(): - return - - row = cell_index.row() - column = cell_index.column() - - # 只对数据行和检验列显示上下文菜单 - if row < 2: # 忽略表头行 - return - - # 获取工程号 - order_id_item = self.process_table.item(row, 1) - if not order_id_item: - return - - order_id = order_id_item.text().strip() - if not order_id: - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 创建上下文菜单 - menu = QMenu(self) - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 判断是否是检验列(非包装列) - packaging_start_col = 2 + len(enabled_configs) - if column >= 2 and column < packaging_start_col: - # 是检验列 - config_index = column - 2 - if config_index < len(enabled_configs): - config = enabled_configs[config_index] - position = config.get('position') - - # 添加查询数据库菜单项 - check_action = menu.addAction("检查数据库记录") - check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id)) - - # 显示菜单 - menu.exec_(self.process_table.viewport().mapToGlobal(pos)) - - except Exception as e: - logging.error(f"显示表格上下文菜单失败: {str(e)}") - - def check_database_record(self, order_id, position, tray_id): - """检查数据库记录 - - Args: - order_id: 工程号 - position: 位置序号 - tray_id: 托盘号 - """ - try: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 获取检验数据 - inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id) - - # 查找对应位置的数据 - matching_data = None - for data in inspection_data: - if data.get('position') == position: - matching_data = data - break - - # 显示结果 - if matching_data: - value = matching_data.get('value') - status = matching_data.get('status') - - message = f"数据库记录:\n\n" - message += f"工程号: {order_id}\n" - message += f"位置: {position}\n" - message += f"值: {value}\n" - message += f"状态: {status}\n" - - QMessageBox.information(self, "数据库记录", message) - else: - QMessageBox.warning(self, "数据库记录", f"未找到工程号 {order_id} 位置 {position} 的数据") - - except Exception as e: - logging.error(f"检查数据库记录失败: {str(e)}") - QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}") - - def show_operation_status(self, status, operation_type, pallet_type): - """在右上角显示操作状态 - - Args: - status: 状态文本 - operation_type: 操作类型 (input/output) - pallet_type: 托盘类型 - """ - # 确定要添加标签的容器 - if operation_type == "input": - container = self.material_content - else: - container = self.output_content - - # 如果已存在状态标签,则移除它 - status_label_name = f"{operation_type}_status_label" - if hasattr(self, status_label_name): - old_label = getattr(self, status_label_name) - old_label.deleteLater() - - # 创建新的状态标签 - status_label = QLabel(f"{status}: {pallet_type}", container) - status_label.setFont(self.second_title_font) - status_label.setStyleSheet("color: red; background-color: transparent;") - status_label.setAlignment(Qt.AlignRight | Qt.AlignTop) - - # 使用绝对定位,放置在右上角 - status_label.setGeometry(container.width() - 250, 5, 240, 30) - - # 确保标签始终保持在顶层显示 - status_label.raise_() - status_label.show() - - # 保存标签引用 - setattr(self, status_label_name, status_label) - - # 保存原始的resize事件处理函数 - if not hasattr(container, "_original_resize_event"): - container._original_resize_event = container.resizeEvent - - # 添加窗口大小变化事件处理,确保标签位置随窗口调整 - container.resizeEvent = lambda event: self.adjust_status_label_position(event, container, status_label) - - def adjust_status_label_position(self, event, container, label): - """调整状态标签位置,确保始终在右上角 - - Args: - event: 窗口大小变化事件 - container: 标签所在的容器 - label: 状态标签 - """ - # 更新标签位置,保持在右上角 - label.setGeometry(container.width() - 250, 5, 240, 30) - - # 调用原始的resizeEvent(如果有的话) - original_resize = getattr(container, "_original_resize_event", None) - if original_resize: - original_resize(event) - - # ==================== Modbus监控系统相关方法 ==================== - - def setup_modbus_monitor(self): - """设置Modbus监控系统""" - # 获取Modbus监控器实例 - self.modbus_monitor = get_modbus_monitor() - - # 注册寄存器处理器 - self._register_modbus_handlers() - - # 连接信号槽 - self._connect_modbus_signals() - - # 启动监控 - self.modbus_monitor.start() - - logging.info("Modbus监控系统已设置") - - def _register_modbus_handlers(self): - """注册寄存器处理器""" - # 获取Modbus监控器实例 - monitor = get_modbus_monitor() - - # 注册D6处理器,处理NG信号 - monitor.register_handler(6, NGHandler(self.machine_handlers.handle_ng)) - - # 注册D11处理器,处理称重数据 - monitor.register_handler(11, WeightDataHandler(self.machine_handlers.handle_weight_data)) - - # 注册D13处理器,处理贴标信号 - monitor.register_handler(13, LabelSignalHandler(self.machine_handlers.handle_label_signal)) - - # 注册D20-D24处理器,处理各种状态信息 - monitor.register_handler(20, LoadingFeedbackHandler(self.handle_loading_feedback)) - monitor.register_handler(21, UnloadingFeedbackHandler(self.handle_unloading_feedback)) - monitor.register_handler(22, Error1Handler(self.machine_handlers.handle_error_1)) - monitor.register_handler(23, Error2Handler(self.machine_handlers.handle_error_2)) - monitor.register_handler(24, Error3Handler(self.machine_handlers.handle_error_3)) - - # 注册急停信号处理器 - monitor.register_handler(25, EmergencyStopHandler(self.handle_emergency_stop)) - - # 注册下料层数和位置处理器 - monitor.register_handler(4, UnloadingLevelHandler(self.handle_unloading_level)) - monitor.register_handler(5, UnloadingPositionHandler(self.handle_unloading_position)) - - # 注册电力消耗处理器并保存引用以便连接信号 - self.electricity_handler = ElectricityHandler() - monitor.register_handler(30, self.electricity_handler) - - logging.info("已注册所有Modbus寄存器处理器") - - def _connect_modbus_signals(self): - """连接Modbus信号""" - # 连接Modbus状态变化信号 - self.modbus_monitor.monitor_status_changed.connect(self.handle_modbus_status_change) - - # 连接寄存器变化信号 - self.modbus_monitor.register_changed.connect(self.handle_register_change) - - # 连接寄存器错误信号 - self.modbus_monitor.register_error.connect(self.handle_register_error) - - # 连接电力数据变化信号 - self.electricity_handler.electricity_data_changed.connect(self.update_electricity_statistics) - - # 连接贴标信号 - self.machine_handlers.label_signal_changed.connect(self.handle_label_signal) - - # 连接称重数据变化信号 - self.machine_handlers.weight_changed.connect(self.handle_weight_data) - - # 连接NG信号 - self.machine_handlers.ng_changed.connect(self.handle_ng) - - # 连接故障信号 - self.machine_handlers.error_1_changed.connect(self.handle_error_1) - self.machine_handlers.error_2_changed.connect(self.handle_error_2) - self.machine_handlers.error_3_changed.connect(self.handle_error_3) - - # 连接上下料反馈信号 - self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback) - self.machine_handlers.unloading_feedback_changed.connect(self.handle_unloading_feedback) - - # 连接急停信号 - self.emergency_stop_signal.connect(self._handle_emergency_stop_ui) - - # 立即更新一次用电量数据 - self.update_electricity_statistics() - - # 立即更新一次订单数量和产量统计数据 - self.update_order_statistics() - - logging.info("已连接所有Modbus信号") - - def update_electricity_statistics(self, value=None): - """更新电力消耗统计数据到项目表格 - - Args: - value: 可选的当前电力消耗值,用于信号触发时 - """ - try: - from dao.electricity_dao import ElectricityDAO - electricity_dao = ElectricityDAO() - - # 获取电力消耗统计数据 - statistics = electricity_dao.get_electricity_statistics() - - # 设置表格项(日、月、年、累计用电量) - # 当日用电量 - day_item = QTableWidgetItem(str(round(statistics['day'], 2))) - day_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(0, 0, day_item) - - # 当月用电量 - month_item = QTableWidgetItem(str(round(statistics['month'], 2))) - month_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(1, 0, month_item) - - # 当年用电量 - year_item = QTableWidgetItem(str(round(statistics['year'], 2))) - year_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(2, 0, year_item) - - # 累计用电量 - all_item = QTableWidgetItem(str(round(statistics['all'], 2))) - all_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(3, 0, all_item) - - # 只在调试级别输出详细信息,减少日志量 - if value is not None: - logging.info(f"电力数据变化触发UI更新:当前值={value}") - else: - logging.debug(f"已更新电力消耗统计数据: 日={statistics['day']}, 月={statistics['month']}, 年={statistics['year']}, 累计={statistics['all']}") - - except Exception as e: - logging.error(f"更新电力消耗统计数据失败: {str(e)}") - - def _convert_to_kg(self, weight_in_g): - """ - 将克转换为千克 - Args: - weight_in_g: 重量(克) - Returns: - float: 重量(千克) - """ - return round(weight_in_g / 1000.0, 3) # 保留3位小数 - - @Slot(int) - def handle_weight_data(self, weight_in_g): - """处理称重数据变化""" - try: - current_time = time.time() - - # 转换重量单位并立即更新UI显示 - weight_in_kg = self._convert_to_kg(weight_in_g) - logging.info(f"[显示] 称重数据: {weight_in_kg}kg (原始值: {weight_in_g}g)") - self.weight_label.setText(f"重量: {weight_in_kg}kg") - - # 检测重量从接近0到较大值的变化,判断为新产品 - if self._current_weight is not None and self._current_weight < 0.1 and weight_in_kg > 0.5: - logging.info(f"检测到新产品放上,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg") - self._weight_processed = False # 重置处理标记,允许处理新产品 - - # 更新当前重量和时间 - self._current_weight = weight_in_kg - self._last_weight_time = current_time - - # 取消之前的定时器(如果存在) - if self._stability_check_timer is not None: - self._stability_check_timer.stop() - self._stability_check_timer.deleteLater() - - # 创建新的定时器 - self._stability_check_timer = QTimer() - self._stability_check_timer.setSingleShot(True) # 单次触发 - self._stability_check_timer.timeout.connect(lambda: self._check_weight_stability(weight_in_kg)) - self._stability_check_timer.start(self._weight_stable_threshold * 1000) # 转换为毫秒 - - # 尝试获取表格行数据,用于日志记录 - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) - - # 记录表格行状态,仅用于日志记录,不影响后续处理 - if data_row >= self.process_table.rowCount(): - logging.warning(f"选中的行 {data_row} 超出了表格范围") - else: - # 获取工程号,仅用于日志记录 - gc_note_item = self.process_table.item(data_row, 1) - if gc_note_item: - gc_note = gc_note_item.text().strip() - if gc_note: - logging.info(f"当前处理的工程号: {gc_note}, 行: {data_row}") - else: - logging.warning("工程号为空") - else: - logging.warning("无法获取工程号") - except Exception as e: - logging.error(f"处理称重数据时发生错误: {str(e)}") - # 确保重新连接信号 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass - - def _check_weight_stability(self, original_weight_kg): - """ - 检查重量是否稳定 - Args: - original_weight_kg: 开始检查时的重量(千克) - """ - try: - # 如果当前重量与定时器启动时的重量相同,说明这段时间内没有新的重量数据 - if self._current_weight == original_weight_kg and self._current_weight > 0.5: - logging.info(f"重量 {original_weight_kg}kg 在{self._weight_stable_threshold}秒内保持稳定") - - # 如果这个重量与上一次处理的重量接近(±0.1kg),且标记已处理,则跳过 - if self._weight_processed and abs(original_weight_kg - self._last_processed_weight) < 0.1: - logging.info(f"跳过处理:重量 {original_weight_kg}kg 与上次处理的重量 {self._last_processed_weight}kg 接近且已处理") - return - - # 尝试写入D10寄存器并处理稳定重量,带有重试机制 - self._handle_stable_weight_with_retry(original_weight_kg, max_retries=3) - else: - logging.info(f"重量在{self._weight_stable_threshold}秒内发生变化,从 {original_weight_kg}kg 变为 {self._current_weight}kg") - except Exception as e: - logging.error(f"检查重量稳定性时发生错误: {str(e)}") - finally: - # 清理定时器 - if self._stability_check_timer is not None: - self._stability_check_timer.deleteLater() - self._stability_check_timer = None - - def _handle_stable_weight_with_retry(self, weight_kg, max_retries=3): - """ - 使用重试机制处理稳定重量 - Args: - weight_kg: 稳定的重量值(千克) - max_retries: 最大重试次数 - """ - retry_count = 0 - success = False - last_error = None - modbus = ModbusUtils() - client = None - - try: - # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) - client = modbus.get_client() - if not client: - logging.error("无法获取Modbus客户端连接") - return - - # 重试机制写入寄存器 - while retry_count < max_retries and not success: - try: - # 称重稳定后,给寄存器 D10 为 1 表示已经称重完成 - success = modbus.write_register_until_success(client, 10, 1) - if success: - logging.info(f"成功写入D10寄存器值为1,表示称重完成") - break - except Exception as e: - last_error = e - retry_count += 1 - if retry_count < max_retries: - delay = 0.5 * (2 ** retry_count) # 指数退避 - logging.warning(f"写入D10寄存器失败,尝试第{retry_count}次重试,等待{delay:.1f}秒: {str(e)}") - time.sleep(delay) - - if not success: - logging.error(f"写入D10寄存器失败,已尝试{max_retries}次: {str(last_error)}") - return - - # 处理稳定重量 - self._process_stable_weight(weight_kg) - # 调用打印方法 - self._print_weight_label(weight_kg) - - # 设置已处理标记和上次处理的重量 - self._weight_processed = True - self._last_processed_weight = weight_kg - logging.info(f"已标记重量 {weight_kg}kg 为已处理") - - except Exception as e: - logging.error(f"处理稳定重量时发生错误: {str(e)}") - finally: - # 释放客户端连接回连接池 - if client: - modbus.close_client(client) - - def _process_stable_weight(self, weight_kg): - """ - 处理稳定的称重数据 - Args: - weight_kg: 稳定的重量值(千克) - """ - try: - # 忽略接近0的重量值,这可能表示产品已被移除 - if weight_kg < 0.1: # 小于100g的重量视为无效 - logging.info(f"忽略接近零的重量值: {weight_kg}kg,可能表示产品已被移除") - return - - # 获取数据行数 - if self.process_table.rowCount() <= 2: # 没有数据行 - logging.warning("没有可用的数据行来写入称重数据") - return - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 计算称重列索引 - 称重位置在检验列之后的第二列(贴标后面) - weight_col = 2 + len(enabled_configs) + 1 - # 计算净重列索引 - 净重位置在检验列之后的第三列(称重后面) - net_weight_col = 2 + len(enabled_configs) + 2 - - # 基于状态查找行:优先查找状态为inspected的行 - data_row = None - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 首先查找状态为inspected的行 - for row in range(2, self.process_table.rowCount()): - gc_note_item = self.process_table.item(row, 1) - if gc_note_item: - row_gc_note = gc_note_item.text().strip() - tray_id = self.tray_edit.currentText() - status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) - if status == 'inspected': - data_row = row - logging.info(f"找到状态为inspected的行: {data_row}, 工程号: {row_gc_note}") - break - - # 如果没有找到inspected状态的行,回退到原有逻辑 - if data_row is None: - # 查找第一个没有称重数据的行 - for row in range(2, self.process_table.rowCount()): - weight_item = self.process_table.item(row, weight_col) - if not weight_item or not weight_item.text().strip(): - data_row = row - break - - # 如果仍然没有找到,使用当前选中行或第一个数据行 - if data_row is None: - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) - logging.info(f"未找到状态为inspected的行或没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") - else: - logging.info(f"找到没有称重数据的行: {data_row}") - else: - logging.info(f"将使用状态为inspected的行: {data_row}") - - # 获取工程号 - gc_note = self.process_table.item(data_row, 1) - if not gc_note: - logging.warning("无法获取工程号") - return - - gc_note = gc_note.text().strip() - if not gc_note: - logging.warning("工程号为空") - return - - # 暂时断开信号连接,避免触发cellChanged信号 - try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass - - # 设置称重值单元格(显示千克) - weight_item = QTableWidgetItem(str(weight_kg)) - weight_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(data_row, weight_col, weight_item) - - # 保存到数据库(使用千克) - tray_id = self.tray_edit.currentText() - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass") - - # 保存净重到数据库(毛重-工字轮重量,单位都是千克) - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - gzl_zl_raw = inspection_dao.get_gzl_zl(self._current_order_code) - gzl_zl = 0.0 - try: - if gzl_zl_raw: - gzl_zl = float(gzl_zl_raw) - except (ValueError, TypeError): - logging.warning(f"无法将工字轮重量 '{gzl_zl_raw}' 转换为浮点数,将使用默认值 0.0") - - net_weight_kg = round(weight_kg - gzl_zl,3) - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass") - - # 设置净重单元格(显示千克) - net_weight_item = QTableWidgetItem(str(net_weight_kg)) - net_weight_item.setTextAlignment(Qt.AlignCenter) - self.process_table.setItem(data_row, net_weight_col, net_weight_item) - - # 如果开启 api 模式,则调用接口添加到包装记录 - if AppMode.is_api(): - from dao.inspection_dao import InspectionDAO - from apis.gc_api import GcApi - inspection_dao = InspectionDAO() - # 调用接口 - gc_api = GcApi() - axios_num = self.get_axios_num_by_order_id(self._current_order_code) + 1 - # 获取订单信息和其他信息,两者都已经是字典格式 - info = {} - order_info = inspection_dao.get_order_info(self._current_order_code) - info.update(order_info) - # 获取包装号 - - info['xpack'] = self.tray_edit.currentText() - info['spack'] = self.tray_edit.currentText() - order_others_info = inspection_dao.get_order_others_info(gc_note, self._current_order_code, tray_id) - info.update(order_others_info) - info['data_corp'] = order_info['data_corp'] - info['zh'] = axios_num - info['mzl'] = weight_kg - info['printsl'] = 1 - info['pono'] = self._current_order_code - info["dycz"] = info.get("cz") - info['qd'] = self._current_gc_qd - # 获取本机IP地址 - # import socket - # try: - # # 通过连接外部服务器获取本机IP(不实际建立连接) - # s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # s.connect(("8.8.8.8", 80)) - # local_ip = s.getsockname()[0] - # s.close() - # info['nw_ip'] = local_ip.replace('.', '') - # except Exception as e: - # logging.error(f"获取本机IP失败: {str(e)}") - # # 如果获取失败,使用本地回环地址 - # info['nw_ip'] = '127.0.0.1'.replace('.', '') - info['nw_ip'] = '192.168.1.246' - # 调用接口添加到包装记录 - response = gc_api.add_order_info(info) - if response.get("status",False): - logging.info(f"添加订单信息成功: {response.get('data',{})}") - else: - QMessageBox.warning(self, f"提示", response.get("message",{})) - - # 保存贴标数据到数据库 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") - - # 重新连接信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}") - - # 更新产品状态为weighed - inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed') - logging.info(f"工程号 {gc_note} 的称重已完成,状态更新为weighed") - - except Exception as e: - logging.error(f"处理称重数据时发生错误: {str(e)}") - # 确保重新连接信号 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass - - def _print_weight_label(self, weight_kg): - """ - 打印重量标签 注意:目前打印是写入数据库打印,不需要再次调用 - Args: - weight_kg: 稳定的重量值(千克) - """ - try: - logging.info(f"开始打印重量标签,重量:{weight_kg}kg") - # TODO: 实现打印逻辑 - pass - except Exception as e: - logging.error(f"打印重量标签时发生错误: {str(e)}") - - @Slot(int, str) - def handle_label_signal(self, signal, status): - """处理贴标信号""" - logging.info(f"[处理] 贴标信号: {status} (值={signal})") - - # 更新UI显示 - self.label_status_label.setText(f"贴标: {status}") - - # 只有当信号为贴标完成(1)时才进行处理 - if signal == 1: - try: - modbus = ModbusUtils() - client = None - try: - # 获取Modbus客户端(现在使用连接池,不会每次都创建新连接) - client = modbus.get_client() - if not client: - logging.error("无法获取Modbus客户端连接") - return - # 先将寄存器回复为0,否则复原周期内、会把新来的数据也pass - modbus.write_register_until_success(client, 13, 0) - except Exception as e: - logging.error(f"复原寄存器失败{e}") - finally: - client.close() - # 获取数据行数 - if self.process_table.rowCount() <= 2: # 没有数据行 - logging.warning("没有可用的数据行来写入贴标数据") - return - - # 基于状态查找行:优先查找状态为weighed的行 - data_row = None - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 首先查找状态为weighed的行 - for row in range(2, self.process_table.rowCount()): - gc_note_item = self.process_table.item(row, 1) - if gc_note_item: - row_gc_note = gc_note_item.text().strip() - tray_id = self.tray_edit.currentText() - status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id) - if status == 'weighed': - data_row = row - logging.info(f"找到状态为weighed的行: {data_row}, 工程号: {row_gc_note}") - break - - # 如果没有找到weighed状态的行,回退到原有逻辑 - if data_row is None: - # 使用当前选中的行或第一个数据行 - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) - logging.info(f"未找到状态为weighed的行,使用当前选中行或第一个数据行: {data_row}") - else: - logging.info(f"将使用状态为weighed的行: {data_row}") - - # 确保行存在 - if data_row >= self.process_table.rowCount(): - logging.warning(f"选中的行 {data_row} 超出了表格范围") - return - - # 获取工程号 - order_id_item = self.process_table.item(data_row, 1) - if not order_id_item: - logging.warning("无法获取工程号") - return - - gc_note = order_id_item.text().strip() - if not gc_note: - logging.warning("工程号为空") - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 计算贴标列索引 - 贴标位置在检验列之后的第一列 - label_col = 2 + len(enabled_configs) - - # 生成贴标号(托盘号+轴号) - axios_num = self.get_axios_num(tray_id)+1 - - # 断开单元格变更信号,避免程序自动写入时触发 - try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass - - # 创建并设置贴标单元格 - label_item = QTableWidgetItem(str(axios_num)) - label_item.setTextAlignment(Qt.AlignCenter) - - # 写入单元格 - self.process_table.setItem(data_row, label_col, label_item) - logging.info(f"已将贴标数据 {axios_num} 写入表格单元格 [{data_row}, {label_col}]") - - # 在这里添加保存贴标数据到数据库的代码 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass") - logging.info(f"已将贴标数据 {axios_num} 保存到数据库") - - # 更新产品状态为labeled - inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled') - logging.info(f"工程号 {gc_note} 的贴标已完成,状态更新为labeled") - - # 调用加载到包装记录的方法 - self.load_finished_record_to_package_record(self._current_order_code,gc_note, tray_id) - logging.info(f"贴标完成,已将工程号 {gc_note} 的记录加载到包装记录") - - # 删除当前处理的行 - self.process_table.removeRow(data_row) - logging.info(f"已删除处理完成的行 {data_row}") - - # 复原寄存器 12 为 0 - modbus = ModbusUtils() - client = modbus.get_client() - modbus.write_register_until_success(client, 12, 0) - modbus.close_client(client) - - # 更新订单数量和产量统计数据 - self.update_order_statistics() - - # 重新连接单元格变更信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - except Exception as e: - logging.error(f"处理贴标完成信号失败: {str(e)}") - # 确保信号重新连接 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass - - @Slot(bool, str) - def handle_modbus_status_change(self, is_connected, message): - """处理Modbus连接状态变化""" - if is_connected: - self.modbus_status_label.setText("Modbus: 已连接") - self.modbus_status_label.setStyleSheet("color: green;") - logging.info(f"Modbus已连接: {message}") - else: - self.modbus_status_label.setText("Modbus: 未连接") - self.modbus_status_label.setToolTip(message) - self.modbus_status_label.setStyleSheet("color: red;") - logging.warning(f"Modbus连接断开: {message}") - - @Slot(int, str) - def handle_register_error(self, address, error_msg): - """处理寄存器读取错误""" - logging.warning(f"[处理] 寄存器D{address}错误: {error_msg}") - # 在这里可以添加错误处理逻辑 - pass - - # 上料按钮样式控制方法 - def restore_input_button_style(self): - """恢复上料按钮的原始样式""" - try: - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - border: 1px solid #2196f3; - } - QPushButton:hover { - background-color: #bbdefb; - } - """ - self.input_button.setStyleSheet(button_style) - logging.info("已恢复上料按钮原始样式") - except Exception as e: - logging.error(f"恢复上料按钮样式失败: {str(e)}") - - def fill_input_button_style(self): - """填充上料按钮样式 - 蓝色背景,白色字体""" - try: - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - background-color: #2196F3; - color: white; - border: 1px solid #2196F3; - } - QPushButton:hover { - background-color: #1e88e5; - color: white; - } - """ - self.input_button.setStyleSheet(button_style) - logging.info("已填充上料按钮样式") - except Exception as e: - logging.error(f"填充上料按钮样式失败: {str(e)}") - - # 下料按钮样式控制方法 - def restore_output_button_style(self): - """恢复下料按钮的原始样式""" - try: - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - border: 1px solid #ffc107; - } - QPushButton:hover { - background-color: #ffecb3; - } - """ - self.output_button.setStyleSheet(button_style) - logging.info("已恢复下料按钮原始样式") - except Exception as e: - logging.error(f"恢复下料按钮样式失败: {str(e)}") - - def fill_output_button_style(self): - """填充下料按钮样式 - 黄色背景,白色字体""" - try: - button_style = """ - QPushButton { - padding: 8px 16px; - font-weight: bold; - border-radius: 4px; - background-color: #FFC107; - color: white; - border: 1px solid #FFC107; - } - QPushButton:hover { - background-color: #ffb300; - color: white; - } - """ - self.output_button.setStyleSheet(button_style) - logging.info("已填充下料按钮样式") - except Exception as e: - logging.error(f"填充下料按钮样式失败: {str(e)}") - - @Slot(int, int) - def handle_register_change(self, address, value): - """处理寄存器变化""" - logging.info(f"[处理] 寄存器D{address}变化: {value}") - - # 当D0寄存器有值时,填充上料按钮样式 - if address == 0 and value > 0: - self.fill_input_button_style() - logging.info(f"D0寄存器有值({value}),填充上料按钮样式") - - # 当D4寄存器有值时,填充下料按钮样式 - elif address == 4 and value > 0: - self.fill_output_button_style() - logging.info(f"D4寄存器有值({value}),填充下料按钮样式") - - # 当D0寄存器为 0 时,恢复上料按钮样式 - if address == 0 and value == 0: - self.restore_input_button_style() - logging.info(f"D0寄存器为 0 ,恢复上料按钮样式") - - # 当D4寄存器为 0 时,恢复下料按钮样式 - elif address == 4 and value == 0: - self.restore_output_button_style() - logging.info(f"D4寄存器为 0 ,恢复下料按钮样式") - elif address == 2 and value == 0: - self.restore_start_button_style() - logging.info(f"D2寄存器为 0 ,恢复开始按钮样式") - elif address == 3 and value == 0: - self.restore_start_button_style() - logging.info(f"D3寄存器为 0 ,恢复开始按钮样式") - elif address ==2 and value == 1: - self.fill_start_button_style() - logging.info(f"D2寄存器为 1 ,填充开始按钮样式") - elif address == 3 and value == 1: - self.fill_start_button_style() - logging.info(f"D3寄存器为 1 ,填充开始按钮样式") - # 当D11寄存器变为0时,复位D10寄存器为0 - elif address == 11 and value == 0: - try: - logging.info("检测到D11寄存器变为0,正在复位D10寄存器") - modbus = ModbusUtils() - client = modbus.get_client() - modbus.write_register_until_success(client, 10, 0) - logging.info("成功复位D10寄存器为0") - modbus.close_client(client) - except Exception as e: - logging.error(f"复位D10寄存器失败: {str(e)}") - @Slot(int, str) - def handle_loading_feedback(self, status, desc): - """处理上料信息反馈""" - message = desc - try: - if status == 1: - modbus = ModbusUtils() - client = modbus.get_client() - # 睡 0.5 秒,用于延缓modbus 监听 - time.sleep(0.5) - modbus.write_register_until_success(client, 2, 0) - if self._current_stow_num > 0: - completed_layer_num = self._current_stow_num - self._current_stow_num -= 1 - if self._current_stow_num == 0: - self._is_loading_active = False # 任务完成,标记为非活动 - self._loading_info = None - logging.info("所有层拆垛完成,清空上料信息") - message = f"第 {completed_layer_num} 层(最后一层)拆垛完成!" - # 重置寄存器 0 和 2 为 0 - modbus.write_register_until_success(client, 0, 0) - modbus.write_register_until_success(client, 2, 0) - self.loading_feedback_signal.emit("input", message) - # 恢复开始按钮原始样式 - self.restore_start_button_style() - else: - logging.info(f"当前层拆垛完成,剩余层数: {self._current_stow_num}") - message = f"第 {completed_layer_num} 层拆垛完成。" - self.loading_feedback_signal.emit("input", message) - #通知寄存器,进行第几层拆垛 - modbus.write_register_until_success(client,0 ,self._current_stow_num) - except Exception as e: - logging.error(f"处理上料信息反馈失败: {str(e)}") - # 不在这里显示对话框,而是通过信号传递错误信息 - self.loading_feedback_signal.emit("error", f"处理上料信息反馈失败: {str(e)}") - finally: - modbus.close_client(client) - - def _handle_loading_feedback_ui(self, status_type, desc): - """在主线程中处理上料UI更新""" - try: - # 如果上料任务仍在进行,更新层数显示 - if self._loading_info and self._current_stow_num > 0: - self.show_operation_status("拆垛层数", "input", str(self._current_stow_num)) - else: - # 上料任务完成,清除状态显示 - self.clear_operation_status("input") - # 上料任务完成,恢复上料按钮样式 - self.restore_input_button_style() - logging.info("上料任务完成,恢复上料按钮样式") - - QMessageBox.information(self, "上料操作", desc) - except Exception as e: - logging.error(f"处理上料UI更新失败: {str(e)}") - - @Slot(int, str) - def handle_unloading_feedback(self, status, desc): - """处理下料信息反馈""" - logging.info(f"[处理] 下料信息: {status}, {desc}") - # 如果下料完成(status=1),显示状态信息,处理下料流程 - if status == 1: - modbus = ModbusUtils() - client = modbus.get_client() - - try: - # 睡 0.5 秒,用于延缓modbus 监听 - time.sleep(0.5) - # 临时重置寄存器3(下料启动)为0,等待用户下一次启动 - modbus.write_register_until_success(client, 3, 0) - - # 如果当前下料层数小于总层数,则将层数加1并写入寄存器4 - if self._current_unload_num < self._total_unload_num: - # 保存当前完成的层数用于消息显示 - completed_tier = self._current_unload_num - - # 当前层已完成,层数加1表示开始下一层 - self._current_unload_num += 1 - logging.info(f"当前层{completed_tier}下料完成,更新到下一层:当前={self._current_unload_num}, 总数={self._total_unload_num}") - - # 将新的层数写入寄存器4 - modbus.write_register_until_success(client, 4, self._current_unload_num) - logging.info(f"已将新层数{self._current_unload_num}写入寄存器4") - - # 不直接更新UI,而是通过信号将数据传递给主线程处理 - # 通过信号触发UI更新 - 显示前一层完成的消息 - message = f"第{completed_tier}层下料完成,请启动第{self._current_unload_num}层下料" - self.unloading_feedback_signal.emit("output", message) - - # 恢复开始按钮原始样式 - self.restore_start_button_style() - else: - # 所有层都下料完成,重置寄存器和计数器 - modbus.write_register_until_success(client, 3, 0) # 确保下料启动寄存器为0 - modbus.write_register_until_success(client, 4, 0) # 重置下料层数寄存器为0 - - # 记录完成的信息用于消息显示 - final_tier = self._current_unload_num - total_tier = self._total_unload_num - tray_code = self._current_unload_info.get('tray_code', '') if self._current_unload_info else '' - - # 重置计数器和信息 - self._current_unload_num = 0 - # 不重置总层数,以便可以继续使用相同的总层数 - # self._total_unload_num = 0 - self._current_unload_info = None - - logging.info(f"托盘 {tray_code} 的所有 {total_tier} 层下料完成,重置当前层数") - - # 通过信号触发UI更新,而不是直接操作UI - message = f"托盘 {tray_code} 的所有 {total_tier} 层下料已全部完成" - self.unloading_feedback_signal.emit("output", message) - - # 恢复开始按钮原始样式 - self.restore_start_button_style() - except Exception as e: - logging.error(f"处理下料反馈时发生错误: {str(e)}") - # 不在这里显示对话框,而是通过信号传递错误信息 - self.unloading_feedback_signal.emit("error", f"处理下料反馈失败: {str(e)}") - finally: - modbus.close_client(client) - - def _handle_unloading_feedback_ui(self, status_type, desc): - """在主线程中处理下料完成的事件通知""" - try: - if status_type == "error": - QMessageBox.critical(self, "错误", desc) - return - - # 显示事件消息 - if "全部完成" in desc: - QMessageBox.information(self, "下料完成", desc) - # 任务完成,清除状态显示 - self.clear_operation_status("output") - self.unloading_level_label.setText("下料层数:--") - self.unloading_position_label.setText("下料位置:--") - # 下料任务完成,恢复下料按钮样式 - self.restore_output_button_style() - logging.info("下料任务完成,恢复下料按钮样式") - elif "请启动" in desc: - QMessageBox.information(self, "下料层完成", desc) - - # 如果当前下料信息存在且层数有效,更新UI显示 - if self._current_unload_info and self._current_unload_num > 0: - self.show_operation_status("下料层数", "output", f"{self._current_unload_num}/{self._total_unload_num}") - - except Exception as e: - logging.error(f"处理下料UI更新失败: {str(e)}") - - def _update_error_status(self): - """更新故障状态显示""" - # 收集所有故障信息 - error_codes = [ - getattr(self, 'error_1', 0), - getattr(self, 'error_2', 0), - getattr(self, 'error_3', 0) - ] - - # 检查是否有故障 - has_error = any(code > 0 for code in error_codes) - - if has_error: - # 收集所有错误信息 - errors = [] - error_map = self.machine_handlers.error_map - - if getattr(self, 'error_1', 0) > 0: - errors.append(f"故障1: {error_map.get(self.error_1, '未知')}") - if getattr(self, 'error_2', 0) > 0: - errors.append(f"故障2: {error_map.get(self.error_2, '未知')}") - if getattr(self, 'error_3', 0) > 0: - errors.append(f"故障3: {error_map.get(self.error_3, '未知')}") - - self.error_status_label.setText("故障: 有") - self.error_status_label.setToolTip("\n".join(errors)) - self.error_status_label.setStyleSheet("color: red; font-weight: bold;") - else: - self.error_status_label.setText("故障: 无") - self.error_status_label.setToolTip("") - self.error_status_label.setStyleSheet("color: green; font-weight: bold;") - - @Slot(int, str) - def handle_error_1(self, error_code, error_desc): - """机器人视觉报警""" - logging.info(f"[处理] 机器人视觉报警: {error_desc}") - from utils.register_handlers import Error1Handler - error_handler = Error1Handler() - detailed_desc = error_handler.error_map.get(error_code, f"机器人视觉报警-{error_code}") - - # 保存故障码 - self.error_1 = error_code - self._update_error_status() - - # 只有当错误码为1、2或3时才弹框提示 - if error_code in [1, 2, 3]: - QMessageBox.warning(self, "机器人视觉报警", f"机器人视觉报警: {detailed_desc}") - # 获取Modbus连接 - modbus = ModbusUtils() - client = modbus.get_client() - - # 根据错误码可以添加不同的处理逻辑 - # 这里先简单处理,对所有错误都复位相关寄存器 - modbus.write_register_until_success(client, 2, 0) - modbus.write_register_until_success(client, 0, 0) - modbus.close_client(client) - - @Slot(int, str) - def handle_error_2(self, error_code, error_desc): - """滚筒线报警""" - logging.info(f"[处理] 滚筒线报警: {error_desc}") - from utils.register_handlers import Error2Handler - error_handler = Error2Handler() - detailed_desc = error_handler.error_map.get(error_code, f"滚筒线报警-{error_code}") - # 保存故障码 - self.error_2 = error_code - self._update_error_status() - - # 如果有故障,显示提示(对任何错误码都弹框) - if error_code in [1, 2]: - QMessageBox.warning(self, "滚筒线报警", f"滚筒线报警: {detailed_desc}") - # 获取Modbus连接 - modbus = ModbusUtils() - client = modbus.get_client() - - # 根据错误码可以添加不同的处理逻辑 - # 这里先简单处理,对所有错误都复位相关寄存器 - modbus.write_register_until_success(client, 3, 0) - modbus.write_register_until_success(client, 4, 0) - modbus.close_client(client) - - @Slot(int, str) - def handle_error_3(self, error_code, error_desc): - """拆码垛报警""" - logging.info(f"[处理] 拆码垛报警: {error_desc}") - from utils.register_handlers import Error3Handler - error_handler = Error3Handler() - detailed_desc = error_handler.error_map.get(error_code, f"拆码垛报警-{error_code}") - # 保存故障码 - self.error_3 = error_code - self._update_error_status() - modbus = ModbusUtils() - client = modbus.get_client() - # 如果有故障,显示提示 - if error_code == 1: - QMessageBox.warning(self, "异常", f"异常: {detailed_desc}") - modbus.write_register_until_success(client, 2, 0) - modbus.write_register_until_success(client, 0, 0) - modbus.close_client(client) - # 移除在下料区域显示异常信息的代码 - elif error_code == 2: - QMessageBox.warning(self, "异常", f"异常: {detailed_desc}") - modbus.write_register_until_success(client, 3, 0) - modbus.write_register_until_success(client, 4, 0) - modbus.close_client(client) - - @Slot(int) - def handle_unloading_level(self, level): - """处理下料层数信息(来自Modbus)""" - # 只在层数发生变化时记录日志 - if self._current_unload_num != level: - logging.info(f"下料层数变化:{self._current_unload_num} -> {level}") - - # 更新当前层数 - self._current_unload_num = level - - # 更新保存的下料信息中的当前层数值 - if self._current_unload_info: - self._current_unload_info['tier'] = str(level) - - # 通过信号在主线程中更新UI - self.unloading_level_ui_signal.emit(level) - else: - # 即使层数相同,也更新UI以确保显示正确 - self.unloading_level_ui_signal.emit(level) - - @Slot(int) - def handle_unloading_level_ui(self, level): - """在主线程中更新下料层数UI""" - try: - # 更新显示 - self.unloading_level_label.setText(f"下料层数:{level}") - - # 如果有下料信息且层数大于0,更新右上角显示 - if level > 0 and self._current_unload_info: - # 确保使用固定的总层数 - total_tier = self._total_unload_num - - # 更新右上角显示,使用实际层数值 - self.show_operation_status("下料层数", "output", f"{level}/{total_tier}") - logging.info(f"更新右上角下料层数显示:{level}/{total_tier}") - except Exception as e: - logging.error(f"更新下料层数UI失败: {str(e)}") - - @Slot(int) - def handle_unloading_position(self, position): - """处理下料位置信息""" - # 通过信号在主线程中更新UI - self.unloading_position_ui_signal.emit(position) - - @Slot(int) - def handle_unloading_position_ui(self, position): - """在主线程中更新下料位置UI""" - try: - self.unloading_position_label.setText(f"下料位置:{position}") - except Exception as e: - logging.error(f"更新下料位置UI失败: {str(e)}") - - @Slot(int) - def handle_ng(self, ng): - """处理NG信号, 将当前处理的数据添加到包装记录中,毛重和净重设为0""" - if ng == 1: - try: - # 获取最后一条数据行 - total_rows = self.process_table.rowCount() - if total_rows <= 2: # 只有表头行,没有数据行 - logging.warning("没有可用的数据行来处理NG信号") - return - - # 使用最后一条数据行 - data_row = total_rows - 1 - - # 获取工程号 - order_id_item = self.process_table.item(data_row, 1) - if not order_id_item: - logging.warning("无法获取工程号") - return - - order_id = order_id_item.text().strip() - if not order_id: - logging.warning("工程号为空") - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 计算贴标列索引 - label_col = 2 + len(enabled_configs) - - # 获取贴标值 - label_item = self.process_table.item(data_row, label_col) - label_value = label_item.text() if label_item else "" - - # 如果贴标值为空,生成一个新的贴标值 - if not label_value: - # 初始化托盘号对应的序号 - if tray_id not in self.init_seq: - self.init_seq[tray_id] = 1 - - # 生成贴标号(托盘号+序号) - label_value = f"{self.init_seq[tray_id]}-NG" - self.init_seq[tray_id] += 1 - - # 保存贴标数据到数据库 - self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass") - else: - # 如果贴标值已存在但不包含NG标记,添加NG标记 - if "NG" not in label_value: - label_value = f"{label_value}-NG" - # 更新贴标数据 - self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass") - - # 设置毛重和净重为0 - self.save_inspection_data(order_id, tray_id, 12, 12, "0", "pass") - self.save_inspection_data(order_id, tray_id, 13, 13, "0", "pass") - - # 获取当前时间作为完成时间 - finish_time = datetime.now() - - # 将数据写入到数据库表 inspection_pack_data - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - inspection_dao.save_package_record(order_id, tray_id, label_value, "0", "0", finish_time) - - # 删除当前处理的行 - self.process_table.removeRow(data_row) - - # 回显数据 - self.show_pack_item() - - # 更新订单数量和产量统计数据 - self.update_order_statistics() - - logging.info(f"NG信号处理完成: 工程号={order_id}, 托盘号={tray_id}, 贴标值={label_value}") - except Exception as e: - logging.error(f"处理NG信号时发生错误: {str(e)}") - finally: - # 复原NG信号 - modbus = ModbusUtils() - client = modbus.get_client() - modbus.write_register_until_success(client, 6, 0) - modbus.close_client(client) - - def register_serial_callbacks(self): - """注册串口数据回调函数""" - try: - # 注册米电阻数据回调 - self.serial_manager.callbacks['mdz_data'] = self.on_mdz_data_received - - # 注册线径数据回调 - self.serial_manager.callbacks['xj_data'] = self.on_diameter_data_received - - # 注册扫码器数据回调 - self.serial_manager.callbacks['scanner_data'] = self.on_scanner_data_received - - # 自动打开已配置的串口 - self.serial_manager.auto_open_configured_ports() - - logging.info("已注册串口数据回调函数") - except Exception as e: - logging.error(f"注册串口数据回调函数失败: {str(e)}") - - def on_mdz_data_received(self, port_name, data): - """米电阻数据接收回调函数 - - Args: - port_name: 串口名称 - data: 接收到的数据 - """ - try: - # 解析数据 - data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) - logging.info(f"收到米电阻数据: {data_str} 来自 {port_name}") - - # 提取米电阻值,格式为"米电阻数据: xxx" - if "米电阻数据:" in data_str: - value_str = data_str.split("米电阻数据:")[1].strip() - try: - # 转换为浮点数 - mdz_value = float(value_str) - - # 查找米电阻对应的检验项配置 - mdz_config = None - enabled_configs = self.inspection_manager.get_enabled_configs() - for config in enabled_configs: - if config.get('name') == 'mdz' or config.get('display_name') == '米电阻': - mdz_config = config - break - - if mdz_config: - # 找到对应的检验项,将数据写入对应的单元格 - self.set_inspection_value('mdz', mdz_config, mdz_value) - else: - logging.warning("未找到米电阻对应的检验项配置") - - except ValueError: - logging.warning(f"米电阻数据格式错误: {value_str}") - else: - logging.warning(f"收到的数据不包含米电阻数据标记: {data_str}") - except Exception as e: - logging.error(f"处理米电阻数据失败: {str(e)}") - - def on_diameter_data_received(self, port_name, data): - """线径数据接收回调函数 - 采用类似称重的逻辑,不使用会话机制""" - try: - data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) - logging.info(f"收到线径数据: {data_str} 来自 {port_name}") - - # 提取线径值 - if "线径数据:" in data_str: - value_str = data_str.split("线径数据:")[1].strip() - try: - xj_value = round(float(value_str)/10000, 3) - self.statusBar().showMessage(f"线径数据: {xj_value:.3f}", 2000) - - # 查找线径对应的检验项配置和列 - xj_config = None - xj_column = None - enabled_configs = self.inspection_manager.get_enabled_configs() - for i, config in enumerate(enabled_configs): - if config.get('name') == 'xj' or config.get('display_name') == '线径': - xj_config = config - xj_column = 2 + i - break - if not xj_config or xj_column is None: - logging.warning("未找到线径对应的检验项配置或列索引") - return - - # 忽略接近0的值或异常值 - if xj_value < 0.001 or xj_value > 10: - logging.info(f"忽略异常线径值: {xj_value}") - return - - # 保存测量值到内部列表用于稳定性检测 - # 使用类属性存储最近的测量值,用于稳定性检测 - if not hasattr(self, '_diameter_measurements'): - self._diameter_measurements = [] - self._diameter_measurements.append(xj_value) - if len(self._diameter_measurements) > 5: - self._diameter_measurements.pop(0) - - # 显示临时值到状态栏 - if len(self._diameter_measurements) < 5: - self.statusBar().showMessage(f"线径数据收集中: {xj_value:.3f} ({len(self._diameter_measurements)}/5)", 2000) - return - - # 检查稳定性 - measurements = self._diameter_measurements[-5:] - min_value = min(measurements) - max_value = max(measurements) - avg_value = sum(measurements) / len(measurements) - error_range = avg_value * 0.04 # 允许4%误差 - - if max_value - min_value <= error_range: - # 数据稳定,可以保存 - final_value = avg_value # 使用平均值作为最终值 - - # 查找第一个没有线径数据的行 - data_row = None - for row in range(2, self.process_table.rowCount()): - cell_item = self.process_table.item(row, xj_column) - if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0': - data_row = row - break - - # 如果没找到空行,使用当前选中行或第一个数据行 - if data_row is None: - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 - logging.info(f"未找到没有线径数据的行,使用当前选中行或第一个数据行: {data_row}") - else: - logging.info(f"找到没有线径数据的行: {data_row}") - - # 获取工程号 - gc_note_item = self.process_table.item(data_row, 1) - if not gc_note_item: - logging.warning("无法获取工程号") - return - - gc_note = gc_note_item.text().strip() - if not gc_note: - logging.warning("工程号为空") - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 公差校验 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - bccd, tccd = inspection_dao.get_xj_range(self._current_order_code) - - if bccd is not None and tccd is not None: - if float(bccd) - 0.5 <= final_value <= float(tccd) + 0.5: - # 使用set_inspection_value保存数据 - self.set_inspection_value('xj', xj_config, final_value) - logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") - else: - reply = QMessageBox.question( - self, - '确认保存', - f"线径 {final_value:.3f} 不在公差范围内 ({bccd} - {tccd}),\n是否继续保存?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - if reply == QMessageBox.Yes: - self.set_inspection_value('xj', xj_config, final_value) - logging.info(f"已将超出公差范围的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") - else: - logging.info(f"用户取消保存超出范围的线径值: {final_value:.3f}") - else: - # 无公差范围,直接保存 - self.set_inspection_value('xj', xj_config, final_value) - logging.info(f"已将线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})") - - # 重置测量列表,准备下一次测量 - self._diameter_measurements = [] - self.statusBar().showMessage(f"线径数据已保存: {final_value:.3f}", 2000) - else: - # 数据不稳定,继续收集 - self.statusBar().showMessage(f"线径数据不稳定: {min_value:.3f} - {max_value:.3f}, 继续测量", 2000) - logging.warning(f"线径测量数据不稳定,范围: {min_value:.3f} - {max_value:.3f}, 平均值: {avg_value:.3f}") - - except ValueError: - logging.warning(f"线径数据格式错误: {value_str}") - else: - logging.warning(f"收到的数据不包含线径数据标记: {data_str}") - except Exception as e: - logging.error(f"处理线径数据失败: {str(e)}") - - def _save_diameter_to_order(self, order_id, config, value): - """基于工程号保存线径值,确保即使行号变化也能保存到正确的产品""" - try: - # 查找工程号对应的行 - target_row = None - for row in range(2, self.process_table.rowCount()): - order_id_item = self.process_table.item(row, 1) - if order_id_item and order_id_item.text().strip() == order_id: - target_row = row - break - - if target_row is not None: - # 使用set_inspection_value保存数据 - self.set_inspection_value('xj', config, value) - logging.info(f"已将线径值 {value:.3f} 保存到工程号 {order_id} (行 {target_row})") - else: - logging.warning(f"找不到工程号 {order_id} 对应的行,无法保存线径值") - except Exception as e: - logging.error(f"保存线径值到工程号失败: {str(e)}") - - def _reset_diameter_session(self): - """重置线径测量会话状态""" - self._diameter_session_active = False - self._diameter_session_target_row = None - self._diameter_session_order_id = None - self._diameter_session_measurements = [] - self._diameter_session_start_time = 0 - - def on_scanner_data_received(self, port_name, data): - """扫码器数据接收回调函数 - - Args: - port_name: 串口名称 - data: 接收到的数据 - """ - try: - # 解析数据 - data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) - logging.info(f"收到扫码器数据: {data_str} 来自 {port_name}") - - # 提取扫码数据,格式为"扫码数据: xxx" - if "扫码数据:" in data_str: - scan_data = data_str.split("扫码数据:")[1].strip() - logging.info(f"提取到扫码数据: {scan_data}") - - # 使用焦点跟踪器设置文本并触发回车事件 - from utils.focus_tracker import FocusTracker - focus_tracker = FocusTracker.get_instance() - - if focus_tracker.set_text_and_trigger_enter(scan_data): - logging.info(f"已将扫码数据 '{scan_data}' 设置到当前焦点输入框并触发回车事件") - else: - # 如果没有焦点输入框,则默认设置到工程号输入框 - logging.info(f"没有焦点输入框,默认将扫码数据设置到工程号输入框") - - # 临时断开returnPressed信号连接,避免setText触发信号 - try: - self.order_edit.returnPressed.disconnect(self.handle_order_enter) - except Exception: - logging.debug("returnPressed信号未连接或断开失败") - - # 设置工程号到输入框 - self.order_edit.setText(scan_data) - - # 重新连接returnPressed信号 - self.order_edit.returnPressed.connect(self.handle_order_enter) - - # 调用一次handle_order_enter方法 - self.handle_order_enter() - else: - logging.warning(f"收到的数据不包含扫码数据标记: {data_str}") - except Exception as e: - logging.error(f"处理扫码器数据失败: {str(e)}") - # 确保信号重新连接 - try: - self.order_edit.returnPressed.connect(self.handle_order_enter) - except Exception: - pass - - def set_inspection_value(self, data_type, config, value): - """设置检验项目值到表格中 - - Args: - data_type: 数据类型,'mdz'表示米电阻,'xj'表示线径 - config: 检验项配置 - value: 检验值 - """ - try: - # 获取检验项的列索引 - config_id = config.get('id') - config_position = config.get('position') - col_index = None - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 根据检验项配置查找对应的列索引 - for i, cfg in enumerate(enabled_configs): - if cfg.get('id') == config_id: - col_index = 2 + i # 检验列从第3列开始 - break - - if col_index is None: - logging.warning(f"未找到{data_type}对应的列索引") - return - - # 检查表格是否有数据行 - if self.process_table.rowCount() <= 2: # 只有表头行 - order_id = self.order_edit.text().strip() - if order_id: - self.add_new_inspection_row(order_id) - data_row = 2 # 新添加的行 - else: - logging.warning("无法添加新行,订单号为空") - return - - # 对于线径数据,如果有活跃会话,优先使用会话锁定的工程号 - if data_type == 'xj' and hasattr(self, '_diameter_session_active') and self._diameter_session_active and self._diameter_session_order_id: - # 查找会话工程号对应的行 - data_row = None - for row in range(2, self.process_table.rowCount()): - order_id_item = self.process_table.item(row, 1) - if order_id_item and order_id_item.text().strip() == self._diameter_session_order_id: - data_row = row - break - - if data_row is not None: - logging.info(f"使用线径会话锁定的工程号 {self._diameter_session_order_id} 对应的行 {data_row}") - else: - logging.warning(f"找不到线径会话锁定的工程号 {self._diameter_session_order_id} 对应的行,将使用默认行选择逻辑") - # 继续使用默认逻辑 - data_row = None - else: - # 默认行选择逻辑 - data_row = None - - # 如果没有找到特定行,使用默认逻辑查找第一个没有该检测数据的行 - if data_row is None: - for row in range(2, self.process_table.rowCount()): - cell_item = self.process_table.item(row, col_index) - if not cell_item or not cell_item.text().strip() or (data_type == 'xj' and cell_item.text().strip() == '0'): - data_row = row - break - - # 如果没有找到没有该检测数据的行,使用当前选中行或第一个数据行 - if data_row is None: - current_row = self.process_table.currentRow() - data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行(索引为2) - logging.info(f"未找到没有{data_type}数据的行,使用当前选中行或第一个数据行: {data_row}") - else: - logging.info(f"找到没有{data_type}数据的行: {data_row}") - - # 获取工程号 - order_id_item = self.process_table.item(data_row, 1) - if not order_id_item: - logging.warning("无法获取工程号") - return - - order_id = order_id_item.text().strip() - if not order_id: - logging.warning("工程号为空") - return - - # 暂时断开信号连接,避免触发cellChanged信号 - try: - self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed) - except: - pass - - # 格式化值并设置单元格 - formatted_value = str(value) - if config.get('data_type') == 'number': - # 格式化数字,线径保留3位小数,其他保留2位小数 - if data_type == 'xj': - formatted_value = f"{value:.3f}" - else: - formatted_value = f"{value:.2f}" - - # 设置单元格值 - item = QTableWidgetItem(formatted_value) - item.setTextAlignment(Qt.AlignCenter) - item.setData(Qt.UserRole, config_id) # 保存配置ID,用于识别检验项 - self.process_table.setItem(data_row, col_index, item) - - # 验证数据是否在有效范围内 - status = "pass" - if config.get('data_type') == 'number': - min_value = config.get('min_value') - max_value = config.get('max_value') - if (min_value is not None and value < min_value) or (max_value is not None and value > max_value): - status = "fail" - item.setBackground(QBrush(QColor("#ffcdd2"))) # 浅红色 - else: - item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - - # 保存到数据库,但只在非加载状态下 - if not self._loading_data_in_progress: - tray_id = self.tray_edit.currentText() - self.save_inspection_data(self._current_order_code, order_id, tray_id, config_position, config_id, formatted_value, status) - # 不需要在这里主动触发数据重新加载,因为handle_inspection_cell_changed会处理 - - # 重新连接信号 - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - - logging.info(f"已将{data_type}数据 {formatted_value} 写入工程号 {order_id} (行 {data_row}, 列 {col_index})") - - except Exception as e: - logging.error(f"设置检验项值失败: {str(e)}") - # 确保重新连接信号 - try: - self.process_table.cellChanged.connect(self.handle_inspection_cell_changed) - except: - pass - - def handle_tray_changed(self): - """处理托盘号变更事件,启动监听并加载数据""" - try: - tray_id = self.tray_edit.currentText() - if tray_id: - logging.info(f"托盘号变更为 {tray_id},启动监听") - # 初始化托盘号对应的序号(如果不存在) - if tray_id not in self.init_seq: - self.init_seq[tray_id] = 1 - logging.info(f"初始化托盘号 {tray_id} 的序号为 1") - - # 加载检验数据 - self._safe_load_data() - - # 无论_safe_load_data是否成功,都确保显示包装记录 - # 临时保存当前加载状态 - prev_loading_state = getattr(self, '_loading_data_in_progress', False) - - try: - # 设置加载状态为True,避免无限循环调用 - self._loading_data_in_progress = True - # 强制显示包装记录 - self.show_pack_item() - logging.info(f"托盘号变更:直接调用显示包装记录, 托盘号={tray_id}") - finally: - # 恢复之前的加载状态 - self._loading_data_in_progress = prev_loading_state - - except Exception as e: - logging.error(f"处理托盘号变更失败: {str(e)}") - - def handle_order_code_received(self, order_code): - """处理从加载对话框接收到的订单号""" - logging.info(f"主窗口接收到订单号: {order_code}") - # 存储当前订单号 - self._current_order_code = order_code - # 更新订单号输入框 - self.order_no_input.setText(order_code) - - # 如果是接口模式 - if AppMode.is_api(): - # 获取订单的详细信息 - from apis.gc_api import GcApi - gc_api = GcApi() - # 获取工程号信息 - order_response = gc_api.get_order_info(order_code, self.corp_id) - - if order_response.get("status", False): - # 获取订单的工程号数据 - gc_notes = order_response.get("data", []) - if gc_notes and len(gc_notes) > 0: - # 更新上料区域信息表格 - self.update_info_table(gc_notes[0]) - else: - # 非接口模式,获取本地数据库中的订单信息 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - order_info = inspection_dao.get_order_info(order_code) - - # 更新上料区域信息表格 - self.update_info_table(order_info) - - def on_report(self): - """报表按钮点击处理""" - try: - dialog = ReportDialog(self) - dialog.exec_() - except Exception as e: - logging.error(f"打开报表对话框失败: {str(e)}") - QMessageBox.warning(self, "错误", f"打开报表对话框失败: {str(e)}") - - def init_camera_display(self): - """初始化上料区信息表格,不包含任何相机相关代码""" - # 上料区的信息表格已经在UI类的create_left_panel方法中初始化 - # 这个方法保留是为了兼容性,但不做任何操作 - pass - - def initialize_camera(self): - """初始化相机的空方法,保留是为了兼容性""" - pass - - def _start_camera_display(self): - """相机显示的空方法,保留是为了兼容性""" - pass - - def update_camera_ui(self, is_camera_ready): - """相机UI更新的空方法,保留是为了兼容性""" - pass - - def handle_camera_status(self, is_connected, message): - """相机状态处理的空方法,保留是为了兼容性""" - pass - - @Slot(int, str) - def handle_emergency_stop(self, value, desc): - """处理急停信号""" - logging.info(f"[处理] 急停信号: {desc}") - - # 保存一个急停状态变量 - self.emergency_stop = value - - # 当急停信号为1时,重置D2和D3寄存器 - if value == 1: - try: - modbus = ModbusUtils() - client = modbus.get_client() - - # 重置D2和D3寄存器 - modbus.write_register_until_success(client, 2, 0) - modbus.write_register_until_success(client, 3, 0) - - # 通过信号在主线程中处理UI更新 - self.emergency_stop_signal.emit(value, desc) - - modbus.close_client(client) - except Exception as e: - logging.error(f"处理急停信号失败: {str(e)}") - else: - # 急停信号解除,在主线程中恢复错误状态显示 - self.emergency_stop_signal.emit(value, desc) - - def _handle_emergency_stop_ui(self, value, desc): - """在主线程中处理急停信号UI更新""" - try: - if value == 1: - # 显示警告对话框 - QMessageBox.warning(self, "急停警告", "监听到急停信号") - - # 恢复按钮样式 - 确保在主线程中执行 - logging.info("急停触发:开始恢复按钮样式") - self.restore_start_button_style() - logging.info("急停触发:已恢复开始按钮样式") - - if hasattr(self, 'restore_output_button_style'): - self.restore_output_button_style() - logging.info("急停触发:已恢复下料按钮样式") - - if hasattr(self, 'restore_input_button_style'): - self.restore_input_button_style() - logging.info("急停触发:已恢复上料按钮样式") - - # 更新错误状态标签 - self.error_status_label.setText("故障: 急停") - self.error_status_label.setToolTip("急停按钮被触发") - self.error_status_label.setStyleSheet("color: red; font-weight: bold;") - - logging.info("急停UI已更新:按钮样式已恢复,状态标签已更新") - else: - # 急停信号解除,恢复错误状态显示 - self._update_error_status() - - # 恢复故障状态标签 - self.error_status_label.setText("故障: 正常") - self.error_status_label.setStyleSheet("color: green; font-weight: bold;") - - logging.info("急停解除UI已更新:状态标签已恢复") - except Exception as e: - logging.error(f"处理急停UI更新失败: {str(e)}") - - def create_right_panel(self): - """创建右侧面板""" - # 创建右侧整体框架 - self.right_frame = QFrame() - self.right_frame.setFrameShape(QFrame.NoFrame) # 移除框架边框 - self.right_frame.setLineWidth(0) - self.right_layout.addWidget(self.right_frame) - - # 右侧整体使用垂直布局,不设置边距 - self.right_frame_layout = QVBoxLayout(self.right_frame) - self.right_frame_layout.setContentsMargins(0, 0, 0, 0) - self.right_frame_layout.setSpacing(0) - - # 创建一个垂直分割器,用于控制两个表格的高度比例 - self.right_splitter = QSplitter(Qt.Vertical) - - # 创建微丝产线表格的容器 - self.process_container = QWidget() - self.process_container_layout = QVBoxLayout(self.process_container) - self.process_container_layout.setContentsMargins(0, 0, 0, 0) - self.process_container_layout.setSpacing(0) - - # 创建包装记录表格的容器 - self.record_container = QWidget() - self.record_container_layout = QVBoxLayout(self.record_container) - self.record_container_layout.setContentsMargins(0, 0, 0, 0) - self.record_container_layout.setSpacing(0) - - # 创建微丝产线表格 - self.create_process_table() - - # 创建带删除按钮的标题行 - self.create_process_title_with_button() - - # 将微丝产线表格添加到容器中 - self.process_container_layout.addWidget(self.process_frame) - - # 创建包装记录表格 - self.create_record_table() - self.record_container_layout.addWidget(self.record_frame) - - # 将两个容器添加到分割器中 - self.right_splitter.addWidget(self.process_container) - self.right_splitter.addWidget(self.record_container) - - # 设置初始大小比例:微丝产线占1/3,包装记录占2/3 - self.right_splitter.setSizes([100, 200]) # 比例为1:2 - - # 将分割器添加到右侧布局 - self.right_frame_layout.addWidget(self.right_splitter) - - def create_process_title_with_button(self): - """创建带删除按钮的微丝产线标题行""" - # 获取原来的标题 - original_title = self.process_title.text() - - # 移除原来的标题 - self.process_layout.removeWidget(self.process_title) - self.process_title.deleteLater() - - # 创建标题容器 - title_container = QWidget() - title_layout = QHBoxLayout(title_container) - title_layout.setContentsMargins(5, 0, 5, 0) - - # 创建新的标题标签 - self.process_title = QLabel(original_title) - self.process_title.setFont(self.second_title_font) - self.process_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - - # 创建删除按钮 - self.delete_row_button = QPushButton("删除选中行") - self.delete_row_button.setFont(self.normal_font) - self.delete_row_button.setStyleSheet(""" - QPushButton { - padding: 5px 10px; - background-color: #ffebee; - border: 1px solid #f44336; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #ffcdd2; - } - """) - self.delete_row_button.clicked.connect(self.handle_delete_row) - - # 打印托盘号按钮 - self.print_tray_button = QPushButton("打印托盘号") - self.print_tray_button.setFont(self.normal_font) - self.print_tray_button.setStyleSheet(""" - QPushButton { - padding: 5px 8px; - background-color: #e3f2fd; - border: 1px solid #2196f3; - } - QPushButton:hover { - background-color: #c8e6c9; - } - """) - self.print_tray_button.clicked.connect(lambda: self.handle_tray_complete(ismt=False)) - - # 打印选中行按钮 - self.print_row_button = QPushButton("打印选中行") - self.print_row_button.setFont(self.normal_font) - self.print_row_button.setStyleSheet(""" - QPushButton { - padding: 5px 8px; - background-color: #e0e0e0; - border: 1px solid #cccccc; - } - QPushButton:hover { - background-color: #f5f5f5; - } - """) - self.print_row_button.clicked.connect(self.handle_print_row) - - # 将标题和按钮添加到布局中 - title_layout.addWidget(self.process_title) - title_layout.addStretch() - title_layout.addWidget(self.print_row_button) - title_layout.addWidget(self.delete_row_button) - title_layout.addWidget(self.print_tray_button) - - # 设置容器样式 - title_container.setFixedHeight(40) - title_container.setStyleSheet("background-color: #f8f8f8; border-bottom: 1px solid #dddddd;") - - # 将标题容器添加到布局中 - self.process_layout.insertWidget(0, title_container) - - def handle_delete_row(self): - """处理删除按钮点击事件,删除选中的微丝产线表格行""" - try: - # 获取当前选中的行 - selected_rows = self.process_table.selectionModel().selectedRows() - if not selected_rows: - # 如果没有选中整行,则获取当前选中的单元格所在行 - current_row = self.process_table.currentRow() - if current_row >= 2: # 确保不是表头行 - selected_rows = [self.process_table.model().index(current_row, 0)] - else: - QMessageBox.warning(self, "提示", "请先选择要删除的数据行") - return - - # 确认删除 - reply = QMessageBox.question( - self, - "确认删除", - "确定要删除选中的数据吗?此操作不可恢复。", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply != QMessageBox.Yes: - return - - # 从数据库中删除数据 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 按行号降序排序,以便从后往前删除 - rows_to_delete = sorted([index.row() for index in selected_rows], reverse=True) - - for row in rows_to_delete: - if row < 2: # 跳过表头行 - continue - - # 获取工程号 - gc_note_item = self.process_table.item(row, 1) - if not gc_note_item: - continue - - gc_note = gc_note_item.text().strip() - if not gc_note: - continue - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 从数据库中删除该工程号的检验数据 - inspection_dao.delete_inspection_data(self._current_order_code, gc_note, tray_id) - logging.info(f"已从数据库中删除工程号 {gc_note} 的检验数据") - - # 从表格中删除行 - self.process_table.removeRow(row) - logging.info(f"已从表格中删除第 {row} 行") - - # 重新加载数据 - self._safe_load_data() - - # 显示成功消息 - QMessageBox.information(self, "删除成功", "已成功删除选中的数据") - - except Exception as e: - logging.error(f"删除数据失败: {str(e)}") - QMessageBox.critical(self, "错误", f"删除数据失败: {str(e)}") - - - - def handle_tray_complete(self, ismt=True): - """托盘完成或打印托盘号事件 - Args: - ismt: 是否满托,True为满托,False为未满托 - """ - try: - # 获取托盘号 - tray_id = self.tray_edit.currentText() - if not tray_id: - QMessageBox.warning(self, "提示", "请先选择托盘号") - return - if ismt: - # 确认对话框 - reply = QMessageBox.question( - self, - "确认完成", - f"确认将托盘 {tray_id} 标记为已满托?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply != QMessageBox.Yes: - return - - # 调用接口 - from apis.gc_api import GcApi - gc_api = GcApi() - - # 准备参数 - params = { - 'ismt': ismt, - 'corp_id': self.corp_id, - 'tray_id': tray_id, - 'ip': '192.168.1.246' - } - - # 调用接口 - response = gc_api.ismt_option(params) - - # 处理响应 - if response.get('status', False): - if ismt: - QMessageBox.information(self, "成功", "托盘已标记为完成") - logging.info(f"托盘 {tray_id} 已标记为完成") - else: - QMessageBox.information(self, "成功", "托盘号已打印") - logging.info(f"托盘号 {tray_id} 已打印") - else: - error_msg = response.get('message', '未知错误') - QMessageBox.warning(self, "失败", f"标记托盘完成失败: {error_msg}") - logging.error(f"标记托盘 {tray_id} 完成失败: {error_msg}") - - except Exception as e: - logging.error(f"处理托盘完成失败: {str(e)}") - QMessageBox.critical(self, "错误", f"处理托盘完成失败: {str(e)}") - - def update_info_table(self, order_info): - """根据订单信息更新上料区域的信息表格 - - Args: - order_info: 订单信息字典 - """ - try: - if not order_info: - logging.warning("订单信息为空,无法更新上料区域信息表格") - return - - logging.info(f"更新上料区域信息表格: {order_info}") - - # 使用UI类中定义的字段映射关系 - field_mapping = self.FIELD_MAPPING - - # 记录 order_info 中的所有键,用于调试 - logging.debug(f"订单信息键: {list(order_info.keys())}") - - # 更新表格内容 - for field_name, field_key in field_mapping.items(): - if field_name in self.info_values: - value = "" - # 对可能存在的键进行安全检查 - if field_key and field_key in order_info: - value = str(order_info[field_key]) - # 特殊处理线径公差 - if field_name == "线径公差" and "bccd" in order_info and "tccd" in order_info: - bccd = order_info.get("bccd") - tccd = order_info.get("tccd") - if bccd is not None and tccd is not None: - value = f"{bccd} - {tccd}" - if field_name == "强度范围" and "bqd" in order_info and "tqd" in order_info: - bqd = order_info.get("bqd") - tqd = order_info.get("tqd") - if bqd is not None and tqd is not None: - value = f"{bqd} - {tqd}" - self.info_values[field_name].setText(value) - else: - logging.warning(f"字段名 '{field_name}' 在info_values中不存在") - - logging.info("上料区域信息表格更新完成") - - except Exception as e: - logging.error(f"更新上料区域信息表格失败: {str(e)}") - # 记录异常堆栈,便于调试 - import traceback - logging.error(traceback.format_exc()) - - def handle_print_row(self): - """处理打印按钮点击事件,打印选中的微丝产线表格行""" - try: - # 获取当前选中的行 - selected_rows = self.process_table.selectionModel().selectedRows() - if not selected_rows: - # 如果没有选中整行,则获取当前选中的单元格所在行 - current_row = self.process_table.currentRow() - if current_row >= 2: # 确保不是表头行 - selected_rows = [self.process_table.model().index(current_row, 0)] - else: - QMessageBox.warning(self, "提示", "请先选择要打印的数据行") - return - - # 确认打印 - reply = QMessageBox.question( - self, - "确认打印", - "确定要打印选中的数据吗?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply != QMessageBox.Yes: - return - - # 获取选中的行索引(只处理第一个选中的行) - row = selected_rows[0].row() - if row < 2: # 跳过表头行 - return - - # 获取工程号(用于日志记录) - gc_note_item = self.process_table.item(row, 1) - if not gc_note_item: - QMessageBox.warning(self, "提示", "无法获取工程号信息") - return - - gc_note = gc_note_item.text().strip() - if not gc_note: - QMessageBox.warning(self, "提示", "工程号不能为空") - return - - # 向D12寄存器写入1,触发打印 - from utils.modbus_utils import ModbusUtils - modbus = ModbusUtils() - client = modbus.get_client() - - if not client: - QMessageBox.critical(self, "错误", "无法连接到Modbus服务器") - return - - try: - # 向D12寄存器写入1,表示打印请求 - success = modbus.write_register(client, 12, 1) - if success: - logging.info(f"已向D12寄存器写入1,触发打印工程号 {gc_note} 的数据") - else: - QMessageBox.warning(self, "警告", "发送打印请求失败,请检查PLC连接") - finally: - # 释放客户端连接 - modbus.close_client(client) - - except Exception as e: - logging.error(f"打印数据失败: {str(e)}") - QMessageBox.critical(self, "错误", f"打印数据失败: {str(e)}") - - def update_order_statistics(self): - """更新订单数量和产量统计数据到项目表格""" - try: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - - # 获取订单数量和产量统计数据 - statistics = inspection_dao.get_order_statistics() - - # 设置表格项(日、月、年、累计订单数量) - # 当日订单数量 - day_cnt_item = QTableWidgetItem(str(statistics['order_cnt_day'])) - day_cnt_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(0, 1, day_cnt_item) - - # 当月订单数量 - month_cnt_item = QTableWidgetItem(str(statistics['order_cnt_month'])) - month_cnt_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(1, 1, month_cnt_item) - - # 当年订单数量 - year_cnt_item = QTableWidgetItem(str(statistics['order_cnt_year'])) - year_cnt_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(2, 1, year_cnt_item) - - # 累计订单数量 - all_cnt_item = QTableWidgetItem(str(statistics['order_cnt_all'])) - all_cnt_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(3, 1, all_cnt_item) - - # 设置表格项(日、月、年、累计产量) - # 当日产量 - day_num_item = QTableWidgetItem(str(round(statistics['order_num_day'], 2))) - day_num_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(0, 2, day_num_item) - - # 当月产量 - month_num_item = QTableWidgetItem(str(round(statistics['order_num_month'], 2))) - month_num_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(1, 2, month_num_item) - - # 当年产量 - year_num_item = QTableWidgetItem(str(round(statistics['order_num_year'], 2))) - year_num_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(2, 2, year_num_item) - - # 累计产量 - all_num_item = QTableWidgetItem(str(round(statistics['order_num_all'], 2))) - all_num_item.setTextAlignment(Qt.AlignCenter) - self.project_table.setItem(3, 2, all_num_item) - - logging.debug(f"已更新订单数量和产量统计数据: 日订单={statistics['order_cnt_day']}, 月订单={statistics['order_cnt_month']}, 年订单={statistics['order_cnt_year']}, 累计订单={statistics['order_cnt_all']}, 日产量={statistics['order_num_day']}, 月产量={statistics['order_num_month']}, 年产量={statistics['order_num_year']}, 累计产量={statistics['order_num_all']}") - - except Exception as e: - logging.error(f"更新订单数量和产量统计数据失败: {str(e)}") - import traceback - logging.error(traceback.format_exc()) - - def _log_focus_widget_info(self, widget): - """记录当前焦点控件的信息,用于调试""" - try: - from PySide6.QtWidgets import QLineEdit, QComboBox - widget_type = "未知" - if isinstance(widget, QLineEdit): - widget_type = "输入框" - elif isinstance(widget, QComboBox): - widget_type = "下拉框" - - widget_name = widget.objectName() if widget else "无" - widget_text = "" - - if isinstance(widget, QLineEdit): - widget_text = widget.text() - elif isinstance(widget, QComboBox): - widget_text = widget.currentText() - - logging.info(f"当前焦点控件: 类型={widget_type}, 名称={widget_name}, 文本={widget_text}") - except Exception as e: - logging.error(f"记录焦点控件信息失败: {e}") - - def handle_inspection_cell_changed(self, row, column): - """处理检验表格单元格内容变更事件""" - try: - # 只处理数据行的检验列变更 - if row < 2: # 忽略表头行 - return - - # 忽略首尾两列(序号和工程号) - if column < 2: - return - - # 获取工程号 - order_item = self.process_table.item(row, 1) - if not order_item: - return - - gc_note = order_item.text().strip() - if not gc_note: - return - - # 获取托盘号 - tray_id = self.tray_edit.currentText() - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 判断是否是检验列(非包装列) - packaging_start_col = 2 + len(enabled_configs) - - # 获取单元格内容 - cell_item = self.process_table.item(row, column) - if not cell_item: - return - - value = cell_item.text().strip() - - # 默认设置为通过状态 - status = 'pass' - - # 记录当前正在处理的数据类型,用于日志输出 - data_type = "检验" - - if column >= 2 and column < packaging_start_col: - # 是检验列 - config_index = column - 2 - if config_index < len(enabled_configs): - config = enabled_configs[config_index] - data_type = config['display_name'] - - # 显示临时状态消息 - self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000) - - # 验证数据有效性 - # 设置单元格颜色为浅绿色,表示已填写 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) - - # 保持当前状态不变,由状态管理逻辑处理 - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - status = inspection_dao.get_product_status(self._current_order_code, gc_note, tray_id) - self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status) - - # 判断是否是包装列 - elif column == packaging_start_col: - # 贴标列 - data_type = "贴标" - self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存贴标数据,position和config_id都是11 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status) - - elif column == packaging_start_col + 1: - # 毛重列 - data_type = "毛重" - self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存毛重数据,position和config_id都是12 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status) - elif column == packaging_start_col + 2: - # 净重列 - data_type = "净重" - self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000) - # 设置单元格颜色为通过 - cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 - # 保存净重数据,position和config_id都是13 - self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status) - - # 记录详细日志 - logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}") - - # 检查是否完成检验并更新状态 - self.check_inspection_completed(row) - - except Exception as e: - logging.error(f"处理检验单元格变更失败: {str(e)}") - self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000) - finally: - # 延迟一段时间后再触发查询,避免频繁刷新UI - # 但要避免在加载过程中触发新的加载 - if not self._loading_data_in_progress: - QTimer.singleShot(1000, self._safe_load_data) - - def validate_inspection_value(self, config, value): - """验证检验值是否有效 - - Args: - config: 检验配置 - value: 检验值 - - Returns: - bool: 是否有效 - """ - try: - # 特殊处理贴标和称重数据 - 这些数据默认都是有效的 - if config.get('position') in [11, 12]: # 11是贴标,12是称重 - return True - - # 检查值是否为空 - if not value and config.get('required', False): - return False - - # 根据数据类型验证 - data_type = config.get('data_type') - - if data_type == 'number': - # 数值类型验证 - try: - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - - num_value = float(value) - min_value = config.get('min_value') - max_value = config.get('max_value') - - if min_value is not None and num_value < min_value: - return False - - if max_value is not None and num_value > max_value: - return False - - return True - except ValueError: - return False - - elif data_type == 'enum': - # 枚举类型验证 - enum_values = config.get('enum_values') - if enum_values and isinstance(enum_values, list): - # 如果值为空且不是必填,则视为有效 - if not value and not config.get('required', False): - return True - return value in enum_values - return False - - # 文本类型不做特殊验证 - return True - - except Exception as e: - logging.error(f"验证检验值失败: {str(e)}") - return False - - def check_inspection_completed(self, row): - """检查行是否有至少一个检验项已完成,如果是则更新状态为inspected - - Args: - row: 行索引 - - Returns: - bool: 是否有至少一个检验项已完成 - """ - try: - # 获取工程号 - gc_note_item = self.process_table.item(row, 1) - if not gc_note_item: - return False - - gc_note = gc_note_item.text().strip() - tray_id = self.tray_edit.currentText() - - # 获取启用的检验配置 - enabled_configs = self.inspection_manager.get_enabled_configs() - - # 检查是否有至少一个检验项有值 - has_any_value = False - for i, config in enumerate(enabled_configs): - col_index = 2 + i - item = self.process_table.item(row, col_index) - if item and item.text().strip(): - has_any_value = True - break - - # 如果有至少一个检验项有值,更新状态为inspected - if has_any_value: - from dao.inspection_dao import InspectionDAO - inspection_dao = InspectionDAO() - inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'inspected') - logging.info(f"工程号 {gc_note} 的检验已完成,状态更新为inspected") - - return has_any_value - except Exception as e: - logging.error(f"检查检验完成状态失败: {str(e)}") - return False \ No newline at end of file