From c36189f2555f7be2d3aa4857b7abb754a302dc65 Mon Sep 17 00:00:00 2001 From: zhu-mengmeng <15588200382@163.com> Date: Tue, 10 Jun 2025 16:13:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=98=E7=9B=98=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE=E8=A1=A8=E5=92=8C=E7=9B=B8=E5=85=B3?= =?UTF-8?q?UI=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E4=B8=BB?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BB=A5=E6=94=AF=E6=8C=81=E6=89=98=E7=9B=98?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=80=89=E6=8B=A9=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2=E4=BB=A5=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=89=98=E7=9B=98=E7=B1=BB=E5=9E=8B=EF=BC=8C=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E4=BB=A5=E5=8C=85=E5=90=AB?= =?UTF-8?q?Modbus=E8=AE=BE=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- config/app_config.json | 4 + dao/pallet_type_dao.py | 293 +++++++++++++++++ db/jtDB.db | Bin 40960 -> 40960 bytes db/schema.sql | 28 +- modbus_register_tester.py | 171 ++++++++++ modbus_server.py | 76 +++++ ui/pallet_type_settings_ui.py | 375 ++++++++++++++++++++++ ui/settings_ui.py | 36 ++- utils/modbus_utils.py | 209 ++++++++++++ utils/pallet_type_manager.py | 137 ++++++++ widgets/main_window.py | 298 ++++++++++++++++-- widgets/pallet_type_settings_widget.py | 420 +++++++++++++++++++++++++ widgets/settings_widget.py | 42 ++- 14 files changed, 2052 insertions(+), 40 deletions(-) create mode 100644 dao/pallet_type_dao.py create mode 100644 modbus_register_tester.py create mode 100644 modbus_server.py create mode 100644 ui/pallet_type_settings_ui.py create mode 100644 utils/modbus_utils.py create mode 100644 utils/pallet_type_manager.py create mode 100644 widgets/pallet_type_settings_widget.py diff --git a/.gitignore b/.gitignore index fb15c32..42df938 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ htmlcov/ # 构建文件 build/ dist/ -*.egg-info/ \ No newline at end of file +*.egg-info/ +config/app_config.json.bak diff --git a/config/app_config.json b/config/app_config.json index 25b7a77..d777b81 100644 --- a/config/app_config.json +++ b/config/app_config.json @@ -22,5 +22,9 @@ "default_exposure": 20000, "default_gain": 10, "default_framerate": 30 + }, + "modbus": { + "host": "localhost", + "port": "5020" } } \ No newline at end of file diff --git a/dao/pallet_type_dao.py b/dao/pallet_type_dao.py new file mode 100644 index 0000000..00869e4 --- /dev/null +++ b/dao/pallet_type_dao.py @@ -0,0 +1,293 @@ +import logging +from datetime import datetime +from utils.sql_utils import SQLUtils + +class PalletTypeDAO: + """托盘类型数据访问对象""" + + def __init__(self): + """初始化数据访问对象""" + self.db = SQLUtils('sqlite', database='db/jtDB.db') + + def __del__(self): + """析构函数,确保数据库连接关闭""" + if hasattr(self, 'db'): + self.db.close() + + def get_all_pallet_types(self, include_disabled=False): + """获取所有托盘类型 + + Args: + include_disabled: 是否包含禁用的类型 + + Returns: + list: 托盘类型列表 + """ + try: + if include_disabled: + sql = """ + SELECT id, type_name, operation_type, description, enabled, sort_order + FROM pallet_types + WHERE is_deleted = FALSE + ORDER BY sort_order + """ + params = () + else: + sql = """ + SELECT id, type_name, operation_type, description, enabled, sort_order + FROM pallet_types + WHERE is_deleted = FALSE AND enabled = TRUE + ORDER BY sort_order + """ + params = () + + self.db.cursor.execute(sql, params) + results = self.db.cursor.fetchall() + + pallet_types = [] + for row in results: + pallet_type = { + 'id': row[0], + 'type_name': row[1], + 'operation_type': row[2], + 'description': row[3], + 'enabled': bool(row[4]), + 'sort_order': row[5] + } + pallet_types.append(pallet_type) + + return pallet_types + except Exception as e: + logging.error(f"获取托盘类型失败: {str(e)}") + return [] + + def get_pallet_types_by_operation(self, operation_type, include_disabled=False): + """根据操作类型获取托盘类型 + + Args: + operation_type: 操作类型 (input/output) + include_disabled: 是否包含禁用的类型 + + Returns: + list: 托盘类型列表 + """ + try: + if include_disabled: + sql = """ + SELECT id, type_name, operation_type, description, enabled, sort_order + FROM pallet_types + WHERE operation_type = ? AND is_deleted = FALSE + ORDER BY sort_order + """ + params = (operation_type,) + else: + sql = """ + SELECT id, type_name, operation_type, description, enabled, sort_order + FROM pallet_types + WHERE operation_type = ? AND is_deleted = FALSE AND enabled = TRUE + ORDER BY sort_order + """ + params = (operation_type,) + + self.db.cursor.execute(sql, params) + results = self.db.cursor.fetchall() + + pallet_types = [] + for row in results: + pallet_type = { + 'id': row[0], + 'type_name': row[1], + 'operation_type': row[2], + 'description': row[3], + 'enabled': bool(row[4]), + 'sort_order': row[5] + } + pallet_types.append(pallet_type) + + return pallet_types + except Exception as e: + logging.error(f"获取托盘类型失败: {str(e)}") + return [] + + def get_pallet_type_by_id(self, pallet_type_id): + """根据ID获取托盘类型 + + Args: + pallet_type_id: 托盘类型ID + + Returns: + dict: 托盘类型信息,未找到则返回None + """ + try: + sql = """ + SELECT id, type_name, operation_type, description, enabled, sort_order + FROM pallet_types + WHERE id = ? AND is_deleted = FALSE + """ + params = (pallet_type_id,) + + self.db.cursor.execute(sql, params) + row = self.db.cursor.fetchone() + + if row: + pallet_type = { + 'id': row[0], + 'type_name': row[1], + 'operation_type': row[2], + 'description': row[3], + 'enabled': bool(row[4]), + 'sort_order': row[5] + } + return pallet_type + else: + return None + except Exception as e: + logging.error(f"获取托盘类型失败: {str(e)}") + return None + + def create_pallet_type(self, data, username='system'): + """创建托盘类型 + + Args: + data: 托盘类型数据 + username: 操作用户 + + Returns: + int: 新创建的托盘类型ID,失败返回None + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + sql = """ + INSERT INTO pallet_types ( + type_name, operation_type, description, enabled, sort_order, + create_time, create_by, update_time, update_by, is_deleted + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + + params = ( + data.get('type_name'), + data.get('operation_type'), + data.get('description', ''), + data.get('enabled', True), + data.get('sort_order', 999), + current_time, + username, + current_time, + username, + False + ) + + self.db.execute_update(sql, params) + + # 获取新插入的ID + self.db.cursor.execute("SELECT last_insert_rowid()") + new_id = self.db.cursor.fetchone()[0] + + return new_id + except Exception as e: + logging.error(f"创建托盘类型失败: {str(e)}") + return None + + def update_pallet_type(self, pallet_type_id, data, username='system'): + """更新托盘类型 + + Args: + pallet_type_id: 托盘类型ID + data: 更新数据 + username: 操作用户 + + Returns: + bool: 更新是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 构建更新SQL + update_fields = [] + params = [] + + # 可更新的字段 + allowed_fields = [ + 'type_name', 'operation_type', 'description', 'enabled', 'sort_order' + ] + + for field in allowed_fields: + if field in data: + update_fields.append(f"{field} = ?") + params.append(data[field]) + + # 添加更新时间和更新人 + update_fields.append("update_time = ?") + params.append(current_time) + update_fields.append("update_by = ?") + params.append(username) + + # 添加ID到参数列表 + params.append(pallet_type_id) + + # 构建SQL + sql = f""" + UPDATE pallet_types + SET {', '.join(update_fields)} + WHERE id = ? + """ + + self.db.execute_update(sql, params) + return True + except Exception as e: + logging.error(f"更新托盘类型失败: {str(e)}") + return False + + def delete_pallet_type(self, pallet_type_id, username='system'): + """删除托盘类型(软删除) + + Args: + pallet_type_id: 托盘类型ID + username: 操作用户 + + Returns: + bool: 删除是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + sql = """ + UPDATE pallet_types + SET is_deleted = TRUE, update_time = ?, update_by = ? + WHERE id = ? + """ + params = (current_time, username, pallet_type_id) + + self.db.execute_update(sql, params) + return True + except Exception as e: + logging.error(f"删除托盘类型失败: {str(e)}") + return False + + def toggle_pallet_type(self, pallet_type_id, enabled, username='system'): + """启用或禁用托盘类型 + + Args: + pallet_type_id: 托盘类型ID + enabled: 是否启用 + username: 操作用户 + + Returns: + bool: 操作是否成功 + """ + try: + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + sql = """ + UPDATE pallet_types + SET enabled = ?, update_time = ?, update_by = ? + WHERE id = ? AND is_deleted = FALSE + """ + params = (enabled, current_time, username, pallet_type_id) + + self.db.execute_update(sql, params) + return True + except Exception as e: + logging.error(f"更新托盘类型启用状态失败: {str(e)}") + return False \ No newline at end of file diff --git a/db/jtDB.db b/db/jtDB.db index 74ab1785d0416235343dd410eb54be8d96023a9c..95c908a912e121cc71857d1a73676cf6b19c5e27 100644 GIT binary patch delta 1084 zcmZoTz|?SnX@Zmx6AJ?a0~Zi;05KyF^G?(;k^_qBwQ2J5|6t%?l49UX=0DFT$hnyJ z9NP(&3?`|Kjq?~CTf8~g#Z^@qJ0eRGlX6lE5_57=OX5o^3sQ@b*<8*+u8twD3L%b8 zKCTMLGLt`Z8nS}a$4^e+QmEI}Re07pt@2^$gAK}@` z#HG18gJq634-5Ye2LA8-5BN{<@7OFTu!3JmfQ4C=5!qWDn@`!>8Za_!<|z0nFAfS= zUOxu@E&Lh${CwN^V)&SNSMd4)^_ub4M{w{ks7p$6G8#)7OL7<*8W?DS2wfus1Bi2h z3}8@X8W|Xw>Kd5o8X71Vnpzo|SQ#4?R~DC~=HiryV8>yyDa>S3gQxZLy0g$sFtRc< z!)mm#l_}KD2sRX>4M2_t61qmnM$hZcKr`9U%G40eUv|G~h{lEJ{2%zvIwkaIEb zIkpol8Jh(Kt};uul(Dn76tT05o0>9qvX>+#<)mij6&Iu?Z_Z$uqs_(4e~N+sJO2az zQ$Xc=_&1-jw>4m7+{{t%Q{J5Y6@vs@F$3QmK6~Crysf+%Jg0f`cm%oEaR+jJ;hN56 z#d(*rfm4~Sm?ML|mxG&sF}nf38{1`|LDp={v5kV0CuT`bE-80ol$gA*Op2L@$82(9 znhuMRm67@6s?wav3(BHdIF2$XPIQ!=>{u2*`EOb@3*R4xxQUHX%u$U3KuwY?W;{HW zliye9h*($|m=;$Sm!#%C>zwg?_6)|2t_9_uDm8Tx=q$9ji3B1H5vf3+m`+S diff --git a/db/schema.sql b/db/schema.sql index af4527c..29d05d2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -74,5 +74,31 @@ create table if not exists inspection_pack_data update_time TIMESTAMP, update_by VARCHAR(50), is_deleted BOOLEAN -) +); + +-- 创建托盘类型配置表 +CREATE TABLE IF NOT EXISTS pallet_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_name VARCHAR(50) NOT NULL, -- 托盘类型名称 + operation_type VARCHAR(20) NOT NULL, -- 操作类型: input(上料), output(下料) + description TEXT, -- 描述 + enabled BOOLEAN DEFAULT TRUE, -- 是否启用 + sort_order INTEGER NOT NULL, -- 排序顺序 + create_time TIMESTAMP NOT NULL, + create_by VARCHAR(50) NOT NULL, + update_time TIMESTAMP, + update_by VARCHAR(50), + is_deleted BOOLEAN DEFAULT FALSE +); + +-- 插入默认托盘类型数据 +INSERT OR IGNORE INTO pallet_types ( + type_name, operation_type, description, enabled, sort_order, create_time, create_by +) VALUES +('标准托盘', 'input', '标准上料托盘', TRUE, 1, CURRENT_TIMESTAMP, 'system'), +('小型托盘', 'input', '小型上料托盘', TRUE, 2, CURRENT_TIMESTAMP, 'system'), +('大型托盘', 'input', '大型上料托盘', TRUE, 3, CURRENT_TIMESTAMP, 'system'), +('标准托盘', 'output', '标准下料托盘', TRUE, 4, CURRENT_TIMESTAMP, 'system'), +('小型托盘', 'output', '小型下料托盘', TRUE, 5, CURRENT_TIMESTAMP, 'system'), +('大型托盘', 'output', '大型下料托盘', TRUE, 6, CURRENT_TIMESTAMP, 'system'); diff --git a/modbus_register_tester.py b/modbus_register_tester.py new file mode 100644 index 0000000..3700877 --- /dev/null +++ b/modbus_register_tester.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Modbus Register Tester + +A simple utility to test reading and writing to Modbus registers. +This can be used to verify that the virtual Modbus server is working correctly. + +Usage: + python modbus_register_tester.py [--host HOST] [--port PORT] [--action {read|write}] + [--address ADDRESS] [--value VALUE] [--count COUNT] + +Options: + --host HOST Modbus server host [default: localhost] + --port PORT Modbus server port [default: 5020] + --action {read|write} Action to perform [default: read] + --address ADDRESS Register address to read/write [default: 1] + --value VALUE Value to write (only for write action) + --count COUNT Number of registers to read (only for read action) [default: 1] +""" + +import argparse +import logging +import sys +from pymodbus.client import ModbusTcpClient + +# Configure logging +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +def read_registers(client, address, count=1): + """Read holding registers + + Args: + client: Modbus client + address: Register address + count: Number of registers to read + + Returns: + list: Register values + """ + try: + result = client.read_holding_registers(address=address, count=count) + if result.isError(): + logger.error(f"读取寄存器D{address}失败: {result}") + return None + + logger.info(f"读取寄存器D{address}-D{address+count-1}成功: {result.registers}") + return result.registers + except Exception as e: + logger.error(f"读取寄存器D{address}时发生错误: {str(e)}") + return None + +def write_register(client, address, value): + """Write to a holding register + + Args: + client: Modbus client + address: Register address + value: Value to write + + Returns: + bool: True if successful, False otherwise + """ + try: + result = client.write_registers(address=address, values=[value]) + if result.isError(): + logger.error(f"写入寄存器D{address}值{value}失败: {result}") + return False + + logger.info(f"写入寄存器D{address}值{value}成功") + + # Verify the write by reading back the value + read_result = read_registers(client, address) + if read_result and read_result[0] == value: + logger.info(f"验证写入成功: D{address} = {value}") + return True + else: + logger.warning(f"验证写入失败: D{address} 预期 {value},实际 {read_result[0] if read_result else 'unknown'}") + return False + except Exception as e: + logger.error(f"写入寄存器D{address}值{value}时发生错误: {str(e)}") + return False + +def connect_client(host, port): + """Connect to Modbus server + + Args: + host: Server host + port: Server port + + Returns: + ModbusTcpClient: Connected client or None if failed + """ + try: + client = ModbusTcpClient(host=host, port=port, timeout=10) + logger.info(f"尝试连接到 Modbus 服务器 {host}:{port}") + + is_connected = client.connect() + if is_connected: + logger.info(f"成功连接到 Modbus 服务器 {host}:{port}") + return client + else: + logger.error(f"无法连接到 Modbus 服务器 {host}:{port}") + return None + except Exception as e: + logger.error(f"连接 Modbus 服务器 {host}:{port} 时发生错误: {str(e)}") + return None + +def close_client(client): + """Close Modbus client connection + + Args: + client: Modbus client + """ + if client: + client.close() + logger.info("Modbus 客户端连接已关闭") + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description='Modbus Register Tester') + parser.add_argument('--host', type=str, default='localhost', + help='Modbus server host') + parser.add_argument('--port', type=int, default=5020, + help='Modbus server port') + parser.add_argument('--action', type=str, choices=['read', 'write'], + default='read', help='Action to perform') + parser.add_argument('--address', type=int, default=1, + help='Register address to read/write') + parser.add_argument('--value', type=int, + help='Value to write (only for write action)') + parser.add_argument('--count', type=int, default=1, + help='Number of registers to read (only for read action)') + return parser.parse_args() + +if __name__ == '__main__': + args = parse_arguments() + + # Validate arguments + if args.action == 'write' and args.value is None: + logger.error("写入操作需要指定 --value 参数") + sys.exit(1) + + client = None + try: + # Connect to the server + client = connect_client(args.host, args.port) + if not client: + sys.exit(1) + + # Perform the requested action + if args.action == 'read': + result = read_registers(client, args.address, args.count) + if result is None: + sys.exit(1) + else: # write + success = write_register(client, args.address, args.value) + if not success: + sys.exit(1) + + except KeyboardInterrupt: + logger.info("操作被用户中断") + sys.exit(0) + except Exception as e: + logger.error(f"发生错误: {str(e)}") + sys.exit(1) + finally: + close_client(client) \ No newline at end of file diff --git a/modbus_server.py b/modbus_server.py new file mode 100644 index 0000000..9def43c --- /dev/null +++ b/modbus_server.py @@ -0,0 +1,76 @@ +from pymodbus.server import StartTcpServer +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +import logging +import threading +import time + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class ModbusServer: + def __init__(self, host="localhost", port=5020): + self.host = host + self.port = port + # 创建数据存储 + self.store = ModbusSlaveContext( + di=ModbusSequentialDataBlock(0, [0] * 8000), # 离散输入 + co=ModbusSequentialDataBlock(0, [0] * 8000), # 线圈 + hr=ModbusSequentialDataBlock(0, [0] * 8000), # 保持寄存器 + ir=ModbusSequentialDataBlock(0, [0] * 8000), # 输入寄存器 + ) + self.context = ModbusServerContext(slaves=self.store, single=True) + + # 初始化一些默认值 + self.store.setValues(3, 20, [0]) # D20 初始为手动模式 + self.store.setValues(3, 21, [0]) # D21 初始为未启动 + self.store.setValues(3, 22, [0]) # D22 初始为未锁定 + + def start(self): + """启动服务器""" + logger.info(f"启动 Modbus TCP 服务器于 {self.host}:{self.port}") + self.server_thread = threading.Thread( + target=StartTcpServer, + args=(self.context,), + kwargs={'address': (self.host, self.port)} + ) + self.server_thread.daemon = True + self.server_thread.start() + + def set_auto_mode(self): + """设置为自动模式""" + self.store.setValues(3, 20, [2]) + logger.info("已设置为自动模式") + + def set_manual_mode(self): + """设置为手动模式""" + self.store.setValues(3, 20, [0]) + logger.info("已设置为手动模式") + + def get_register_value(self, address): + """获取寄存器值""" + values = self.store.getValues(3, address, 1) + return values[0] + + def print_status(self): + """打印当前状态""" + mode = "自动" if self.get_register_value(20) == 2 else "手动" + running = "是" if self.get_register_value(22) == 1 else "否" + logger.info(f"当前模式: {mode}") + logger.info(f"是否运行中: {running}") + +if __name__ == "__main__": + # 创建并启动服务器 + server = ModbusServer() + server.start() + + # 设置为自动模式 + server.set_auto_mode() + + try: + while True: + server.print_status() + time.sleep(5) # 每5秒打印一次状态 + except KeyboardInterrupt: + logger.info("服务器停止运行") \ No newline at end of file diff --git a/ui/pallet_type_settings_ui.py b/ui/pallet_type_settings_ui.py new file mode 100644 index 0000000..a8022db --- /dev/null +++ b/ui/pallet_type_settings_ui.py @@ -0,0 +1,375 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, + QLineEdit, QCheckBox, QComboBox, QPushButton, QGroupBox, + QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, + QSpinBox, QDoubleSpinBox, QFrame, QScrollArea, QStackedWidget +) +from PySide6.QtGui import QFont, QBrush, QColor +from PySide6.QtCore import Qt, Signal + +class PalletTypeSettingsUI(QWidget): + """托盘类型设置UI""" + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.init_ui() + + def init_ui(self): + """初始化UI""" + # 设置字体 + self.title_font = QFont("微软雅黑", 14, QFont.Bold) + self.normal_font = QFont("微软雅黑", 11) + self.small_font = QFont("微软雅黑", 9) + + # 设置背景颜色,便于识别 + self.setStyleSheet("background-color: #f5f5f5;") + + # 创建主布局 + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(15) + + # 标题 + self.title_label = QLabel("托盘类型配置") + self.title_label.setFont(self.title_font) + self.title_label.setAlignment(Qt.AlignCenter) + self.title_label.setStyleSheet("color: #1a237e; padding: 10px;") + self.main_layout.addWidget(self.title_label) + + # 说明文本 + self.desc_label = QLabel("配置上料和下料托盘类型,点击上料/下料按钮切换显示对应类型。") + self.desc_label.setWordWrap(True) + self.desc_label.setStyleSheet("color: #666666; padding: 0px 10px 10px 10px;") + self.main_layout.addWidget(self.desc_label) + + # 创建操作类型选择按钮 + self.operation_layout = QHBoxLayout() + self.operation_layout.setContentsMargins(0, 0, 0, 0) + self.operation_layout.setSpacing(20) + + self.input_button = QPushButton("上料类型") + self.input_button.setFont(self.normal_font) + self.input_button.setFixedHeight(40) + self.input_button.setStyleSheet(""" + QPushButton { + background-color: #2196f3; + color: white; + border: none; + border-radius: 5px; + } + QPushButton:hover { + background-color: #1e88e5; + } + QPushButton:pressed { + background-color: #1976d2; + } + QPushButton:checked { + background-color: #1565c0; + border: 2px solid #0d47a1; + } + """) + self.input_button.setCheckable(True) + self.input_button.setChecked(True) + + self.output_button = QPushButton("下料类型") + self.output_button.setFont(self.normal_font) + self.output_button.setFixedHeight(40) + self.output_button.setStyleSheet(""" + QPushButton { + background-color: #ff9800; + color: white; + border: none; + border-radius: 5px; + } + QPushButton:hover { + background-color: #fb8c00; + } + QPushButton:pressed { + background-color: #f57c00; + } + QPushButton:checked { + background-color: #ef6c00; + border: 2px solid #e65100; + } + """) + self.output_button.setCheckable(True) + + self.operation_layout.addWidget(self.input_button) + self.operation_layout.addWidget(self.output_button) + self.main_layout.addLayout(self.operation_layout) + + # 创建堆叠部件,用于切换上料和下料类型配置 + self.stacked_widget = QStackedWidget() + + # 创建上料类型配置页面 + self.input_widget = self.create_pallet_type_widget("input") + self.stacked_widget.addWidget(self.input_widget) + + # 创建下料类型配置页面 + self.output_widget = self.create_pallet_type_widget("output") + self.stacked_widget.addWidget(self.output_widget) + + # 默认显示上料类型 + self.stacked_widget.setCurrentIndex(0) + + self.main_layout.addWidget(self.stacked_widget, 1) + + # 底部按钮区域 + self.button_layout = QHBoxLayout() + self.button_layout.setContentsMargins(0, 10, 0, 0) + + self.save_button = QPushButton("保存配置") + self.save_button.setFont(self.normal_font) + self.save_button.setFixedSize(120, 40) + self.save_button.setStyleSheet(""" + QPushButton { + background-color: #4caf50; + color: white; + border: none; + border-radius: 5px; + } + QPushButton:hover { + background-color: #45a049; + } + QPushButton:pressed { + background-color: #3d8b40; + } + """) + + self.reset_button = QPushButton("重置") + self.reset_button.setFont(self.normal_font) + self.reset_button.setFixedSize(120, 40) + self.reset_button.setStyleSheet(""" + QPushButton { + background-color: #f44336; + color: white; + border: none; + border-radius: 5px; + } + QPushButton:hover { + background-color: #e53935; + } + QPushButton:pressed { + background-color: #d32f2f; + } + """) + + self.button_layout.addStretch() + self.button_layout.addWidget(self.reset_button) + self.button_layout.addSpacing(20) + self.button_layout.addWidget(self.save_button) + + self.main_layout.addLayout(self.button_layout) + + # 连接信号和槽 + self.input_button.clicked.connect(self.show_input_types) + self.output_button.clicked.connect(self.show_output_types) + + def create_pallet_type_widget(self, operation_type): + """创建托盘类型配置部件 + + Args: + operation_type: 操作类型 (input/output) + + Returns: + QWidget: 托盘类型配置部件 + """ + widget = QWidget() + layout = QVBoxLayout(widget) + layout.setContentsMargins(0, 10, 0, 0) + layout.setSpacing(15) + + # 创建表格 + table = QTableWidget() + table.setFont(self.normal_font) + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["类型名称", "描述", "排序", "启用"]) + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) + table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) + table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) + table.verticalHeader().setVisible(False) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.setSelectionMode(QAbstractItemView.SingleSelection) + table.setAlternatingRowColors(True) + table.setStyleSheet(""" + QTableWidget { + border: 1px solid #ddd; + border-radius: 5px; + background-color: #ffffff; + alternate-background-color: #f5f5f5; + } + QHeaderView::section { + background-color: #f0f0f0; + padding: 6px; + border: 1px solid #ddd; + font-weight: bold; + } + """) + + # 设置表格属性,用于标识操作类型 + table.setObjectName(f"{operation_type}_table") + table.setProperty("operation_type", operation_type) + + layout.addWidget(table) + + # 创建表单布局,用于添加/编辑托盘类型 + form_group = QGroupBox("添加/编辑托盘类型") + form_group.setFont(self.normal_font) + form_layout = QFormLayout(form_group) + form_layout.setContentsMargins(15, 25, 15, 15) + form_layout.setSpacing(10) + + # 类型名称 + type_name_label = QLabel("类型名称:") + type_name_label.setFont(self.normal_font) + type_name_input = QLineEdit() + type_name_input.setFont(self.normal_font) + type_name_input.setObjectName(f"{operation_type}_type_name_input") + form_layout.addRow(type_name_label, type_name_input) + + # 描述 + desc_label = QLabel("描述:") + desc_label.setFont(self.normal_font) + desc_input = QLineEdit() + desc_input.setFont(self.normal_font) + desc_input.setObjectName(f"{operation_type}_desc_input") + form_layout.addRow(desc_label, desc_input) + + # 排序 + sort_order_label = QLabel("排序:") + sort_order_label.setFont(self.normal_font) + sort_order_spin = QSpinBox() + sort_order_spin.setFont(self.normal_font) + sort_order_spin.setObjectName(f"{operation_type}_sort_order_spin") + sort_order_spin.setRange(1, 999) + sort_order_spin.setValue(100) + form_layout.addRow(sort_order_label, sort_order_spin) + + # 是否启用 + enabled_check = QCheckBox("启用") + enabled_check.setFont(self.normal_font) + enabled_check.setObjectName(f"{operation_type}_enabled_check") + enabled_check.setChecked(True) + form_layout.addRow("", enabled_check) + + # 添加表单按钮 + form_button_layout = QHBoxLayout() + + add_button = QPushButton("添加") + add_button.setFont(self.normal_font) + add_button.setObjectName(f"{operation_type}_add_button") + add_button.setStyleSheet(""" + QPushButton { + background-color: #4caf50; + color: white; + border: none; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #45a049; + } + QPushButton:pressed { + background-color: #3d8b40; + } + """) + + update_button = QPushButton("更新") + update_button.setFont(self.normal_font) + update_button.setObjectName(f"{operation_type}_update_button") + update_button.setStyleSheet(""" + QPushButton { + background-color: #2196f3; + color: white; + border: none; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #1e88e5; + } + QPushButton:pressed { + background-color: #1976d2; + } + """) + update_button.setEnabled(False) + + delete_button = QPushButton("删除") + delete_button.setFont(self.normal_font) + delete_button.setObjectName(f"{operation_type}_delete_button") + delete_button.setStyleSheet(""" + QPushButton { + background-color: #f44336; + color: white; + border: none; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #e53935; + } + QPushButton:pressed { + background-color: #d32f2f; + } + """) + delete_button.setEnabled(False) + + cancel_button = QPushButton("取消") + cancel_button.setFont(self.normal_font) + cancel_button.setObjectName(f"{operation_type}_cancel_button") + cancel_button.setStyleSheet(""" + QPushButton { + background-color: #9e9e9e; + color: white; + border: none; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #757575; + } + QPushButton:pressed { + background-color: #616161; + } + """) + cancel_button.setEnabled(False) + + form_button_layout.addWidget(add_button) + form_button_layout.addWidget(update_button) + form_button_layout.addWidget(delete_button) + form_button_layout.addWidget(cancel_button) + + form_layout.addRow("", form_button_layout) + + layout.addWidget(form_group) + + # 添加隐藏字段,用于存储当前编辑的ID + widget.setProperty("current_edit_id", -1) + + return widget + + def show_input_types(self): + """显示上料类型""" + self.input_button.setChecked(True) + self.output_button.setChecked(False) + self.stacked_widget.setCurrentIndex(0) + + def show_output_types(self): + """显示下料类型""" + self.input_button.setChecked(False) + self.output_button.setChecked(True) + self.stacked_widget.setCurrentIndex(1) + + def set_form_enabled(self, enabled): + """设置表单是否可编辑""" + self.input_button.setEnabled(enabled) + self.output_button.setEnabled(enabled) + self.save_button.setEnabled(enabled) + self.reset_button.setEnabled(enabled) + + # 禁用所有表格和表单 + for widget in [self.input_widget, self.output_widget]: + for child in widget.findChildren(QWidget): + child.setEnabled(enabled) \ No newline at end of file diff --git a/ui/settings_ui.py b/ui/settings_ui.py index 2498f67..5ad530a 100644 --- a/ui/settings_ui.py +++ b/ui/settings_ui.py @@ -1,14 +1,15 @@ from PySide6.QtWidgets import ( QWidget, QLabel, QLineEdit, QPushButton, QComboBox, QGridLayout, QHBoxLayout, QVBoxLayout, QTabWidget, QFrame, QFormLayout, QGroupBox, QRadioButton, QSpacerItem, QSizePolicy, - QTableWidget, QTableWidgetItem, QHeaderView, QSlider + QTableWidget, QTableWidgetItem, QHeaderView, QSlider, QCheckBox ) -from PySide6.QtGui import QFont -from PySide6.QtCore import Qt +from PySide6.QtGui import QFont, QBrush, QColor +from PySide6.QtCore import Qt, Signal, QSize class SettingsUI(QWidget): def __init__(self, parent=None): super().__init__(parent) + self.parent = parent self.init_ui() def init_ui(self): @@ -42,6 +43,7 @@ class SettingsUI(QWidget): self.create_auth_tab() self.create_user_tab() self.create_param_tab() + self.create_pallet_type_tab() def create_camera_tab(self): # 相机设置选项卡 @@ -208,14 +210,14 @@ class SettingsUI(QWidget): self.db_type_group.setFont(self.normal_font) self.db_type_layout = QHBoxLayout() - self.sqlite_radio = QRadioButton("SQLite") + self.sqlite_radio = QCheckBox("SQLite") self.sqlite_radio.setFont(self.normal_font) self.sqlite_radio.setChecked(True) - self.pgsql_radio = QRadioButton("PostgreSQL") + self.pgsql_radio = QCheckBox("PostgreSQL") self.pgsql_radio.setFont(self.normal_font) - self.mysql_radio = QRadioButton("MySQL") + self.mysql_radio = QCheckBox("MySQL") self.mysql_radio.setFont(self.normal_font) self.db_type_layout.addWidget(self.sqlite_radio) @@ -385,4 +387,24 @@ class SettingsUI(QWidget): self.param_layout.addWidget(self.param_placeholder) self.param_layout.addStretch(1) - self.tab_widget.addTab(self.param_tab, "参数配置") \ No newline at end of file + self.tab_widget.addTab(self.param_tab, "参数配置") + + def create_pallet_type_tab(self): + # 托盘类型设置选项卡 + self.pallet_type_tab = QWidget() + self.pallet_type_layout = QVBoxLayout(self.pallet_type_tab) + self.pallet_type_layout.setContentsMargins(20, 20, 20, 20) + + # 占位标签 + self.pallet_type_placeholder = QLabel("正在加载托盘类型设置...") + self.pallet_type_placeholder.setFont(self.normal_font) + self.pallet_type_placeholder.setAlignment(Qt.AlignCenter) + self.pallet_type_placeholder.setStyleSheet("color: #888888; padding: 20px;") + self.pallet_type_layout.addWidget(self.pallet_type_placeholder) + + self.tab_widget.addTab(self.pallet_type_tab, "托盘类型") + + def back_to_main(self): + """返回主页""" + if self.parent and hasattr(self.parent, 'show_main_page'): + self.parent.show_main_page() \ No newline at end of file diff --git a/utils/modbus_utils.py b/utils/modbus_utils.py new file mode 100644 index 0000000..5713f33 --- /dev/null +++ b/utils/modbus_utils.py @@ -0,0 +1,209 @@ +from flask import Flask, jsonify, request, make_response +from pymodbus.client import ModbusTcpClient +import time +import logging +from .config_loader import ConfigLoader +# 配置 Flask 日志级别 +log = logging.getLogger('werkzeug') +log.setLevel(logging.WARNING) + + +# Modbus TCP 配置 + +class ModbusUtils: + + def __init__(self) -> None: + # 初始化 modbus 配置 + config = ConfigLoader.get_instance() + self.MODBUS_HOST = config.get_value("modbus.host") + self.MODBUS_PORT = config.get_value("modbus.port") + + def get_client(self): + # 创建Modbus TCP客户端实例,指定服务器的IP地址和端口号 + # client = ModbusTcpClient('localhost', port=5020) + client = ModbusTcpClient(self.MODBUS_HOST, port=self.MODBUS_PORT, timeout=10) # 增加超时时间 + logging.info(f"Attempting to connect to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}") + try: + is_connected = client.connect() #确保客户端已连接 + if is_connected: + logging.info(f"Successfully connected to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}") + else: + logging.error(f"Failed to connect to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}. client.connect() returned False.") + except Exception as e: + logging.error(f"Exception during connection to Modbus server {self.MODBUS_HOST}:{self.MODBUS_PORT}: {e}") + # Optionally, re-raise or handle as appropriate + return None # Or raise an exception + return client + + + def close_client(self, client): + # 关闭客户端连接 + if client: + client.close() + + # 新增十进制转成二进制 + @staticmethod + def decimal_to_binary(decimal): + """十进制转16位二进制,右对齐""" + return format(decimal, '016b') + + # 新增二进制转成十进制 + @staticmethod + def binary_to_decimal(binary): + """二进制字符串转十进制""" + return int(binary, 2) + + # 新增十进制转16进制 + @staticmethod + def decimal_to_hex(decimal): + return hex(decimal)[2:] + + + @staticmethod + def split_data(data): + ''' + 解析位置编码获取寄存器地址 + :param data: 位置编码,例如 "01-003-02" + :return: 寄存器地址 + + 规则说明: + - column: 库区编号,01表示A库(D100-D108),02表示B库(D110-D118) + - level: 层号(1-9),对应寄存器最后一位,如第2层对应D101 + - row: 排号(1-16),对应寄存器的位索引 + ''' + data_list = data.split('-') + column = data_list[0] + row = data_list[1] + level = data_list[2] + return column, row, level + + def get_hex_str(self, data): + """ + 解析位置编码获取寄存器地址 + :param data: 位置编码,例如 "01-003-02" + :return: 寄存器地址 + + 规则说明: + - column: 库区编号,01表示A库(D100-D108),02表示B库(D110-D118) + - level: 层号(1-9),对应寄存器最后一位,如第2层对应D101 + - row: 排号(1-16),对应寄存器的位索引 + + 示例: + 01-003-02 => D101.2 (A库第2层,第3排) + 02-005-06 => D115.4 (B库第6层,第5排) + """ + column, row, level = self.split_data(data) + # 计算基础地址:A库从100开始,B库从110开始 + base_address = 100 + (int(column) - 1) * 10 + # 加上层号得到最终地址 + register_address = base_address + (int(level) - 1) + return register_address + + + @staticmethod + def check_register_value(client, address, expected_value, timeout_seconds=5): + """ + 校验指定寄存器地址中的数据是否达到期望值 + :param client: ModbusTcp客户端 + :param address: 寄存器地址 + :param expected_value: 期望值 + :param timeout_seconds: 超时时间(秒) + :return: True表示达到期望值,False表示超时 + """ + try: + start_time = time.time() + while True: + result = client.read_holding_registers(address=address, count=1) + if not result.isError() and result.registers[0] == expected_value: + logging.info(f"寄存器D{address}值: {result.registers[0]} 达到期望值{expected_value}") + return True + + if time.time() - start_time > timeout_seconds: + logging.warning(f"寄存器D{address} {timeout_seconds}秒内未达到期望值{expected_value}") + return False + + time.sleep(0.2) # 添加短暂延时,避免过于频繁的读取 + except Exception as e: + logging.error(f"校验寄存器D{address}值时发生错误: {str(e)}") + return False + + def write_register_until_success(self, client, address, value, expected_value=None, timeout_seconds=5): + """ + 循环写入寄存器直到成功或超时 + :param client: ModbusTcp客户端 + :param address: 寄存器地址 + :param value: 要写入的值 + :param expected_value: 期望值(如果为None,则使用value作为期望值) + :param timeout_seconds: 超时时间(秒) + :return: True表示写入成功,False表示超时 + """ + if expected_value is None: + expected_value = value + + start_time = time.time() + while True: + # 写入值 + logging.info(f"写入寄存器D{address}值: {value}") + client.write_registers(address=address, values=[value]) + logging.info(f"写入寄存器D{address}值: {value}成功") + # 检查是否达到期望值 + if self.check_register_value(client, address, expected_value): + return True + + if time.time() - start_time > timeout_seconds: + logging.warning(f"寄存器D{address} {timeout_seconds}秒内写入失败") + return False + + time.sleep(0.2) # 添加短暂延时,避免过于频繁的写入 + + + @staticmethod + def handle_error(error_msg, task_number='', is_emergency_stop=False): + """通用错误处理函数 + :param error_msg: 错误信息 + :param task_number: 任务编号,默认为空 + :param is_emergency_stop: 是否是急停引起的错误 + """ + logging.error(error_msg) + pass + # return response, 500 + + # 添加便捷方法,用于读取保持寄存器 + def read_holding_register(self, client, address, count=1): + """ + 读取保持寄存器 + :param client: ModbusTcp客户端 + :param address: 寄存器地址 + :param count: 读取的寄存器数量 + :return: 读取结果,失败返回None + """ + try: + result = client.read_holding_registers(address=address, count=count) + if result.isError(): + logging.error(f"读取寄存器D{address}失败: {result}") + return None + return result.registers + except Exception as e: + logging.error(f"读取寄存器D{address}时发生错误: {str(e)}") + return None + + # 添加便捷方法,用于写入保持寄存器 + def write_register(self, client, address, value): + """ + 写入保持寄存器 + :param client: ModbusTcp客户端 + :param address: 寄存器地址 + :param value: 要写入的值 + :return: 是否写入成功 + """ + try: + result = client.write_registers(address=address, values=[value]) + if result.isError(): + logging.error(f"写入寄存器D{address}值{value}失败: {result}") + return False + logging.info(f"写入寄存器D{address}值{value}成功") + return True + except Exception as e: + logging.error(f"写入寄存器D{address}值{value}时发生错误: {str(e)}") + return False + diff --git a/utils/pallet_type_manager.py b/utils/pallet_type_manager.py new file mode 100644 index 0000000..57ff650 --- /dev/null +++ b/utils/pallet_type_manager.py @@ -0,0 +1,137 @@ +import logging +from dao.pallet_type_dao import PalletTypeDAO + +class PalletTypeManager: + """托盘类型管理器,用于管理托盘类型配置""" + + _instance = None + + @classmethod + def get_instance(cls): + """获取单例实例""" + if cls._instance is None: + cls._instance = PalletTypeManager() + return cls._instance + + def __init__(self): + """初始化托盘类型管理器""" + self.dao = PalletTypeDAO() + self.pallet_types = [] + self.reload_pallet_types() + + def reload_pallet_types(self): + """重新加载托盘类型""" + try: + self.pallet_types = self.dao.get_all_pallet_types(include_disabled=True) + logging.info(f"已加载{len(self.pallet_types)}个托盘类型") + return True + except Exception as e: + logging.error(f"加载托盘类型失败: {str(e)}") + return False + + def get_all_pallet_types(self, include_disabled=False): + """获取所有托盘类型 + + Args: + include_disabled: 是否包含禁用的类型 + + Returns: + list: 托盘类型列表 + """ + if include_disabled: + return self.pallet_types + else: + return [pallet_type for pallet_type in self.pallet_types if pallet_type['enabled']] + + def get_pallet_types_by_operation(self, operation_type, include_disabled=False): + """根据操作类型获取托盘类型 + + Args: + operation_type: 操作类型 (input/output) + include_disabled: 是否包含禁用的类型 + + Returns: + list: 托盘类型列表 + """ + if include_disabled: + return [pallet_type for pallet_type in self.pallet_types + if pallet_type['operation_type'] == operation_type] + else: + return [pallet_type for pallet_type in self.pallet_types + if pallet_type['operation_type'] == operation_type and pallet_type['enabled']] + + def get_pallet_type_by_id(self, pallet_type_id): + """根据ID获取托盘类型 + + Args: + pallet_type_id: 托盘类型ID + + Returns: + dict: 托盘类型信息,未找到则返回None + """ + for pallet_type in self.pallet_types: + if pallet_type['id'] == pallet_type_id: + return pallet_type + return None + + def create_pallet_type(self, data, username='system'): + """创建托盘类型 + + Args: + data: 托盘类型数据 + username: 操作用户 + + Returns: + int: 新创建的托盘类型ID,失败返回None + """ + result = self.dao.create_pallet_type(data, username) + if result: + self.reload_pallet_types() + return result + + def update_pallet_type(self, pallet_type_id, data, username='system'): + """更新托盘类型 + + Args: + pallet_type_id: 托盘类型ID + data: 更新数据 + username: 操作用户 + + Returns: + bool: 更新是否成功 + """ + result = self.dao.update_pallet_type(pallet_type_id, data, username) + if result: + self.reload_pallet_types() + return result + + def delete_pallet_type(self, pallet_type_id, username='system'): + """删除托盘类型 + + Args: + pallet_type_id: 托盘类型ID + username: 操作用户 + + Returns: + bool: 删除是否成功 + """ + result = self.dao.delete_pallet_type(pallet_type_id, username) + if result: + self.reload_pallet_types() + return result + + def toggle_pallet_type(self, pallet_type_id, enabled, username='system'): + """启用或禁用托盘类型 + + Args: + pallet_type_id: 托盘类型ID + enabled: 是否启用 + username: 操作用户 + + Returns: + bool: 操作是否成功 + """ + result = self.dao.toggle_pallet_type(pallet_type_id, enabled, username) + if result: + self.reload_pallet_types() + return result \ No newline at end of file diff --git a/widgets/main_window.py b/widgets/main_window.py index 0960ab2..7cf4ce3 100644 --- a/widgets/main_window.py +++ b/widgets/main_window.py @@ -4,11 +4,11 @@ import logging import json from datetime import datetime from pathlib import Path - +from utils.modbus_utils import ModbusUtils # 导入PySide6 from PySide6.QtWidgets import ( QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel, QMainWindow, - QTableWidget, QMenu + QTableWidget, QMenu, QComboBox, QFormLayout, QDialog, QVBoxLayout, QHBoxLayout, QPushButton ) from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QBrush, QColor @@ -21,6 +21,8 @@ from widgets.camera_settings_widget import CameraSettingsWidget # 导入检验配置管理器 from utils.inspection_config_manager import InspectionConfigManager +# 导入托盘类型管理器 +from utils.pallet_type_manager import PalletTypeManager class MainWindow(MainWindowUI): @@ -45,6 +47,16 @@ class MainWindow(MainWindowUI): # 初始化检验配置管理器 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) + # 只有在相机启用时创建相机显示组件 if self.camera_enabled: # 创建相机显示组件并添加到上料区 @@ -74,6 +86,9 @@ class MainWindow(MainWindowUI): # 设置中央部件为堆叠部件 self.setCentralWidget(self.stacked_widget) + # 添加托盘类型选择下拉框 + self.add_pallet_type_selectors() + # 连接信号和槽 self.connect_signals() @@ -94,6 +109,50 @@ class MainWindow(MainWindowUI): logging.info(f"主窗口已创建,用户: {user_name}") + def add_pallet_type_selectors(self): + """添加托盘类型选择下拉框""" + # 创建上料托盘类型选择下拉框 + self.input_pallet_type_label = QLabel("上料托盘类型:") + self.input_pallet_type_label.setFont(self.normal_font) + self.input_pallet_type_label.setVisible(False) + self.material_form_layout.addRow(self.input_pallet_type_label) + + self.input_pallet_type_combo = QComboBox() + self.input_pallet_type_combo.setFont(self.normal_font) + self.input_pallet_type_combo.setVisible(False) + self.material_form_layout.addRow("", self.input_pallet_type_combo) + + # 创建下料托盘类型选择下拉框 + self.output_pallet_type_label = QLabel("下料托盘类型:") + self.output_pallet_type_label.setFont(self.normal_font) + self.output_pallet_type_label.setVisible(False) + self.output_form_layout.addRow(self.output_pallet_type_label) + + self.output_pallet_type_combo = QComboBox() + self.output_pallet_type_combo.setFont(self.normal_font) + self.output_pallet_type_combo.setVisible(False) + self.output_form_layout.addRow("", self.output_pallet_type_combo) + + # 加载托盘类型数据 + self.update_pallet_types() + + def update_pallet_types(self): + """更新托盘类型下拉框""" + # 重新加载托盘类型数据 + self.pallet_type_manager.reload_pallet_types() + + # 更新上料托盘类型 + input_types = self.pallet_type_manager.get_pallet_types_by_operation("input") + self.input_pallet_type_combo.clear() + for pallet_type in input_types: + self.input_pallet_type_combo.addItem(pallet_type['type_name'], pallet_type['id']) + + # 更新下料托盘类型 + output_types = self.pallet_type_manager.get_pallet_types_by_operation("output") + self.output_pallet_type_combo.clear() + for pallet_type in output_types: + self.output_pallet_type_combo.addItem(pallet_type['type_name'], pallet_type['id']) + def load_config(self): """加载配置文件""" config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json") @@ -192,41 +251,161 @@ class MainWindow(MainWindowUI): def handle_input(self): """处理上料按钮点击事件""" - logging.info("上料按钮被点击") + # 创建对话框 + dialog = QDialog(self) + dialog.setWindowTitle("上料操作") + dialog.setFixedSize(400, 250) - QMessageBox.information(self, "操作提示", "开始上料操作") - # 这里添加上料相关的业务逻辑 + # 对话框布局 + layout = QVBoxLayout(dialog) + + # 添加提示信息 + info_label = QLabel("请选择上料托盘类型:") + info_label.setFont(self.normal_font) + layout.addWidget(info_label) + + # 添加托盘类型选择 + pallet_combo = QComboBox() + pallet_combo.setFont(self.normal_font) + # 复制当前托盘类型选择器的内容 + for i in range(self.input_pallet_type_combo.count()): + pallet_combo.addItem(self.input_pallet_type_combo.itemText(i)) + layout.addWidget(pallet_combo) + + # 添加按钮 + button_layout = QHBoxLayout() + confirm_button = QPushButton("确认") + confirm_button.setFont(self.normal_font) + confirm_button.setStyleSheet("background-color: #e3f2fd; border: 1px solid #2196f3; padding: 8px 16px; font-weight: bold; border-radius: 4px;") + + cancel_button = QPushButton("取消") + cancel_button.setFont(self.normal_font) + cancel_button.setStyleSheet("padding: 8px 16px; font-weight: bold; border-radius: 4px;") + + button_layout.addStretch() + button_layout.addWidget(confirm_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接按钮信号 + confirm_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + result = dialog.exec() + + # 如果用户确认,则执行上料操作 + if result == QDialog.Accepted: + selected_type = pallet_combo.currentText() + + # 执行Modbus操作 + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 上料 D2 寄存器写入 1 + if modbus.write_register_until_success(client, 2, 1): + # 创建状态标签并显示在右上角 + self.show_operation_status("上料中", "input", selected_type) + 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): """处理下料按钮点击事件""" - logging.info("下料按钮被点击") - QMessageBox.information(self, "操作提示", "开始下料操作") - # 这里添加下料相关的业务逻辑 + # 创建对话框 + dialog = QDialog(self) + dialog.setWindowTitle("下料操作") + dialog.setFixedSize(400, 250) + + # 对话框布局 + layout = QVBoxLayout(dialog) + + # 添加提示信息 + info_label = QLabel("请选择下料托盘类型:") + info_label.setFont(self.normal_font) + layout.addWidget(info_label) + + # 添加托盘类型选择 + pallet_combo = QComboBox() + pallet_combo.setFont(self.normal_font) + # 复制当前托盘类型选择器的内容 + for i in range(self.output_pallet_type_combo.count()): + pallet_combo.addItem(self.output_pallet_type_combo.itemText(i)) + layout.addWidget(pallet_combo) + + # 添加按钮 + button_layout = QHBoxLayout() + confirm_button = QPushButton("确认") + confirm_button.setFont(self.normal_font) + confirm_button.setStyleSheet("background-color: #fff8e1; border: 1px solid #ffc107; padding: 8px 16px; font-weight: bold; border-radius: 4px;") + + cancel_button = QPushButton("取消") + cancel_button.setFont(self.normal_font) + cancel_button.setStyleSheet("padding: 8px 16px; font-weight: bold; border-radius: 4px;") + + button_layout.addStretch() + button_layout.addWidget(confirm_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接按钮信号 + confirm_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + result = dialog.exec() + + # 如果用户确认,则执行下料操作 + if result == QDialog.Accepted: + selected_type = pallet_combo.currentText() + + # 执行Modbus操作 + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 下料 D3 寄存器写入 1 + if modbus.write_register_until_success(client, 3, 1): + # 创建状态标签并显示在右上角 + self.show_operation_status("下料中", "output", selected_type) + 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_start(self): - """处理开始按钮点击事件""" - logging.info("开始按钮被点击") - - # 只有在相机启用时处理相机显示 - if self.camera_enabled and hasattr(self, 'camera_display'): - # 开始显示相机画面 - if self.camera_display.camera_manager.isOpen: - self.camera_display.start_display() - - QMessageBox.information(self, "操作提示", "生产线已启动") - # 这里添加启动生产线的业务逻辑 + """处理启动按钮点击事件""" + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 启动 D1 寄存器写入 1 + if not modbus.write_register_until_success(client, 1, 1): + 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_stop(self): - """处理暂停按钮点击事件""" - logging.info("暂停按钮被点击") - - # 只有在相机启用时处理相机显示 - if self.camera_enabled and hasattr(self, 'camera_display'): - # 停止显示相机画面 - self.camera_display.stop_display() - - QMessageBox.information(self, "操作提示", "生产线已暂停") - # 这里添加暂停生产线的业务逻辑 + """处理停止按钮点击事件""" + modbus = ModbusUtils() + client = modbus.get_client() + try: + # 停止 D1 寄存器写入 0 + if not modbus.write_register_until_success(client, 1, 0): + 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_camera_status(self, is_connected, message): """处理相机状态变化""" @@ -1052,4 +1231,63 @@ class MainWindow(MainWindowUI): except Exception as e: logging.error(f"检查数据库记录失败: {str(e)}") - QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}") \ No newline at end of file + 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.normal_font) + status_label.setStyleSheet("color: red; background-color: transparent;") + status_label.setAlignment(Qt.AlignRight | Qt.AlignTop) + + # 使用绝对定位,放置在右上角 + status_label.setGeometry(container.width() - 200, 5, 190, 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() - 200, 5, 190, 30) + + # 调用原始的resizeEvent(如果有的话) + original_resize = getattr(container, "_original_resize_event", None) + if original_resize: + original_resize(event) \ No newline at end of file diff --git a/widgets/pallet_type_settings_widget.py b/widgets/pallet_type_settings_widget.py new file mode 100644 index 0000000..73289c8 --- /dev/null +++ b/widgets/pallet_type_settings_widget.py @@ -0,0 +1,420 @@ +import logging +from PySide6.QtWidgets import QTableWidget, QTableWidgetItem, QMessageBox, QCheckBox, QPushButton, QLineEdit, QSpinBox +from PySide6.QtCore import Qt, Signal +from ui.pallet_type_settings_ui import PalletTypeSettingsUI +from utils.pallet_type_manager import PalletTypeManager + +class PalletTypeSettingsWidget(PalletTypeSettingsUI): + """托盘类型设置部件""" + + # 定义信号 + signal_pallet_types_changed = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + + # 初始化托盘类型管理器 + self.pallet_type_manager = PalletTypeManager.get_instance() + + # 连接信号和槽 + self.connect_signals() + + # 加载数据 + self.load_pallet_types() + + def connect_signals(self): + """连接信号和槽""" + # 保存和重置按钮 + self.save_button.clicked.connect(self.save_all_pallet_types) + self.reset_button.clicked.connect(self.load_pallet_types) + + # 上料类型表格和按钮 + input_table = self.input_widget.findChild(QTableWidget, "input_table") + input_table.itemSelectionChanged.connect(lambda: self.handle_table_selection("input")) + + input_add_button = self.input_widget.findChild(QPushButton, "input_add_button") + input_add_button.clicked.connect(lambda: self.add_pallet_type("input")) + + input_update_button = self.input_widget.findChild(QPushButton, "input_update_button") + input_update_button.clicked.connect(lambda: self.update_pallet_type("input")) + + input_delete_button = self.input_widget.findChild(QPushButton, "input_delete_button") + input_delete_button.clicked.connect(lambda: self.delete_pallet_type("input")) + + input_cancel_button = self.input_widget.findChild(QPushButton, "input_cancel_button") + input_cancel_button.clicked.connect(lambda: self.cancel_edit("input")) + + # 下料类型表格和按钮 + output_table = self.output_widget.findChild(QTableWidget, "output_table") + output_table.itemSelectionChanged.connect(lambda: self.handle_table_selection("output")) + + output_add_button = self.output_widget.findChild(QPushButton, "output_add_button") + output_add_button.clicked.connect(lambda: self.add_pallet_type("output")) + + output_update_button = self.output_widget.findChild(QPushButton, "output_update_button") + output_update_button.clicked.connect(lambda: self.update_pallet_type("output")) + + output_delete_button = self.output_widget.findChild(QPushButton, "output_delete_button") + output_delete_button.clicked.connect(lambda: self.delete_pallet_type("output")) + + output_cancel_button = self.output_widget.findChild(QPushButton, "output_cancel_button") + output_cancel_button.clicked.connect(lambda: self.cancel_edit("output")) + + def load_pallet_types(self): + """加载托盘类型数据""" + try: + # 重新加载数据 + self.pallet_type_manager.reload_pallet_types() + + # 加载上料类型 + self.load_operation_pallet_types("input") + + # 加载下料类型 + self.load_operation_pallet_types("output") + + logging.info("托盘类型数据已加载") + except Exception as e: + logging.error(f"加载托盘类型数据失败: {str(e)}") + QMessageBox.critical(self, "错误", f"加载托盘类型数据失败: {str(e)}") + + def load_operation_pallet_types(self, operation_type): + """加载指定操作类型的托盘类型数据 + + Args: + operation_type: 操作类型 (input/output) + """ + # 获取表格 + table = self.get_table_by_operation_type(operation_type) + if not table: + return + + # 清空表格 + table.setRowCount(0) + + # 获取数据 + pallet_types = self.pallet_type_manager.get_pallet_types_by_operation(operation_type, include_disabled=True) + + # 填充表格 + for row, pallet_type in enumerate(pallet_types): + table.insertRow(row) + + # 类型名称 + type_name_item = QTableWidgetItem(pallet_type['type_name']) + type_name_item.setData(Qt.UserRole, pallet_type['id']) + table.setItem(row, 0, type_name_item) + + # 描述 + desc_item = QTableWidgetItem(pallet_type['description'] or "") + table.setItem(row, 1, desc_item) + + # 排序 + sort_order_item = QTableWidgetItem(str(pallet_type['sort_order'])) + table.setItem(row, 2, sort_order_item) + + # 启用状态 + enabled_check = QCheckBox() + enabled_check.setChecked(pallet_type['enabled']) + enabled_check.stateChanged.connect(lambda state, row=row, id=pallet_type['id']: + self.toggle_pallet_type(id, state == Qt.Checked)) + table.setCellWidget(row, 3, enabled_check) + + # 重置表单 + self.reset_form(operation_type) + + def get_table_by_operation_type(self, operation_type): + """根据操作类型获取表格 + + Args: + operation_type: 操作类型 (input/output) + + Returns: + QTableWidget: 表格部件 + """ + if operation_type == "input": + return self.input_widget.findChild(QTableWidget, "input_table") + elif operation_type == "output": + return self.output_widget.findChild(QTableWidget, "output_table") + return None + + def get_form_values(self, operation_type): + """获取表单值 + + Args: + operation_type: 操作类型 (input/output) + + Returns: + dict: 表单值 + """ + widget = self.input_widget if operation_type == "input" else self.output_widget + + type_name_input = widget.findChild(QLineEdit, f"{operation_type}_type_name_input") + desc_input = widget.findChild(QLineEdit, f"{operation_type}_desc_input") + sort_order_spin = widget.findChild(QSpinBox, f"{operation_type}_sort_order_spin") + enabled_check = widget.findChild(QCheckBox, f"{operation_type}_enabled_check") + + return { + 'type_name': type_name_input.text().strip(), + 'operation_type': operation_type, + 'description': desc_input.text().strip(), + 'sort_order': sort_order_spin.value(), + 'enabled': enabled_check.isChecked() + } + + def set_form_values(self, operation_type, values): + """设置表单值 + + Args: + operation_type: 操作类型 (input/output) + values: 表单值 + """ + widget = self.input_widget if operation_type == "input" else self.output_widget + + type_name_input = widget.findChild(QLineEdit, f"{operation_type}_type_name_input") + desc_input = widget.findChild(QLineEdit, f"{operation_type}_desc_input") + sort_order_spin = widget.findChild(QSpinBox, f"{operation_type}_sort_order_spin") + enabled_check = widget.findChild(QCheckBox, f"{operation_type}_enabled_check") + + type_name_input.setText(values.get('type_name', '')) + desc_input.setText(values.get('description', '')) + sort_order_spin.setValue(values.get('sort_order', 100)) + enabled_check.setChecked(values.get('enabled', True)) + + def reset_form(self, operation_type): + """重置表单 + + Args: + operation_type: 操作类型 (input/output) + """ + widget = self.input_widget if operation_type == "input" else self.output_widget + + # 重置表单值 + self.set_form_values(operation_type, { + 'type_name': '', + 'description': '', + 'sort_order': 100, + 'enabled': True + }) + + # 重置当前编辑ID + widget.setProperty("current_edit_id", -1) + + # 重置按钮状态 + add_button = widget.findChild(QPushButton, f"{operation_type}_add_button") + update_button = widget.findChild(QPushButton, f"{operation_type}_update_button") + delete_button = widget.findChild(QPushButton, f"{operation_type}_delete_button") + cancel_button = widget.findChild(QPushButton, f"{operation_type}_cancel_button") + + add_button.setEnabled(True) + update_button.setEnabled(False) + delete_button.setEnabled(False) + cancel_button.setEnabled(False) + + def handle_table_selection(self, operation_type): + """处理表格选择事件 + + Args: + operation_type: 操作类型 (input/output) + """ + table = self.get_table_by_operation_type(operation_type) + if not table: + return + + # 获取选中行 + selected_rows = table.selectionModel().selectedRows() + if not selected_rows: + return + + # 获取选中行数据 + row = selected_rows[0].row() + pallet_type_id = table.item(row, 0).data(Qt.UserRole) + + # 获取托盘类型数据 + pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id) + if not pallet_type: + return + + # 设置表单值 + self.set_form_values(operation_type, pallet_type) + + # 设置当前编辑ID + widget = self.input_widget if operation_type == "input" else self.output_widget + widget.setProperty("current_edit_id", pallet_type_id) + + # 设置按钮状态 + add_button = widget.findChild(QPushButton, f"{operation_type}_add_button") + update_button = widget.findChild(QPushButton, f"{operation_type}_update_button") + delete_button = widget.findChild(QPushButton, f"{operation_type}_delete_button") + cancel_button = widget.findChild(QPushButton, f"{operation_type}_cancel_button") + + add_button.setEnabled(False) + update_button.setEnabled(True) + delete_button.setEnabled(True) + cancel_button.setEnabled(True) + + def validate_form(self, operation_type): + """验证表单 + + Args: + operation_type: 操作类型 (input/output) + + Returns: + bool: 表单是否有效 + """ + values = self.get_form_values(operation_type) + + if not values['type_name']: + QMessageBox.warning(self, "警告", "托盘类型名称不能为空") + return False + + return True + + def add_pallet_type(self, operation_type): + """添加托盘类型 + + Args: + operation_type: 操作类型 (input/output) + """ + if not self.validate_form(operation_type): + return + + try: + # 获取表单值 + values = self.get_form_values(operation_type) + + # 创建托盘类型 + result = self.pallet_type_manager.create_pallet_type(values) + if result: + logging.info(f"添加托盘类型成功: {values['type_name']}") + + # 重新加载数据 + self.load_operation_pallet_types(operation_type) + + # 发送信号 + self.signal_pallet_types_changed.emit() + else: + QMessageBox.critical(self, "错误", "添加托盘类型失败") + except Exception as e: + logging.error(f"添加托盘类型失败: {str(e)}") + QMessageBox.critical(self, "错误", f"添加托盘类型失败: {str(e)}") + + def update_pallet_type(self, operation_type): + """更新托盘类型 + + Args: + operation_type: 操作类型 (input/output) + """ + if not self.validate_form(operation_type): + return + + try: + # 获取当前编辑ID + widget = self.input_widget if operation_type == "input" else self.output_widget + pallet_type_id = widget.property("current_edit_id") + if pallet_type_id < 0: + QMessageBox.warning(self, "警告", "请先选择要编辑的托盘类型") + return + + # 获取表单值 + values = self.get_form_values(operation_type) + + # 更新托盘类型 + result = self.pallet_type_manager.update_pallet_type(pallet_type_id, values) + if result: + logging.info(f"更新托盘类型成功: {values['type_name']}") + + # 重新加载数据 + self.load_operation_pallet_types(operation_type) + + # 发送信号 + self.signal_pallet_types_changed.emit() + else: + QMessageBox.critical(self, "错误", "更新托盘类型失败") + except Exception as e: + logging.error(f"更新托盘类型失败: {str(e)}") + QMessageBox.critical(self, "错误", f"更新托盘类型失败: {str(e)}") + + def delete_pallet_type(self, operation_type): + """删除托盘类型 + + Args: + operation_type: 操作类型 (input/output) + """ + try: + # 获取当前编辑ID + widget = self.input_widget if operation_type == "input" else self.output_widget + pallet_type_id = widget.property("current_edit_id") + if pallet_type_id < 0: + QMessageBox.warning(self, "警告", "请先选择要删除的托盘类型") + return + + # 确认删除 + pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id) + if not pallet_type: + QMessageBox.warning(self, "警告", "找不到要删除的托盘类型") + return + + reply = QMessageBox.question(self, "确认删除", + f"确定要删除托盘类型 '{pallet_type['type_name']}' 吗?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply != QMessageBox.Yes: + return + + # 删除托盘类型 + result = self.pallet_type_manager.delete_pallet_type(pallet_type_id) + if result: + logging.info(f"删除托盘类型成功: {pallet_type['type_name']}") + + # 重新加载数据 + self.load_operation_pallet_types(operation_type) + + # 发送信号 + self.signal_pallet_types_changed.emit() + else: + QMessageBox.critical(self, "错误", "删除托盘类型失败") + except Exception as e: + logging.error(f"删除托盘类型失败: {str(e)}") + QMessageBox.critical(self, "错误", f"删除托盘类型失败: {str(e)}") + + def cancel_edit(self, operation_type): + """取消编辑 + + Args: + operation_type: 操作类型 (input/output) + """ + # 重置表单 + self.reset_form(operation_type) + + # 取消表格选择 + table = self.get_table_by_operation_type(operation_type) + if table: + table.clearSelection() + + def toggle_pallet_type(self, pallet_type_id, enabled): + """启用或禁用托盘类型 + + Args: + pallet_type_id: 托盘类型ID + enabled: 是否启用 + """ + try: + # 更新启用状态 + result = self.pallet_type_manager.toggle_pallet_type(pallet_type_id, enabled) + if result: + logging.info(f"更新托盘类型启用状态成功: {pallet_type_id} -> {enabled}") + + # 发送信号 + self.signal_pallet_types_changed.emit() + else: + QMessageBox.critical(self, "错误", "更新托盘类型启用状态失败") + except Exception as e: + logging.error(f"更新托盘类型启用状态失败: {str(e)}") + QMessageBox.critical(self, "错误", f"更新托盘类型启用状态失败: {str(e)}") + + def save_all_pallet_types(self): + """保存所有托盘类型""" + # 目前没有需要批量保存的操作,所有操作都是实时保存的 + QMessageBox.information(self, "提示", "托盘类型配置已保存") + + # 发送信号 + self.signal_pallet_types_changed.emit() \ No newline at end of file diff --git a/widgets/settings_widget.py b/widgets/settings_widget.py index d80263a..d15f8b7 100644 --- a/widgets/settings_widget.py +++ b/widgets/settings_widget.py @@ -3,6 +3,7 @@ import logging from ui.settings_ui import SettingsUI from utils.sql_utils import SQLUtils from widgets.inspection_settings_widget import InspectionSettingsWidget +from widgets.pallet_type_settings_widget import PalletTypeSettingsWidget class SettingsWidget(SettingsUI): def __init__(self, parent=None): @@ -31,6 +32,26 @@ class SettingsWidget(SettingsUI): else: logging.error("无法找到inspection_layout布局") + # 创建托盘类型设置部件 + logging.info("创建PalletTypeSettingsWidget实例") + self.pallet_type_settings = PalletTypeSettingsWidget() + + # 移除临时占位符标签并添加托盘类型设置部件 + if hasattr(self, 'pallet_type_placeholder'): + logging.info("移除托盘类型临时占位符") + self.pallet_type_layout.removeWidget(self.pallet_type_placeholder) + self.pallet_type_placeholder.hide() + self.pallet_type_placeholder.deleteLater() + else: + logging.warning("未找到托盘类型临时占位符标签") + + # 检查布局是否可用 + if hasattr(self, 'pallet_type_layout'): + logging.info("添加托盘类型设置部件到布局") + self.pallet_type_layout.addWidget(self.pallet_type_settings) + else: + logging.error("无法找到pallet_type_layout布局") + # 连接信号和槽 self.connect_signals() @@ -51,6 +72,13 @@ class SettingsWidget(SettingsUI): # 检验配置变更信号 self.inspection_settings.signal_configs_changed.connect(self.handle_inspection_configs_changed) + + # 托盘类型配置变更信号 + self.pallet_type_settings.signal_pallet_types_changed.connect(self.handle_pallet_types_changed) + + # 返回按钮 + if hasattr(self, 'back_button'): + self.back_button.clicked.connect(self.back_to_main) def update_db_ui_state(self): """根据选择的数据库类型更新UI状态""" @@ -165,4 +193,16 @@ class SettingsWidget(SettingsUI): logging.info("检验配置已更新") # 如果有父窗口,通知父窗口更新检验配置 if self.parent and hasattr(self.parent, 'update_inspection_columns'): - self.parent.update_inspection_columns() \ No newline at end of file + self.parent.update_inspection_columns() + + def handle_pallet_types_changed(self): + """处理托盘类型配置变更""" + logging.info("托盘类型配置已更新") + # 如果有父窗口,通知父窗口更新托盘类型 + if self.parent and hasattr(self.parent, 'update_pallet_types'): + self.parent.update_pallet_types() + + def back_to_main(self): + """返回主页""" + if self.parent and hasattr(self.parent, 'show_main_page'): + self.parent.show_main_page() \ No newline at end of file