添加托盘类型配置表和相关UI功能,更新主窗口以支持托盘类型选择,增强设置界面以管理托盘类型,修改配置文件以包含Modbus设置。

This commit is contained in:
zhu-mengmeng 2025-06-10 16:13:22 +08:00
parent b51848e2af
commit c36189f255
14 changed files with 2052 additions and 40 deletions

3
.gitignore vendored
View File

@ -30,4 +30,5 @@ htmlcov/
# 构建文件
build/
dist/
*.egg-info/
*.egg-info/
config/app_config.json.bak

View File

@ -22,5 +22,9 @@
"default_exposure": 20000,
"default_gain": 10,
"default_framerate": 30
},
"modbus": {
"host": "localhost",
"port": "5020"
}
}

293
dao/pallet_type_dao.py Normal file
View File

@ -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

Binary file not shown.

View File

@ -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');

171
modbus_register_tester.py Normal file
View File

@ -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)

76
modbus_server.py Normal file
View File

@ -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("服务器停止运行")

View File

@ -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)

View File

@ -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, "参数配置")
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()

209
utils/modbus_utils.py Normal file
View File

@ -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-D10802表示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-D10802表示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

View File

@ -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

View File

@ -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)}")
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)

View File

@ -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()

View File

@ -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()
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()