jiateng_ws/widgets/main_window.py

5291 lines
251 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
RegisterChangeHandler
)
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, QTextEdit
)
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) # 用于在主线程中处理急停信号
diameter_warning_signal = Signal(float, str, str) # 参数final_value, bccd, tccd
# 新增的线径警告弹框信号 - 参数:值, 最小值, 最大值
diameter_alert_signal = Signal(float, float, float)
# 新增的重量警告弹框信号 - 参数:值, 最小值, 最大值
weight_alert_signal = Signal(float, float, float)
# 不需要单独的spack信号我们直接从loading_data中获取spack
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_processing_row = None # 当前正在处理的行索引
# 初始化系统变量
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._current_spack = "" # 添加全局变量存储当前spack值
# 线径数据处理相关属性
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 # 当前工程号的强度数据
# 添加对上料和下料对话框的引用
self.loading_dialog = None # 上料对话框
self.unloading_dialog = None # 下料对话框
# 信号的连接在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.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.update_package_statistics()
# 创建状态处理器实例
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()
# 加载库房和线材类型数据
self.load_warehouse_data()
self.load_wire_type_data()
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 get_current_row_axios_num(self, data_row):
"""获取当前行的轴号,优先使用用户输入的轴号,否则使用数据库中的轴号+1
Args:
data_row: 数据行索引
Returns:
int: 轴号
"""
try:
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 计算贴标列索引 - 贴标位置在检验列之后的第一列
label_col = 2 + len(enabled_configs)
# 获取当前行的贴标值(轴号)
label_item = self.process_table.item(data_row, label_col)
if label_item:
current_axios = label_item.text().strip()
if current_axios and current_axios.isdigit():
# 如果当前行有有效的轴号,直接使用
axios_num = int(current_axios)
logging.info(f"使用当前行轴号: {axios_num}")
return axios_num
# 如果当前行没有轴号,使用数据库中的轴号+1
tray_id = self.tray_edit.text()
db_axios_num = self.get_axios_num(tray_id)
axios_num = db_axios_num + 1
logging.info(f"当前行无轴号,使用数据库轴号+1: {axios_num}")
return axios_num
except Exception as e:
logging.error(f"获取当前行轴号失败: {str(e)}")
# 出错时使用数据库轴号+1作为备选
tray_id = self.tray_edit.text()
db_axios_num = self.get_axios_num(tray_id)
return db_axios_num + 1
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 改为 QLineEdit
# QLineEdit 需要连接textChanged信号来监听文本变化
self.tray_edit.textChanged.connect(self.handle_tray_changed)
# 原来的连接代码:
# self.tray_edit.currentTextChanged.connect(self.handle_tray_changed)
# self.tray_edit.currentIndexChanged.connect(self.handle_tray_changed)
# 不需要单独的spack信号连接
# 连接按钮事件
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.virtual_order_button.clicked.connect(self.handle_virtual_order)
# 连接托盘完成按钮事件
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)
# 连接炉号查询按钮信号
if hasattr(self, 'luno_query_button'):
self.luno_query_button.clicked.connect(self.handle_luno_query)
# 连接新的线径警告弹框信号
self.diameter_alert_signal.connect(self.show_diameter_alert)
# 连接重量警告弹框信号
self.weight_alert_signal.connect(self.show_weight_alert)
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()
# 加载库房数据
self.load_warehouse_data()
# 加载线材类型数据
self.load_wire_type_data()
logging.info("显示主页面")
def load_pallet_codes(self):
"""从托盘类型管理器加载托盘号并更新到tray_edit"""
try:
# 获取当前文本,以便保留用户选择
current_text = self.tray_edit.text()
# 获取托盘号
pallet_codes = self.pallet_type_manager.get_pallet_code()
# 这里可以存储托盘号列表,以便在其他地方使用
self._pallet_codes = pallet_codes if pallet_codes else []
# 不再清空或更改当前托盘号,保留现有值
logging.info(f"已加载托盘号列表,共 {len(self._pallet_codes)} 个,当前托盘号保持为 '{current_text}'")
except Exception as e:
logging.error(f"加载托盘号列表失败: {str(e)}")
self._pallet_codes = []
def load_warehouse_data(self):
"""从API加载库房数据并更新到信息表格的库房组件"""
try:
# 获取信息表格中的库房组件
warehouse_combo = self.info_values.get("库房")
if not warehouse_combo:
logging.warning("未找到库房组件")
return
# 获取当前选中的库房,以便保留用户选择
current_warehouse = warehouse_combo.currentText()
# 清空当前项目
warehouse_combo.clear()
# 调用API获取库房数据
from apis.gc_api import GcApi
gc_api = GcApi()
# 调用get_params接口获取库房信息
response = gc_api.get_params("库房档案", "XC", self.corp_id)
if response.get("status", False):
warehouse_data = response.get("data", [])
if warehouse_data and len(warehouse_data) > 0:
# 添加库房到下拉框
for warehouse in warehouse_data:
warehouse_name = warehouse.get("combtext", "")
if warehouse_name:
warehouse_combo.addItem(warehouse_name)
# 默认选择成品库房
default_index = warehouse_combo.findText("成品库")
if default_index != -1:
warehouse_combo.setCurrentIndex(default_index)
elif warehouse_combo.count() > 0:
# 如果没有找到成品库,选择第一个
warehouse_combo.setCurrentIndex(0)
# 如果有之前的选择,尝试恢复它
if current_warehouse:
index = warehouse_combo.findText(current_warehouse)
if index != -1:
warehouse_combo.setCurrentIndex(index)
logging.info(f"已加载库房数据,共 {len(warehouse_data)}")
else:
logging.warning("未找到库房数据,库房列表将为空")
else:
logging.error(f"获取库房数据失败: {response.get('message', '未知错误')}")
# 如果API调用失败添加默认选项
warehouse_combo.addItem("成品库")
warehouse_combo.addItem("退回仓")
warehouse_combo.addItem("散装库")
warehouse_combo.addItem("不合格库(线材)")
warehouse_combo.addItem("废丝库")
warehouse_combo.setCurrentIndex(0)
except Exception as e:
logging.error(f"加载库房数据失败: {str(e)}")
# 如果加载失败,添加默认选项
warehouse_combo = self.info_values.get("库房")
if warehouse_combo:
warehouse_combo.clear()
warehouse_combo.addItem("成品库")
warehouse_combo.addItem("退回仓")
warehouse_combo.addItem("散装库")
warehouse_combo.addItem("不合格库(线材)")
warehouse_combo.addItem("废丝库")
warehouse_combo.setCurrentIndex(0)
def load_wire_type_data(self):
"""从API加载线材类型数据并更新到信息表格的线材类型组件"""
try:
logging.info("开始加载线材类型数据...")
# 获取信息表格中的线材类型组件
wire_type_combo = self.info_values.get("线材类型")
if not wire_type_combo:
logging.warning("未找到线材类型组件")
logging.info(f"info_values中的键: {list(self.info_values.keys())}")
return
# 获取当前选中的线材类型,以便保留用户选择
current_wire_type = wire_type_combo.currentText()
# 清空当前项目
wire_type_combo.clear()
# 调用API获取线材类型数据
from apis.gc_api import GcApi
gc_api = GcApi()
logging.info(f"调用线材类型APIcorp_id: {self.corp_id}")
# 调用get_wire_type_params接口获取线材类型信息
response = gc_api.get_wire_type_params(self.corp_id)
logging.info(f"线材类型API响应: {response}")
if response.get("status", False):
wire_type_data = response.get("data", [])
if wire_type_data and len(wire_type_data) > 0:
# 添加线材类型到下拉框
for wire_type in wire_type_data:
wire_type_name = wire_type.get("combtext", "")
if wire_type_name:
wire_type_combo.addItem(wire_type_name)
# 默认选择第一个
if wire_type_combo.count() > 0:
wire_type_combo.setCurrentIndex(0)
# 如果有之前的选择,尝试恢复它
if current_wire_type and current_wire_type != "请选择":
index = wire_type_combo.findText(current_wire_type)
if index != -1:
wire_type_combo.setCurrentIndex(index)
logging.info(f"已加载线材类型数据,共 {len(wire_type_data)}")
else:
logging.warning("未找到线材类型数据,线材类型列表将为空")
else:
# 如果API返回的不是status格式直接尝试解析数据
if isinstance(response, list):
wire_type_data = response
logging.info(f"直接解析列表数据,共 {len(wire_type_data)}")
if wire_type_data and len(wire_type_data) > 0:
# 添加线材类型到下拉框
for wire_type in wire_type_data:
wire_type_name = wire_type.get("combtext", "")
if wire_type_name:
wire_type_combo.addItem(wire_type_name)
# 默认选择第一个
if wire_type_combo.count() > 0:
wire_type_combo.setCurrentIndex(0)
# 如果有之前的选择,尝试恢复它
if current_wire_type and current_wire_type != "请选择":
index = wire_type_combo.findText(current_wire_type)
if index != -1:
wire_type_combo.setCurrentIndex(index)
logging.info(f"已加载线材类型数据,共 {len(wire_type_data)}")
else:
logging.warning("未找到线材类型数据,线材类型列表将为空")
else:
logging.error(f"获取线材类型数据失败: {response}")
# 如果API调用失败添加默认选项
wire_type_combo.addItem("经线")
wire_type_combo.addItem("纬线")
wire_type_combo.addItem("径线-H")
wire_type_combo.addItem("径线-J")
wire_type_combo.setCurrentIndex(0)
except Exception as e:
logging.error(f"加载线材类型数据失败: {str(e)}")
# 如果加载失败,添加默认选项
wire_type_combo = self.info_values.get("线材类型")
if wire_type_combo:
wire_type_combo.clear()
wire_type_combo.addItem("经线")
wire_type_combo.addItem("纬线")
wire_type_combo.addItem("径线-H")
wire_type_combo.addItem("径线-J")
wire_type_combo.setCurrentIndex(0)
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.text()
# 启动监听(不论后续是否确认上料)
# 启动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
self.loading_dialog = LoadingDialog(parent=self,user_id=self.user_id,user_name=self.user_name,corp_id=self.corp_id)
dialog = self.loading_dialog # 使用类属性引用对话框
# 获取当前主窗口中选择的托盘号,并设置到上料对话框中
current_tray_id = self.tray_edit.text()
if current_tray_id:
dialog.tray_input.setText(current_tray_id)
logging.info(f"将主窗口当前托盘号 {current_tray_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:
# 从对话框中获取订单号、托盘号和spack并更新到主窗口
try:
loading_data = dialog.get_loading_data()
order_code = loading_data.get("order_id", "").strip()
tray_code = loading_data.get("tray_id", "").strip()
spack = loading_data.get("spack")
self._current_spack = spack or self._current_spack or tray_code # 保存spack到全局变量
self._current_order_code = order_code
self.tray_edit.setText(tray_code)
logging.info(f"从上料对话框获取数据: order_code={order_code}, tray_code={tray_code}, spack={spack}")
except Exception as e:
logging.error(f"获取上料数据失败: {str(e)}")
order_code = dialog.order_input.text().strip()
tray_code = dialog.tray_input.text().strip()
self._current_order_code = order_code
self._current_spack = tray_code # 如果获取失败使用托盘号作为spack
self.tray_edit.setText(tray_code)
logging.info(f"从上料对话框直接获取数据: order_code={order_code}, tray_code={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()
self.unloading_dialog = UnloadingDialog(self, self.user_id)
dialog = self.unloading_dialog # 使用类属性引用对话框
# 获取当前主窗口中选择的托盘号,并设置到下料对话框中
current_tray_id = self.tray_edit.text()
if current_tray_id:
dialog.tray_input.setText(current_tray_id)
logging.info(f"将主窗口当前托盘号 {current_tray_id} 设置到下料对话框")
# 如果有之前的下料信息,作为参考显示在对话框中,但允许用户修改
if self._current_unload_info:
# 确保使用最新的层数值
if hasattr(self, '_current_unload_num'):
self._current_unload_info['tier'] = str(self._current_unload_num)
dialog.set_unloading_info(self._current_unload_info)
logging.info(f"显示之前的下料信息作为参考,当前层数:{self._current_unload_info['tier']}")
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寄存器设置为1D4寄存器已包含当前下料层数
"""
modbus = ModbusUtils()
client = modbus.get_client()
try:
# 判断当前操作类型(通过检查当前下料信息是否存在)
# 下料模式 - 开始下料操作
# 确保寄存器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}")
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:
# 判断当前操作类型(通过检查当前下料信息是否存在)
success3 = modbus.write_register_until_success(client, 3, 0)
success2 = modbus.write_register_until_success(client, 2, 0)
if success3 and success2:
logging.info("停止操作成功")
else:
logging.error("停止操作失败")
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 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:
# 防抖机制:记录上次处理的工程号和时间
current_time = time.time()
last_order_time = getattr(self, '_last_order_time', 0)
last_order_gc = getattr(self, '_last_order_gc', '')
# 如果是相同的工程号且间隔小于1秒则忽略此次处理
if last_order_gc == gc_note and current_time - last_order_time < 1.0:
logging.info(f"忽略重复处理工程号: {gc_note},间隔太短")
return
# 更新最后处理的工程号和时间
self._last_order_gc = gc_note
self._last_order_time = current_time
# 清除最近处理的线径工程号,确保新扫码的工程号不会使用之前的线径数据
if hasattr(self, '_last_processed_gc_note'):
logging.info(f"清除最近处理线径数据的工程号: {self._last_processed_gc_note}")
self._last_processed_gc_note = None
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","")
self._current_gc_sc_gch = gc_info.get("sc_gch", "") # 保存sc_gch字段
# 先获取当前 info_table 已有的数据
order_info = {}
for field_name, label in self.info_values.items():
# 过滤掉线径公差字段,因为线径数据是通过线径仪获取的,不需要从前端托盘入库地方获取
if field_name == "线径公差":
logging.debug(f"跳过线径公差字段,该字段由线径仪获取")
continue
order_info_key = self.FIELD_MAPPING.get(field_name)
if order_info_key and label is not None:
try:
# 根据控件类型选择合适的方法获取文本
if isinstance(label, QTextEdit):
order_info[order_info_key] = label.toPlainText()
elif isinstance(label, QComboBox):
order_info[order_info_key] = label.currentText()
elif field_name == "炉号":
# 炉号字段是容器需要找到其中的QLineEdit
luno_container = label
if luno_container:
for child in luno_container.children():
if hasattr(child, 'text') and hasattr(child, 'setText'):
order_info[order_info_key] = child.text()
break
else:
# 如果没有找到子组件,设置为空字符串
order_info[order_info_key] = ""
else:
order_info[order_info_key] = ""
elif hasattr(label, 'text'):
# 其他有text方法的控件
order_info[order_info_key] = label.text()
else:
# 对于没有text方法的控件使用空字符串
order_info[order_info_key] = ""
except RuntimeError as e:
# 如果对象已被删除,记录错误并跳过
logging.warning(f"控件对象已被删除,字段: {field_name}, 错误: {str(e)}")
continue
except Exception as e:
# 其他异常也记录并跳过
logging.warning(f"获取控件文本失败,字段: {field_name}, 错误: {str(e)}")
continue
# 更新/补充 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:
# 弹框提示,获取接口中的 message
QMessageBox.warning(self, "提示", "获取工程号信息失败")
else:
# 直接添加新行
self.add_new_inspection_row(gc_note, self._current_order_code)
# 清空工程号输入框
self.order_edit.clear()
# 将光标重新定位到工程号输入框
self.order_edit.setFocus()
# 更新订单数量和产量统计数据
self.update_order_statistics()
# 确保完整加载所有检验数据,包括线径数据
self._safe_load_data()
else:
logging.warning("工程号为空,忽略处理")
def handle_virtual_order(self):
"""处理虚拟工程号按钮点击事件"""
try:
# 获取当前订单代码
order_code = self._current_order_code
if not order_code:
QMessageBox.warning(self, "提示", "请先选择订单")
return
# 生成基于时间戳的虚拟工程号
import datetime
timestamp = datetime.datetime.now()
virtual_gc_note = f"{timestamp.strftime('%Y%m%d_%H%M%S')}"
# 直接添加虚拟工程号到微丝产线表格,跳过接口调用
self.add_new_inspection_row(virtual_gc_note, order_code)
# 清空工程号输入框
self.order_edit.clear()
# 将光标重新定位到工程号输入框
self.order_edit.setFocus()
logging.info(f"已生成虚拟工程号并直接添加: {virtual_gc_note}")
# 确保完整加载所有检验数据,包括线径数据
self._safe_load_data()
except Exception as e:
logging.error(f"生成虚拟工程号失败: {str(e)}")
QMessageBox.critical(self, "错误", f"生成虚拟工程号失败: {str(e)}")
def add_new_inspection_row(self, gc_note, order_code):
"""在微丝产线表格中添加一条新记录,添加到表格末尾
Args:
gc_note: 工程号
order_info: 从接口获取的工程号信息
"""
# 设置当前正在处理的工程号,确保它在重新加载时显示在最后
self._current_gc_note = gc_note
# 记录添加时间,用于确保最新添加的工程号在最后
self._current_gc_note_timestamp = time.time()
logging.info(f"设置最新扫码的工程号: {gc_note}, 订单号: {order_code}, 时间戳: {self._current_gc_note_timestamp}")
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)
# 设置单元格属性以标识其关联的检验项
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)
# 将工程号和托盘号保存到数据库,确保能够正确关联
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
tray_id = self.tray_edit.text()
# 为每个检验位置创建一个空记录,确保工程号在数据库中存在
# 只为没有自动填充值的配置创建空记录
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,
'package_id': self._current_spack or tray_id
}]
inspection_dao.save_inspection_data(self._current_order_code,gc_note, data)
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
# 为贴标和称重也创建空记录
for position in [11, 12, 13]: # 11是贴标12是毛重13是净重
data = [{
'position': position,
'config_id': position,
'value': '',
'status': 'init', # 设置初始状态
'remark': '',
'tray_id': tray_id,
'package_id': package_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', package_id)
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()
# 清除当前工程号标记和时间戳
self._current_gc_note = None
self._current_gc_note_timestamp = None
def handle_inspection_cell_changed(self, row, column):
"""处理微丝包装单元格内容变更
Args:
row: 行索引
column: 列索引
"""
# 创建唯一键,包含行、列、工程号
try:
# 获取工程号,用于创建更精确的唯一键
order_item = self.process_table.item(row, 1)
gc_note = order_item.text().strip() if order_item else ""
# 创建包含行、列、工程号的唯一键
cell_key = f"{row}_{column}_{gc_note}"
current_time = time.time()
# 获取上次处理时间
last_process_times = getattr(self, '_last_cell_process_times', {})
last_process_time = last_process_times.get(cell_key, 0)
# 如果同一单元格在1秒内被处理过则忽略
if current_time - last_process_time < 1.0:
logging.info(f"防抖:跳过重复处理单元格 [{row}, {column}], 工程号={gc_note}间隔小于1秒")
return
# 更新处理时间
if not hasattr(self, '_last_cell_process_times'):
self._last_cell_process_times = {}
self._last_cell_process_times[cell_key] = current_time
# 只处理数据行的检验列变更
if row < 2: # 忽略表头行
return
# 忽略首尾两列(序号和工程号)
if column < 2:
return
# 获取工程号
if not gc_note:
return
# 获取托盘号
tray_id = self.tray_edit.text()
# 获取启用的检验配置
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:
# 使用类变量保存定时器,避免创建多个定时器
if hasattr(self, '_load_data_timer') and self._load_data_timer is not None:
self._load_data_timer.stop()
self._load_data_timer = QTimer()
self._load_data_timer.setSingleShot(True)
self._load_data_timer.timeout.connect(self._safe_load_data)
self._load_data_timer.start(1000)
def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status, package_id=None):
"""保存检验数据到数据库
Args:
order_id: 订单号
gc_note: 工程号
tray_id: 托盘号
position: 位置序号
config_id: 配置ID
value: 检验值
status: 状态
package_id: 箱号(spack)如果为None则使用当前的_current_spack
"""
# 防抖机制:记录上次保存的数据和时间
current_time = time.time()
save_key = f"{gc_note}_{position}_{config_id}_{value}"
last_save_time = getattr(self, '_last_save_time', 0)
last_save_key = getattr(self, '_last_save_key', '')
# 如果是相同的数据且间隔小于0.5秒,则忽略此次保存
if last_save_key == save_key and current_time - last_save_time < 0.5:
logging.info(f"防抖机制:跳过保存相同数据 {save_key}间隔小于0.5秒")
return
# 更新最后保存的数据和时间
self._last_save_key = save_key
self._last_save_time = current_time
# 检查是否已存在相同记录
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
existing_data = inspection_dao.get_inspection_data_by_config(order_id, gc_note, tray_id, position, config_id)
if existing_data and str(existing_data.get('value')) == str(value):
logging.info(f"幂等处理:跳过保存已存在的检验数据 {gc_note}_{position}_{config_id}={value}")
return
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}")
# 构建数据
# 如果没有提供package_id则使用当前的_current_spack
if package_id is None:
package_id = getattr(self, '_current_spack', '') or tray_id
data = [{
'position': position,
'config_id': config_id,
'value': value,
'status': status,
'remark': '',
'tray_id': tray_id,
'package_id': package_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.text()
# 检查是否有加载操作正在进行
if getattr(self, '_loading_data_in_progress', False):
logging.warning(f"已有数据加载操作正在进行,忽略此次请求 (托盘号: {tray_id})")
return
# 检查是否有递归调用
call_stack_depth = getattr(self, '_safe_load_call_depth', 0) + 1
self._safe_load_call_depth = call_stack_depth
# 如果调用栈深度超过3可能存在递归调用
if call_stack_depth > 3:
logging.warning(f"检测到可能的递归调用 _safe_load_data调用栈深度: {call_stack_depth},忽略此次请求")
self._safe_load_call_depth -= 1
return
try:
# 防抖机制:记录上次加载的时间
current_time = time.time()
last_safe_load_time = getattr(self, '_last_safe_load_time', 0)
# 如果间隔小于0.5秒,则忽略此次加载
if current_time - last_safe_load_time < 0.5:
logging.debug(f"_safe_load_data 被频繁调用,忽略此次请求 (托盘号: {tray_id})")
return
# 更新最后加载的时间
self._last_safe_load_time = current_time
# 检查是否已有定时器在运行
if hasattr(self, '_safe_load_timer') and self._safe_load_timer is not None:
self._safe_load_timer.stop()
self._safe_load_timer = None
logging.debug("已停止之前的加载定时器")
# 创建新的定时器,延迟执行实际加载操作
self._safe_load_timer = QTimer()
self._safe_load_timer.setSingleShot(True)
self._safe_load_timer.timeout.connect(self._do_safe_load)
self._safe_load_timer.start(500) # 500毫秒后执行
finally:
# 减少调用栈深度计数
self._safe_load_call_depth -= 1
def _do_safe_load(self):
"""实际执行数据加载的方法,由定时器触发"""
# 获取当前托盘号,用于日志记录
tray_id = self.tray_edit.text()
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}")
# 加载完成后显示包装记录
try:
self.show_pack_item()
logging.info(f"在_do_safe_load中调用show_pack_item, 托盘号: {tray_id}")
except Exception as e:
logging.error(f"在_do_safe_load中调用show_pack_item失败: {str(e)}")
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
# 清理定时器引用
self._safe_load_timer = None
def load_finished_inspection_data(self):
"""加载未完成的检验数据并显示在表格中"""
# 注意此方法通常应通过_safe_load_data调用以防止循环
# 添加表格更新锁,避免并发更新
if hasattr(self, '_table_updating') and self._table_updating:
logging.info("表格正在更新中,忽略此次加载请求")
return
self._table_updating = True
# 记录信号连接状态
signal_was_connected = False
try:
# 获取托盘号
tray_id = self.tray_edit.text()
# 防止短时间内重复加载相同数据
current_time = time.time()
last_load_time = getattr(self, '_last_load_time', 0)
last_load_tray = getattr(self, '_last_load_tray', '')
# 如果是同一托盘且间隔小于1秒则忽略此次加载
if last_load_tray == tray_id and current_time - last_load_time < 1.0:
logging.debug(f"忽略重复加载托盘 {tray_id} 的数据,间隔太短")
return
# 更新最后加载的托盘和时间
self._last_load_tray = tray_id
self._last_load_time = current_time
# 确保 _loading_info 中的托盘号与当前托盘号一致
if tray_id and self._loading_info:
self._loading_info['tray_code'] = tray_id
logging.debug(f"同步 _loading_info 中的托盘号为: {tray_id}")
# 使用InspectionDAO获取未完成的检验数据
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取箱号
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
# 使用get_inspection_data_unfinished获取未完成的数据
unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id, package_id)
# 检查信号是否已连接,并断开单元格变更信号
try:
signal_was_connected = self.process_table.receivers(self.process_table.cellChanged) > 0
if signal_was_connected:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
logging.debug("已断开单元格变更信号")
except Exception as e:
logging.debug(f"断开单元格变更信号失败: {str(e)}")
signal_was_connected = False
# 清空表格现有数据行,只保留表头
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) # 只保留表头的两行
# 只有在之前信号已连接的情况下才重新连接
if signal_was_connected:
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.debug("已重新连接单元格变更信号")
except Exception as e:
logging.warning(f"重新连接单元格变更信号失败: {str(e)}")
# 加载包装记录
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()
# 获取当前正在处理的工程号(如果有的话)
current_gc_note = getattr(self, '_current_gc_note', None)
current_timestamp = getattr(self, '_current_gc_note_timestamp', None)
# 按创建时间排序,但确保新添加的工程号在最后
sorted_gc_notes = inspection_dao.get_orders_by_create_time(list(orders_data.keys()))
# 如果有当前正在处理的工程号,将其移到最后
if current_gc_note and current_gc_note in sorted_gc_notes and current_timestamp:
logging.info(f"将当前处理的工程号 {current_gc_note} 移到最后")
sorted_gc_notes.remove(current_gc_note)
sorted_gc_notes.append(current_gc_note)
logging.info(f"按创建时间排序后的工程号列表: {sorted_gc_notes}")
# 如果没有当前正在处理的工程号,记录日志
if not current_gc_note:
logging.info("没有当前正在处理的工程号")
# 收集所有要设置的单元格,批量设置
cells_to_set = []
# 按排序后的工程号顺序添加数据
for gc_note in sorted_gc_notes:
# 添加工程号到表格的第二列
item = QTableWidgetItem(gc_note)
item.setTextAlignment(Qt.AlignCenter)
self.process_table.insertRow(row_idx)
self.process_table.setItem(row_idx, 1, item)
# 添加序号到表格的第一列
item = QTableWidgetItem(str(row_idx - 1)) # 序号从1开始
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, 0, item)
# 获取该工程号的所有数据
data_list = orders_data[gc_note]
# 按位置分组,确保每个位置只有一条数据(取最新的)
position_data = {}
for data in data_list:
position = data['position']
if position not in position_data or data.get('update_time', 0) > position_data[position].get('update_time', 0):
position_data[position] = data
# 添加检验数据到表格
for position, data in position_data.items():
value = data['value']
config_id = data['config_id']
# 找到对应的列索引
col_idx = None
# 处理检验列position 1-10
if 1 <= position <= 10:
for i, config in enumerate(enabled_configs):
if config['id'] == config_id:
col_idx = 2 + i
break
# 添加贴标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)))
# 额外加载该工程号的线径数据(即使产品已完成)
for i, config in enumerate(enabled_configs):
if config.get('name') == 'xj' or config.get('display_name') == '线径':
xj_col = 2 + i
# 检查表格中是否已有线径数据
xj_item = self.process_table.item(row_idx, xj_col)
if not xj_item or not xj_item.text().strip():
# 如果表格中没有线径数据,尝试从数据库加载
xj_data = inspection_dao.get_inspection_data_by_config(
self._current_order_code, gc_note, tray_id, position=config.get('position'), config_id=config.get('id')
)
if xj_data and xj_data.get('value'):
# 设置线径数据到表格
xj_item = QTableWidgetItem(str(xj_data.get('value')))
xj_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, xj_col, xj_item)
logging.info(f"从数据库加载线径数据: {xj_data.get('value')} 到工程号 {gc_note}")
break
row_idx += 1
# 设置表格为可编辑状态
self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
# 只有在之前信号已连接的情况下才重新连接
if signal_was_connected:
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.debug("已重新连接单元格变更信号")
except Exception as e:
logging.warning(f"重新连接单元格变更信号失败: {str(e)}")
except Exception as e:
logging.error(f"加载未完成的检验数据失败: {str(e)}")
QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}")
finally:
# 释放表格更新锁
self._table_updating = False
def load_finished_record_to_package_record(self, order_id, gc_note, tray_id, axios_num=None, package_id=None):
"""加载已完成检验数据到包装记录
Args:
order_id: 工程号
tray_id: 托盘号
axios_num: 轴号如果为None则使用数据库中的轴号
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 首先检查该工程号的包装记录是否已存在
if inspection_dao.check_package_record_exists(order_id, gc_note, tray_id, package_id):
logging.warning(f"工程号 {gc_note} 托盘号 {tray_id} 的包装记录已存在,跳过添加")
return
# 获取该工程号的所有检验数据
inspection_data = inspection_dao.get_inspection_data_by_order(order_id, gc_note, tray_id, package_id)
if not inspection_data:
logging.warning(f"未找到工程号 {gc_note} 托盘号 {tray_id} 的检验数据")
return
# 使用传入的轴号,如果没有传入则使用数据库中的轴号
if axios_num is not None:
label_value = axios_num
logging.info(f"使用传入的轴号: {label_value}")
else:
label_value = self.get_axios_num_by_order_id(self._current_order_code)
logging.info(f"使用数据库中的轴号: {label_value}")
# 从检验数据中获取贴标和称重数据
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 is 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), weight_value, net_weight_value, finish_time, gc_note, package_id)
# 回显数据,但避免循环调用
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.text()
logging.info(f"显示包装记录,当前托盘号: {tray_id}")
if not tray_id:
logging.warning("托盘号为空,无法显示包装记录")
# 清空表格
self.record_table.setRowCount(0)
# 托盘号为空时不调用update_package_statistics
return
# 获取箱号
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
# 读取已包装的记录信息
package_record = inspection_dao.get_package_record(tray_id, package_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:
# 检查task_table是否存在
if not hasattr(self, 'task_table'):
logging.warning("task_table不存在无法更新统计数据")
return
# 获取当前的订单号和托盘号
order_id = self._current_order_code if hasattr(self, '_current_order_code') else None
tray_id = self.tray_edit.text() if hasattr(self, 'tray_edit') else ""
# 初始化订单API统计数据
order_api_stats = {
'count_all': 0, # 订单完成轴数默认值
'weight_all': 0 # 订单完成数量默认值
}
# 初始化托盘API统计数据
tray_api_stats = {
'tray_zzs': 0, # 托盘完成轴数默认值
'tray_ybzsl': 0 # 托盘完成数量默认值
}
# 检查是否开启API模式
if AppMode.is_api() and order_id:
# 使用API获取包装统计数据
from apis.gc_api import GcApi
gc_api = GcApi()
# 获取订单包装统计数据
order_response = gc_api.get_package_statistics(order_id, self.corp_id)
if order_response and order_response.get("status", False):
order_data = order_response.get("data", {})
order_api_stats = {
'count_all': order_data.get("zzs", 0), # 订单完成轴数
'weight_all': float(order_data.get("ybzsl", 0)) # 订单完成数量
}
logging.info(f"从API获取订单完成统计数据成功: {order_api_stats}")
else:
error_msg = order_response.get('message', '未知错误') if order_response else "API响应为空"
logging.warning(f"从API获取订单完成统计数据失败: {error_msg}")
# 如果有托盘号,获取托盘包装统计数据
if tray_id:
tray_response = gc_api.get_tray_package_statistics(order_id, tray_id, self.corp_id)
if tray_response and tray_response.get("status", False):
tray_data = tray_response.get("data", {})
tray_api_stats = {
'tray_zzs': tray_data.get("tray_zzs", 0), # 托盘完成轴数
'tray_ybzsl': float(tray_data.get("tray_ybzsl", 0)) # 托盘完成数量
}
logging.info(f"从API获取托盘完成统计数据成功: {tray_api_stats}")
else:
error_msg = tray_response.get('message', '未知错误') if tray_response else "API响应为空"
logging.warning(f"从API获取托盘完成统计数据失败: {error_msg}")
# 更新任务表格中的订单完成轴数(第一列)
total_count_item = QTableWidgetItem(str(order_api_stats['count_all']))
total_count_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 0, total_count_item)
# 更新任务表格中的订单完成数量(第二列)
total_kg_item = QTableWidgetItem(f"{order_api_stats['weight_all']:.2f}")
total_kg_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 1, total_kg_item)
# 更新任务表格中的托盘完成轴数(第三列)
tray_count_item = QTableWidgetItem(str(tray_api_stats['tray_zzs']))
tray_count_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 2, tray_count_item)
# 更新任务表格中的托盘完成数量(第四列)
tray_kg_item = QTableWidgetItem(f"{tray_api_stats['tray_ybzsl']:.2f}")
tray_kg_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 3, tray_kg_item)
# 更新表头显示(确保表头显示正确)
# 首先检查表头项是否存在,如果不存在则创建
for col in range(4):
if self.task_table.horizontalHeaderItem(col) is None:
header_item = QTableWidgetItem()
self.task_table.setHorizontalHeaderItem(col, header_item)
# 设置表头文本
self.task_table.horizontalHeaderItem(0).setText("订单完成轴数")
self.task_table.horizontalHeaderItem(1).setText("订单完成数量")
self.task_table.horizontalHeaderItem(2).setText("托盘完成轴数")
self.task_table.horizontalHeaderItem(3).setText("托盘完成数量")
logging.info(f"已更新包装记录统计数据: 订单完成轴数={order_api_stats['count_all']}, 订单完成数量={order_api_stats['weight_all']:.2f}, 托盘完成轴数={tray_api_stats['tray_zzs']}, 托盘完成数量={tray_api_stats['tray_ybzsl']:.2f}")
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.text()
# 创建上下文菜单
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("检查数据库记录")
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id, package_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, package_id=None):
"""检查数据库记录
Args:
order_id: 工程号
position: 位置序号
tray_id: 托盘号
package_id: 箱号(spack)如果为None则使用当前的_current_spack
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 如果没有提供package_id则使用当前的_current_spack
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
# 获取检验数据
inspection_data = inspection_dao.get_inspection_data_by_order(order_id, order_id, tray_id, package_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"
message += f"托盘号: {tray_id}\n"
message += f"箱号: {package_id}\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: 托盘类型
"""
try:
if operation_type == "input":
# 更新拆垛层数显示
if hasattr(self, 'stow_level_label'):
self.stow_level_label.setText(f"拆垛层数: {pallet_type}")
logging.debug(f"更新拆垛层数显示: {pallet_type}")
else:
# 更新下料层数显示
if hasattr(self, 'unload_level_label'):
self.unload_level_label.setText(f"下料层数: {pallet_type}")
logging.debug(f"更新下料层数显示: {pallet_type}")
except Exception as e:
logging.error(f"更新状态显示失败: {str(e)}")
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))
# 注册寄存器2和3的处理器开始按钮样式控制
monitor.register_handler(2, RegisterChangeHandler(self.handle_register_change, 2))
monitor.register_handler(3, RegisterChangeHandler(self.handle_register_change, 3))
# 注册电力消耗处理器并保存引用以便连接信号
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)
# 连接贴标信号 - 先断开再连接,避免重复连接
try:
self.machine_handlers.label_signal_changed.disconnect(self.handle_label_signal)
except Exception:
logging.debug("贴标信号未连接,无需断开")
self.machine_handlers.label_signal_changed.connect(self.handle_label_signal)
# 连接称重数据变化信号 - 先断开再连接,避免重复连接
try:
self.machine_handlers.weight_changed.disconnect(self.handle_weight_data)
except Exception:
logging.debug("称重信号未连接,无需断开")
self.machine_handlers.weight_changed.connect(self.handle_weight_data)
# 连接NG信号 - 先断开再连接,避免重复连接
try:
self.machine_handlers.ng_changed.disconnect(self.handle_ng)
except Exception:
logging.debug("NG信号未连接无需断开")
self.machine_handlers.ng_changed.connect(self.handle_ng)
# 连接故障信号 - 先断开再连接,避免重复连接
try:
self.machine_handlers.error_1_changed.disconnect(self.handle_error_1)
except Exception:
logging.debug("故障1信号未连接无需断开")
self.machine_handlers.error_1_changed.connect(self.handle_error_1)
try:
self.machine_handlers.error_2_changed.disconnect(self.handle_error_2)
except Exception:
logging.debug("故障2信号未连接无需断开")
self.machine_handlers.error_2_changed.connect(self.handle_error_2)
try:
self.machine_handlers.error_3_changed.disconnect(self.handle_error_3)
except Exception:
logging.debug("故障3信号未连接无需断开")
self.machine_handlers.error_3_changed.connect(self.handle_error_3)
# 连接上下料反馈信号 - 先断开再连接,避免重复连接
try:
self.machine_handlers.loading_feedback_changed.disconnect(self.handle_loading_feedback)
except Exception:
logging.debug("上料反馈信号未连接,无需断开")
self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback)
try:
self.machine_handlers.unloading_feedback_changed.disconnect(self.handle_unloading_feedback)
except Exception:
logging.debug("下料反馈信号未连接,无需断开")
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()
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, 2) # 保留2位小数
@Slot(int)
def handle_weight_data(self, weight_in_g):
"""处理称重数据变化"""
try:
# 添加防抖机制,避免短时间内重复处理相同重量
current_time = time.time()
last_weight_time = getattr(self, '_last_weight_data_time', 0)
last_weight_value = getattr(self, '_last_weight_data_value', 0)
# 如果是相同的重量且间隔小于0.2秒,则忽略此次调用
if abs(weight_in_g - last_weight_value) < 10 and current_time - last_weight_time < 0.2:
return
# 更新最后处理的重量和时间
self._last_weight_data_time = current_time
self._last_weight_data_value = weight_in_g
# 转换重量单位并立即更新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 # 重置处理标记,允许处理新产品
# 检测重量从较大值变为接近0判断为产品移除
elif self._current_weight is not None and self._current_weight > 0.5 and weight_in_kg < 0.1:
logging.info(f"检测到产品被移除,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg")
self._weight_processed = False # 重置处理标记,为下一个产品做准备
self._last_processed_weight = 0 # 重置上次处理的重量
# 更新当前重量和时间
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:
# 设置最小有效重量阈值,低于此值的重量不会被处理
MIN_VALID_WEIGHT = 0.5 # 0.5kg
# 如果当前重量低于最小有效重量阈值,则直接跳过
if self._current_weight < MIN_VALID_WEIGHT:
logging.info(f"当前重量 {self._current_weight}kg 低于最小有效重量阈值 {MIN_VALID_WEIGHT}kg跳过处理")
return
# 如果当前重量与定时器启动时的重量相同,说明这段时间内没有新的重量数据
if self._current_weight == original_weight_kg and self._current_weight > MIN_VALID_WEIGHT:
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: 最大重试次数
"""
# 添加防抖机制,避免短时间内重复处理相同重量
current_time = time.time()
last_stable_time = getattr(self, '_last_stable_weight_time', 0)
last_stable_value = getattr(self, '_last_stable_weight_value', 0)
# 如果是相同的重量且间隔小于1秒则忽略此次调用
if abs(weight_kg - last_stable_value) < 0.1 and current_time - last_stable_time < 1.0:
logging.info(f"防抖机制:跳过处理相同的稳定重量 {weight_kg}kg间隔小于1秒")
return
# 更新最后处理的稳定重量和时间
self._last_stable_weight_time = current_time
self._last_stable_weight_value = weight_kg
retry_count = 0
last_error = None
modbus = ModbusUtils()
client = None
try:
# 获取Modbus客户端现在使用连接池不会每次都创建新连接
client = modbus.get_client()
if not client:
logging.error("无法获取Modbus客户端连接")
return
logging.info(f"开始处理稳定重量: {weight_kg}kg")
# 这里不再写入D10=1全部交由_process_stable_weight处理
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 为已处理")
# 重置当前重量为0避免两个产品之间的重量判断问题
# 这样可以确保下一个产品必须从接近0的值开始测量
logging.info(f"已重置当前重量为0等待下一个产品")
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: 稳定的重量值(千克)
"""
# 添加处理锁,避免并发处理
if hasattr(self, '_processing_weight_lock') and self._processing_weight_lock:
logging.warning(f"已有称重处理进行中,忽略本次请求: {weight_kg}kg")
return
# 设置处理锁
self._processing_weight_lock = True
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 = self._find_next_row_to_process('inspected')
# 如果没有找到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}")
# 设置当前处理行
self._current_processing_row = 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
# 创建唯一键,包含工程号、重量值和时间戳
current_time = time.time()
process_key = f"{gc_note}_{weight_kg}_{int(current_time/60)}" # 按分钟级别去重
# 检查是否已处理过相同的键
last_process_key = getattr(self, '_last_weight_process_key', '')
if process_key == last_process_key:
logging.info(f"跳过重复处理:工程号 {gc_note} 的重量 {weight_kg}kg 在短时间内已处理")
return
# 更新处理键
self._last_weight_process_key = process_key
# 保存净重到数据库(毛重-工字轮重量,单位都是千克)
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)
# 检查轴重要求范围
if "轴重要求" in self.info_values and self.info_values["轴重要求"] is not None:
try:
zzyq_value = self.info_values["轴重要求"].text().strip()
if zzyq_value and "-" in zzyq_value:
# 解析轴重要求范围,格式如 "12.5-13.5"
parts = zzyq_value.split("-")
if len(parts) == 2:
min_weight = float(parts[0].strip())
max_weight = float(parts[1].strip())
# 检查称重值是否在范围内
if net_weight_kg < min_weight or net_weight_kg > max_weight:
# 写入寄存器D10 给值 2 表示重量超出范围
try:
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 10, 2)
modbus.close_client(client)
except Exception as e:
logging.error(f"写入Modbus寄存器失败: {str(e)}")
# 使用信号触发弹框显示 - 避免主线程阻塞
logging.warning(f"称重值 {net_weight_kg:.2f}kg 超出轴重要求范围 ({min_weight:.1f} - {max_weight:.1f}kg),发送信号显示警告")
try:
self.weight_alert_signal.emit(net_weight_kg, min_weight, max_weight)
except Exception as e:
logging.error(f"发送重量警告信号失败: {str(e)}", exc_info=True)
# 阻止继续执行,等待用户处理
logging.warning(f"称重值 {weight_kg:.3f}kg 超出轴重要求范围,已阻止保存")
return
else:
logging.info(f"称重值 {weight_kg:.3f}kg 在轴重要求范围内 ({min_weight:.1f} - {max_weight:.1f}kg)")
# 只有在范围内才写入D10=1
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 10, 1)
modbus.close_client(client)
logging.info("已写入D10=1通知PLC称重合格")
else:
logging.debug(f"轴重要求格式不正确: '{zzyq_value}'")
else:
logging.debug(f"轴重要求格式不正确或为空,跳过范围检查: '{zzyq_value}'")
except (ValueError, AttributeError) as e:
logging.warning(f"解析轴重要求失败,跳过范围检查: {str(e)}")
else:
logging.debug("轴重要求字段不存在或为空,跳过范围检查")
# 没有轴重要求时直接写入D10=1
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 10, 1)
modbus.close_client(client)
logging.info("无轴重要求已写入D10=1通知PLC称重合格")
# 暂时断开信号连接避免触发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.text()
existing_data = inspection_dao.get_inspection_data_by_config(self._current_order_code, gc_note, tray_id, 12, 12)
if existing_data and str(existing_data.get('value')) == str(weight_kg):
logging.info(f"跳过保存:工程号 {gc_note} 的称重数据 {weight_kg} 已存在")
else:
# 保存到数据库(使用千克)
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass")
logging.info(f"已保存称重数据: {weight_kg}kg 到工程号 {gc_note}")
# 检查是否已存在相同净重记录
existing_net_data = inspection_dao.get_inspection_data_by_config(self._current_order_code, gc_note, tray_id, 13, 13)
if existing_net_data and str(existing_net_data.get('value')) == str(net_weight_kg):
logging.info(f"跳过保存:工程号 {gc_note} 的净重数据 {net_weight_kg} 已存在")
else:
# 保存净重数据
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass")
logging.info(f"已保存净重数据: {net_weight_kg}kg 到工程号 {gc_note}")
# 设置净重单元格(显示千克)
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)
# 获取轴号,优先使用当前行的轴号
axios_num = self.get_current_row_axios_num(data_row)
# 如果开启 api 模式,则调用接口添加到包装记录
if AppMode.is_api():
from dao.inspection_dao import InspectionDAO
from apis.gc_api import GcApi
inspection_dao = InspectionDAO()
# 调用接口
gc_api = GcApi()
# 构建info字典以数据库数据为基础前端数据作为补充
info = {}
# 1. 首先从数据库获取基础订单信息
order_info = inspection_dao.get_order_info(self._current_order_code)
if order_info:
info.update(order_info)
# 2. 从前端获取用户修改的数据,作为补充和更新
logging.info(f"开始从前端获取数据info_values中的字段: {list(self.info_values.keys())}")
# 专门检查线材类型字段
if "线材类型" in self.info_values:
wire_type_widget = self.info_values["线材类型"]
if isinstance(wire_type_widget, QComboBox):
wire_type_value = wire_type_widget.currentText()
logging.info(f"线材类型字段当前值: '{wire_type_value}'")
logging.info(f"线材类型下拉框选项: {[wire_type_widget.itemText(i) for i in range(wire_type_widget.count())]}")
else:
logging.warning(f"线材类型字段不是QComboBox而是: {type(wire_type_widget)}")
else:
logging.warning("线材类型字段不在info_values中")
for field_name, field_key in self.FIELD_MAPPING.items():
# 过滤掉线径公差字段,因为线径数据是通过线径仪获取的,不需要从前端托盘入库地方获取
if field_name == "线径公差" or field_name == "材质" or field_name == "打印材质":
logging.debug(f"跳过线径公差、材质、打印材质字段,该字段由接口获取")
continue
logging.debug(f"处理字段: {field_name} -> {field_key}")
if field_name in self.info_values and self.info_values[field_name] is not None:
try:
# 根据控件类型获取最新值
if isinstance(self.info_values[field_name], QTextEdit):
current_value = self.info_values[field_name].toPlainText().strip()
elif isinstance(self.info_values[field_name], QComboBox):
current_value = self.info_values[field_name].currentText().strip()
elif field_name == "炉号":
# 炉号字段是容器需要找到其中的QLineEdit
luno_container = self.info_values[field_name]
current_value = ""
if luno_container:
for child in luno_container.children():
if hasattr(child, 'text') and hasattr(child, 'setText'):
current_value = child.text().strip()
break
elif hasattr(self.info_values[field_name], 'text'):
current_value = self.info_values[field_name].text().strip()
else:
current_value = ""
# 对于线材类型字段,即使值为"请选择"也要包含在接口调用中
if field_name == "线材类型":
info[field_key] = current_value
# 如果前端有值则更新info字典覆盖数据库中的值
elif current_value:
info[field_key] = current_value
logging.debug(f"从前端更新字段 '{field_name}': '{current_value}'")
# 如果前端没有值但数据库有值,保持数据库的值
elif field_key in info:
logging.debug(f"保持数据库字段 '{field_name}': '{info[field_key]}'")
else:
logging.debug(f"字段 '{field_name}' 前端值为空且数据库中也无值")
except Exception as e:
logging.warning(f"获取前端字段 '{field_name}' 失败: {str(e)}")
continue
else:
logging.debug(f"字段 '{field_name}' 不在info_values中或为None")
# 3. 添加其他必要的信息
xpack = self.tray_edit.text()
info['xpack'] = xpack
# 使用全局变量中的spack值如果为空则使用xpack值
info['spack'] = self._current_spack
info['zh'] = axios_num
info['mzl'] = weight_kg
info['printsl'] = 1
logging.info(f"使用的xpack: {xpack}, spack: {info['spack']}")
info["dycz"] = info.get("cz")
info['qd'] = self._current_gc_qd
info['sc_gch'] = gc_note
order_others_info = inspection_dao.get_order_others_info(gc_note, self._current_order_code, tray_id)
if order_others_info:
info.update(order_others_info)
if 'data_corp' not in info and order_info and 'data_corp' in order_info:
info['data_corp'] = order_info['data_corp']
# 获取本机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
except Exception as e:
logging.error(f"获取本机IP失败: {str(e)}")
# 如果获取失败,使用本地回环地址
info['nw_ip'] = '127.0.0.1'
# 调试:检查线材类型字段
wire_type_value = info.get('xclx', 'NOT_FOUND')
logging.info(f"完整的info字典: {info}")
# 调用接口添加到包装记录
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",{}))
# 新增如果勾选了圆形标签checkbox则向寄存器D15写入1
if hasattr(self, "round_label_checkbox") and self.round_label_checkbox.isChecked():
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 15, 1)
modbus.close_client(client)
logging.info("已通知PLC当前使用圆形标签")
# 保存贴标数据到数据库
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass")
# 更新产品状态为weighed
inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'weighed')
logging.info(f"工程号 {gc_note} 的称重已完成状态更新为weighed")
# 重新连接信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}")
# 清除当前处理行的跟踪,因为称重完成后需要等待贴标
self._current_processing_row = None
except Exception as e:
logging.error(f"处理称重数据时发生错误: {str(e)}")
# 确保重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
finally:
# 释放处理锁
self._processing_weight_lock = False
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()
time.sleep(0.1)
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
# 使用新的查找逻辑,确保顺序处理
data_row = self._find_next_row_to_process('weighed')
# 如果没有找到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}")
# 设置当前处理行
self._current_processing_row = 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.text()
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 计算贴标列索引 - 贴标位置在检验列之后的第一列
label_col = 2 + len(enabled_configs)
# 生成贴标号(托盘号+轴号),优先使用当前行的轴号
axios_num = self.get_current_row_axios_num(data_row)
# 断开单元格变更信号,避免程序自动写入时触发
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}]")
package_id = self._current_spack if hasattr(self, '_current_spack') and self._current_spack else tray_id
# 在这里添加保存贴标数据到数据库的代码
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass", package_id)
logging.info(f"已将贴标数据 {axios_num} 保存到数据库")
from dao.inspection_dao import InspectionDAO
from apis.gc_api import GcApi
inspection_dao = InspectionDAO()
# 更新产品状态为labeled
inspection_dao.update_product_status(self._current_order_code, gc_note, tray_id, 'labeled', package_id)
logging.info(f"工程号 {gc_note} 的贴标已完成状态更新为labeled")
# 获取当前行的轴号,用于保存到包装记录
# 使用写入单元格的轴号,而不是重新计算轴号
axios_num_to_use = axios_num
logging.info(f"使用写入单元格的轴号: {axios_num_to_use}")
# 调用加载到包装记录的方法,传入正确的轴号
self.load_finished_record_to_package_record(self._current_order_code, gc_note, tray_id, axios_num_to_use, package_id)
logging.info(f"贴标完成,已将工程号 {gc_note} 的记录加载到包装记录,轴号: {axios_num_to_use}")
# 删除当前处理的行
self.process_table.removeRow(data_row)
logging.info(f"已删除处理完成的行 {data_row}")
# 清除当前处理行的跟踪
self._current_processing_row = None
# 复原寄存器 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.update_package_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}")
# 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 D3寄存器为 0 ,恢复开始按钮样式")
# D3寄存器控制下料按钮样式
elif address == 3 and value == 0:
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 ,填充下料按钮样式")
# 当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 # 任务完成,标记为非活动
message = f"{completed_layer_num} 层(最后一层)拆垛完成!"
# 重置寄存器 0 和 2 为 0
modbus.write_register_until_success(client, 0, 0)
modbus.write_register_until_success(client, 2, 0)
# 确保保留当前托盘号
current_tray_id = self.tray_edit.text()
if current_tray_id and self._loading_info:
self._loading_info['tray_code'] = current_tray_id
logging.info(f"上料完成,保留托盘号: {current_tray_id}")
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.restore_input_button_style()
logging.info("上料任务完成,恢复上料按钮样式,保留托盘号")
# 确保 _loading_info 不被清空
if self._loading_info:
# 记录当前托盘号,确保不会丢失
current_tray_id = self.tray_edit.text()
if current_tray_id:
self._loading_info['tray_code'] = current_tray_id
logging.info(f"上料任务完成,保留托盘号: {current_tray_id}")
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
# 保存当前完成的层数用于消息显示
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)
# 更新下料信息中的层数值,无论对话框是否打开
if self._current_unload_info:
self._current_unload_info['tier'] = str(self._current_unload_num)
logging.info(f"更新下料信息中的层数为:{self._current_unload_num}")
# 如果下料对话框正在显示,同步更新其中的层数
if hasattr(self, 'unloading_dialog') and self.unloading_dialog is not None and hasattr(self.unloading_dialog, 'tier_input'):
self.unloading_dialog.tier_input.setText(str(self._current_unload_num))
logging.info(f"同步更新下料对话框中的层数为:{self._current_unload_num}")
# 恢复开始按钮原始样式
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:
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}")
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.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.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)
# 如果下料对话框正在显示,同步更新其中的层数
if hasattr(self, 'unloading_dialog') and self.unloading_dialog is not None and hasattr(self.unloading_dialog, 'tier_input'):
self.unloading_dialog.tier_input.setText(str(level))
logging.info(f"同步更新下料对话框中的层数为:{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:
# 更新显示
if hasattr(self, 'unload_level_label'):
self.unload_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:
if hasattr(self, 'unload_position_label'):
self.unload_position_label.setText(f"下料位置: {position}")
logging.debug(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.text()
# 获取启用的检验配置
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()
# 更新包装记录统计数据
self.update_package_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):
"""线径数据接收回调函数 - 采用类似称重的逻辑,不使用会话机制"""
# 添加处理锁,避免并发处理导致卡死
if hasattr(self, '_processing_diameter_lock') and self._processing_diameter_lock:
logging.warning(f"已有线径数据处理进行中,忽略本次请求")
return
# 设置处理锁,并添加超时保护
self._processing_diameter_lock = True
self._diameter_processing_start_time = time.time()
# 添加超时检测定时器
if hasattr(self, '_diameter_timeout_timer') and self._diameter_timeout_timer is not None:
self._diameter_timeout_timer.stop()
self._diameter_timeout_timer = QTimer()
self._diameter_timeout_timer.setSingleShot(True)
self._diameter_timeout_timer.timeout.connect(self._reset_diameter_processing_lock)
self._diameter_timeout_timer.start(5000) # 5秒超时保护
modbus_client = None
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)
# 更新最后处理的线径数据和时间 - 只记录,不进行防抖过滤
current_time = time.time()
self._last_diameter_value = xj_value
self._last_diameter_time = current_time
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)
# 保留最近的6个测量值增加缓冲区大小以便更快收集足够的数据
if len(self._diameter_measurements) > 6:
self._diameter_measurements.pop(0)
# 显示临时值到状态栏
if len(self._diameter_measurements) < 3:
self.statusBar().showMessage(f"线径数据收集中: {xj_value:.3f} ({len(self._diameter_measurements)}/3)", 2000)
return
# 检查稳定性 - 使用最近的3个测量值
measurements = self._diameter_measurements[-3:]
min_value = min(measurements)
max_value = max(measurements)
avg_value = sum(measurements) / len(measurements)
error_range = avg_value * 0.05 # 允许5%误差,增加容错范围
if max_value - min_value <= error_range:
# 数据稳定,可以保存
final_value = avg_value # 使用平均值作为最终值
# 查找第一个没有线径数据的行
data_row = None
# 如果有最近处理的线径工程号,优先查找该工程号对应的行
if hasattr(self, '_last_processed_gc_note') and self._last_processed_gc_note:
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._last_processed_gc_note:
# 检查该行的线径单元格是否为空
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
logging.info(f"找到最近处理的线径工程号 {self._last_processed_gc_note} 对应的行: {data_row}")
break
# 如果没有找到最近处理的工程号对应的行,再查找第一个没有线径数据的行
if data_row is 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
# 记录最近处理的线径工程号,用于后续线径数据处理
self._last_processed_gc_note = gc_note
logging.info(f"记录最近处理线径数据的工程号: {gc_note}")
# 获取托盘号
tray_id = self.tray_edit.text()
# 检查线径公差范围 - 基于前端【线径公差】字段
if "线径公差" in self.info_values and self.info_values["线径公差"] is not None:
try:
xjgc_value = self.info_values["线径公差"].text().strip()
if xjgc_value and "-" in xjgc_value:
# 解析线径公差范围,格式如 "0.157 - 0.163",忽略两边空格后分割
parts = xjgc_value.split("-")
if len(parts) == 2:
min_diameter = float(parts[0].strip())
max_diameter = float(parts[1].strip())
# 检查线径值是否在范围内
if final_value < min_diameter or final_value > max_diameter:
# 使用信号触发弹框显示 - 避免主线程阻塞
logging.warning(f"线径值 {final_value:.3f}mm 超出线径公差范围 ({min_diameter:.3f} - {max_diameter:.3f}mm),发送信号显示警告")
try:
self.diameter_alert_signal.emit(final_value, min_diameter, max_diameter)
except Exception as e:
logging.error(f"发送线径警告信号失败: {str(e)}", exc_info=True)
# 重置测量列表,防止重复触发
self._diameter_measurements = []
# 阻止继续执行,等待用户处理
logging.warning(f"线径值 {final_value:.3f}mm 超出线径公差范围,已阻止保存")
return
else:
logging.info(f"线径值 {final_value:.3f}mm 在线径公差范围内 ({min_diameter:.3f} - {max_diameter:.3f}mm)")
else:
logging.debug(f"线径公差格式不正确: '{xjgc_value}'")
else:
logging.debug(f"线径公差格式不正确或为空,跳过范围检查: '{xjgc_value}'")
except (ValueError, AttributeError) as e:
logging.warning(f"解析线径公差失败,跳过范围检查: {str(e)}")
else:
logging.debug("线径公差字段不存在或为空,跳过范围检查")
# 保存线径数据到表格
try:
# 使用set_inspection_value保存数据
self.set_inspection_value('xj', xj_config, final_value)
logging.info(f"已将稳定的线径值 {final_value:.3f} 保存到工程号 {gc_note} (行 {data_row})")
# 重置测量列表,准备下一次测量
self._diameter_measurements = []
except Exception as e:
logging.error(f"保存线径数据到表格失败: {str(e)}")
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)}")
import traceback
logging.error(f"错误详情: {traceback.format_exc()}")
finally:
# 确保关闭Modbus客户端连接
if modbus_client:
try:
ModbusUtils().close_client(modbus_client)
except Exception as e:
logging.error(f"关闭Modbus客户端连接失败: {str(e)}")
# 释放处理锁
if hasattr(self, '_processing_diameter_lock'):
self._processing_diameter_lock = False
# 停止超时定时器
if hasattr(self, '_diameter_timeout_timer') and self._diameter_timeout_timer is not None:
self._diameter_timeout_timer.stop()
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}")
# 防抖机制:记录上次扫码的数据和时间
current_time = time.time()
last_scan_time = getattr(self, '_last_scan_time', 0)
last_scan_data = getattr(self, '_last_scan_data', '')
# 如果是相同的数据且间隔小于1秒则忽略此次扫码
if last_scan_data == scan_data and current_time - last_scan_time < 1.0:
logging.info(f"忽略重复扫码数据: {scan_data},间隔太短")
return
# 更新最后扫码的数据和时间
self._last_scan_data = scan_data
self._last_scan_time = current_time
# 使用焦点跟踪器设置文本并触发回车事件
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: 检验值
"""
# 添加防递归锁,避免死循环
if hasattr(self, '_set_inspection_value_lock') and self._set_inspection_value_lock:
logging.warning(f"检测到递归调用set_inspection_value忽略本次调用: {data_type}={value}")
return
self._set_inspection_value_lock = True
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._current_gc_note = order_id
self._current_gc_note_timestamp = time.time()
logging.info(f"在set_inspection_value中设置最新扫码的工程号: {order_id}, 订单号: {self._current_order_code}, 时间戳: {self._current_gc_note_timestamp}")
self.add_new_inspection_row(order_id, self._current_order_code)
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:
# 新增:检查是否有最近添加的工程号,如果有,优先保存到之前的行
if data_type == 'xj' and hasattr(self, '_last_processed_gc_note') and self._last_processed_gc_note:
# 查找最近处理的工程号对应的行
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._last_processed_gc_note:
# 检查该行的线径单元格是否为空
cell_item = self.process_table.item(row, col_index)
if not cell_item or not cell_item.text().strip() or cell_item.text().strip() == '0':
data_row = row
logging.info(f"找到最近处理的工程号 {self._last_processed_gc_note} 对应的空线径单元格,行: {data_row}")
break
# 如果没有找到最近处理的工程号对应的行,或者该行已有线径数据,再查找第一个没有该检测数据的行
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
# 记录最近处理的工程号,用于后续线径数据处理
if data_type == 'xj':
self._last_processed_gc_note = order_id
logging.info(f"记录最近处理线径数据的工程号: {order_id}")
# 暂时断开信号连接避免触发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.text()
self.save_inspection_data(self._current_order_code, order_id, tray_id, config_position, config_id, formatted_value, status)
# 不需要在这里主动触发数据重新加载因为handle_inspection_cell_changed会处理
# 重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except Exception as e:
logging.warning(f"重新连接cellChanged信号失败: {str(e)}")
logging.info(f"成功设置{data_type}{formatted_value} 到工程号 {order_id} 的行 {data_row}")
except Exception as e:
logging.error(f"设置检验值失败: {str(e)}")
finally:
# 释放锁
self._set_inspection_value_lock = False
def handle_tray_changed(self, tray_id=None):
"""处理托盘号变更事件,启动监听并加载数据"""
try:
# 如果没有提供托盘号,则使用当前文本框中的值
if tray_id is None:
tray_id = self.tray_edit.text()
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
# 更新包装统计数据
self.update_package_statistics()
logging.info(f"托盘号变更:更新包装统计数据, 托盘号={tray_id}")
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
# 如果是接口模式
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])
# 订单号输入框回显 mo 字段
self.order_no_input.setText(gc_notes[0].get('mo', ''))
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)
# 订单号输入框回显 mo 字段
if order_info:
self.order_no_input.setText(order_info.get('mo', '').strip())
# 更新包装统计数据
self.update_package_statistics()
logging.info(f"订单号变更:更新包装统计数据, 订单号={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):
"""初始化上料区信息表格,不包含任何相机相关代码"""
# 上料区的信息表格已经在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;
}
""")
# 删除按钮的信号连接已在 connect_signals 函数中处理,这里不需要重复连接
# 打印托盘号按钮
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:
# 判断当前焦点在哪个表格
focused_widget = self.focusWidget()
if focused_widget == self.process_table or self.process_table.hasFocus():
# 删除微丝产线表格行
self._delete_process_table_rows()
elif focused_widget == self.record_table or self.record_table.hasFocus():
# 删除包装记录表格行
self._delete_package_table_rows()
else:
# 如果都没有焦点,检查哪个表格有选中行
if self.process_table.selectionModel().hasSelection():
self._delete_process_table_rows()
elif self.record_table.selectionModel().hasSelection():
self._delete_package_table_rows()
else:
return
except Exception as e:
logging.error(f"删除数据失败: {str(e)}")
QMessageBox.critical(self, "错误", f"删除数据失败: {str(e)}")
def _delete_process_table_rows(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:
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.text()
# 如果开启 api 模式,则调用接口删除远程数据
if AppMode.is_api():
from apis.gc_api import GcApi
gc_api = GcApi()
# 调用删除接口
response = gc_api.remove_order_info(tray_id, self._current_order_code, gc_note)
if response.get("status", False):
logging.info(f"已从远程删除订单信息: 托盘号={tray_id}, 订单号={self._current_order_code}, 工程号={gc_note}")
else:
error_msg = response.get("message", "未知错误")
logging.warning(f"远程删除订单信息失败: {error_msg}")
# 可以选择是否继续删除本地数据,这里选择继续
# 从数据库中删除该工程号的检验数据
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()
# 确保清除选中状态,防止误操作
self.process_table.clearSelection()
self.process_table.setCurrentItem(None)
# 显示成功消息
QMessageBox.information(self, "删除成功", "已成功删除选中的微丝产线数据")
except Exception as e:
logging.error(f"删除微丝产线数据失败: {str(e)}")
QMessageBox.critical(self, "错误", f"删除微丝产线数据失败: {str(e)}")
def _delete_package_table_rows(self):
"""删除包装记录表格选中的行"""
try:
# 获取当前选中的行
selected_rows = self.record_table.selectionModel().selectedRows()
if not selected_rows:
# 如果没有选中整行,则获取当前选中的单元格所在行
current_row = self.record_table.currentRow()
if current_row >= 0: # 包装记录表格没有表头行
selected_rows = [self.record_table.model().index(current_row, 0)]
else:
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 < 0: # 跳过无效行
continue
# 获取订单号第2列
order_id_item = self.record_table.item(row, 1)
if not order_id_item:
logging.warning(f"{row} 行订单号为空,跳过删除")
continue
order_id = order_id_item.text().strip()
if not order_id:
logging.warning(f"{row} 行订单号为空,跳过删除")
continue
# 获取工程号第3列
gc_note_item = self.record_table.item(row, 2)
if not gc_note_item:
logging.warning(f"{row} 行工程号为空,跳过删除")
continue
gc_note = gc_note_item.text().strip()
if not gc_note:
logging.warning(f"{row} 行工程号为空,跳过删除")
continue
# 获取托盘号(从前端组件获取)
tray_id = self.tray_edit.text()
if not tray_id:
logging.warning("托盘号为空,跳过删除")
continue
# 如果开启 api 模式,则调用接口删除远程数据
if AppMode.is_api():
from apis.gc_api import GcApi
gc_api = GcApi()
# 调用删除接口
response = gc_api.remove_order_info(tray_id, order_id, gc_note)
if response.get("status", False):
logging.info(f"已从远程删除订单信息: 托盘号={tray_id}, 订单号={order_id}, 工程号={gc_note}")
else:
error_msg = response.get("message", "未知错误")
logging.warning(f"远程删除订单信息失败: {error_msg}")
# 可以选择是否继续删除本地数据,这里选择继续
# 从数据库中删除该包装记录
success = inspection_dao.delete_package_record(order_id, gc_note, tray_id)
if success:
logging.info(f"已从数据库中删除包装记录: 订单号={order_id}, 工程号={gc_note}, 托盘号={tray_id}")
else:
logging.warning(f"删除包装记录失败: 订单号={order_id}, 工程号={gc_note}, 托盘号={tray_id}")
continue
# 从表格中删除行
self.record_table.removeRow(row)
logging.info(f"已从包装记录表格中删除第 {row}")
# 重新加载包装记录
self.show_pack_item()
# 确保清除选中状态,防止误操作
self.record_table.clearSelection()
self.record_table.setCurrentItem(None)
# 显示成功消息
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.text()
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()
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()
except Exception as e:
logging.error(f"获取本机IP失败: {str(e)}")
# 如果获取失败,使用本地回环地址
# 准备参数
params = {
'ismt': ismt,
'corp_id': self.corp_id,
'tray_id': tray_id,
'ip': local_ip
}
# 调用接口
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} 已打印")
# 向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")
else:
QMessageBox.warning(self, "警告", "发送打印请求失败请检查PLC连接")
finally:
# 释放客户端连接
modbus.close_client(client)
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):
"""根据订单信息更新上料区域的信息表格"""
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 and self.info_values[field_name] is not None:
try:
# 获取当前控件中的值
current_value = ""
if isinstance(self.info_values[field_name], QTextEdit):
current_value = self.info_values[field_name].toPlainText()
elif isinstance(self.info_values[field_name], QComboBox):
current_value = self.info_values[field_name].currentText()
elif field_name == "炉号":
# 炉号字段是容器需要找到其中的QLineEdit
luno_container = self.info_values[field_name]
if luno_container:
for child in luno_container.children():
if hasattr(child, 'text') and hasattr(child, 'setText'):
current_value = child.text()
break
elif hasattr(self.info_values[field_name], 'text'):
# 其他有text方法的控件
current_value = self.info_values[field_name].text()
else:
# 对于没有text方法的控件使用空字符串
current_value = ""
except RuntimeError as e:
# 如果对象已被删除,记录错误并跳过
logging.warning(f"控件对象已被删除,字段: {field_name}, 错误: {str(e)}")
continue
except Exception as e:
# 其他异常也记录并跳过
logging.warning(f"获取控件文本失败,字段: {field_name}, 错误: {str(e)}")
continue
# 获取API返回的新值
new_value = ""
if field_key and field_key in order_info:
raw_value = order_info[field_key]
# 处理None值将其转换为空字符串
if raw_value is None:
new_value = ""
else:
new_value = str(raw_value)
# 特殊处理线径公差
if field_name == "线径公差" and "bccd" in order_info and "tccd" in order_info:
bccd = order_info.get("bccd")
tccd = order_info.get("tccd")
# 处理None值只有两个值都不为None时才组合
if bccd is not None and tccd is not None:
new_value = f"{bccd} - {tccd}"
elif bccd is not None:
new_value = str(bccd)
elif tccd is not None:
new_value = str(tccd)
else:
new_value = ""
# 特殊处理强度范围
if field_name == "强度范围" and "bqd" in order_info and "tqd" in order_info:
bqd = order_info.get("bqd")
tqd = order_info.get("tqd")
# 处理None值只有两个值都不为None时才组合
if bqd is not None and tqd is not None:
new_value = f"{bqd} - {tqd}"
elif bqd is not None:
new_value = str(bqd)
elif tqd is not None:
new_value = str(tqd)
else:
new_value = ""
# 智能更新逻辑:强度字段允许空值覆盖,其他字段空值不覆盖
should_update = False
if field_name == "强度": # 强度字段允许空值覆盖
should_update = new_value != current_value
else: # 其他字段:只有新值不为空且与当前值不同时才更新
should_update = new_value and new_value != current_value
if should_update:
# 进行更新
try:
if isinstance(self.info_values[field_name], QTextEdit):
self.info_values[field_name].setPlainText(new_value)
elif isinstance(self.info_values[field_name], QComboBox):
# 对于QComboBox尝试找到匹配的选项并设置
combo = self.info_values[field_name]
index = combo.findText(new_value)
if index != -1:
combo.setCurrentIndex(index)
else:
# 如果没找到匹配的选项,添加新选项
combo.addItem(new_value)
combo.setCurrentText(new_value)
elif field_name == "炉号":
# 炉号字段是容器需要找到其中的QLineEdit并设置文本
luno_container = self.info_values[field_name]
if luno_container:
for child in luno_container.children():
if hasattr(child, 'text') and hasattr(child, 'setText'):
child.setText(new_value)
break
else:
self.info_values[field_name].setText(new_value)
logging.debug(f"更新字段 '{field_name}': '{current_value}' -> '{new_value}'")
except RuntimeError as e:
# 如果对象已被删除,记录错误并跳过
logging.warning(f"控件对象已被删除,无法更新字段: {field_name}, 错误: {str(e)}")
continue
except Exception as e:
# 其他异常也记录并跳过
logging.warning(f"更新控件文本失败,字段: {field_name}, 错误: {str(e)}")
continue
elif not new_value and current_value and field_name != "强度":
# 新值为空但当前值不为空,且不是强度字段,保持当前值
logging.debug(f"保持字段 '{field_name}' 的当前值: '{current_value}' (API返回空值)")
elif not new_value and not current_value:
# 新值和当前值都为空,无需操作
logging.debug(f"字段 '{field_name}' 新值和当前值都为空,无需更新")
else:
# 新值与当前值相同,无需更新
logging.debug(f"字段 '{field_name}' 值未变化: '{current_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:
# 确认打印
reply = QMessageBox.question(
self,
"确认打印",
"确定要打印选中的数据吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
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")
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, QTextEdit
widget_type = "未知"
if isinstance(widget, QLineEdit):
widget_type = "输入框"
elif isinstance(widget, QComboBox):
widget_type = "下拉框"
elif isinstance(widget, QTextEdit):
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()
elif isinstance(widget, QTextEdit):
widget_text = widget.toPlainText()
logging.info(f"当前焦点控件: 类型={widget_type}, 名称={widget_name}, 文本={widget_text}")
except Exception as e:
logging.error(f"记录焦点控件信息失败: {e}")
def _find_next_row_to_process(self, target_status):
"""查找下一个需要处理的行
Args:
target_status: 目标状态(如 'inspected', 'weighed'
Returns:
int: 行索引如果没找到返回None
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 确定搜索的起始行
start_row = 2 # 数据行从第2行开始
# 如果当前有正在处理的行,从该行开始搜索
if self._current_processing_row is not None and self._current_processing_row >= 2:
start_row = self._current_processing_row
# 从起始行开始,按顺序查找状态匹配的行
for row in range(start_row, 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()
if row_gc_note: # 确保工程号不为空
tray_id = self.tray_edit.text()
status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id)
if status == target_status:
logging.info(f"找到状态为{target_status}的行: {row}, 工程号: {row_gc_note}")
return row # 找到第一个匹配的行就立即返回
# 如果从当前处理行开始没找到,从头开始搜索
if self._current_processing_row is not None and self._current_processing_row > 2:
for row in range(2, self._current_processing_row):
gc_note_item = self.process_table.item(row, 1)
if gc_note_item:
row_gc_note = gc_note_item.text().strip()
if row_gc_note: # 确保工程号不为空
tray_id = self.tray_edit.text()
status = inspection_dao.get_product_status(self._current_order_code, row_gc_note, tray_id)
if status == target_status:
logging.info(f"从头开始找到状态为{target_status}的行: {row}, 工程号: {row_gc_note}")
return row # 找到第一个匹配的行就立即返回
logging.warning(f"未找到状态为{target_status}的行")
return None
except Exception as e:
logging.error(f"查找下一个处理行时发生错误: {str(e)}")
return None
def handle_inspection_cell_changed(self, row, column):
"""处理检验表格单元格内容变更事件"""
# 创建唯一键,包含行、列、工程号
try:
# 获取工程号,用于创建更精确的唯一键
order_item = self.process_table.item(row, 1)
gc_note = order_item.text().strip() if order_item else ""
# 创建包含行、列、工程号的唯一键
cell_key = f"{row}_{column}_{gc_note}"
current_time = time.time()
# 获取上次处理时间
last_process_times = getattr(self, '_last_cell_process_times', {})
last_process_time = last_process_times.get(cell_key, 0)
# 如果同一单元格在1秒内被处理过则忽略
if current_time - last_process_time < 1.0:
logging.info(f"防抖:跳过重复处理单元格 [{row}, {column}], 工程号={gc_note}间隔小于1秒")
return
# 更新处理时间
if not hasattr(self, '_last_cell_process_times'):
self._last_cell_process_times = {}
self._last_cell_process_times[cell_key] = current_time
# 只处理数据行的检验列变更
if row < 2: # 忽略表头行
return
# 忽略首尾两列(序号和工程号)
if column < 2:
return
# 获取工程号
if not gc_note:
return
# 获取托盘号
tray_id = self.tray_edit.text()
# 获取启用的检验配置
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:
# 使用类变量保存定时器,避免创建多个定时器
if hasattr(self, '_load_data_timer') and self._load_data_timer is not None:
self._load_data_timer.stop()
self._load_data_timer = QTimer()
self._load_data_timer.setSingleShot(True)
self._load_data_timer.timeout.connect(self._safe_load_data)
self._load_data_timer.start(1000)
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.text()
# 获取启用的检验配置
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
def handle_luno_query(self):
"""处理炉号查询按钮点击事件"""
try:
# 导入炉号查询对话框
from widgets.luno_query_dialog import LunoQueryDialog
# 创建炉号查询对话框
luno_dialog = LunoQueryDialog(self)
# 连接炉号选择信号
luno_dialog.luno_selected.connect(self.on_luno_selected)
# 显示对话框
luno_dialog.exec()
except Exception as e:
logging.error(f"打开炉号查询对话框失败: {str(e)}")
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "错误", f"打开炉号查询对话框失败: {str(e)}")
def on_luno_selected(self, luno_data):
"""处理炉号选择事件
Args:
luno_data: 选中的炉号数据字典
"""
try:
# 获取炉号输入框
luno_input = None
luno_container = self.info_values.get("炉号")
if luno_container:
# 查找容器中的QLineEdit
for child in luno_container.children():
if hasattr(child, 'text') and hasattr(child, 'setText'):
luno_input = child
break
if luno_input:
# 设置炉号值
luno_value = luno_data.get("luono", "")
luno_input.setText(luno_value)
logging.info(f"已选择炉号: {luno_value}")
# 显示状态消息
self.statusBar().showMessage(f"已选择炉号: {luno_value}", 2000)
else:
logging.warning("未找到炉号输入框")
except Exception as e:
logging.error(f"处理炉号选择失败: {str(e)}")
from PySide6.QtWidgets import QMessageBox
QMessageBox.critical(self, "错误", f"处理炉号选择失败: {str(e)}")
def _reset_diameter_processing_lock(self):
"""重置线径处理锁,用于超时保护"""
if hasattr(self, '_processing_diameter_lock') and self._processing_diameter_lock:
processing_time = time.time() - getattr(self, '_diameter_processing_start_time', time.time())
logging.warning(f"线径数据处理超时,强制释放锁。处理时间: {processing_time:.2f}")
self._processing_diameter_lock = False
@Slot(float, float, float)
def show_diameter_alert(self, value, min_value, max_value):
"""显示线径超出范围警告 - 通过信号触发使用exec_强制显示"""
try:
# 使用更强制的方式显示警告对话框
from PySide6.QtWidgets import QApplication
# 记录当前时间,用于跟踪弹框显示时间
start_time = time.time()
logging.info(f"开始创建线径警告弹框,时间: {start_time}")
# 创建一个模态对话框 - 使用exec_方式显示
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical) # 使用Critical图标更明显
msg.setWindowTitle('警告:线径超出范围!')
msg.setText(f"<b>线径值 {value:.3f}mm 超出范围!</b><br><br>允许范围: {min_value:.3f} - {max_value:.3f}mm")
msg.setStandardButtons(QMessageBox.Ok)
# 设置样式,使其更显眼
msg.setStyleSheet("""
QMessageBox {
background-color: #ffeeee;
border: 3px solid #ff0000;
font-size: 14px;
}
QLabel {
color: #ff0000;
font-size: 16px;
font-weight: bold;
min-width: 250px;
min-height: 80px;
}
QPushButton {
background-color: #ff6666;
color: white;
font-weight: bold;
min-width: 80px;
min-height: 30px;
}
""")
# 确保对话框显示在所有窗口之上
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
# 强制处理事件确保UI更新
QApplication.processEvents()
# 使用定时器在2秒后自动点击确定按钮
def auto_close():
try:
# 查找确定按钮并点击
for button in msg.buttons():
if msg.buttonRole(button) == QMessageBox.AcceptRole:
button.click()
logging.info(f"已自动点击确定按钮,弹框显示时长: {time.time() - start_time:.2f}")
return
# 如果没有找到确定按钮,直接关闭
msg.done(QMessageBox.Ok)
logging.info(f"已自动关闭弹框,弹框显示时长: {time.time() - start_time:.2f}")
except Exception as e:
logging.error(f"自动关闭弹框失败: {str(e)}")
# 设置自动关闭定时器
QTimer.singleShot(2000, auto_close)
# 显示弹框并阻塞直到用户关闭或自动关闭
logging.info(f"即将显示线径警告弹框...")
msg.exec_() # 使用exec_而不是show确保弹框显示
# 记录日志
logging.info(f"线径警告弹框已关闭,总显示时长: {time.time() - start_time:.2f}")
except Exception as e:
logging.error(f"显示线径警告弹框失败: {str(e)}", exc_info=True)
@Slot(float, float, float)
def show_weight_alert(self, value, min_value, max_value):
"""显示重量超出范围警告 - 通过信号触发使用exec_强制显示"""
try:
# 使用更强制的方式显示警告对话框
from PySide6.QtWidgets import QApplication
# 记录当前时间,用于跟踪弹框显示时间
start_time = time.time()
logging.info(f"开始创建重量警告弹框,时间: {start_time}")
# 创建一个模态对话框 - 使用exec_方式显示
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical) # 使用Critical图标更明显
msg.setWindowTitle('警告:重量超出范围!')
msg.setText(f"<b>称重值 {value:.2f}kg 超出范围!</b><br><br>允许范围: {min_value:.1f} - {max_value:.1f}kg")
msg.setStandardButtons(QMessageBox.Ok)
# 设置样式,使其更显眼
msg.setStyleSheet("""
QMessageBox {
background-color: #ffeeee;
border: 3px solid #ff0000;
font-size: 14px;
}
QLabel {
color: #ff0000;
font-size: 16px;
font-weight: bold;
min-width: 250px;
min-height: 80px;
}
QPushButton {
background-color: #ff6666;
color: white;
font-weight: bold;
min-width: 80px;
min-height: 30px;
}
""")
# 确保对话框显示在所有窗口之上
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
# 强制处理事件确保UI更新
QApplication.processEvents()
# 使用定时器在2秒后自动点击确定按钮
def auto_close():
try:
# 查找确定按钮并点击
for button in msg.buttons():
if msg.buttonRole(button) == QMessageBox.AcceptRole:
button.click()
logging.info(f"已自动点击确定按钮,弹框显示时长: {time.time() - start_time:.2f}")
return
# 如果没有找到确定按钮,直接关闭
msg.done(QMessageBox.Ok)
logging.info(f"已自动关闭弹框,弹框显示时长: {time.time() - start_time:.2f}")
except Exception as e:
logging.error(f"自动关闭弹框失败: {str(e)}")
# 设置自动关闭定时器
QTimer.singleShot(2000, auto_close)
# 显示弹框并阻塞直到用户关闭或自动关闭
logging.info(f"即将显示重量警告弹框...")
msg.exec_() # 使用exec_而不是show确保弹框显示
# 记录日志
logging.info(f"重量警告弹框已关闭,总显示时长: {time.time() - start_time:.2f}")
except Exception as e:
logging.error(f"显示重量警告弹框失败: {str(e)}", exc_info=True)
# 不需要单独的handle_spack_received方法我们直接从loading_data中获取spack
def safe_str(val):
return "" if val is None else str(val)