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, ) 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 # 初始化系统变量 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._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 # 标识上料任务是否正在进行 # 信号的连接在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.camera_enabled = self.config.get('camera', {}).get('enabled', False) # 初始化检验配置管理器 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.camera_display = None self.material_placeholder = None # 初始化上料区显示 self.init_camera_display() # 为下料区添加占位标签,确保它保持为空 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) # 主页面 # 不在这里直接初始化相机设置组件 # 延迟创建,保证创建的时候SettingsUI的所有控件都已经准备好 self.camera_settings = None # 设置中央部件为堆叠部件 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() 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.process_table.setContextMenuPolicy(Qt.CustomContextMenu) self.process_table.customContextMenuRequested.connect(self.show_table_context_menu) # 只有在相机启用时连接相机信号 if self.camera_enabled and hasattr(self, 'camera_display'): self.camera_display.signal_camera_status.connect(self.handle_camera_status) # 连接报表按钮点击事件 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() # 处理相机显示 if self.camera_enabled and self.camera_display: from widgets.camera_manager import CameraManager camera_manager = CameraManager.get_instance() # 检查相机是否已打开 if camera_manager.isOpen: # 更新UI显示相机画面 self.update_camera_ui(True) # 如果相机未在采集,则开始采集 if not camera_manager.isGrabbing: # 使用内部方法启动相机显示 QTimer.singleShot(100, self._start_camera_display) logging.info("主页面显示:启动相机显示") else: # 如果相机未打开,尝试重新初始化 QTimer.singleShot(100, self.initialize_camera) logging.info("主页面显示:尝试初始化相机") # 加载托盘号列表 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) if success0: # 创建状态标签并显示在右上角 self.show_operation_status("拆垛层数", "input", str(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 = 1 # 从第一层开始 self._current_unload_info = unloading_info logging.info(f"下料任务设置:总层数={self._total_unload_num}, 当前层数={self._current_unload_num}") # 将初始层数(1)写入寄存器 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) # 统一更新UI显示 tray_code = self._current_unload_info.get('tray_code', '') self.show_operation_status("下料层数", "output", f"{self._current_unload_num}/{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() 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() 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): """处理相机状态变化""" if is_connected: logging.info("相机已连接并显示") self.update_camera_ui(True) else: logging.warning(f"相机显示问题: {message}") # 更新占位符文本 if self.material_placeholder: self.material_placeholder.setText(f"相机错误: {message}" if message else "相机未连接") self.update_camera_ui(False) def handle_camera_connection(self, is_connected, message): """处理相机连接状态变化""" if is_connected: logging.info("相机已连接") # 如果当前在主页面,直接开始显示相机画面 if self.stacked_widget.currentWidget() == self.central_widget: self.camera_display.start_display() else: if message: logging.warning(f"相机连接失败: {message}") else: logging.info("相机已断开") # 如果相机断开,确保停止显示 self.camera_display.stop_display() def handle_camera_params_changed(self, exposure_time, gain, frame_rate): """处理相机参数变化""" logging.info(f"相机参数已更新: 曝光={exposure_time:.1f}μs, 增益={gain:.1f}dB, 帧率={frame_rate:.1f}fps") # 这里可以添加对相机参数变化的处理逻辑 def handle_camera_error(self, error_msg): """处理相机错误""" logging.error(f"相机错误: {error_msg}") QMessageBox.warning(self, "相机错误", error_msg) def closeEvent(self, event): """窗口关闭事件""" # 停止Modbus监控 if hasattr(self, 'modbus_monitor'): logging.info("停止Modbus监控") self.modbus_monitor.stop() # 处理相机关闭 if self.camera_enabled and self.camera_display: # 停止相机显示 self.camera_display.stop_display() # 关闭相机设备 try: from widgets.camera_manager import CameraManager camera_manager = CameraManager.get_instance() if camera_manager.isOpen: camera_manager.close_device() logging.info("相机设备已关闭") except Exception as e: logging.error(f"关闭相机设备失败: {str(e)}") # 停止串口监听 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}") #判断是否是接口,如果不是接口直接添加如果是则走接口 # 如果开启接口模式,则需要调用接口同步到业务库 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': '', # 默认设置为通过状态 'remark': '', 'tray_id': tray_id }] inspection_dao.save_inspection_data(self._current_order_code,gc_note,gc_note, data) # 为贴标和称重也创建空记录 for position in [11, 12, 13]: # 11是贴标,12是毛重,13是净重 data = [{ 'position': position, 'config_id': position, 'value': '', 'status': 'pass', # 默认设置为通过状态 'remark': '', 'tray_id': tray_id }] inspection_dao.save_inspection_data(self._current_order_code,gc_note, data) logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}条") 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) # 验证数据有效性 if self.validate_inspection_value(config, value): # 设置单元格颜色为通过 cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色 status = 'pass' else: # 设置单元格颜色为警告 cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色 status = 'warning' # 保存到数据库 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}") 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() # 记录保存前的详细日志 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) 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)) # 注册电力消耗处理器 monitor.register_handler(30, ElectricityHandler()) logging.info("已注册所有Modbus寄存器处理器") def _connect_modbus_signals(self): """连接Modbus信号槽""" # 连接监控器状态信号 self.modbus_monitor.monitor_status_changed.connect(self.handle_modbus_status_change) self.modbus_monitor.register_error.connect(self.handle_register_error) self.machine_handlers.ng_changed.connect(self.handle_ng) # 直接连接寄存器变化信号 self.modbus_monitor.register_changed.connect(self.handle_register_change) # 连接机器状态信号 self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback) self.machine_handlers.unloading_feedback_changed.connect(self.handle_unloading_feedback) 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.weight_changed.connect(self.handle_weight_data) self.machine_handlers.label_signal_changed.connect(self.handle_label_signal) 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: 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 为 1 表示已经称重完成 modbus = ModbusUtils() client = modbus.get_client() modbus.write_register_until_success(client, 10, 1) modbus.close_client(client) # 处理稳定重量 self._process_stable_weight(original_weight_kg) # 调用打印方法 self._print_weight_label(original_weight_kg) # 设置已处理标记和上次处理的重量 self._weight_processed = True self._last_processed_weight = original_weight_kg logging.info(f"已标记重量 {original_weight_kg}kg 为已处理") 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 _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 # 查找第一个没有称重数据的行 data_row = 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"未找到没有称重数据的行,使用当前选中行或第一个数据行: {data_row}") else: logging.info(f"找到没有称重数据的行: {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 # 获取本机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}") 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: # 获取数据行数 if self.process_table.rowCount() <= 2: # 没有数据行 logging.warning("没有可用的数据行来写入贴标数据") return # 获取当前选中的行或第一个数据行 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} 超出了表格范围") 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.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}") # 重新连接单元格变更信号 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 @Slot(int, int) def handle_register_change(self, address, value): """处理寄存器变化""" logging.info(f"[处理] 寄存器D{address}变化: {value}") # 当D11寄存器变为0时,复位D10寄存器为0 if 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") 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: # 当前层已完成,准备下一层 self._current_unload_num += 1 logging.info(f"当前层下料完成,更新层数:当前={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"第{self._current_unload_num-1}层下料完成,请启动第{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("下料位置:--") elif "请启动" in desc: QMessageBox.information(self, "下料层完成", desc) 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)""" # 只更新内存中的当前层数,UI更新通过信号槽完成 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) @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: tray_code = self._current_unload_info.get('tray_code', '') # 确保使用固定的总层数 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() 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): """线径数据接收回调函数 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: # 转换为浮点数 xj_value = float(value_str) # 查找线径对应的检验项配置 xj_config = None enabled_configs = self.inspection_manager.get_enabled_configs() for config in enabled_configs: if config.get('name') == 'xj' or config.get('display_name') == '线径': xj_config = config break if xj_config: 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 bccd <= xj_value <= tccd: self.set_inspection_value('xj', xj_config, xj_value) else: logging.warning(f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd})") reply = QMessageBox.question( self, '确认保存', f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd}),\n是否继续保存?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.set_inspection_value('xj', xj_config, xj_value) else: logging.info(f"用户取消保存超出范围的线径值: {xj_value}") # TODO:后续根据实际情况实现 pass else: logging.info(f"未找到订单 {self._current_order_code} 的线径公差范围,直接保存值 {xj_value}") self.set_inspection_value('xj', xj_config, xj_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_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: gc_note = data_str.split("扫码数据:")[1].strip() logging.info(f"提取到工程号: {gc_note}") # 设置工程号到输入框 self.order_edit.setText(gc_note) # 模拟按下回车键,触发handle_order_enter方法 self.handle_order_enter() else: logging.warning(f"收到的数据不包含扫码数据标记: {data_str}") except Exception as e: logging.error(f"处理扫码器数据失败: {str(e)}") 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 # 查找第一个没有该检测数据的行 data_row = 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(): 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': # 格式化数字,保留2位小数 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(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} 写入行 {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 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): """初始化相机显示区域""" try: # 清理之前的组件(如果有) if self.camera_display: self.material_content_layout.removeWidget(self.camera_display) self.camera_display.deleteLater() self.camera_display = None if self.material_placeholder: self.material_content_layout.removeWidget(self.material_placeholder) self.material_placeholder.deleteLater() self.material_placeholder = None # 清空布局中的所有项目 while self.material_content_layout.count(): item = self.material_content_layout.takeAt(0) if item.widget(): item.widget().deleteLater() # 创建占位标签 self.material_placeholder = QLabel("相机初始化中..." if self.camera_enabled else "相机功能已禁用") self.material_placeholder.setAlignment(Qt.AlignCenter) self.material_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;") self.material_content_layout.addWidget(self.material_placeholder) # 创建相机显示组件 self.camera_display = CameraDisplayWidget() self.camera_display.signal_camera_status.connect(self.handle_camera_status) # 先隐藏相机组件,直到确认相机可用 self.material_content_layout.addWidget(self.camera_display) self.camera_display.hide() # 如果相机功能已启用,尝试初始化相机 if self.camera_enabled: # 启动相机初始化过程 QTimer.singleShot(500, self.initialize_camera) logging.info("相机初始化已安排") else: logging.info("相机功能已禁用,不进行初始化") self.material_placeholder.show() self.camera_display.hide() except Exception as e: logging.error(f"初始化相机显示区域失败: {str(e)}") def initialize_camera(self): """初始化相机并显示画面""" try: if not self.camera_enabled: return logging.info("开始初始化相机...") # 获取相机管理器实例 from widgets.camera_manager import CameraManager camera_manager = CameraManager.get_instance() # 枚举设备 devices = camera_manager.enum_devices() if not devices or len(devices) == 0: self.material_placeholder.setText("未检测到相机设备") logging.warning("未检测到相机设备") return # 打开第一个相机设备 device_index = 0 success = camera_manager.open_device(device_index) if success: logging.info(f"相机已成功打开,设备索引: {device_index}") # 更新UI self.update_camera_ui(True) # 立即开始显示相机画面 QTimer.singleShot(100, lambda: self._start_camera_display()) else: self.material_placeholder.setText("相机打开失败") logging.error("相机打开失败") except Exception as e: self.material_placeholder.setText("相机初始化错误") logging.error(f"初始化相机失败: {str(e)}") def _start_camera_display(self): """开始显示相机画面(内部方法)""" try: if self.camera_display and self.camera_enabled: # 确保相机组件可见 self.camera_display.setVisible(True) self.camera_display.raise_() # 开始显示 success = self.camera_display.start_display() if success: # 确保占位符隐藏 if self.material_placeholder: self.material_placeholder.setVisible(False) logging.info("相机显示已成功启动") else: # 如果启动失败,显示占位符 if self.material_placeholder: self.material_placeholder.setText("相机显示启动失败") self.material_placeholder.setVisible(True) logging.error("相机显示启动失败") except Exception as e: logging.error(f"启动相机显示失败: {str(e)}") def update_camera_ui(self, is_camera_ready): """更新相机UI显示 Args: is_camera_ready: 相机是否准备就绪 """ try: if is_camera_ready and self.camera_enabled: # 显示相机画面,隐藏占位符 if self.camera_display: self.camera_display.setVisible(True) self.camera_display.raise_() # 确保相机组件在最上层 if self.material_placeholder: self.material_placeholder.setVisible(False) logging.info("相机UI已更新:显示相机画面") else: # 隐藏相机画面,显示占位符 if self.camera_display: self.camera_display.setVisible(False) if self.material_placeholder: self.material_placeholder.setVisible(True) self.material_placeholder.raise_() # 确保占位符在最上层 if not self.camera_enabled: self.material_placeholder.setText("相机功能已禁用") elif not is_camera_ready: self.material_placeholder.setText("相机未就绪") logging.info(f"相机UI已更新:显示占位符 (相机启用={self.camera_enabled}, 相机就绪={is_camera_ready})") except Exception as e: logging.error(f"更新相机UI失败: {str(e)}") def handle_camera_status(self, is_connected, message): """处理相机状态变化""" if is_connected: logging.info("相机已连接并显示") self.update_camera_ui(True) else: logging.warning(f"相机显示问题: {message}") # 更新占位符文本 if self.material_placeholder: self.material_placeholder.setText(f"相机错误: {message}" if message else "相机未连接") self.update_camera_ui(False) @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, "急停警告", "监听到急停信号") # 更新错误状态标签 self.error_status_label.setText("故障: 急停") self.error_status_label.setToolTip("急停按钮被触发") self.error_status_label.setStyleSheet("color: red; font-weight: bold;") else: # 急停信号解除,恢复错误状态显示 self._update_error_status() # 恢复故障状态标签 self.label_status_label.setText("故障: 正常") self.label_status_label.setStyleSheet("") except Exception as e: logging.error(f"处理急停UI更新失败: {str(e)}")