jiateng_ws/widgets/main_window.py

3238 lines
148 KiB
Python
Raw Normal View History

2025-06-07 10:45:09 +08:00
import os
import sys
import logging
import json
2025-06-07 10:45:09 +08:00
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,
2025-06-30 11:19:07 +08:00
UnloadingPositionHandler,
EmergencyStopHandler
)
2025-06-26 18:26:22 +08:00
from utils.electricity_monitor import ElectricityHandler
2025-06-07 10:45:09 +08:00
# 导入PySide6
from PySide6.QtWidgets import (
2025-06-28 15:01:36 +08:00
QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel,
QTableWidget, QMenu, QComboBox, QFormLayout, QDialog, QVBoxLayout,
)
from PySide6.QtCore import Qt, QTimer, Slot, Signal
2025-06-07 10:45:09 +08:00
from PySide6.QtGui import QBrush, QColor
import time
2025-06-07 10:45:09 +08:00
# 导入UI
from ui.main_window_ui import MainWindowUI
2025-06-27 15:14:30 +08:00
# 导入相机显示组件
2025-06-07 10:45:09 +08:00
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
2025-06-24 11:21:33 +08:00
from widgets.report_dialog import ReportDialog
from widgets.unloading_dialog_widget import UnloadingDialog
2025-06-07 10:45:09 +08:00
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
2025-06-30 11:19:07 +08:00
emergency_stop_signal = Signal(int, str) # 用于在主线程中处理急停信号
def __init__(self, user_id=None, user_name=None, corp_name=None, corp_id=None):
"""初始化主窗口"""
super().__init__(user_id)
# 初始化用户信息
2025-06-07 10:45:09 +08:00
self.user_id = user_id
self.user_name = user_name
self.corp_name = corp_name
self.corp_id = corp_id
# 初始化系统变量
self._current_weight = 0.0 # 当前重量
self._last_weight_time = 0 # 上次称重时间
self._stability_check_timer = None # 稳定性检查定时器
self._weight_stable_threshold = 2 # 重量稳定阈值(秒)
self._weight_processed = False # 新增:标记当前重量是否已处理,避免重复处理
self._last_processed_weight = 0.0 # 新增:记录上次处理的重量
# 初始化数据加载状态标志
self._loading_data_in_progress = False # 数据加载状态标志,防止循环调用
self._current_order_code = None # 存储当前订单号
self.init_seq = {} # 初始化轴包装的序号
# 初始化拆垛和下料相关的属性
self._current_stow_num = 0 # 当前拆垛层数
self._current_unload_num = 0 # 当前下料层数
self._total_unload_num = 0 # 总下料层数
self._current_unload_info = None # 存储当前下料信息
self._loading_info = None # 存储上料对话框的信息
self._is_loading_active = False # 标识上料任务是否正在进行
2025-06-30 11:19:07 +08:00
# 信号的连接在connect_signals方法中统一处理不在这里连接
# 称重相关变量
self._current_weight = None # 当前称重值(千克)
self._last_weight_time = None # 最后一次称重时间
self._weight_stable_threshold = 2 # 重量稳定阈值(秒)
self._stability_check_timer = None # 用于检查重量稳定性的定时器
2025-06-07 10:45:09 +08:00
# 设置窗口标题
if user_name and corp_name:
self.setWindowTitle(f"腾智微丝产线包装系统 ({corp_name})")
# 加载配置文件
self.config = self.load_config()
self.camera_enabled = self.config.get('camera', {}).get('enabled', False)
# 初始化检验配置管理器
self.inspection_manager = InspectionConfigManager.get_instance()
# 初始化托盘类型管理器
self.pallet_type_manager = PalletTypeManager.get_instance()
# 创建表单布局,用于添加托盘类型选择控件
self.material_form_layout = QFormLayout()
self.material_content_layout.addLayout(self.material_form_layout)
self.output_form_layout = QFormLayout()
self.output_content_layout.addLayout(self.output_form_layout)
2025-06-30 11:19:07 +08:00
# 创建相机显示组件和占位标签
self.camera_display = None
self.material_placeholder = None
# 初始化上料区显示
self.init_camera_display()
2025-06-07 10:45:09 +08:00
# 为下料区添加占位标签,确保它保持为空
self.output_placeholder = QWidget()
self.output_placeholder.setStyleSheet("background-color: #f0f0f0;")
placeholder_layout = QVBoxLayout(self.output_placeholder)
placeholder_layout.setAlignment(Qt.AlignCenter)
# 添加标题标签
title_label = QLabel("下料区")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #888888;")
title_label.setFont(self.second_title_font)
placeholder_layout.addWidget(title_label)
2025-06-07 10:45:09 +08:00
self.output_content_layout.addWidget(self.output_placeholder)
# 添加下料信息标签
self.unloading_level_label = QLabel("下料层数:--")
self.unloading_position_label = QLabel("下料位置:--")
placeholder_layout.addWidget(self.unloading_level_label)
placeholder_layout.addWidget(self.unloading_position_label)
self.unloading_level_label.setStyleSheet("color: #888888; font-weight: bold;")
self.unloading_position_label.setStyleSheet("color: #888888; font-weight: bold;")
self.unloading_level_label.setFont(self.normal_font)
self.unloading_position_label.setFont(self.normal_font)
2025-06-07 10:45:09 +08:00
# 创建堆叠部件
self.stacked_widget = QStackedWidget()
self.stacked_widget.addWidget(self.central_widget) # 主页面
# 不在这里直接初始化相机设置组件
# 延迟创建保证创建的时候SettingsUI的所有控件都已经准备好
self.camera_settings = None
# 设置中央部件为堆叠部件
self.setCentralWidget(self.stacked_widget)
# # 添加托盘类型选择下拉框
# self.add_pallet_type_selectors()
2025-06-07 10:45:09 +08:00
# 连接信号和槽
self.connect_signals()
2025-06-07 10:45:09 +08:00
# 默认显示主页面
self.stacked_widget.setCurrentIndex(0)
# 配置检验列 - 使用检验配置管理器获取启用的列数和标题
self.update_inspection_columns()
2025-06-07 10:45:09 +08:00
# 设置表格上下文菜单
self.process_table.setContextMenuPolicy(Qt.CustomContextMenu)
# 加载未完成的检验数据
self._safe_load_data()
# 加载已完成检验数据
self.show_pack_item()
# 创建状态处理器实例
self.machine_handlers = MachineStatusHandlers()
# 添加状态显示到状态栏
self.modbus_status_label = QLabel("Modbus: 未连接")
self.weight_label = QLabel("重量: --")
self.label_status_label = QLabel("贴标: 无贴标")
self.error_status_label = QLabel("故障: 无")
# 设置样式
self.error_status_label.setStyleSheet("color: green; font-weight: bold;")
# 添加到状态栏
self.statusBar().addPermanentWidget(self.modbus_status_label)
self.statusBar().addPermanentWidget(self.weight_label)
self.statusBar().addPermanentWidget(self.label_status_label)
self.statusBar().addPermanentWidget(self.error_status_label)
self.statusBar().addPermanentWidget(QLabel(" "))
2025-06-07 10:45:09 +08:00
logging.info(f"主窗口已创建,用户: {user_name}")
# 初始化串口管理器
self.serial_manager = SerialManager()
# 注册串口数据回调函数
self.register_serial_callbacks()
# 加载托盘号列表
self.load_pallet_codes()
2025-06-30 09:58:16 +08:00
# 恢复开始按钮原始样式
self.restore_start_button_style()
def get_axios_num(self,tray_id):
"""获取托盘号对应的轴号"""
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
axios_num = inspection_dao.get_axios_num(tray_id)
return axios_num
def get_axios_num_by_order_id(self, order_id):
"""获取订单号对应的轴号"""
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
axios_num = inspection_dao.get_axios_num_by_order_id(order_id)
return axios_num
def load_config(self):
"""加载配置文件"""
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
logging.info(f"已加载配置文件: {config_path}")
return config
except Exception as e:
logging.error(f"加载配置文件失败: {e}")
return {}
2025-06-07 10:45:09 +08:00
def connect_signals(self):
"""连接信号槽"""
# 连接微丝产线表格单元格变更信号槽
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
2025-06-07 10:45:09 +08:00
# 连接菜单动作
self.main_action.triggered.connect(self.show_main_page)
self.settings_action.triggered.connect(self.show_settings_page)
# 工程号输入框回车事件
self.order_edit.returnPressed.connect(self.handle_order_enter)
# 托盘号输入框回车和切换事件,触发未加载数据查询
# QComboBox没有returnPressed信号只有currentTextChanged和activated信号
self.tray_edit.currentTextChanged.connect(self.handle_tray_changed)
self.tray_edit.activated.connect(self.handle_tray_changed) # 当用户选择一项时触发
2025-06-07 10:45:09 +08:00
# 连接按钮事件
self.input_button.clicked.connect(self.handle_input)
self.output_button.clicked.connect(self.handle_output)
2025-06-07 10:45:09 +08:00
self.start_button.clicked.connect(self.handle_start)
self.stop_button.clicked.connect(self.handle_stop)
# 设置表格上下文菜单
self.process_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.process_table.customContextMenuRequested.connect(self.show_table_context_menu)
2025-06-30 18:24:24 +08:00
# 移除这里的相机信号连接在init_camera_display方法中处理
# 相机信号会在init_camera_display中连接
2025-06-24 11:21:33 +08:00
# 连接报表按钮点击事件
self.report_button.clicked.connect(self.on_report)
2025-06-30 11:19:07 +08:00
# 连接加载反馈信号
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)
2025-06-07 10:45:09 +08:00
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, ["检验项"])
2025-06-07 10:45:09 +08:00
def show_main_page(self):
2025-06-30 18:24:24 +08:00
"""显示主页面"""
try:
self.stacked_widget.setCurrentWidget(self.central_widget)
2025-06-30 11:19:07 +08:00
2025-06-30 18:24:24 +08:00
# 先检查并更新相机状态确保UI显示与实际状态一致
self.update_camera_enabled_state()
# 更新检验列配置
self.update_inspection_columns()
# 加载未完成的检验数据
self._safe_load_data()
# 处理相机显示
if self.camera_enabled:
# 如果camera_display不存在先初始化相机显示组件
if not hasattr(self, 'camera_display') or self.camera_display is None:
logging.info("相机显示组件不存在,尝试初始化")
self.init_camera_display()
if self.camera_display:
try:
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
# 检查相机是否已打开
if camera_manager.isOpen:
# 更新UI显示相机画面
self.update_camera_ui(True)
# 如果相机未在采集,则开始采集
if not camera_manager.isGrabbing:
# 使用内部方法启动相机显示
QTimer.singleShot(100, self._start_camera_display)
logging.info("主页面显示:启动相机显示")
else:
# 如果相机未打开,尝试重新初始化
QTimer.singleShot(100, self.initialize_camera)
logging.info("主页面显示:尝试初始化相机")
except Exception as e:
logging.error(f"处理相机显示时发生错误: {str(e)}")
# 确保UI能正确显示占位符
if self.material_placeholder:
self.material_placeholder.setText(f"相机错误: {str(e)}")
self.update_camera_ui(False)
2025-06-30 11:19:07 +08:00
else:
2025-06-30 18:24:24 +08:00
# 相机功能未启用,确保正确显示占位符
if hasattr(self, 'material_placeholder') and self.material_placeholder:
self.material_placeholder.setText("相机功能已禁用")
self.update_camera_ui(False)
logging.info("相机功能已禁用,显示占位符")
# 加载托盘号列表
self.load_pallet_codes()
logging.info("显示主页面")
except Exception as e:
logging.error(f"显示主页面时发生错误: {str(e)}")
# 确保基本的UI显示正常
if hasattr(self, 'material_placeholder') and self.material_placeholder:
self.material_placeholder.setText(f"初始化界面错误: {str(e)}")
self.material_placeholder.show()
2025-06-07 10:45:09 +08:00
def load_pallet_codes(self):
"""从托盘类型管理器加载托盘号并更新到tray_edit"""
try:
# 获取当前文本,以便保留用户选择
current_text = self.tray_edit.currentText()
# 清空当前项目
self.tray_edit.clear()
# 获取托盘号
pallet_codes = self.pallet_type_manager.get_pallet_code()
if pallet_codes and len(pallet_codes) > 0:
# 添加托盘号到下拉框
self.tray_edit.addItems(pallet_codes)
# 如果有之前的选择,尝试恢复它
index = self.tray_edit.findText(current_text)
if index != -1:
self.tray_edit.setCurrentIndex(index)
else:
self.tray_edit.setCurrentIndex(-1)
self.tray_edit.setCurrentText("")
logging.info(f"已加载托盘号,共 {len(pallet_codes)}")
else:
# 如果没有托盘号,则不添加任何项目,保持为空
logging.warning("未找到托盘号,托盘号列表将为空")
self.tray_edit.setCurrentText("")
except Exception as e:
logging.error(f"加载托盘号失败: {str(e)}")
# 如果加载失败,确保下拉框为空
self.tray_edit.clear()
self.tray_edit.setCurrentText("")
2025-06-07 10:45:09 +08:00
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)
2025-06-30 18:24:24 +08:00
# 连接相机设置组件的信号
try:
# 获取相机设置组件实例
if hasattr(self.settings_window.settings_widget, 'camera_settings'):
camera_settings = self.settings_window.settings_widget.camera_settings
# 连接相机连接状态信号
camera_settings.signal_camera_connection.connect(self.handle_camera_connection)
# 连接相机参数变化信号
camera_settings.signal_camera_params_changed.connect(self.handle_camera_params_changed)
# 连接相机错误信号
camera_settings.signal_camera_error.connect(self.handle_camera_error)
logging.info("已连接相机设置组件的信号")
else:
logging.error("无法找到相机设置组件")
except Exception as e:
logging.error(f"连接相机设置组件信号时发生错误: {str(e)}")
# 显示设置窗口
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
2025-06-30 18:24:24 +08:00
# 检查相机状态是否发生变化
logging.info("设置已变更,检查相机状态...")
try:
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
camera_enabled_in_config = self.config.get('camera', {}).get('enabled', False)
# 检查相机实际状态与配置状态是否一致
if camera_manager.isOpen and not camera_enabled_in_config:
logging.info("检测到相机已打开但配置为禁用,更新配置")
self.update_camera_enabled_state(True)
elif not camera_manager.isOpen and camera_enabled_in_config:
logging.info("检测到相机已关闭但配置为启用,更新配置")
self.update_camera_enabled_state(False)
else:
# 即使状态一致也更新一次以确保UI正确
self.update_camera_enabled_state(camera_enabled_in_config)
except Exception as e:
logging.error(f"检查相机状态时发生错误: {str(e)}")
# 更新串口管理器配置
self.serial_manager.reload_config()
2025-06-30 09:58:16 +08:00
# 重新打开已配置的串口
self.serial_manager.auto_open_configured_ports()
2025-06-30 17:35:19 +08:00
# 重新注册串口回调函数
self.register_serial_callbacks()
# 重新加载托盘号
self.load_pallet_codes()
2025-06-30 17:35:19 +08:00
logging.info("设置已更新,重新加载配置并重新打开串口,已重新注册扫码器回调")
2025-06-07 10:45:09 +08:00
def handle_input(self):
"""处理上料按钮点击事件"""
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 启动监听(不论后续是否确认上料)
# 启动Modbus监控
if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running():
self.setup_modbus_monitor()
logging.info("已在上料操作前启动Modbus监控")
# 启动串口监听
self.serial_manager.auto_open_configured_ports()
2025-06-07 10:45:09 +08:00
# 启动键盘监听器
self.serial_manager.start_keyboard_listener()
logging.info("已在上料操作前启动键盘监听器")
# 创建上料对话框
from widgets.loading_dialog_widget import LoadingDialog
dialog = LoadingDialog(parent=self,user_id=self.user_id,user_name=self.user_name,corp_id=self.corp_id)
# 如果已有上料信息,作为参考显示在对话框中,但允许用户修改
if self._loading_info and self._current_stow_num > 0:
dialog.order_input.setText(self._loading_info.get('order_code', ''))
dialog.tray_input.setText(self._loading_info.get('tray_code', ''))
dialog.axis_value.setText(self._loading_info.get('axis_value', '--'))
dialog.quantity_value.setText(self._loading_info.get('quantity_value', '--'))
dialog.weight_value.setText(self._loading_info.get('weight_value', '--'))
dialog.pallet_tier_value.setText(str(self._current_stow_num))
# 不禁用输入框,允许用户修改
# 连接订单号信号
dialog.order_code_signal.connect(self.handle_order_code_received)
# 显示对话框
result = dialog.exec()
# 如果用户确认,则执行上料操作
if result == QDialog.Accepted:
# 从对话框中获取订单号和托盘号,并更新到主窗口
order_code = dialog.order_input.text()
tray_code = dialog.tray_input.text()
self._current_order_code = order_code
self.tray_edit.setCurrentText(tray_code)
# 获取托盘料值作为拆垛层数
stow_num = dialog.pallet_tier_value.text()
if stow_num == "--" or not stow_num:
QMessageBox.warning(self, "错误", "未获取到托盘料信息,请重试")
return
# 始终使用用户最新输入的信息
self._current_stow_num = int(stow_num)
# 保存上料信息
self._loading_info = {
'order_code': dialog.order_input.text(),
'tray_code': dialog.tray_input.text(),
'axis_value': dialog.axis_value.text(),
'quantity_value': dialog.quantity_value.text(),
'weight_value': dialog.weight_value.text(),
}
# 执行Modbus操作
modbus = ModbusUtils()
client = modbus.get_client()
try:
2025-06-28 15:01:36 +08:00
# 上料 D0 给到层数,等待点击开始后,进行上料
success0 = modbus.write_register_until_success(client, 0, self._current_stow_num)
2025-06-28 15:01:36 +08:00
if success0:
# 创建状态标签并显示在右上角
self.show_operation_status("拆垛层数", "input", str(self._current_stow_num))
else:
QMessageBox.information(self, "操作提示", "上料失败")
except Exception as e:
logging.error(f"上料操作失败: {str(e)}")
QMessageBox.critical(self, "错误", f"上料操作失败: {str(e)}")
finally:
modbus.close_client(client)
2025-06-07 10:45:09 +08:00
def handle_output(self):
"""处理下料按钮点击事件"""
try:
# 启动监听(不论后续是否确认下料)
if not hasattr(self, 'modbus_monitor') or not self.modbus_monitor.is_running():
self.setup_modbus_monitor()
self.serial_manager.auto_open_configured_ports()
self.serial_manager.start_keyboard_listener()
dialog = UnloadingDialog(self, self.user_id)
# 如果有之前的下料信息,作为参考显示在对话框中,但允许用户修改
if self._current_unload_info:
dialog.set_unloading_info(self._current_unload_info)
logging.info(f"显示之前的下料信息作为参考")
if dialog.exec_() == QDialog.Accepted:
# 获取用户最新输入的下料信息
unloading_info = dialog.get_unloading_info()
# 始终使用用户最新输入的信息
self._total_unload_num = int(unloading_info.get('tier', '3'))
self._current_unload_num = 1 # 从第一层开始
self._current_unload_info = unloading_info
logging.info(f"下料任务设置:总层数={self._total_unload_num}, 当前层数={self._current_unload_num}")
# 将初始层数(1)写入寄存器
modbus = ModbusUtils()
client = modbus.get_client()
try:
modbus.write_register_until_success(client, 4, self._current_unload_num)
logging.info(f"下料初始化成功:层数 {self._current_unload_num} 已写入寄存器4")
finally:
modbus.close_client(client)
# 统一更新UI显示
tray_code = self._current_unload_info.get('tray_code', '')
self.show_operation_status("下料层数", "output", f"{self._current_unload_num}/{self._total_unload_num} ")
else:
logging.info("下料对话框已取消")
except Exception as e:
logging.error(f"处理下料操作失败: {str(e)}")
QMessageBox.critical(self, "错误", f"处理下料操作失败: {str(e)}")
2025-06-30 09:58:16 +08:00
def restore_start_button_style(self):
"""恢复开始按钮的原始样式"""
try:
# 使用与main_window_ui.py中初始化时相同的样式只恢复背景色
button_style = """
2025-06-30 09:58:16 +08:00
QPushButton {
padding: 8px 16px;
font-weight: bold;
2025-06-30 09:58:16 +08:00
border-radius: 4px;
border: 1px solid #4caf50;
2025-06-30 09:58:16 +08:00
}
QPushButton:hover {
background-color: #d7eeda;
2025-06-30 09:58:16 +08:00
}
"""
self.start_button.setStyleSheet(button_style)
logging.info("已恢复开始按钮原始样式")
2025-06-30 09:58:16 +08:00
except Exception as e:
logging.error(f"恢复开始按钮样式失败: {str(e)}")
2025-06-30 09:58:16 +08:00
def fill_start_button_style(self):
"""填充开始按钮样式 - 绿色背景,白色字体"""
try:
# 使用与main_window_ui.py中初始化时相同的样式只改变背景色和文字颜色
button_style = """
2025-06-30 09:58:16 +08:00
QPushButton {
padding: 8px 16px;
font-weight: bold;
border-radius: 4px;
2025-06-30 09:58:16 +08:00
background-color: #4caf50;
color: white;
border: 1px solid #4caf50;
}
QPushButton:hover {
background-color: #45a049;
2025-06-30 09:58:16 +08:00
color: white;
}
"""
self.start_button.setStyleSheet(button_style)
2025-06-30 09:58:16 +08:00
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:
# 判断当前操作类型(通过检查当前下料信息是否存在)
if self._current_unload_info and self._current_unload_num > 0:
# 下料模式 - 开始下料操作
# 确保寄存器3(下料启动)设为1寄存器4已在handle_output中设置了当前层数
2025-06-30 09:58:16 +08:00
success2 = modbus.write_register_until_success(client, 2, 1)
success3 = modbus.write_register_until_success(client, 3, 1)
2025-06-30 09:58:16 +08:00
if success2 and success3:
logging.info(f"开始下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}")
QMessageBox.information(self, "操作提示", f"开始下料操作:当前第{self._current_unload_num}")
2025-06-30 09:58:16 +08:00
# 填充按钮样式
self.fill_start_button_style()
else:
QMessageBox.warning(self, "错误", "开始下料操作失败")
else:
# 上料模式 - 默认操作
# 写入当前层数到D0寄存器
success0 = modbus.write_register_until_success(client, 0, self._current_stow_num)
success2 = modbus.write_register_until_success(client, 2, 1)
if success0 and success2:
self._is_loading_active = True # 标记上料任务已开始
logging.info(f"开始上料操作:当前层数 {self._current_stow_num}")
2025-06-30 09:58:16 +08:00
# 填充按钮样式
self.fill_start_button_style()
else:
QMessageBox.warning(self, "错误", "开始上料操作失败")
2025-06-30 09:58:16 +08:00
except Exception as e:
logging.error(f"开始操作失败: {str(e)}")
QMessageBox.critical(self, "错误", f"开始操作失败: {str(e)}")
finally:
modbus.close_client(client)
def handle_stop(self):
"""处理停止按钮点击事件,根据当前操作类型(上料/下料)停止相应的操作并关闭modbus监控"""
modbus = ModbusUtils()
client = modbus.get_client()
try:
# 判断当前操作类型(通过检查当前下料信息是否存在)
if self._current_unload_info and self._current_unload_num > 0:
# 下料模式 - 停止下料操作
success3 = modbus.write_register_until_success(client, 3, 0)
if success3:
logging.info(f"停止下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}")
QMessageBox.information(self, "操作提示", "已停止下料操作")
2025-06-30 09:58:16 +08:00
# 恢复按钮原始样式
self.restore_start_button_style()
else:
QMessageBox.warning(self, "错误", "停止下料操作失败")
else:
# 上料模式 - 停止上料操作
success2 = modbus.write_register_until_success(client, 2, 0)
if success2:
self._is_loading_active = False # 标记上料任务已停止
logging.info("停止上料操作")
QMessageBox.information(self, "操作提示", "已停止上料操作")
2025-06-30 09:58:16 +08:00
# 恢复按钮原始样式
self.restore_start_button_style()
else:
QMessageBox.warning(self, "错误", "停止上料操作失败")
2025-06-30 09:58:16 +08:00
except Exception as e:
logging.error(f"停止操作失败: {str(e)}")
QMessageBox.critical(self, "错误", f"停止操作失败: {str(e)}")
finally:
modbus.close_client(client)
# 停止Modbus监控
if hasattr(self, 'modbus_monitor'):
logging.info("停止Modbus监控")
self.modbus_monitor.stop()
# 停止串口监听
self.serial_manager.stop_keyboard_listener()
self.serial_manager.close_all_ports()
def clear_operation_status(self, operation_type):
"""清除右上角的操作状态显示。"""
status_label_name = f"{operation_type}_status_label"
if hasattr(self, status_label_name):
try:
getattr(self, status_label_name).deleteLater()
delattr(self, status_label_name)
logging.info(f"已清除 '{operation_type}' 状态标签。")
except AttributeError:
pass # Failsafe
2025-06-07 10:45:09 +08:00
def handle_camera_status(self, is_connected, message):
"""处理相机状态变化"""
2025-06-30 18:24:24 +08:00
try:
if is_connected:
logging.info("相机已连接并显示 - handle_camera_status")
self.update_camera_ui(True)
else:
if message:
logging.warning(f"相机显示问题: {message} - handle_camera_status")
else:
logging.warning("相机未连接 - handle_camera_status")
# 更新占位符文本
if self.material_placeholder:
self.material_placeholder.setText(f"相机错误: {message}" if message else "相机未连接")
self.update_camera_ui(False)
except Exception as e:
logging.error(f"处理相机状态变化时发生错误: {str(e)}")
# 确保在发生错误时,用户界面仍能正确显示
2025-06-30 11:19:07 +08:00
if self.material_placeholder:
2025-06-30 18:24:24 +08:00
self.material_placeholder.setText(f"相机状态处理错误: {str(e)}")
self.material_placeholder.show()
if self.camera_display:
self.camera_display.hide()
2025-06-07 10:45:09 +08:00
def handle_camera_connection(self, is_connected, message):
"""处理相机连接状态变化"""
2025-06-30 18:24:24 +08:00
try:
if is_connected:
logging.info("相机已连接")
# 更新相机启用状态
self.update_camera_enabled_state(True)
# 如果当前在主页面,直接开始显示相机画面
if self.stacked_widget.currentWidget() == self.central_widget:
if hasattr(self, 'camera_display') and self.camera_display:
self.camera_display.start_display()
2025-06-07 10:45:09 +08:00
else:
2025-06-30 18:24:24 +08:00
if message:
logging.warning(f"相机连接失败: {message}")
else:
logging.info("相机已断开")
# 如果相机断开,确保停止显示
if hasattr(self, 'camera_display') and self.camera_display:
self.camera_display.stop_display()
# 更新相机启用状态 - 只有在明确断开时才设置为False
if not message: # 只有在主动断开时才更新状态为False
self.update_camera_enabled_state(False)
except Exception as e:
logging.error(f"处理相机连接状态变化失败: {str(e)}")
if self.material_placeholder:
self.material_placeholder.setText(f"相机连接错误: {str(e)}")
self.update_camera_ui(False)
2025-06-07 10:45:09 +08:00
def handle_camera_params_changed(self, exposure_time, gain, frame_rate):
"""处理相机参数变化"""
logging.info(f"相机参数已更新: 曝光={exposure_time:.1f}μs, 增益={gain:.1f}dB, 帧率={frame_rate:.1f}fps")
# 这里可以添加对相机参数变化的处理逻辑
def handle_camera_error(self, error_msg):
"""处理相机错误"""
logging.error(f"相机错误: {error_msg}")
QMessageBox.warning(self, "相机错误", error_msg)
def closeEvent(self, event):
"""窗口关闭事件"""
# 停止Modbus监控
if hasattr(self, 'modbus_monitor'):
logging.info("停止Modbus监控")
self.modbus_monitor.stop()
2025-06-30 11:19:07 +08:00
# 处理相机关闭
if self.camera_enabled and self.camera_display:
# 停止相机显示
self.camera_display.stop_display()
2025-06-30 11:19:07 +08:00
# 关闭相机设备
try:
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
if camera_manager.isOpen:
camera_manager.close_device()
logging.info("相机设备已关闭")
except Exception as e:
logging.error(f"关闭相机设备失败: {str(e)}")
2025-06-07 10:45:09 +08:00
# 停止串口监听
self.serial_manager.stop_keyboard_listener()
self.serial_manager.close_all_ports()
2025-06-07 10:45:09 +08:00
# 接受关闭事件
event.accept()
def handle_order_enter(self):
"""处理工程号输入框按下回车事件"""
logging.info("工程号输入框按下回车事件")
# 获取当前输入的工程号
gc_note = self.order_edit.text().strip()
if gc_note:
logging.info(f"输入的工程号: {gc_note}")
#判断是否是接口,如果不是接口直接添加如果是则走接口
# 如果开启接口模式,则需要调用接口同步到业务库
self.add_new_inspection_row(gc_note, self._current_order_code)
else:
logging.warning("工程号为空")
QMessageBox.warning(self, "输入提示", "请输入有效的工程号")
def add_new_inspection_row(self, gc_note, order_code):
"""在微丝产线表格中添加一条新记录,添加到表格末尾
Args:
gc_note: 工程号
order_info: 从接口获取的工程号信息
"""
try:
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 断开单元格变更信号,避免加载过程中触发保存
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 计算新行的行索引(添加到末尾)
data_start_row = self.process_table.rowCount()
# 在末尾添加新行
self.process_table.insertRow(data_start_row)
# 计算新行的序号(最后一个序号+1
new_seq = 1 # 默认为1
if data_start_row > 2: # 如果有其他数据行
prev_seq_item = self.process_table.item(data_start_row - 1, 0)
if prev_seq_item:
try:
prev_seq = int(prev_seq_item.text())
new_seq = prev_seq + 1
except ValueError:
new_seq = data_start_row - 1 # 备选方案:使用行索引作为序号
# 添加工程号到表格的第二列
item = QTableWidgetItem(gc_note)
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, 1, item)
# 添加序号到表格的第一列
item = QTableWidgetItem(str(new_seq))
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, 0, item)
# 获取订单信息
order_info = self.inspection_manager.get_order_info(order_code)
# 检验列设置为可编辑状态
for i, config in enumerate(enabled_configs):
col_index = 2 + i # 检验列从第3列开始
# 创建单元格
item = QTableWidgetItem("")
item.setTextAlignment(Qt.AlignCenter)
# 如果有order_info数据尝试匹配字段并设置值
if order_info:
config_name = config.get('name')
# 检查order_info中是否有与config_name匹配的键
if config_name in order_info:
value = str(order_info[config_name])
item = QTableWidgetItem(value)
item.setTextAlignment(Qt.AlignCenter)
# 设置单元格背景为浅绿色,表示自动填充
item.setBackground(QBrush(QColor("#c8e6c9")))
# 保存到数据库
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
tray_id = self.tray_edit.currentText()
data = [{
'position': config.get('position'),
'config_id': config.get('id'),
'value': value,
'status': 'pass', # 默认设置为通过状态
'remark': '',
'tray_id': tray_id
}]
inspection_dao.save_inspection_data(self._current_order_code,gc_note, data)
logging.info(f"自动填充字段 {config_name} 值为 {value}")
# 设置单元格属性以标识其关联的检验项
item.setData(Qt.UserRole, config.get('id'))
self.process_table.setItem(data_start_row, col_index, item)
# 包装列设置为可编辑状态
packaging_start_col = 2 + len(enabled_configs)
for i in range(2): # 贴标和称重
col_index = packaging_start_col + i
item = QTableWidgetItem("")
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, col_index, item)
# 设置表格为可编辑状态
self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
# 重新连接单元格内容变更信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
# 选中新添加的行
self.process_table.selectRow(data_start_row)
# 限制最大行数
self.limit_table_rows(10) # 最多保留10行数据
# 将工程号和托盘号保存到数据库,确保能够正确关联
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
tray_id = self.tray_edit.currentText()
# 为每个检验位置创建一个空记录,确保工程号在数据库中存在
# 只为没有自动填充值的配置创建空记录
for config in enabled_configs:
config_name = config.get('name')
# 如果order_info中没有对应的键或者order_info为None
if not order_info or config_name not in order_info:
data = [{
'position': config.get('position'),
'config_id': config.get('id'),
'value': '',
'status': '', # 默认设置为通过状态
'remark': '',
'tray_id': tray_id
}]
inspection_dao.save_inspection_data(self._current_order_code,gc_note,gc_note, data)
# 为贴标和称重也创建空记录
2025-06-14 00:34:40 +08:00
for position in [11, 12, 13]: # 11是贴标12是毛重13是净重
data = [{
'position': position,
'config_id': position,
'value': '',
'status': 'pass', # 默认设置为通过状态
'remark': '',
'tray_id': tray_id
}]
inspection_dao.save_inspection_data(self._current_order_code,gc_note, data)
logging.info(f"已添加工程号 {gc_note} 的新记录,显示在第{new_seq}")
except Exception as e:
logging.error(f"添加新记录失败: {str(e)}")
QMessageBox.warning(self, "添加失败", f"添加新记录失败: {str(e)}")
finally:
# 重新加载数据确保UI显示正确
self._safe_load_data()
def limit_table_rows(self, max_rows):
"""限制表格最大行数
Args:
max_rows: 最大行数不包括表头行
"""
try:
# 计算数据总行数
data_rows = self.process_table.rowCount() - 2 # 减去表头行
# 如果超过最大行数,删除多余的行
if data_rows > max_rows:
# 要删除的行数
rows_to_remove = data_rows - max_rows
# 从最后一行开始删除
for i in range(rows_to_remove):
self.process_table.removeRow(self.process_table.rowCount() - 1)
logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove}")
except Exception as e:
logging.error(f"限制表格行数失败: {str(e)}")
def handle_inspection_cell_changed(self, row, column):
"""处理微丝包装单元格内容变更
Args:
row: 行索引
column: 列索引
"""
try:
# 只处理数据行的检验列变更
if row < 2: # 忽略表头行
return
# 忽略首尾两列(序号和工程号)
if column < 2:
return
# 获取工程号
order_item = self.process_table.item(row, 1)
if not order_item:
return
gc_note = order_item.text().strip()
if not gc_note:
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 判断是否是检验列(非包装列)
packaging_start_col = 2 + len(enabled_configs)
# 获取单元格内容
cell_item = self.process_table.item(row, column)
if not cell_item:
return
value = cell_item.text().strip()
# 默认设置为通过状态
status = 'pass'
# 记录当前正在处理的数据类型,用于日志输出
data_type = "检验"
if column >= 2 and column < packaging_start_col:
# 是检验列
config_index = column - 2
if config_index < len(enabled_configs):
config = enabled_configs[config_index]
data_type = config['display_name']
# 显示临时状态消息
self.statusBar().showMessage(f"正在保存检验数据: {data_type}={value}", 1000)
# 验证数据有效性
if self.validate_inspection_value(config, value):
# 设置单元格颜色为通过
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
status = 'pass'
else:
# 设置单元格颜色为警告
cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色
status = 'warning'
# 保存到数据库
self.save_inspection_data(self._current_order_code, gc_note, tray_id, config['position'], config['id'], value, status)
# 判断是否是包装列
elif column == packaging_start_col:
# 贴标列
data_type = "贴标"
self.statusBar().showMessage(f"正在保存贴标数据: {value}", 1000)
# 设置单元格颜色为通过
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
# 保存贴标数据position和config_id都是11
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, value, status)
elif column == packaging_start_col + 1:
2025-06-14 00:34:40 +08:00
# 毛重列
data_type = "毛重"
self.statusBar().showMessage(f"正在保存称重数据: {value}", 1000)
# 设置单元格颜色为通过
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
2025-06-14 00:34:40 +08:00
# 保存毛重数据position和config_id都是12
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, value, status)
2025-06-14 00:34:40 +08:00
elif column == packaging_start_col + 2:
# 净重列
data_type = "净重"
self.statusBar().showMessage(f"正在保存净重数据: {value}", 1000)
# 设置单元格颜色为通过
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
# 保存净重数据position和config_id都是13
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, value, status)
# 记录详细日志
logging.info(f"处理单元格变更: 行={row}, 列={column}, 类型={data_type}, 工程号={gc_note}, 值={value}, 状态={status}")
except Exception as e:
logging.error(f"处理检验单元格变更失败: {str(e)}")
self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000)
finally:
# 延迟一段时间后再触发查询避免频繁刷新UI
# 但要避免在加载过程中触发新的加载
if not self._loading_data_in_progress:
QTimer.singleShot(1000, self._safe_load_data)
def validate_inspection_value(self, config, value):
"""验证检验值是否有效
Args:
config: 检验配置
value: 检验值
Returns:
bool: 是否有效
"""
try:
# 特殊处理贴标和称重数据 - 这些数据默认都是有效的
if config.get('position') in [11, 12]: # 11是贴标12是称重
return True
# 检查值是否为空
if not value and config.get('required', False):
return False
# 根据数据类型验证
data_type = config.get('data_type')
if data_type == 'number':
# 数值类型验证
try:
# 如果值为空且不是必填,则视为有效
if not value and not config.get('required', False):
return True
num_value = float(value)
min_value = config.get('min_value')
max_value = config.get('max_value')
if min_value is not None and num_value < min_value:
return False
if max_value is not None and num_value > max_value:
return False
return True
except ValueError:
return False
elif data_type == 'enum':
# 枚举类型验证
enum_values = config.get('enum_values')
if enum_values and isinstance(enum_values, list):
# 如果值为空且不是必填,则视为有效
if not value and not config.get('required', False):
return True
return value in enum_values
return False
# 文本类型不做特殊验证
return True
except Exception as e:
logging.error(f"验证检验值失败: {str(e)}")
return False
def save_inspection_data(self, order_id, gc_note, tray_id, position, config_id, value, status):
"""保存检验数据到数据库
Args:
order_id: 订单号
gc_note: 工程号
position: 位置序号
config_id: 配置ID
value: 检验值
status: 状态
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
modbus = ModbusUtils()
client = modbus.get_client()
# 记录保存前的详细日志
logging.info(f"正在保存检验数据: 工程号={gc_note}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}")
# 构建数据
data = [{
'position': position,
'config_id': config_id,
'value': value,
'status': status,
'remark': '',
'tray_id': tray_id
}]
# 保存到数据库
inspection_dao.save_inspection_data(order_id, gc_note, data)
except Exception as e:
logging.error(f"保存检验数据失败: {str(e)}")
# 显示错误消息
QMessageBox.warning(self, "保存失败", f"保存检验数据错误: {str(e)[:50]}...")
self.statusBar().showMessage(f"保存检验数据错误: {str(e)[:50]}...", 3000)
def _safe_load_data(self):
"""安全地加载数据,避免循环调用"""
# 获取当前托盘号,用于日志记录
tray_id = self.tray_edit.currentText()
if self._loading_data_in_progress:
# 如果已经在加载数据,不要再次触发
logging.debug(f"已有数据加载正在进行,忽略此次请求 (托盘号: {tray_id})")
return
try:
self._loading_data_in_progress = True
self.load_finished_inspection_data()
logging.info(f"数据加载完成,托盘号: {tray_id}")
except Exception as e:
logging.error(f"安全加载数据失败: {str(e)}, 托盘号: {tray_id}")
# 即使加载失败,也尝试显示包装记录
try:
self.show_pack_item()
logging.info(f"加载失败后尝试显示包装记录, 托盘号: {tray_id}")
except Exception as ex:
logging.error(f"加载失败后显示包装记录失败: {str(ex)}, 托盘号: {tray_id}")
finally:
self._loading_data_in_progress = False
def load_finished_inspection_data(self):
"""加载未完成的检验数据并显示在表格中"""
# 注意此方法通常应通过_safe_load_data调用以防止循环
try:
# 使用InspectionDAO获取未完成的检验数据
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 使用get_inspection_data_unfinished获取未完成的数据
unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id)
# 断开单元格变更信号,避免加载过程中触发保存
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 清空表格现有数据行,只保留表头
while self.process_table.rowCount() > 2:
self.process_table.removeRow(2)
if not unfinished_data:
logging.info(f"托盘号 {tray_id} 没有未完成的检验数据")
# 确保表格完全清空,只保留表头行
self.process_table.setRowCount(2) # 只保留表头的两行
# 重新连接单元格变更信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
# 加载包装记录
return
logging.info(f"已加载未完成的检验数据,共 {len(unfinished_data)} 条记录")
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 按工程号分组
orders_data = {}
for data in unfinished_data:
gc_note = data['gc_note']
if gc_note not in orders_data:
orders_data[gc_note] = []
orders_data[gc_note].append(data)
# 添加数据到表格 - 从第3行开始添加数据
row_idx = 2
# 使用DAO方法按创建时间排序工程号确保FIFO顺序最早创建的在最前面
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
sorted_gc_notes = inspection_dao.get_orders_by_create_time(list(orders_data.keys()))
for gc_note in sorted_gc_notes:
items = orders_data[gc_note]
# 添加新行
self.process_table.insertRow(row_idx)
# 添加序号到第一列
seq_item = QTableWidgetItem(str(row_idx - 1))
seq_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, 0, seq_item)
# 添加工程号到第二列
order_item = QTableWidgetItem(gc_note)
order_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, 1, order_item)
# 添加检验数据
for item in items:
position = item['position']
value = item['value'] if item['value'] else ""
status = item['status']
config_id = item['config_id']
# 找到对应的列索引
col_index = None
for i, config in enumerate(enabled_configs):
if config.get('position') == position:
col_index = 2 + i # 检验列从第3列开始
break
if col_index is not None:
# 创建单元格并设置值
cell_item = QTableWidgetItem(str(value))
cell_item.setTextAlignment(Qt.AlignCenter)
# 存储配置ID用于保存时确定是哪个检验项
cell_item.setData(Qt.UserRole, config_id)
# 设置单元格
self.process_table.setItem(row_idx, col_index, cell_item)
# 添加贴标11和称重数据12
if position == 11: # 贴标
# 贴标列索引 = 2(序号和工程号) + 检验列数
label_col = 2 + len(enabled_configs)
self.process_table.setItem(row_idx, label_col, QTableWidgetItem(str(value)))
elif position == 12: # 称重
# 称重列索引 = 2(序号和工程号) + 检验列数 + 1(贴标)
weight_col = 2 + len(enabled_configs) + 1
self.process_table.setItem(row_idx, weight_col, QTableWidgetItem(str(value)))
2025-06-14 00:34:40 +08:00
elif position == 13: # 净重
# 净重列索引 = 2(序号和工程号) + 检验列数 + 2(贴标和称重)
net_weight_col = 2 + len(enabled_configs) + 2
self.process_table.setItem(row_idx, net_weight_col, QTableWidgetItem(str(value)))
row_idx += 1
# 设置表格为可编辑状态
self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
# 重新连接单元格变更信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except Exception as e:
logging.error(f"加载未完成的检验数据失败: {str(e)}")
QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}")
finally:
# 加载包装记录,但要避免循环调用
# 设置一个标志,防止 show_pack_item 触发更多的数据加载
# 只有在_safe_load_data调用此方法且没有明确设置加载状态的情况下才调用
has_loading_flag = hasattr(self, '_loading_data_in_progress')
is_loading = getattr(self, '_loading_data_in_progress', False)
# 如果是被_safe_load_data调用即已经设置了_loading_data_in_progress则无需额外设置
if has_loading_flag and is_loading:
# 直接调用show_pack_item不改变加载状态
try:
self.show_pack_item()
logging.info("在load_finished_inspection_data中调用show_pack_item")
except Exception as e:
logging.error(f"在load_finished_inspection_data中调用show_pack_item失败: {str(e)}")
# 否则这是直接调用此方法非_safe_load_data需要设置加载状态
elif not is_loading:
self._loading_data_in_progress = True
try:
self.show_pack_item()
logging.info("在load_finished_inspection_data中直接调用show_pack_item")
finally:
self._loading_data_in_progress = False
def load_finished_record_to_package_record(self, order_id, gc_note, tray_id):
"""加载已完成检验数据到包装记录
Args:
order_id: 工程号
tray_id: 托盘号
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取该工程号的所有检验数据
inspection_data = inspection_dao.get_inspection_data_by_order(order_id, gc_note, tray_id)
if not inspection_data:
logging.warning(f"未找到工程号 {gc_note} 托盘号 {tray_id} 的检验数据")
return
# 获取轴号并保存
label_value = self.get_axios_num_by_order_id(self._current_order_code)
# 从检验数据中获取贴标和称重数据
weight_value = ""
2025-06-14 00:34:40 +08:00
net_weight_value = ""
for item in inspection_data:
if item['position'] == 12: # 称重
weight_value = item['value']
2025-06-14 00:34:40 +08:00
elif item['position'] == 13: # 净重
net_weight_value = item['value']
# 只要贴标字段有值,就可以写入包装记录
if label_value == None or label_value == "":
logging.warning(f"工程号 {order_id} 托盘号 {tray_id} 的贴标字段为空,不添加到包装记录")
return
# 获取当前时间作为完成时间
finish_time = datetime.now()
# 将数据写入到数据库表 inspection_pack_data
inspection_dao.save_package_record(order_id, tray_id, str(label_value+1), weight_value,net_weight_value, finish_time,gc_note)
# 回显数据,但避免循环调用
if not getattr(self, '_loading_data_in_progress'):
self._loading_data_in_progress = True
try:
self.show_pack_item()
finally:
self._loading_data_in_progress = False
logging.info(f"已将工程号 {order_id} 托盘号 {tray_id} 的检验数据添加到包装记录并回显")
except Exception as e:
logging.error(f"加载已完成检验数据到包装记录失败: {str(e)}")
QMessageBox.warning(self, "加载失败", f"加载已完成检验数据到包装记录失败: {str(e)}")
def show_pack_item(self):
"""显示包装记录"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取托盘号
tray_id = self.tray_edit.currentText()
logging.info(f"显示包装记录,当前托盘号: {tray_id}")
if not tray_id:
logging.warning("托盘号为空,无法显示包装记录")
# 清空表格
self.record_table.setRowCount(0)
self.update_package_statistics()
return
# 读取已包装的记录信息
package_record = inspection_dao.get_package_record(tray_id)
# 记录获取的数据情况
if package_record:
logging.info(f"成功获取包装记录,托盘号={tray_id},记录数量={len(package_record)}")
else:
logging.info(f"包装记录为空,托盘号={tray_id}")
# 清空表格内容
self.record_table.setRowCount(0)
# 断开包装记录表的信号连接(如果有)
try:
self.record_table.blockSignals(True) # 使用blockSignals替代手动断开信号
except Exception as e:
logging.warning(f"阻止信号失败: {str(e)}")
# 如果没有包装记录,直接返回
if not package_record:
logging.info(f"托盘号 {tray_id} 没有包装记录数据")
self.update_package_statistics()
self.record_table.blockSignals(False) # 恢复信号
return
logging.info(f"托盘号 {tray_id} 已加载包装记录,共 {len(package_record)} 条记录")
# 添加所有包装记录到表格
for index, item in enumerate(package_record):
try:
row_index = self.record_table.rowCount()
self.record_table.insertRow(row_index)
# 设置单元格数据,使用安全的方式访问数据
cell_data = [
str(index + 1), # 序号
str(item[0]) if len(item) > 0 else "", # 订单
str(item[1]) if len(item) > 1 else "", # 工程号
str(item[2]) if len(item) > 2 else "", # 品名
str(item[3]) if len(item) > 3 else "", # 规格
str(item[4]) if len(item) > 4 else "", # 托号
str(item[5]) if len(item) > 5 else "", # 轴包装号
str(item[6]) if len(item) > 6 else "", # 毛重
str(item[7]) if len(item) > 7 else "", # 净重
str(item[8]) if len(item) > 8 else "" # 完成时间
]
# 批量设置单元格
for col, data in enumerate(cell_data):
cell_item = QTableWidgetItem(data)
cell_item.setTextAlignment(Qt.AlignCenter)
self.record_table.setItem(row_index, col, cell_item)
except Exception as e:
logging.error(f"设置第 {index} 行数据时出错: {str(e)}, 数据: {item}")
continue # 继续处理下一行
# 恢复信号
self.record_table.blockSignals(False)
2025-06-16 13:21:17 +08:00
# 更新包装记录统计数据
self.update_package_statistics()
logging.info(f"包装记录显示完成,托盘号={tray_id},总记录数={self.record_table.rowCount()}")
except Exception as e:
logging.error(f"显示包装记录失败: {str(e)}")
self.record_table.blockSignals(False) # 确保信号被恢复
QMessageBox.warning(self, "显示失败", f"显示包装记录失败: {str(e)}")
def update_package_statistics(self):
"""更新包装记录统计数据"""
try:
# 获取包装记录表的行数
package_count = self.record_table.rowCount()
# 更新任务表格中的已完成数量
completed_item = QTableWidgetItem(str(package_count))
completed_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 2, completed_item)
# 计算已完成公斤数(如果称重列有数值)
completed_kg = 0
for row in range(self.record_table.rowCount()):
weight_item = self.record_table.item(row, 6) # 称重列
if weight_item and weight_item.text():
try:
completed_kg += float(weight_item.text())
except ValueError:
pass
# 更新任务表格中的已完成公斤
completed_kg_item = QTableWidgetItem(str(completed_kg))
completed_kg_item.setTextAlignment(Qt.AlignCenter)
self.task_table.setItem(2, 3, completed_kg_item)
logging.info(f"已更新包装记录统计数据: 完成数量={package_count}, 完成公斤={completed_kg}")
except Exception as e:
logging.error(f"更新包装记录统计数据失败: {str(e)}")
def show_table_context_menu(self, pos):
"""显示表格上下文菜单
Args:
pos: 鼠标位置
"""
try:
# 获取当前单元格
cell_index = self.process_table.indexAt(pos)
if not cell_index.isValid():
return
row = cell_index.row()
column = cell_index.column()
# 只对数据行和检验列显示上下文菜单
if row < 2: # 忽略表头行
return
# 获取工程号
order_id_item = self.process_table.item(row, 1)
if not order_id_item:
return
order_id = order_id_item.text().strip()
if not order_id:
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 创建上下文菜单
menu = QMenu(self)
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 判断是否是检验列(非包装列)
packaging_start_col = 2 + len(enabled_configs)
if column >= 2 and column < packaging_start_col:
# 是检验列
config_index = column - 2
if config_index < len(enabled_configs):
config = enabled_configs[config_index]
position = config.get('position')
# 添加查询数据库菜单项
check_action = menu.addAction("检查数据库记录")
check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id))
# 显示菜单
menu.exec_(self.process_table.viewport().mapToGlobal(pos))
except Exception as e:
logging.error(f"显示表格上下文菜单失败: {str(e)}")
def check_database_record(self, order_id, position, tray_id):
"""检查数据库记录
Args:
order_id: 工程号
position: 位置序号
tray_id: 托盘号
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取检验数据
inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id)
# 查找对应位置的数据
matching_data = None
for data in inspection_data:
if data.get('position') == position:
matching_data = data
break
# 显示结果
if matching_data:
value = matching_data.get('value')
status = matching_data.get('status')
message = f"数据库记录:\n\n"
message += f"工程号: {order_id}\n"
message += f"位置: {position}\n"
message += f"值: {value}\n"
message += f"状态: {status}\n"
QMessageBox.information(self, "数据库记录", message)
else:
QMessageBox.warning(self, "数据库记录", f"未找到工程号 {order_id} 位置 {position} 的数据")
except Exception as e:
logging.error(f"检查数据库记录失败: {str(e)}")
QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}")
def show_operation_status(self, status, operation_type, pallet_type):
"""在右上角显示操作状态
Args:
status: 状态文本
operation_type: 操作类型 (input/output)
pallet_type: 托盘类型
"""
# 确定要添加标签的容器
if operation_type == "input":
container = self.material_content
else:
container = self.output_content
# 如果已存在状态标签,则移除它
status_label_name = f"{operation_type}_status_label"
if hasattr(self, status_label_name):
old_label = getattr(self, status_label_name)
old_label.deleteLater()
# 创建新的状态标签
status_label = QLabel(f"{status}: {pallet_type}", container)
status_label.setFont(self.second_title_font)
status_label.setStyleSheet("color: red; background-color: transparent;")
status_label.setAlignment(Qt.AlignRight | Qt.AlignTop)
# 使用绝对定位,放置在右上角
status_label.setGeometry(container.width() - 250, 5, 240, 30)
# 确保标签始终保持在顶层显示
status_label.raise_()
status_label.show()
# 保存标签引用
setattr(self, status_label_name, status_label)
# 保存原始的resize事件处理函数
if not hasattr(container, "_original_resize_event"):
container._original_resize_event = container.resizeEvent
# 添加窗口大小变化事件处理,确保标签位置随窗口调整
container.resizeEvent = lambda event: self.adjust_status_label_position(event, container, status_label)
def adjust_status_label_position(self, event, container, label):
"""调整状态标签位置,确保始终在右上角
Args:
event: 窗口大小变化事件
container: 标签所在的容器
label: 状态标签
"""
# 更新标签位置,保持在右上角
label.setGeometry(container.width() - 250, 5, 240, 30)
# 调用原始的resizeEvent如果有的话
original_resize = getattr(container, "_original_resize_event", None)
if original_resize:
original_resize(event)
# ==================== Modbus监控系统相关方法 ====================
def setup_modbus_monitor(self):
"""设置Modbus监控系统"""
# 获取Modbus监控器实例
self.modbus_monitor = get_modbus_monitor()
# 注册寄存器处理器
self._register_modbus_handlers()
# 连接信号槽
self._connect_modbus_signals()
# 启动监控
self.modbus_monitor.start()
logging.info("Modbus监控系统已设置")
def _register_modbus_handlers(self):
"""注册寄存器处理器"""
2025-06-26 18:26:22 +08:00
# 获取Modbus监控器实例
monitor = get_modbus_monitor()
# 注册D6处理器处理NG信号
2025-06-26 18:26:22 +08:00
monitor.register_handler(6, NGHandler(self.machine_handlers.handle_ng))
# 注册D11处理器处理称重数据
2025-06-26 18:26:22 +08:00
monitor.register_handler(11, WeightDataHandler(self.machine_handlers.handle_weight_data))
# 注册D13处理器处理贴标信号
2025-06-26 18:26:22 +08:00
monitor.register_handler(13, LabelSignalHandler(self.machine_handlers.handle_label_signal))
# 注册D20-D24处理器处理各种状态信息
2025-06-26 18:26:22 +08:00
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))
2025-06-30 11:19:07 +08:00
# 注册急停信号处理器
monitor.register_handler(25, EmergencyStopHandler(self.handle_emergency_stop))
# 注册下料层数和位置处理器
2025-06-26 18:26:22 +08:00
monitor.register_handler(4, UnloadingLevelHandler(self.handle_unloading_level))
monitor.register_handler(5, UnloadingPositionHandler(self.handle_unloading_position))
# 注册电力消耗处理器
monitor.register_handler(30, ElectricityHandler())
logging.info("已注册所有Modbus寄存器处理器")
def _connect_modbus_signals(self):
"""连接Modbus信号槽"""
# 连接监控器状态信号
self.modbus_monitor.monitor_status_changed.connect(self.handle_modbus_status_change)
self.modbus_monitor.register_error.connect(self.handle_register_error)
self.machine_handlers.ng_changed.connect(self.handle_ng)
# 直接连接寄存器变化信号
self.modbus_monitor.register_changed.connect(self.handle_register_change)
# 连接机器状态信号
self.machine_handlers.loading_feedback_changed.connect(self.handle_loading_feedback)
self.machine_handlers.unloading_feedback_changed.connect(self.handle_unloading_feedback)
self.machine_handlers.error_1_changed.connect(self.handle_error_1)
self.machine_handlers.error_2_changed.connect(self.handle_error_2)
self.machine_handlers.error_3_changed.connect(self.handle_error_3)
# 连接称重数据和贴标信号
self.machine_handlers.weight_changed.connect(self.handle_weight_data)
self.machine_handlers.label_signal_changed.connect(self.handle_label_signal)
def _convert_to_kg(self, weight_in_g):
"""
将克转换为千克
Args:
weight_in_g: 重量
Returns:
float: 重量千克
"""
return round(weight_in_g / 1000.0, 3) # 保留3位小数
@Slot(int)
def handle_weight_data(self, weight_in_g):
"""处理称重数据变化"""
try:
current_time = time.time()
# 转换重量单位并立即更新UI显示
weight_in_kg = self._convert_to_kg(weight_in_g)
logging.info(f"[显示] 称重数据: {weight_in_kg}kg (原始值: {weight_in_g}g)")
self.weight_label.setText(f"重量: {weight_in_kg}kg")
# 检测重量从接近0到较大值的变化判断为新产品
if self._current_weight is not None and self._current_weight < 0.1 and weight_in_kg > 0.5:
logging.info(f"检测到新产品放上,重量从 {self._current_weight}kg 变为 {weight_in_kg}kg")
self._weight_processed = False # 重置处理标记,允许处理新产品
# 更新当前重量和时间
self._current_weight = weight_in_kg
self._last_weight_time = current_time
# 取消之前的定时器(如果存在)
if self._stability_check_timer is not None:
self._stability_check_timer.stop()
self._stability_check_timer.deleteLater()
# 创建新的定时器
self._stability_check_timer = QTimer()
self._stability_check_timer.setSingleShot(True) # 单次触发
self._stability_check_timer.timeout.connect(lambda: self._check_weight_stability(weight_in_kg))
self._stability_check_timer.start(self._weight_stable_threshold * 1000) # 转换为毫秒
# 尝试获取表格行数据,用于日志记录
current_row = self.process_table.currentRow()
data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行索引为2
# 记录表格行状态,仅用于日志记录,不影响后续处理
if data_row >= self.process_table.rowCount():
logging.warning(f"选中的行 {data_row} 超出了表格范围")
else:
# 获取工程号,仅用于日志记录
gc_note_item = self.process_table.item(data_row, 1)
if gc_note_item:
gc_note = gc_note_item.text().strip()
if gc_note:
logging.info(f"当前处理的工程号: {gc_note}, 行: {data_row}")
else:
logging.warning("工程号为空")
else:
logging.warning("无法获取工程号")
except Exception as e:
logging.error(f"处理称重数据时发生错误: {str(e)}")
# 确保重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
def _check_weight_stability(self, original_weight_kg):
"""
检查重量是否稳定
Args:
original_weight_kg: 开始检查时的重量千克
"""
try:
# 如果当前重量与定时器启动时的重量相同,说明这段时间内没有新的重量数据
if self._current_weight == original_weight_kg:
logging.info(f"重量 {original_weight_kg}kg 在{self._weight_stable_threshold}秒内保持稳定")
# 如果这个重量与上一次处理的重量接近±0.1kg),且标记已处理,则跳过
if self._weight_processed and abs(original_weight_kg - self._last_processed_weight) < 0.1:
logging.info(f"跳过处理:重量 {original_weight_kg}kg 与上次处理的重量 {self._last_processed_weight}kg 接近且已处理")
return
# 称重稳定后,给寄存器 D10 为 1 表示已经称重完成
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 10, 1)
modbus.close_client(client)
# 处理稳定重量
self._process_stable_weight(original_weight_kg)
# 调用打印方法
self._print_weight_label(original_weight_kg)
# 设置已处理标记和上次处理的重量
self._weight_processed = True
self._last_processed_weight = original_weight_kg
logging.info(f"已标记重量 {original_weight_kg}kg 为已处理")
else:
logging.info(f"重量在{self._weight_stable_threshold}秒内发生变化,从 {original_weight_kg}kg 变为 {self._current_weight}kg")
except Exception as e:
logging.error(f"检查重量稳定性时发生错误: {str(e)}")
finally:
# 清理定时器
if self._stability_check_timer is not None:
self._stability_check_timer.deleteLater()
self._stability_check_timer = None
def _process_stable_weight(self, weight_kg):
"""
处理稳定的称重数据
Args:
weight_kg: 稳定的重量值千克
"""
try:
# 忽略接近0的重量值这可能表示产品已被移除
if weight_kg < 0.1: # 小于100g的重量视为无效
logging.info(f"忽略接近零的重量值: {weight_kg}kg可能表示产品已被移除")
return
# 获取数据行数
if self.process_table.rowCount() <= 2: # 没有数据行
logging.warning("没有可用的数据行来写入称重数据")
return
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 计算称重列索引 - 称重位置在检验列之后的第二列(贴标后面)
weight_col = 2 + len(enabled_configs) + 1
2025-06-14 00:34:40 +08:00
# 计算净重列索引 - 净重位置在检验列之后的第三列(称重后面)
net_weight_col = 2 + len(enabled_configs) + 2
# 查找第一个没有称重数据的行
data_row = None
for row in range(2, self.process_table.rowCount()):
weight_item = self.process_table.item(row, weight_col)
if not weight_item or not weight_item.text().strip():
data_row = row
break
# 如果没有找到没有称重数据的行,使用当前选中行或第一个数据行
if data_row is None:
current_row = self.process_table.currentRow()
data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行索引为2
logging.info(f"未找到没有称重数据的行,使用当前选中行或第一个数据行: {data_row}")
else:
logging.info(f"找到没有称重数据的行: {data_row}")
# 获取工程号
gc_note = self.process_table.item(data_row, 1)
if not gc_note:
logging.warning("无法获取工程号")
return
gc_note = gc_note.text().strip()
if not gc_note:
logging.warning("工程号为空")
return
# 暂时断开信号连接避免触发cellChanged信号
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 设置称重值单元格(显示千克)
weight_item = QTableWidgetItem(str(weight_kg))
weight_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_row, weight_col, weight_item)
# 保存到数据库(使用千克)
tray_id = self.tray_edit.currentText()
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 12, 12, str(weight_kg), "pass")
# 保存净重到数据库(毛重-工字轮重量,单位都是千克)
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
gzl_zl_raw = inspection_dao.get_gzl_zl(self._current_order_code)
gzl_zl = 0.0
try:
if gzl_zl_raw:
gzl_zl = float(gzl_zl_raw)
except (ValueError, TypeError):
logging.warning(f"无法将工字轮重量 '{gzl_zl_raw}' 转换为浮点数,将使用默认值 0.0")
net_weight_kg = round(weight_kg - gzl_zl,3)
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 13, 13, str(net_weight_kg), "pass")
2025-06-14 00:34:40 +08:00
# 设置净重单元格(显示千克)
net_weight_item = QTableWidgetItem(str(net_weight_kg))
2025-06-14 00:34:40 +08:00
net_weight_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_row, net_weight_col, net_weight_item)
# 如果开启 api 模式,则调用接口添加到包装记录
if AppMode.is_api():
from dao.inspection_dao import InspectionDAO
from apis.gc_api import GcApi
inspection_dao = InspectionDAO()
# 调用接口
gc_api = GcApi()
axios_num = self.get_axios_num_by_order_id(self._current_order_code) + 1
# 获取订单信息和其他信息,两者都已经是字典格式
info = {}
order_info = inspection_dao.get_order_info(self._current_order_code)
info.update(order_info)
# 获取包装号
info['xpack'] = self.tray_edit.currentText()
info['spack'] = self.tray_edit.currentText()
order_others_info = inspection_dao.get_order_others_info(gc_note, self._current_order_code, tray_id)
info.update(order_others_info)
info['data_corp'] = order_info['data_corp']
info['zh'] = axios_num
# 获取本机IP地址
# import socket
# try:
# # 通过连接外部服务器获取本机IP不实际建立连接
# s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# s.connect(("8.8.8.8", 80))
# local_ip = s.getsockname()[0]
# s.close()
# info['nw_ip'] = local_ip.replace('.', '')
# except Exception as e:
# logging.error(f"获取本机IP失败: {str(e)}")
# # 如果获取失败,使用本地回环地址
# info['nw_ip'] = '127.0.0.1'.replace('.', '')
info['nw_ip'] = '192.168.1.246'
# 调用接口添加到包装记录
response = gc_api.add_order_info(info)
if response.get("status",False):
logging.info(f"添加订单信息成功: {response.get('data',{})}")
else:
QMessageBox.warning(self, f"提示", response.get("message",{}))
# 保存贴标数据到数据库
2025-06-26 18:26:22 +08:00
self.save_inspection_data(self._current_order_code, gc_note, tray_id, 11, 11, str(axios_num), "pass")
# 重新连接信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.info(f"已将稳定的称重数据 {weight_kg}kg 写入行 {data_row}, 列 {weight_col}")
except Exception as e:
logging.error(f"处理称重数据时发生错误: {str(e)}")
# 确保重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
def _print_weight_label(self, weight_kg):
"""
打印重量标签 注意目前打印是写入数据库打印不需要再次调用
Args:
weight_kg: 稳定的重量值千克
"""
try:
logging.info(f"开始打印重量标签,重量:{weight_kg}kg")
# TODO: 实现打印逻辑
pass
except Exception as e:
logging.error(f"打印重量标签时发生错误: {str(e)}")
@Slot(int, str)
def handle_label_signal(self, signal, status):
"""处理贴标信号"""
logging.info(f"[处理] 贴标信号: {status} (值={signal})")
# 更新UI显示
self.label_status_label.setText(f"贴标: {status}")
# 只有当信号为贴标完成(1)时才进行处理
if signal == 1:
try:
# 获取数据行数
if self.process_table.rowCount() <= 2: # 没有数据行
logging.warning("没有可用的数据行来写入贴标数据")
return
# 获取当前选中的行或第一个数据行
current_row = self.process_table.currentRow()
data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行索引为2
# 确保行存在
if data_row >= self.process_table.rowCount():
logging.warning(f"选中的行 {data_row} 超出了表格范围")
return
# 获取工程号
order_id_item = self.process_table.item(data_row, 1)
if not order_id_item:
logging.warning("无法获取工程号")
return
gc_note = order_id_item.text().strip()
if not gc_note:
logging.warning("工程号为空")
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 计算贴标列索引 - 贴标位置在检验列之后的第一列
label_col = 2 + len(enabled_configs)
# 生成贴标号(托盘号+轴号)
axios_num = self.get_axios_num(tray_id)+1
# 断开单元格变更信号,避免程序自动写入时触发
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 创建并设置贴标单元格
label_item = QTableWidgetItem(str(axios_num))
label_item.setTextAlignment(Qt.AlignCenter)
# 写入单元格
self.process_table.setItem(data_row, label_col, label_item)
logging.info(f"已将贴标数据 {axios_num} 写入表格单元格 [{data_row}, {label_col}]")
# 调用加载到包装记录的方法
self.load_finished_record_to_package_record(self._current_order_code,gc_note, tray_id)
logging.info(f"贴标完成,已将工程号 {gc_note} 的记录加载到包装记录")
# 删除当前处理的行
self.process_table.removeRow(data_row)
logging.info(f"已删除处理完成的行 {data_row}")
# 重新连接单元格变更信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except Exception as e:
logging.error(f"处理贴标完成信号失败: {str(e)}")
# 确保信号重新连接
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
@Slot(bool, str)
def handle_modbus_status_change(self, is_connected, message):
"""处理Modbus连接状态变化"""
if is_connected:
self.modbus_status_label.setText("Modbus: 已连接")
self.modbus_status_label.setStyleSheet("color: green;")
logging.info(f"Modbus已连接: {message}")
else:
self.modbus_status_label.setText("Modbus: 未连接")
self.modbus_status_label.setToolTip(message)
self.modbus_status_label.setStyleSheet("color: red;")
logging.warning(f"Modbus连接断开: {message}")
@Slot(int, str)
def handle_register_error(self, address, error_msg):
"""处理寄存器读取错误"""
logging.warning(f"[处理] 寄存器D{address}错误: {error_msg}")
# 在这里可以添加错误处理逻辑
pass
@Slot(int, int)
def handle_register_change(self, address, value):
"""处理寄存器变化"""
logging.info(f"[处理] 寄存器D{address}变化: {value}")
2025-06-30 17:35:19 +08:00
# 当D11寄存器变为0时复位D10寄存器为0
if address == 11 and value == 0:
try:
logging.info("检测到D11寄存器变为0正在复位D10寄存器")
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 10, 0)
logging.info("成功复位D10寄存器为0")
modbus.close_client(client)
except Exception as e:
logging.error(f"复位D10寄存器失败: {str(e)}")
@Slot(int, str)
def handle_loading_feedback(self, status, desc):
"""处理上料信息反馈"""
2025-06-30 09:58:16 +08:00
message = desc
try:
if status == 1:
modbus = ModbusUtils()
client = modbus.get_client()
# 睡 0.5 秒用于延缓modbus 监听
time.sleep(0.5)
modbus.write_register_until_success(client, 2, 0)
if self._current_stow_num > 0:
completed_layer_num = self._current_stow_num
self._current_stow_num -= 1
if self._current_stow_num == 0:
self._is_loading_active = False # 任务完成,标记为非活动
self._loading_info = None
logging.info("所有层拆垛完成,清空上料信息")
message = f"{completed_layer_num} 层(最后一层)拆垛完成!"
# 重置寄存器 0 和 2 为 0
modbus.write_register_until_success(client, 0, 0)
modbus.write_register_until_success(client, 2, 0)
self.loading_feedback_signal.emit("input", message)
# 恢复开始按钮原始样式
self.restore_start_button_style()
else:
logging.info(f"当前层拆垛完成,剩余层数: {self._current_stow_num}")
message = f"{completed_layer_num} 层拆垛完成。"
self.loading_feedback_signal.emit("input", message)
#通知寄存器,进行第几层拆垛
modbus.write_register_until_success(client,0 ,self._current_stow_num)
except Exception as e:
logging.error(f"处理上料信息反馈失败: {str(e)}")
# 不在这里显示对话框,而是通过信号传递错误信息
self.loading_feedback_signal.emit("error", f"处理上料信息反馈失败: {str(e)}")
finally:
modbus.close_client(client)
def _handle_loading_feedback_ui(self, status_type, desc):
"""在主线程中处理上料UI更新"""
try:
# 如果上料任务仍在进行,更新层数显示
if self._loading_info and self._current_stow_num > 0:
self.show_operation_status("拆垛层数", "input", str(self._current_stow_num))
else:
# 上料任务完成,清除状态显示
self.clear_operation_status("input")
QMessageBox.information(self, "上料操作", desc)
except Exception as e:
logging.error(f"处理上料UI更新失败: {str(e)}")
@Slot(int, str)
def handle_unloading_feedback(self, status, desc):
"""处理下料信息反馈"""
logging.info(f"[处理] 下料信息: {status}, {desc}")
# 如果下料完成(status=1),显示状态信息,处理下料流程
if status == 1:
modbus = ModbusUtils()
client = modbus.get_client()
try:
2025-06-30 09:58:16 +08:00
# 睡 0.5 秒用于延缓modbus 监听
time.sleep(0.5)
# 临时重置寄存器3(下料启动)为0等待用户下一次启动
modbus.write_register_until_success(client, 3, 0)
# 如果当前下料层数小于总层数则将层数加1并写入寄存器4
if self._current_unload_num < self._total_unload_num:
# 当前层已完成,准备下一层
self._current_unload_num += 1
logging.info(f"当前层下料完成,更新层数:当前={self._current_unload_num}, 总数={self._total_unload_num}")
# 将新的层数写入寄存器4
modbus.write_register_until_success(client, 4, self._current_unload_num)
logging.info(f"已将新层数{self._current_unload_num}写入寄存器4")
# 不直接更新UI而是通过信号将数据传递给主线程处理
# 通过信号触发UI更新 - 显示前一层完成的消息
message = f"{self._current_unload_num-1}层下料完成,请启动第{self._current_unload_num}层下料"
self.unloading_feedback_signal.emit("output", message)
2025-06-30 09:58:16 +08:00
# 恢复开始按钮原始样式
self.restore_start_button_style()
else:
# 所有层都下料完成,重置寄存器和计数器
modbus.write_register_until_success(client, 3, 0) # 确保下料启动寄存器为0
modbus.write_register_until_success(client, 4, 0) # 重置下料层数寄存器为0
# 记录完成的信息用于消息显示
final_tier = self._current_unload_num
total_tier = self._total_unload_num
tray_code = self._current_unload_info.get('tray_code', '') if self._current_unload_info else ''
# 重置计数器和信息
self._current_unload_num = 0
# 不重置总层数,以便可以继续使用相同的总层数
# self._total_unload_num = 0
self._current_unload_info = None
logging.info(f"托盘 {tray_code} 的所有 {total_tier} 层下料完成,重置当前层数")
# 通过信号触发UI更新而不是直接操作UI
message = f"托盘 {tray_code} 的所有 {total_tier} 层下料已全部完成"
self.unloading_feedback_signal.emit("output", message)
2025-06-30 09:58:16 +08:00
# 恢复开始按钮原始样式
self.restore_start_button_style()
except Exception as e:
logging.error(f"处理下料反馈时发生错误: {str(e)}")
# 不在这里显示对话框,而是通过信号传递错误信息
self.unloading_feedback_signal.emit("error", f"处理下料反馈失败: {str(e)}")
finally:
modbus.close_client(client)
def _handle_unloading_feedback_ui(self, status_type, desc):
"""在主线程中处理下料完成的事件通知"""
try:
if status_type == "error":
QMessageBox.critical(self, "错误", desc)
return
# 显示事件消息
if "全部完成" in desc:
QMessageBox.information(self, "下料完成", desc)
# 任务完成,清除状态显示
self.clear_operation_status("output")
self.unloading_level_label.setText("下料层数:--")
self.unloading_position_label.setText("下料位置:--")
elif "请启动" in desc:
QMessageBox.information(self, "下料层完成", desc)
except Exception as e:
logging.error(f"处理下料UI更新失败: {str(e)}")
def _update_error_status(self):
"""更新故障状态显示"""
# 收集所有故障信息
error_codes = [
getattr(self, 'error_1', 0),
getattr(self, 'error_2', 0),
getattr(self, 'error_3', 0)
]
# 检查是否有故障
has_error = any(code > 0 for code in error_codes)
if has_error:
# 收集所有错误信息
errors = []
error_map = self.machine_handlers.error_map
if getattr(self, 'error_1', 0) > 0:
errors.append(f"故障1: {error_map.get(self.error_1, '未知')}")
if getattr(self, 'error_2', 0) > 0:
errors.append(f"故障2: {error_map.get(self.error_2, '未知')}")
if getattr(self, 'error_3', 0) > 0:
errors.append(f"故障3: {error_map.get(self.error_3, '未知')}")
self.error_status_label.setText("故障: 有")
self.error_status_label.setToolTip("\n".join(errors))
self.error_status_label.setStyleSheet("color: red; font-weight: bold;")
else:
self.error_status_label.setText("故障: 无")
self.error_status_label.setToolTip("")
self.error_status_label.setStyleSheet("color: green; font-weight: bold;")
@Slot(int, str)
def handle_error_1(self, error_code, error_desc):
"""机器人视觉报警"""
logging.info(f"[处理] 机器人视觉报警: {error_desc}")
from utils.register_handlers import Error1Handler
error_handler = Error1Handler()
detailed_desc = error_handler.error_map.get(error_code, f"机器人视觉报警-{error_code}")
# 保存故障码
self.error_1 = error_code
self._update_error_status()
2025-06-30 17:35:19 +08:00
# 只有当错误码为1、2或3时才弹框提示
if error_code in [1, 2, 3]:
QMessageBox.warning(self, "机器人视觉报警", f"机器人视觉报警: {detailed_desc}")
# 获取Modbus连接
modbus = ModbusUtils()
client = modbus.get_client()
# 根据错误码可以添加不同的处理逻辑
# 这里先简单处理,对所有错误都复位相关寄存器
modbus.write_register_until_success(client, 2, 0)
modbus.write_register_until_success(client, 0, 0)
modbus.close_client(client)
@Slot(int, str)
def handle_error_2(self, error_code, error_desc):
"""滚筒线报警"""
logging.info(f"[处理] 滚筒线报警: {error_desc}")
from utils.register_handlers import Error2Handler
error_handler = Error2Handler()
detailed_desc = error_handler.error_map.get(error_code, f"滚筒线报警-{error_code}")
# 保存故障码
self.error_2 = error_code
self._update_error_status()
# 如果有故障,显示提示(对任何错误码都弹框)
2025-06-30 17:35:19 +08:00
if error_code in [1, 2]:
QMessageBox.warning(self, "滚筒线报警", f"滚筒线报警: {detailed_desc}")
# 获取Modbus连接
modbus = ModbusUtils()
client = modbus.get_client()
# 根据错误码可以添加不同的处理逻辑
# 这里先简单处理,对所有错误都复位相关寄存器
modbus.write_register_until_success(client, 3, 0)
modbus.write_register_until_success(client, 4, 0)
modbus.close_client(client)
@Slot(int, str)
def handle_error_3(self, error_code, error_desc):
"""拆码垛报警"""
logging.info(f"[处理] 拆码垛报警: {error_desc}")
from utils.register_handlers import Error3Handler
error_handler = Error3Handler()
detailed_desc = error_handler.error_map.get(error_code, f"拆码垛报警-{error_code}")
# 保存故障码
self.error_3 = error_code
self._update_error_status()
modbus = ModbusUtils()
client = modbus.get_client()
# 如果有故障,显示提示
if error_code == 1:
QMessageBox.warning(self, "异常", f"异常: {detailed_desc}")
modbus.write_register_until_success(client, 2, 0)
2025-06-30 11:53:01 +08:00
modbus.write_register_until_success(client, 0, 0)
modbus.close_client(client)
2025-06-30 11:19:07 +08:00
# 移除在下料区域显示异常信息的代码
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)
2025-06-30 11:19:07 +08:00
modbus.close_client(client)
2025-06-30 11:53:01 +08:00
@Slot(int)
def handle_unloading_level(self, level):
"""处理下料层数信息来自Modbus"""
# 只更新内存中的当前层数UI更新通过信号槽完成
if self._current_unload_num != level:
logging.info(f"下料层数变化:{self._current_unload_num} -> {level}")
self._current_unload_num = level
# 更新保存的下料信息中的当前层数值
if self._current_unload_info:
self._current_unload_info['tier'] = str(level)
# 通过信号在主线程中更新UI
self.unloading_level_ui_signal.emit(level)
@Slot(int)
def handle_unloading_level_ui(self, level):
"""在主线程中更新下料层数UI"""
try:
# 更新显示
self.unloading_level_label.setText(f"下料层数:{level}")
# 如果有下料信息且层数大于0更新右上角显示
if level > 0 and self._current_unload_info:
tray_code = self._current_unload_info.get('tray_code', '')
# 确保使用固定的总层数
total_tier = self._total_unload_num
self.show_operation_status("下料层数", "output", f"{level}/{total_tier}")
logging.info(f"更新右上角下料层数显示:{level}/{total_tier}")
except Exception as e:
logging.error(f"更新下料层数UI失败: {str(e)}")
@Slot(int)
def handle_unloading_position(self, position):
"""处理下料位置信息"""
# 通过信号在主线程中更新UI
self.unloading_position_ui_signal.emit(position)
@Slot(int)
def handle_unloading_position_ui(self, position):
"""在主线程中更新下料位置UI"""
try:
self.unloading_position_label.setText(f"下料位置:{position}")
except Exception as e:
logging.error(f"更新下料位置UI失败: {str(e)}")
@Slot(int)
def handle_ng(self, ng):
"""处理NG信号, 将当前处理的数据添加到包装记录中毛重和净重设为0"""
if ng == 1:
try:
# 获取最后一条数据行
total_rows = self.process_table.rowCount()
if total_rows <= 2: # 只有表头行,没有数据行
logging.warning("没有可用的数据行来处理NG信号")
return
# 使用最后一条数据行
data_row = total_rows - 1
# 获取工程号
order_id_item = self.process_table.item(data_row, 1)
if not order_id_item:
logging.warning("无法获取工程号")
return
order_id = order_id_item.text().strip()
if not order_id:
logging.warning("工程号为空")
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 计算贴标列索引
label_col = 2 + len(enabled_configs)
# 获取贴标值
label_item = self.process_table.item(data_row, label_col)
label_value = label_item.text() if label_item else ""
# 如果贴标值为空,生成一个新的贴标值
if not label_value:
# 初始化托盘号对应的序号
if tray_id not in self.init_seq:
self.init_seq[tray_id] = 1
# 生成贴标号(托盘号+序号)
label_value = f"{self.init_seq[tray_id]}-NG"
self.init_seq[tray_id] += 1
# 保存贴标数据到数据库
self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass")
else:
# 如果贴标值已存在但不包含NG标记添加NG标记
if "NG" not in label_value:
label_value = f"{label_value}-NG"
# 更新贴标数据
self.save_inspection_data(order_id, tray_id, 11, 11, label_value, "pass")
# 设置毛重和净重为0
self.save_inspection_data(order_id, tray_id, 12, 12, "0", "pass")
self.save_inspection_data(order_id, tray_id, 13, 13, "0", "pass")
# 获取当前时间作为完成时间
finish_time = datetime.now()
# 将数据写入到数据库表 inspection_pack_data
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
inspection_dao.save_package_record(order_id, tray_id, label_value, "0", "0", finish_time)
# 删除当前处理的行
self.process_table.removeRow(data_row)
# 回显数据
self.show_pack_item()
logging.info(f"NG信号处理完成: 工程号={order_id}, 托盘号={tray_id}, 贴标值={label_value}")
except Exception as e:
logging.error(f"处理NG信号时发生错误: {str(e)}")
finally:
# 复原NG信号
modbus = ModbusUtils()
client = modbus.get_client()
modbus.write_register_until_success(client, 6, 0)
modbus.close_client(client)
def register_serial_callbacks(self):
"""注册串口数据回调函数"""
try:
# 注册米电阻数据回调
self.serial_manager.callbacks['mdz_data'] = self.on_mdz_data_received
# 注册线径数据回调
self.serial_manager.callbacks['xj_data'] = self.on_diameter_data_received
2025-06-30 09:58:16 +08:00
# 注册扫码器数据回调
self.serial_manager.callbacks['scanner_data'] = self.on_scanner_data_received
# 自动打开已配置的串口
self.serial_manager.auto_open_configured_ports()
logging.info("已注册串口数据回调函数")
except Exception as e:
logging.error(f"注册串口数据回调函数失败: {str(e)}")
def on_mdz_data_received(self, port_name, data):
"""米电阻数据接收回调函数
Args:
port_name: 串口名称
data: 接收到的数据
"""
try:
# 解析数据
data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data)
logging.info(f"收到米电阻数据: {data_str} 来自 {port_name}")
# 提取米电阻值,格式为"米电阻数据: xxx"
if "米电阻数据:" in data_str:
value_str = data_str.split("米电阻数据:")[1].strip()
try:
# 转换为浮点数
mdz_value = float(value_str)
# 查找米电阻对应的检验项配置
mdz_config = None
enabled_configs = self.inspection_manager.get_enabled_configs()
for config in enabled_configs:
if config.get('name') == 'mdz' or config.get('display_name') == '米电阻':
mdz_config = config
break
if mdz_config:
# 找到对应的检验项,将数据写入对应的单元格
self.set_inspection_value('mdz', mdz_config, mdz_value)
else:
logging.warning("未找到米电阻对应的检验项配置")
except ValueError:
logging.warning(f"米电阻数据格式错误: {value_str}")
else:
logging.warning(f"收到的数据不包含米电阻数据标记: {data_str}")
except Exception as e:
logging.error(f"处理米电阻数据失败: {str(e)}")
def on_diameter_data_received(self, port_name, data):
"""线径数据接收回调函数
Args:
port_name: 串口名称
data: 接收到的数据
"""
try:
# 解析数据
data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data)
logging.info(f"收到线径数据: {data_str} 来自 {port_name}")
# 提取线径值,格式为"线径数据: xxx"
if "线径数据:" in data_str:
value_str = data_str.split("线径数据:")[1].strip()
try:
# 转换为浮点数
xj_value = float(value_str)
# 查找线径对应的检验项配置
xj_config = None
enabled_configs = self.inspection_manager.get_enabled_configs()
for config in enabled_configs:
if config.get('name') == 'xj' or config.get('display_name') == '线径':
xj_config = config
break
if xj_config:
2025-06-25 15:24:55 +08:00
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
bccd, tccd = inspection_dao.get_xj_range(self._current_order_code)
if bccd is not None and tccd is not None:
if bccd <= xj_value <= tccd:
self.set_inspection_value('xj', xj_config, xj_value)
else:
logging.warning(f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd})")
reply = QMessageBox.question(
self,
'确认保存',
f"线径 {xj_value} 不在公差范围内 ({bccd} - {tccd})\n是否继续保存?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.set_inspection_value('xj', xj_config, xj_value)
else:
logging.info(f"用户取消保存超出范围的线径值: {xj_value}")
# TODO后续根据实际情况实现
pass
else:
logging.info(f"未找到订单 {self._current_order_code} 的线径公差范围,直接保存值 {xj_value}")
self.set_inspection_value('xj', xj_config, xj_value)
else:
logging.warning("未找到线径对应的检验项配置")
except ValueError:
logging.warning(f"线径数据格式错误: {value_str}")
else:
logging.warning(f"收到的数据不包含线径数据标记: {data_str}")
except Exception as e:
logging.error(f"处理线径数据失败: {str(e)}")
2025-06-30 09:58:16 +08:00
def on_scanner_data_received(self, port_name, data):
"""扫码器数据接收回调函数
Args:
port_name: 串口名称
data: 接收到的数据
"""
try:
# 解析数据
data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data)
logging.info(f"收到扫码器数据: {data_str} 来自 {port_name}")
# 提取扫码数据,格式为"扫码数据: xxx"
if "扫码数据:" in data_str:
gc_note = data_str.split("扫码数据:")[1].strip()
logging.info(f"提取到工程号: {gc_note}")
# 设置工程号到输入框
self.order_edit.setText(gc_note)
# 模拟按下回车键触发handle_order_enter方法
self.handle_order_enter()
else:
logging.warning(f"收到的数据不包含扫码数据标记: {data_str}")
except Exception as e:
logging.error(f"处理扫码器数据失败: {str(e)}")
def set_inspection_value(self, data_type, config, value):
"""设置检验项目值到表格中
Args:
data_type: 数据类型'mdz'表示米电阻'xj'表示线径
config: 检验项配置
value: 检验值
"""
try:
# 获取检验项的列索引
config_id = config.get('id')
config_position = config.get('position')
col_index = None
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 根据检验项配置查找对应的列索引
for i, cfg in enumerate(enabled_configs):
if cfg.get('id') == config_id:
col_index = 2 + i # 检验列从第3列开始
break
if col_index is None:
logging.warning(f"未找到{data_type}对应的列索引")
return
# 检查表格是否有数据行
if self.process_table.rowCount() <= 2: # 只有表头行
order_id = self.order_edit.text().strip()
if order_id:
self.add_new_inspection_row(order_id)
data_row = 2 # 新添加的行
else:
logging.warning("无法添加新行,订单号为空")
return
# 查找第一个没有该检测数据的行
data_row = None
for row in range(2, self.process_table.rowCount()):
cell_item = self.process_table.item(row, col_index)
if not cell_item or not cell_item.text().strip():
data_row = row
break
# 如果没有找到没有该检测数据的行,使用当前选中行或第一个数据行
if data_row is None:
current_row = self.process_table.currentRow()
data_row = current_row if current_row >= 2 else 2 # 使用第一个数据行索引为2
logging.info(f"未找到没有{data_type}数据的行,使用当前选中行或第一个数据行: {data_row}")
else:
logging.info(f"找到没有{data_type}数据的行: {data_row}")
# 获取工程号
order_id_item = self.process_table.item(data_row, 1)
if not order_id_item:
logging.warning("无法获取工程号")
return
order_id = order_id_item.text().strip()
if not order_id:
logging.warning("工程号为空")
return
# 暂时断开信号连接避免触发cellChanged信号
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 格式化值并设置单元格
formatted_value = str(value)
if config.get('data_type') == 'number':
# 格式化数字保留2位小数
formatted_value = f"{value:.2f}"
# 设置单元格值
item = QTableWidgetItem(formatted_value)
item.setTextAlignment(Qt.AlignCenter)
item.setData(Qt.UserRole, config_id) # 保存配置ID用于识别检验项
self.process_table.setItem(data_row, col_index, item)
# 验证数据是否在有效范围内
status = "pass"
if config.get('data_type') == 'number':
min_value = config.get('min_value')
max_value = config.get('max_value')
if (min_value is not None and value < min_value) or (max_value is not None and value > max_value):
status = "fail"
item.setBackground(QBrush(QColor("#ffcdd2"))) # 浅红色
else:
item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
# 保存到数据库,但只在非加载状态下
if not self._loading_data_in_progress:
tray_id = self.tray_edit.currentText()
self.save_inspection_data(order_id, tray_id, config_position, config_id, formatted_value, status)
# 不需要在这里主动触发数据重新加载因为handle_inspection_cell_changed会处理
# 重新连接信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
logging.info(f"已将{data_type}数据 {formatted_value} 写入行 {data_row}, 列 {col_index}")
except Exception as e:
logging.error(f"设置检验项值失败: {str(e)}")
# 确保重新连接信号
try:
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except:
pass
def handle_tray_changed(self):
"""处理托盘号变更事件,启动监听并加载数据"""
try:
tray_id = self.tray_edit.currentText()
if tray_id:
logging.info(f"托盘号变更为 {tray_id},启动监听")
# 初始化托盘号对应的序号(如果不存在)
if tray_id not in self.init_seq:
self.init_seq[tray_id] = 1
logging.info(f"初始化托盘号 {tray_id} 的序号为 1")
# 加载检验数据
self._safe_load_data()
# 无论_safe_load_data是否成功都确保显示包装记录
# 临时保存当前加载状态
prev_loading_state = getattr(self, '_loading_data_in_progress', False)
try:
# 设置加载状态为True避免无限循环调用
self._loading_data_in_progress = True
# 强制显示包装记录
self.show_pack_item()
logging.info(f"托盘号变更:直接调用显示包装记录, 托盘号={tray_id}")
finally:
# 恢复之前的加载状态
self._loading_data_in_progress = prev_loading_state
except Exception as e:
logging.error(f"处理托盘号变更失败: {str(e)}")
def handle_order_code_received(self, order_code):
"""处理从加载对话框接收到的订单号"""
logging.info(f"主窗口接收到订单号: {order_code}")
# 存储当前订单号
2025-06-24 11:21:33 +08:00
self._current_order_code = order_code
def on_report(self):
"""报表按钮点击处理"""
try:
dialog = ReportDialog(self)
dialog.exec_()
except Exception as e:
logging.error(f"打开报表对话框失败: {str(e)}")
2025-06-30 11:19:07 +08:00
QMessageBox.warning(self, "错误", f"打开报表对话框失败: {str(e)}")
def init_camera_display(self):
"""初始化相机显示区域"""
try:
2025-06-30 18:24:24 +08:00
logging.info(f"开始初始化相机显示区域配置中camera_enabled={self.camera_enabled}")
2025-06-30 18:40:50 +08:00
logging.info(f"上料区域大小: {self.material_content.width()}x{self.material_content.height()}")
2025-06-30 18:24:24 +08:00
# 检查相机管理器状态,以确定相机是否实际已打开
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
is_camera_actually_open = camera_manager.isOpen
if is_camera_actually_open and not self.camera_enabled:
logging.info("检测到相机已实际打开,但配置显示禁用,将更新相机启用状态")
self.camera_enabled = True
# 同时更新配置,确保持久化
self.config['camera'] = {'enabled': True}
from utils.config_loader import ConfigLoader
config_loader = ConfigLoader.get_instance()
config_loader.set_value('camera.enabled', True)
config_loader.save_config()
2025-06-30 11:19:07 +08:00
# 清理之前的组件(如果有)
if self.camera_display:
self.material_content_layout.removeWidget(self.camera_display)
self.camera_display.deleteLater()
self.camera_display = None
if self.material_placeholder:
self.material_content_layout.removeWidget(self.material_placeholder)
self.material_placeholder.deleteLater()
self.material_placeholder = None
# 清空布局中的所有项目
while self.material_content_layout.count():
item = self.material_content_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
2025-06-30 18:24:24 +08:00
# 决定是否应该启用相机功能 - 根据配置或相机实际状态
should_enable_camera = self.camera_enabled or is_camera_actually_open
2025-06-30 11:19:07 +08:00
# 创建占位标签
2025-06-30 18:24:24 +08:00
self.material_placeholder = QLabel("相机初始化中..." if should_enable_camera else "相机功能已禁用")
2025-06-30 11:19:07 +08:00
self.material_placeholder.setAlignment(Qt.AlignCenter)
self.material_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;")
2025-06-30 18:40:50 +08:00
self.material_placeholder.setSizePolicy(QWidget.sizePolicy().Expanding, QWidget.sizePolicy().Expanding)
2025-06-30 11:19:07 +08:00
self.material_content_layout.addWidget(self.material_placeholder)
2025-06-30 18:24:24 +08:00
self.material_placeholder.show()
2025-06-30 11:19:07 +08:00
# 创建相机显示组件
self.camera_display = CameraDisplayWidget()
2025-06-30 18:40:50 +08:00
# 设置合适的尺寸策略,使相机显示能够正确适应上料区域
self.camera_display.setSizePolicy(QWidget.sizePolicy().Expanding, QWidget.sizePolicy().Expanding)
2025-06-30 11:19:07 +08:00
2025-06-30 18:24:24 +08:00
# 显式连接相机状态信号
try:
# 如果已连接,先断开旧连接
self.camera_display.signal_camera_status.disconnect()
except:
pass
# 连接相机状态信号
self.camera_display.signal_camera_status.connect(self.handle_camera_status)
logging.info("已连接相机状态信号")
2025-06-30 11:19:07 +08:00
2025-06-30 18:24:24 +08:00
# 先不添加相机组件,直到确认相机可用时再添加
if should_enable_camera:
2025-06-30 11:19:07 +08:00
# 启动相机初始化过程
QTimer.singleShot(500, self.initialize_camera)
logging.info("相机初始化已安排")
else:
logging.info("相机功能已禁用,不进行初始化")
2025-06-30 18:24:24 +08:00
self.material_placeholder.setText("相机功能已禁用")
2025-06-30 11:19:07 +08:00
except Exception as e:
logging.error(f"初始化相机显示区域失败: {str(e)}")
2025-06-30 18:24:24 +08:00
# 确保占位符显示错误信息
if self.material_placeholder:
self.material_placeholder.setText(f"相机初始化错误: {str(e)}")
self.material_placeholder.show()
2025-06-30 11:19:07 +08:00
def initialize_camera(self):
"""初始化相机并显示画面"""
try:
if not self.camera_enabled:
2025-06-30 18:24:24 +08:00
logging.info("相机功能已禁用,不进行初始化")
2025-06-30 11:19:07 +08:00
return
logging.info("开始初始化相机...")
# 获取相机管理器实例
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
2025-06-30 18:24:24 +08:00
# 检查相机是否已经打开
if camera_manager.isOpen:
logging.info("相机已经打开,直接开始显示")
# 立即开始显示相机画面
QTimer.singleShot(100, lambda: self._start_camera_display())
return
2025-06-30 11:19:07 +08:00
# 枚举设备
devices = camera_manager.enum_devices()
if not devices or len(devices) == 0:
self.material_placeholder.setText("未检测到相机设备")
logging.warning("未检测到相机设备")
return
# 打开第一个相机设备
device_index = 0
success = camera_manager.open_device(device_index)
if success:
logging.info(f"相机已成功打开,设备索引: {device_index}")
# 立即开始显示相机画面
QTimer.singleShot(100, lambda: self._start_camera_display())
else:
self.material_placeholder.setText("相机打开失败")
logging.error("相机打开失败")
except Exception as e:
self.material_placeholder.setText("相机初始化错误")
logging.error(f"初始化相机失败: {str(e)}")
2025-06-30 18:24:24 +08:00
# 异常情况下更新相机状态
self.update_camera_enabled_state(False)
2025-06-30 11:19:07 +08:00
def _start_camera_display(self):
"""开始显示相机画面(内部方法)"""
try:
if self.camera_display and self.camera_enabled:
2025-06-30 18:40:50 +08:00
logging.info(f"开始相机显示,上料区域大小: {self.material_content.width()}x{self.material_content.height()}")
2025-06-30 18:24:24 +08:00
# 先从布局中移除占位符
if self.material_placeholder:
self.material_content_layout.removeWidget(self.material_placeholder)
self.material_placeholder.hide()
# 添加相机显示组件并确保其可见
2025-06-30 18:40:50 +08:00
# 确保相机显示组件的布局策略正确
self.camera_display.setSizePolicy(QWidget.sizePolicy().Expanding, QWidget.sizePolicy().Expanding)
2025-06-30 18:24:24 +08:00
self.material_content_layout.addWidget(self.camera_display)
self.camera_display.show()
2025-06-30 11:19:07 +08:00
2025-06-30 18:40:50 +08:00
# 强制布局更新
self.material_content_layout.update()
self.material_content.updateGeometry()
2025-06-30 11:19:07 +08:00
# 开始显示
success = self.camera_display.start_display()
2025-06-30 18:24:24 +08:00
if not success:
2025-06-30 11:19:07 +08:00
# 如果启动失败,显示占位符
2025-06-30 18:24:24 +08:00
self.material_content_layout.removeWidget(self.camera_display)
self.camera_display.hide()
2025-06-30 11:19:07 +08:00
if self.material_placeholder:
self.material_placeholder.setText("相机显示启动失败")
2025-06-30 18:24:24 +08:00
self.material_content_layout.addWidget(self.material_placeholder)
self.material_placeholder.show()
2025-06-30 11:19:07 +08:00
logging.error("相机显示启动失败")
2025-06-30 18:24:24 +08:00
else:
2025-06-30 18:40:50 +08:00
logging.info(f"相机显示已成功启动,显示区域大小: {self.camera_display.width()}x{self.camera_display.height()}")
2025-06-30 11:19:07 +08:00
except Exception as e:
logging.error(f"启动相机显示失败: {str(e)}")
2025-06-30 18:40:50 +08:00
if self.material_placeholder:
self.material_placeholder.setText(f"相机显示错误: {str(e)}")
self.material_content_layout.addWidget(self.material_placeholder)
self.material_placeholder.show()
2025-06-30 11:19:07 +08:00
def update_camera_ui(self, is_camera_ready):
"""更新相机UI显示
Args:
is_camera_ready: 相机是否准备就绪
"""
try:
if is_camera_ready and self.camera_enabled:
# 显示相机画面,隐藏占位符
if self.material_placeholder:
2025-06-30 18:24:24 +08:00
self.material_content_layout.removeWidget(self.material_placeholder)
self.material_placeholder.hide()
if self.camera_display:
2025-06-30 18:40:50 +08:00
# 确保相机显示组件的布局策略正确
self.camera_display.setSizePolicy(QWidget.sizePolicy().Expanding, QWidget.sizePolicy().Expanding)
2025-06-30 18:24:24 +08:00
# 确保相机显示组件已添加到布局
if self.camera_display.parent() is None:
self.material_content_layout.addWidget(self.camera_display)
2025-06-30 18:40:50 +08:00
2025-06-30 18:24:24 +08:00
self.camera_display.show()
2025-06-30 18:40:50 +08:00
# 强制布局更新
self.material_content_layout.update()
self.material_content.updateGeometry()
logging.info(f"相机UI已更新显示相机画面尺寸: {self.camera_display.width()}x{self.camera_display.height()}")
2025-06-30 11:19:07 +08:00
else:
# 隐藏相机画面,显示占位符
if self.camera_display:
2025-06-30 18:24:24 +08:00
self.material_content_layout.removeWidget(self.camera_display)
self.camera_display.hide()
2025-06-30 11:19:07 +08:00
if self.material_placeholder:
2025-06-30 18:40:50 +08:00
# 确保占位符布局策略正确
self.material_placeholder.setSizePolicy(QWidget.sizePolicy().Expanding, QWidget.sizePolicy().Expanding)
2025-06-30 18:24:24 +08:00
# 确保占位符已添加到布局
if self.material_placeholder.parent() is None:
self.material_content_layout.addWidget(self.material_placeholder)
2025-06-30 18:40:50 +08:00
2025-06-30 18:24:24 +08:00
self.material_placeholder.show()
2025-06-30 18:40:50 +08:00
# 强制布局更新
self.material_content_layout.update()
self.material_content.updateGeometry()
2025-06-30 11:19:07 +08:00
if not self.camera_enabled:
self.material_placeholder.setText("相机功能已禁用")
elif not is_camera_ready:
self.material_placeholder.setText("相机未就绪")
2025-06-30 18:40:50 +08:00
logging.info(f"相机UI已更新显示占位符 (相机启用={self.camera_enabled}, 相机就绪={is_camera_ready})")
2025-06-30 11:19:07 +08:00
except Exception as e:
logging.error(f"更新相机UI失败: {str(e)}")
@Slot(int, str)
def handle_emergency_stop(self, value, desc):
"""处理急停信号"""
logging.info(f"[处理] 急停信号: {desc}")
# 保存一个急停状态变量
self.emergency_stop = value
# 当急停信号为1时重置D2和D3寄存器
if value == 1:
try:
modbus = ModbusUtils()
client = modbus.get_client()
# 重置D2和D3寄存器
modbus.write_register_until_success(client, 2, 0)
modbus.write_register_until_success(client, 3, 0)
# 通过信号在主线程中处理UI更新
self.emergency_stop_signal.emit(value, desc)
modbus.close_client(client)
except Exception as e:
logging.error(f"处理急停信号失败: {str(e)}")
else:
# 急停信号解除,在主线程中恢复错误状态显示
self.emergency_stop_signal.emit(value, desc)
def _handle_emergency_stop_ui(self, value, desc):
"""在主线程中处理急停信号UI更新"""
try:
if value == 1:
# 显示警告对话框
QMessageBox.warning(self, "急停警告", "监听到急停信号")
# 更新错误状态标签
self.error_status_label.setText("故障: 急停")
self.error_status_label.setToolTip("急停按钮被触发")
self.error_status_label.setStyleSheet("color: red; font-weight: bold;")
else:
# 急停信号解除,恢复错误状态显示
self._update_error_status()
2025-06-30 11:53:01 +08:00
# 恢复故障状态标签
self.label_status_label.setText("故障: 正常")
self.label_status_label.setStyleSheet("")
2025-06-30 11:19:07 +08:00
except Exception as e:
2025-06-30 18:24:24 +08:00
logging.error(f"处理急停UI更新失败: {str(e)}")
def update_camera_enabled_state(self, is_enabled=None):
"""更新相机启用状态
Args:
is_enabled: 是否启用相机如果为None则检查相机管理器状态
"""
try:
from widgets.camera_manager import CameraManager
camera_manager = CameraManager.get_instance()
# 如果未提供状态,则根据相机管理器状态确定
if is_enabled is None:
is_enabled = camera_manager.isOpen
# 更新内存中的状态
old_state = self.camera_enabled
self.camera_enabled = is_enabled
# 如果状态发生变化,记录并更新配置
if old_state != is_enabled:
logging.info(f"相机启用状态已从 {old_state} 更改为 {is_enabled}")
# 更新配置
self.config['camera'] = {'enabled': is_enabled}
from utils.config_loader import ConfigLoader
config_loader = ConfigLoader.get_instance()
config_loader.set_value('camera.enabled', is_enabled)
config_loader.save_config()
# 如果已启用,确保相机显示正确初始化
if is_enabled:
# 确保相机显示组件已正确初始化
if not hasattr(self, 'camera_display') or self.camera_display is None:
logging.info("相机启用状态更新:正在初始化相机显示组件")
self.init_camera_display()
else:
# 如果相机已经打开但界面未显示则更新UI
if camera_manager.isOpen:
logging.info("相机已打开更新UI以显示相机画面")
self.update_camera_ui(True)
# 如果相机未在采集,则开始采集
if not camera_manager.isGrabbing:
QTimer.singleShot(100, self._start_camera_display)
else:
# 如果禁用确保更新UI显示占位符
logging.info("相机已禁用更新UI以显示占位符")
if hasattr(self, 'material_placeholder') and self.material_placeholder:
self.material_placeholder.setText("相机功能已禁用")
self.update_camera_ui(False)
# 如果相机仍在采集,则停止采集
if camera_manager.isGrabbing:
if hasattr(self, 'camera_display') and self.camera_display:
self.camera_display.stop_display()
# 如果相机仍在连接,则关闭相机
if camera_manager.isOpen:
camera_manager.close_device()
else:
logging.debug(f"相机启用状态未变化,保持为 {is_enabled}")
except Exception as e:
logging.error(f"更新相机启用状态失败: {str(e)}")
def handle_camera_connection(self, is_connected, message):
"""处理相机连接状态变化"""
try:
if is_connected:
logging.info("相机已连接")
# 更新相机启用状态
self.update_camera_enabled_state(True)
# 如果当前在主页面,直接开始显示相机画面
if self.stacked_widget.currentWidget() == self.central_widget:
if hasattr(self, 'camera_display') and self.camera_display:
self.camera_display.start_display()
else:
if message:
logging.warning(f"相机连接失败: {message}")
else:
logging.info("相机已断开")
# 如果相机断开,确保停止显示
if hasattr(self, 'camera_display') and self.camera_display:
self.camera_display.stop_display()
# 更新相机启用状态 - 只有在明确断开时才设置为False
if not message: # 只有在主动断开时才更新状态为False
self.update_camera_enabled_state(False)
except Exception as e:
logging.error(f"处理相机连接状态变化失败: {str(e)}")
if self.material_placeholder:
self.material_placeholder.setText(f"相机连接错误: {str(e)}")
self.update_camera_ui(False)