更新配置文件以支持串口功能,修改主窗口以初始化串口管理器并处理设置变更

This commit is contained in:
zhu-mengmeng 2025-06-12 17:29:35 +08:00
parent 79eca2b44f
commit fa31dc734d
29 changed files with 2520 additions and 398 deletions

View File

@ -4,7 +4,7 @@
"version": "1.0.0",
"features": {
"enable_serial_ports": false,
"enable_keyboard_listener": false,
"enable_keyboard_listener": true,
"enable_camera": false
}
},
@ -26,5 +26,31 @@
"modbus": {
"host": "localhost",
"port": "5020"
},
"serial":{
"keyboard":{
"trigger_key":"Key.page_up"
},"mdz":{
"bit": 10,
"code": "mdz",
"data_bits": 8,
"parity": "N",
"port": "9600",
"query_cmd": "01 03 00 01 00 07 55 C8",
"query_interval": 5,
"ser": "COM5",
"stop_bits": 1,
"timeout": 1
},"cz":{
"bit": 10,
"code": "cz",
"data_bits": 8,
"parity": "N",
"port": "9600",
"ser": "COM2",
"stable_threshold": 10,
"stop_bits": 1,
"timeout": 1
}
}
}

Binary file not shown.

View File

@ -2,10 +2,10 @@ from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('localhost', port=5020)
client.connect()
# client.write_registers(address=11, values=[114])
client.write_registers(address=6, values=[1])
# client.write_registers(address=11, values=[113])
# client.write_registers(address=6, values=[1])
# client.write_registers(address=5, values=[16])
# client.write_registers(address=13, values=[1])
client.write_registers(address=13, values=[1])
result = client.read_holding_registers(address=13, count=1)

View File

@ -130,8 +130,9 @@ def main():
# 读取配置
config = ConfigLoader.get_instance()
# 打印关键配置信息
enable_serial_ports = config.get_value('app.features.enable_serial_ports', False)
enable_keyboard_listener = config.get_value('app.features.enable_keyboard_listener', False)
enable_serial_ports = config.get_value('serial.printer.enabled', False)
# 键盘监听器配置信息
enable_keyboard_listener = config.get_value('serial.keyboard.enabled', False)
logging.info(f"配置信息 - 启用串口: {enable_serial_ports}, 启用键盘监听: {enable_keyboard_listener}")
# 设置中文翻译器

51
test_keyboard.py Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import logging
import time
from utils.serial_manager import SerialManager
from utils.keyboard_listener import KeyboardListener
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
# 自定义回调函数
def my_pageup_callback():
print("\n" + "="*50)
print("PageUp键被按下自定义回调函数被触发")
print("="*50 + "\n")
def main():
print("初始化SerialManager...")
sm = SerialManager()
# 注册自定义回调函数
kl = KeyboardListener()
kl.register_callback('Key.page_up', my_pageup_callback)
print("启动键盘监听器...")
sm.start_keyboard_listener()
print("键盘监听器已启动按PageUp键触发米电阻查询")
print("程序将运行30秒按Ctrl+C可以提前退出")
try:
for i in range(30):
print(f"等待中 {i+1}/30...", flush=True)
time.sleep(1)
except KeyboardInterrupt:
print("\n用户中断,正在退出...")
finally:
print("停止键盘监听器...")
sm.stop_keyboard_listener(join_thread=True)
print("程序结束")
if __name__ == "__main__":
main()

2
ui/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# ui 包
# 包含所有 UI 相关的类

View File

@ -30,19 +30,6 @@ class InspectionSettingsUI(QWidget):
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("配置检验二级菜单项目至少1项最多6项。启用的项目将显示在微丝产线表格的检验区域。")
self.desc_label.setWordWrap(True)
self.desc_label.setStyleSheet("color: #666666; padding: 0px 10px 10px 10px;")
self.main_layout.addWidget(self.desc_label)
# 创建滚动区域
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)

View File

@ -30,90 +30,11 @@ class PalletTypeSettingsUI(QWidget):
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.main_layout.addWidget(self.output_widget, 1)
# 底部按钮区域
self.button_layout = QHBoxLayout()
@ -162,10 +83,6 @@ class PalletTypeSettingsUI(QWidget):
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):
"""创建托盘类型配置部件
@ -198,64 +115,56 @@ class PalletTypeSettingsUI(QWidget):
border: 1px solid #ddd;
border-radius: 5px;
background-color: #ffffff;
alternate-background-color: #f5f5f5;
alternate-background-color: #f9f9f9;
}
QHeaderView::section {
background-color: #f0f0f0;
padding: 6px;
padding: 5px;
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 = QGroupBox("编辑托盘类型")
form_group.setFont(self.normal_font)
form_layout = QFormLayout(form_group)
form_layout.setContentsMargins(15, 25, 15, 15)
form_layout.setContentsMargins(15, 15, 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)
form_layout.addRow("类型名称:", 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)
form_layout.addRow("描述:", 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)
sort_order_spin.setObjectName(f"{operation_type}_sort_order_spin")
form_layout.addRow("排序:", sort_order_spin)
# 是否启用
# 启用
enabled_check = QCheckBox("启用")
enabled_check.setFont(self.normal_font)
enabled_check.setObjectName(f"{operation_type}_enabled_check")
enabled_check.setChecked(True)
enabled_check.setObjectName(f"{operation_type}_enabled_check")
form_layout.addRow("", enabled_check)
# 添加表单按钮
form_button_layout = QHBoxLayout()
layout.addWidget(form_group)
# 创建按钮区域
button_layout = QHBoxLayout()
add_button = QPushButton("添加")
add_button.setFont(self.normal_font)
@ -265,15 +174,12 @@ class PalletTypeSettingsUI(QWidget):
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
border-radius: 3px;
padding: 5px 15px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
""")
update_button = QPushButton("更新")
@ -284,17 +190,13 @@ class PalletTypeSettingsUI(QWidget):
background-color: #2196f3;
color: white;
border: none;
border-radius: 5px;
border-radius: 3px;
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)
@ -304,17 +206,13 @@ class PalletTypeSettingsUI(QWidget):
background-color: #f44336;
color: white;
border: none;
border-radius: 5px;
border-radius: 3px;
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)
@ -324,52 +222,19 @@ class PalletTypeSettingsUI(QWidget):
background-color: #9e9e9e;
color: white;
border: none;
border-radius: 5px;
border-radius: 3px;
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)
button_layout.addWidget(add_button)
button_layout.addWidget(update_button)
button_layout.addWidget(delete_button)
button_layout.addWidget(cancel_button)
form_layout.addRow("", form_button_layout)
layout.addWidget(form_group)
# 添加隐藏字段用于存储当前编辑的ID
widget.setProperty("current_edit_id", -1)
layout.addLayout(button_layout)
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)

150
ui/serial_settings_ui.py Normal file
View File

@ -0,0 +1,150 @@
from PySide6.QtWidgets import (
QWidget, QLabel, QLineEdit, QComboBox, QCheckBox,
QGridLayout, QGroupBox, QPushButton, QHBoxLayout,
QVBoxLayout, QFormLayout, QSpinBox
)
from PySide6.QtCore import Qt, Signal
class SerialSettingsUI(QWidget):
"""串口设置UI组件"""
# 定义信号
settings_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
main_layout = QVBoxLayout(self)
# 创建全局启用选项
enable_layout = QHBoxLayout()
self.enable_serial_checkbox = QCheckBox("启用串口功能")
self.enable_keyboard_checkbox = QCheckBox("启用键盘监听 (PageUp 触发米电阻查询)")
enable_layout.addWidget(self.enable_serial_checkbox)
enable_layout.addWidget(self.enable_keyboard_checkbox)
enable_layout.addStretch()
main_layout.addLayout(enable_layout)
# 创建串口设置组
serial_group = QGroupBox("串口设置")
serial_layout = QGridLayout(serial_group)
# 米电阻串口设置
mdz_group = QGroupBox("米电阻串口")
mdz_layout = QFormLayout(mdz_group)
# 串口选择
mdz_port_layout = QHBoxLayout()
self.mdz_port_combo = QComboBox()
self.mdz_refresh_btn = QPushButton("刷新")
mdz_port_layout.addWidget(self.mdz_port_combo)
mdz_port_layout.addWidget(self.mdz_refresh_btn)
mdz_layout.addRow("串口:", mdz_port_layout)
# 波特率
self.mdz_baud_combo = QComboBox()
for baud in ["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"]:
self.mdz_baud_combo.addItem(baud)
mdz_layout.addRow("波特率:", self.mdz_baud_combo)
# 数据位
self.mdz_data_bits_combo = QComboBox()
for bits in ["5", "6", "7", "8"]:
self.mdz_data_bits_combo.addItem(bits)
mdz_layout.addRow("数据位:", self.mdz_data_bits_combo)
# 停止位
self.mdz_stop_bits_combo = QComboBox()
for bits in ["1", "1.5", "2"]:
self.mdz_stop_bits_combo.addItem(bits)
mdz_layout.addRow("停止位:", self.mdz_stop_bits_combo)
# 校验位
self.mdz_parity_combo = QComboBox()
for parity in [("无校验", "N"), ("奇校验", "O"), ("偶校验", "E")]:
self.mdz_parity_combo.addItem(parity[0], parity[1])
mdz_layout.addRow("校验位:", self.mdz_parity_combo)
# 查询指令
self.mdz_query_cmd = QLineEdit()
mdz_layout.addRow("查询指令:", self.mdz_query_cmd)
# 查询间隔
self.mdz_query_interval = QSpinBox()
self.mdz_query_interval.setRange(1, 60)
self.mdz_query_interval.setSuffix("")
mdz_layout.addRow("查询间隔:", self.mdz_query_interval)
# 线径串口设置
cz_group = QGroupBox("线径检测串口")
cz_layout = QFormLayout(cz_group)
# 串口选择
cz_port_layout = QHBoxLayout()
self.cz_port_combo = QComboBox()
self.cz_refresh_btn = QPushButton("刷新")
cz_port_layout.addWidget(self.cz_port_combo)
cz_port_layout.addWidget(self.cz_refresh_btn)
cz_layout.addRow("串口:", cz_port_layout)
# 波特率
self.cz_baud_combo = QComboBox()
for baud in ["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"]:
self.cz_baud_combo.addItem(baud)
cz_layout.addRow("波特率:", self.cz_baud_combo)
# 数据位
self.cz_data_bits_combo = QComboBox()
for bits in ["5", "6", "7", "8"]:
self.cz_data_bits_combo.addItem(bits)
cz_layout.addRow("数据位:", self.cz_data_bits_combo)
# 停止位
self.cz_stop_bits_combo = QComboBox()
for bits in ["1", "1.5", "2"]:
self.cz_stop_bits_combo.addItem(bits)
cz_layout.addRow("停止位:", self.cz_stop_bits_combo)
# 校验位
self.cz_parity_combo = QComboBox()
for parity in [("无校验", "N"), ("奇校验", "O"), ("偶校验", "E")]:
self.cz_parity_combo.addItem(parity[0], parity[1])
cz_layout.addRow("校验位:", self.cz_parity_combo)
# 稳定阈值
self.cz_stable_threshold = QSpinBox()
self.cz_stable_threshold.setRange(1, 30)
self.cz_stable_threshold.setSuffix("")
cz_layout.addRow("稳定阈值:", self.cz_stable_threshold)
# 将两个组添加到布局
serial_layout.addWidget(mdz_group, 0, 0)
serial_layout.addWidget(cz_group, 0, 1)
# 设置列伸缩因子使两列等宽比例1:1
serial_layout.setColumnStretch(0, 1)
serial_layout.setColumnStretch(1, 1)
main_layout.addWidget(serial_group)
# 测试按钮
test_layout = QHBoxLayout()
self.test_mdz_btn = QPushButton("测试米电阻串口")
self.test_cz_btn = QPushButton("测试线径串口")
test_layout.addWidget(self.test_mdz_btn)
test_layout.addWidget(self.test_cz_btn)
test_layout.addStretch()
main_layout.addLayout(test_layout)
# 保存按钮
button_layout = QHBoxLayout()
self.save_btn = QPushButton("保存设置")
self.save_btn.setStyleSheet("background-color: #e3f2fd; border: 1px solid #2196f3; padding: 8px 16px; font-weight: bold; border-radius: 4px;")
button_layout.addStretch()
button_layout.addWidget(self.save_btn)
main_layout.addLayout(button_layout)
main_layout.addStretch()

37
ui/settings_window_ui.py Normal file
View File

@ -0,0 +1,37 @@
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QPushButton, QLabel
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QIcon
class SettingsWindowUI(QMainWindow):
"""设置窗口UI"""
# 定义信号
settings_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("系统设置")
self.setMinimumSize(800, 600)
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout(central_widget)
# 创建标签
title_label = QLabel("系统设置")
title_label.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;")
main_layout.addWidget(title_label)
# 创建选项卡
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget)

View File

@ -128,3 +128,39 @@ class ConfigLoader:
# 保存配置
return self.save_config()
def get_config(self, key):
"""
获取serial配置下的指定配置
Args:
key: 配置键例如'mdz', 'cz'
Returns:
dict: 配置值未找到则返回None
"""
if 'serial' not in self.config:
self.config['serial'] = {}
if key not in self.config['serial']:
return None
return self.config['serial'][key]
def set_config(self, key, config_data):
"""
设置serial配置下的指定配置
Args:
key: 配置键例如'mdz', 'cz'
config_data: 要设置的配置数据
Returns:
bool: 是否设置成功
"""
if 'serial' not in self.config:
self.config['serial'] = {}
self.config['serial'][key] = config_data
# 这里不保存配置等待调用save_config方法时一并保存
return True

View File

@ -29,6 +29,42 @@ class InspectionConfigManager:
logging.error(f"加载检验配置失败: {str(e)}")
return False
def save_configs(self, configs, username='system'):
"""保存检验配置列表
Args:
configs: 检验配置列表
username: 操作用户
Returns:
bool: 保存是否成功
"""
try:
# 获取当前所有配置
current_configs = self.get_configs(include_disabled=True)
# 创建位置到配置ID的映射
position_to_id = {}
for config in current_configs:
position = config.get('position')
if position:
position_to_id[position] = config.get('id')
# 更新每个配置
for config in configs:
position = config.get('position')
if position in position_to_id:
# 更新已有配置
config_id = position_to_id[position]
self.update_config(config_id, config, username)
# 重新加载配置
self.reload_configs()
return True
except Exception as e:
logging.error(f"保存检验配置失败: {str(e)}")
return False
def get_configs(self, include_disabled=False):
"""获取检验配置列表

345
utils/keyboard_listener.py Normal file
View File

@ -0,0 +1,345 @@
from pynput.keyboard import Key, Listener, GlobalHotKeys
import logging
import threading
import platform
import os
class KeyboardListener:
"""键盘监听器,用于监听特定按键并触发相应操作"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super(KeyboardListener, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self.listener = None
self.hotkey_listener = None
self.is_running = False
self.callbacks = {}
# 设置日志级别为DEBUG确保能看到所有日志
# 注意如果主程序或其他模块也配置了logging这里的basicConfig可能不会覆盖已有的配置
# 建议在主程序入口处统一配置logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s] - %(message)s',
handlers=[
logging.StreamHandler(), # 确保日志输出到控制台
]
)
# 检测操作系统
self.os_type = platform.system()
logging.info(f"当前操作系统: {self.os_type}")
def start(self, join_thread=False):
"""启动键盘监听
Args:
join_thread: 是否阻塞等待键盘监听线程结束
"""
logging.info("-----> KeyboardListener.start() called <-----") # 显式入口日志
print("-----> KeyboardListener.start() called <-----") # 确保控制台可见
if self.is_running:
logging.info("键盘监听器已经在运行中")
return True
self.is_running = True # 先标记为 True
try:
logging.info(f"OS type: {self.os_type}. Preparing to start listener.")
listener_started_successfully = False
try:
if self.os_type == "Windows":
logging.info("Attempting to use GlobalHotKeys for Windows.")
# _start_hotkey_listener 内部会处理自己的成功/失败和日志
self._start_hotkey_listener()
# 假设 GlobalHotKeys 启动后 is_active 会更新
listener_started_successfully = self.is_active()
else:
logging.info(f"Using normal Listener for {self.os_type}.")
listener_started_successfully = self._start_normal_listener()
except Exception as e:
logging.error(f"启动特定监听器失败: {e}", exc_info=True)
# 失败时重置标志
listener_started_successfully = False
if not listener_started_successfully:
# 如果到这里还没有成功启动任何监听器 (例如 _start_hotkey_listener 内部回退到 _start_normal_listener 也失败了)
# 或者 _start_normal_listener 直接失败
logging.error("Failed to start any keyboard listener (normal or hotkey).")
self.is_running = False # 如果没有成功启动,重置状态
# 不要引发异常,让应用程序继续运行
logging.warning("将继续运行应用程序,但键盘监听功能将不可用")
return False
logging.info(f"Callbacks registered: {list(self.callbacks.keys())}")
# 提示用户如何触发
if 'Key.page_up' in self.callbacks or '<page_up>' in self.callbacks:
logging.info("按下 PageUp 键可触发相应操作")
# 如果需要阻塞等待线程结束
if join_thread:
self.join()
logging.info("-----> KeyboardListener.start() finished successfully <-----")
return True
except Exception as e:
self.is_running = False # 发生异常时重置状态
logging.error(f"CRITICAL: Exception during KeyboardListener.start() [outer try-except]: {e}", exc_info=True)
# 不要引发异常,让应用程序继续运行
logging.warning("将继续运行应用程序,但键盘监听功能将不可用")
return False
def _start_normal_listener(self):
"""启动普通的按键监听"""
logging.info("-----> _start_normal_listener called <-----")
try:
logging.info("Attempting to initialize pynput.keyboard.Listener.")
self.listener = Listener(on_press=self._on_press)
logging.info("pynput.keyboard.Listener instance created.")
self.listener.daemon = False # 修改为非守护线程,防止主线程退出时被强制终止
logging.info("Normal listener daemon set to False.")
logging.info("Attempting to start normal listener thread...")
try:
self.listener.start()
logging.info("Normal listener thread started (or start() method returned).")
except Exception as e:
logging.error(f"启动监听器线程失败: {e}", exc_info=True)
return False
# 使用额外的异常处理来检查是否存活
try:
is_alive = self.listener.is_alive()
logging.info(f"Normal listener thread is_alive() is {is_alive}.")
if is_alive:
logging.info("-----> _start_normal_listener finished successfully <-----")
return True # 表示成功
else:
logging.warning("Normal listener thread is_alive() is False immediately after start. This might indicate an issue.")
logging.info("-----> _start_normal_listener finished with is_alive()=False <-----")
return False # 表示失败
except Exception as e:
logging.error(f"检查监听器线程状态失败: {e}", exc_info=True)
# 假设成功启动,让程序继续运行
return True
except Exception as e:
logging.error(f"CRITICAL ERROR during _start_normal_listener: {e}", exc_info=True)
logging.info("-----> _start_normal_listener finished with exception <-----")
return False # 表示失败
def _start_hotkey_listener(self):
"""启动全局热键监听适用于Windows"""
logging.info("-----> _start_hotkey_listener called <-----") # 显式入口日志
try:
logging.info("启动全局监听.")
hotkey_mapping = {}
page_up_callback = self.callbacks.get('Key.page_up') or self.callbacks.get('<page_up>')
if page_up_callback:
hotkey_mapping['<page_up>'] = page_up_callback
else:
logging.warning("未找到PageUp回调 (Key.page_up 或 <page_up>). 全局热键将不会设置.")
if not hotkey_mapping:
self._start_normal_listener()
return
self.hotkey_listener = GlobalHotKeys(hotkey_mapping)
self.hotkey_listener.daemon = False # 修改为非守护线程,防止主线程退出时被强制终止
logging.info("全局热键监听线程 daemon 设置为 False.")
self.hotkey_listener.start()
# 检查线程是否真的在运行 (这是一个启发式检查is_alive() 可能在线程真正工作前就返回True)
if self.hotkey_listener.is_alive():
logging.info("全局热键监听线程 is_alive() 为 True.")
else:
logging.warning("全局热键监听线程 is_alive() 为 False 立即启动后. 这可能表明存在问题.")
logging.info("全局热键监听 (GlobalHotKeys) 已成功启动 (根据pynput文档, start() 是非阻塞的)")
except Exception as e:
logging.error(f"CRITICAL ERROR during _start_hotkey_listener: {e}", exc_info=True)
# 如果GlobalHotKeys失败回退到普通监听器
self._start_normal_listener()
logging.info("-----> _start_hotkey_listener finished <-----")
def stop(self):
"""停止键盘监听"""
logging.info("Attempting to stop keyboard listeners.")
if not self.is_running:
logging.info("键盘监听器已经停止或未运行")
return
self.is_running = False
if self.listener:
try:
self.listener.stop()
logging.info("常规键盘监听已停止")
except Exception as e:
logging.error(f"停止常规键盘监听失败: {e}", exc_info=True)
finally:
self.listener = None
if self.hotkey_listener:
try:
self.hotkey_listener.stop()
logging.info("全局热键监听已停止")
except Exception as e:
logging.error(f"停止全局热键监听失败: {e}", exc_info=True)
finally:
self.hotkey_listener = None
logging.info("All keyboard listeners stop process completed.")
def register_callback(self, key_name, callback):
"""
注册按键回调函数
Args:
key_name: 按键名称 'Key.page_up' '<f1>' (用于GlobalHotKeys)
callback: 回调函数无参数
"""
self.callbacks[key_name] = callback
logging.info(f"已注册按键回调: {key_name} -> {callback.__name__ if hasattr(callback, '__name__') else callback}")
if key_name == 'Key.page_up':
self.callbacks['<page_up>'] = callback
logging.info(f"已为 PageUp 键额外注册 '<page_up>' (用于全局热键)")
def _on_press(self, key):
"""按键按下事件处理 (主要用于普通Listener)"""
try:
key_str = self._key_to_string(key)
# 增加日志级别,确保按键事件始终被记录
logging.info(f"[Normal Listener] 检测到按键: {key_str} (原始: {key})")
# 直接打印到控制台,确保可见
print(f"\n[键盘事件] 检测到按键: {key_str}\n")
if key_str in self.callbacks:
logging.info(f"[Normal Listener] 触发按键回调 for key: {key_str}")
print(f"\n[键盘事件] 触发回调: {key_str}\n")
try:
self.callbacks[key_str]()
logging.info(f"[Normal Listener] 成功执行回调函数 for key: {key_str}")
return
except Exception as e:
logging.error(f"[Normal Listener] 执行回调时出错: {e}", exc_info=True)
print(f"\n[键盘事件] 回调执行错误: {e}\n")
elif hasattr(key, 'name') and key.name and ('page_up' in key.name.lower() or 'pageup' in key.name.lower()):
logging.info(f"[Normal Listener] 检测到 PageUp 相关的键: {key.name}")
print(f"\n[键盘事件] 检测到PageUp键: {key.name}\n")
page_up_callback = self.callbacks.get('Key.page_up') or self.callbacks.get('<page_up>')
if page_up_callback:
logging.info("[Normal Listener] 使用 'Key.page_up''<page_up>' 注册的回调触发 PageUp")
print("\n[键盘事件] 触发PageUp回调\n")
try:
page_up_callback()
logging.info("[Normal Listener] 成功执行 PageUp 回调函数")
return
except Exception as e:
logging.error(f"[Normal Listener] 执行 PageUp 回调时出错: {e}", exc_info=True)
print(f"\n[键盘事件] PageUp回调执行错误: {e}\n")
else:
logging.warning("[Normal Listener] 检测到 PageUp 键,但未找到对应的回调函数")
print("\n[键盘事件] 检测到PageUp键但未找到回调\n")
else:
logging.info(f"[Normal Listener] 按键 {key_str} 未注册精确回调")
# 添加当前注册的回调列表,便于调试
logging.info(f"[Normal Listener] 当前注册的回调: {list(self.callbacks.keys())}")
print(f"\n[键盘事件] 当前注册的回调: {list(self.callbacks.keys())}\n")
except Exception as e:
logging.error(f"[Normal Listener] 处理按键事件时出错: {e}", exc_info=True)
print(f"\n[键盘事件] 处理按键事件出错: {e}\n")
return
def _key_to_string(self, key):
"""将按键对象转换为字符串格式"""
try:
logging.debug(f"原始按键: {key}, 类型: {type(key)}")
# 特殊处理PageUp键
if str(key).lower() == 'key.page_up' or (hasattr(key, 'name') and key.name and 'page_up' in key.name.lower()):
print("\n[键盘事件] 检测到PageUp键的特殊处理\n")
return 'Key.page_up'
# 常规处理
if hasattr(key, 'name') and key.name:
key_name = f'Key.{key.name}'
logging.debug(f"特殊键,转换为: {key_name}")
return key_name
elif hasattr(key, 'char') and key.char:
logging.debug(f"字符键,转换为: {key.char}")
return key.char
else:
key_str = str(key)
logging.debug(f"其他类型键,转换为: {key_str}")
return key_str
except Exception as e:
logging.error(f"转换按键为字符串时出错: {e}", exc_info=True)
print(f"\n[键盘事件] 按键转换错误: {e}\n")
return str(key)
def is_active(self):
"""检查监听器是否处于活动状态"""
normal_listener_active = self.listener and self.listener.is_alive()
hotkey_listener_active = self.hotkey_listener and self.hotkey_listener.is_alive()
active = self.is_running and (normal_listener_active or hotkey_listener_active)
logging.debug(f"键盘监听器状态: is_running={self.is_running}, normal_active={normal_listener_active}, hotkey_active={hotkey_listener_active}, combined_active={active}")
return active
def trigger_test_event(self):
"""测试方法手动触发PageUp回调"""
page_up_callback = self.callbacks.get('Key.page_up') or self.callbacks.get('<page_up>')
if page_up_callback:
logging.info("手动触发 PageUp 回调进行测试")
print("[测试] 手动触发PageUp回调")
try:
page_up_callback()
return True
except Exception as e:
logging.error(f"测试触发回调时出错: {e}", exc_info=True)
else:
logging.error("未找到用于测试的 PageUp 回调 (未注册 Key.page_up 或 <page_up>)")
print("[测试] 未找到PageUp回调")
return False
def join(self, timeout=None):
"""等待键盘监听线程结束
Args:
timeout: 超时时间如果为None则一直等待
"""
logging.info("等待键盘监听线程结束...")
try:
if self.listener and self.listener.is_alive():
self.listener.join(timeout)
logging.info("常规键盘监听线程已结束或超时")
if self.hotkey_listener and self.hotkey_listener.is_alive():
self.hotkey_listener.join(timeout)
logging.info("热键监听线程已结束或超时")
except Exception as e:
logging.error(f"等待键盘监听线程结束时出错: {e}", exc_info=True)
logging.info("键盘监听线程join操作完成")

View File

@ -89,6 +89,23 @@ class PalletTypeManager:
self.reload_pallet_types()
return result
def add_pallet_type(self, data, username='system'):
"""添加托盘类型create_pallet_type的别名
Args:
data: 托盘类型数据
username: 操作用户
Returns:
bool: 添加是否成功
"""
try:
result = self.create_pallet_type(data, username)
return result is not None
except Exception as e:
logging.error(f"添加托盘类型失败: {str(e)}")
return False
def update_pallet_type(self, pallet_type_id, data, username='system'):
"""更新托盘类型
@ -135,6 +152,37 @@ class PalletTypeManager:
if result:
self.reload_pallet_types()
return result
def update_pallet_type_status(self, pallet_type_id, enabled, username='system'):
"""更新托盘类型状态toggle_pallet_type的别名
Args:
pallet_type_id: 托盘类型ID
enabled: 是否启用
username: 操作用户
Returns:
bool: 操作是否成功
"""
return self.toggle_pallet_type(pallet_type_id, enabled, username)
def save_all_pallet_types(self, username='system'):
"""保存所有托盘类型(实际上是一个空操作,因为每次修改都会立即保存)
Args:
username: 操作用户
Returns:
bool: 操作是否成功
"""
try:
# 实际上每次修改都会立即保存,这里只是为了提供一个统一的接口
logging.info("保存所有托盘类型(空操作)")
return True
except Exception as e:
logging.error(f"保存所有托盘类型失败: {str(e)}")
return False
def get_pallet_type_by_type(self, pallet_type):
"""根据托盘类型值获取托盘数据
@ -150,6 +198,7 @@ class PalletTypeManager:
return result
else:
return None
def get_pallet_type_by_pallet_id(self, pallet_id):
"""根据托盘号获取托盘类型

1140
utils/serial_manager.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,11 +8,11 @@ from camera.CameraParams_const import *
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "camera"))
try:
from PySide6.QtWidgets import QWidget, QMessageBox
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import Qt, Signal
USE_PYSIDE6 = True
except ImportError:
from PyQt5.QtWidgets import QWidget, QMessageBox
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import Qt
from PyQt5.QtCore import pyqtSignal as Signal
USE_PYSIDE6 = False
@ -35,6 +35,7 @@ class CameraSettingsWidget(SettingsUI):
signal_camera_connection = Signal(bool, str) # 相机连接状态信号 (是否连接, 错误消息)
signal_camera_params_changed = Signal(float, float, float) # 相机参数变化信号 (曝光, 增益, 帧率)
signal_camera_error = Signal(str) # 相机错误信号
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
def __init__(self, parent=None):
super().__init__(parent)
@ -305,14 +306,26 @@ class CameraSettingsWidget(SettingsUI):
QMessageBox.warning(self, "错误", "相机参数设置失败!")
def save_camera_params(self):
"""保存相机参数"""
"""保存相机参数到配置文件"""
if not self.camera_manager.isOpen:
QMessageBox.warning(self, "错误", "请先打开相机!")
return
# 实现保存参数到配置文件的功能
# TODO: 待实现
QMessageBox.information(self, "提示", "参数保存功能尚未实现")
# 获取当前参数
exposure = self.exposure_slider.value()
gain = self.gain_slider.value()
frame_rate = self.framerate_slider.value()
# 保存到配置文件
success = self.camera_manager.save_params_to_config(exposure, gain, frame_rate)
if success:
QMessageBox.information(self, "成功", "相机参数已保存到配置文件")
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"相机参数已保存: 曝光={exposure}μs, 增益={gain}dB, 帧率={frame_rate}fps")
else:
QMessageBox.critical(self, "错误", "保存相机参数失败")
logging.error("保存相机参数失败")
def closeEvent(self, event):
"""窗口关闭事件"""

View File

@ -13,6 +13,7 @@ class InspectionSettingsWidget(InspectionSettingsUI):
# 定义信号
signal_configs_changed = Signal() # 配置变更信号
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
def __init__(self, parent=None):
super().__init__(parent)
@ -343,42 +344,25 @@ class InspectionSettingsWidget(InspectionSettingsUI):
if not self.validate_form():
return
# 设置表单禁用,避免保存过程中的错误操作
self.set_form_enabled(False)
# 获取表单数据
configs = []
for position in range(1, 7):
group = self.config_groups[position - 1]
if group.isChecked():
config = self.get_form_data(position)
configs.append(config)
# 收集更新数据
for i in range(6):
position = i + 1
config_id = self.config_ids[i]
# 保存配置
success = self.inspection_manager.save_configs(configs)
# 获取表单数据
data = self.get_form_data(position)
# 检查配置ID是否存在
if config_id is not None:
# 更新已有配置
result = self.inspection_manager.update_config(config_id, data)
if not result:
raise Exception(f"更新检验项目 {position} 失败")
else:
# 新建配置不应该进入这个分支因为默认已经创建了6个配置
logging.warning(f"检验项目 {position} 不存在,需要在初始化时创建")
# 重新加载配置
self.inspection_manager.reload_configs()
# 恢复表单可用
self.set_form_enabled(True)
# 显示成功消息
QMessageBox.information(self, "保存成功", "检验配置已保存成功!")
# 发送配置变更信号
self.signal_configs_changed.emit()
logging.info("已保存检验配置")
if success:
QMessageBox.information(self, "成功", "检验配置已保存")
self.signal_configs_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info("检验配置已保存")
else:
QMessageBox.critical(self, "错误", "保存检验配置失败")
logging.error("保存检验配置失败")
except Exception as e:
logging.error(f"保存检验配置失败: {str(e)}")
QMessageBox.critical(self, "错误", f"保存检验配置失败: {str(e)}")
# 恢复表单可用
self.set_form_enabled(True)

View File

@ -38,7 +38,8 @@ from widgets.camera_settings_widget import CameraSettingsWidget
from utils.inspection_config_manager import InspectionConfigManager
# 导入托盘类型管理器
from utils.pallet_type_manager import PalletTypeManager
# 导入串口管理
from utils.serial_manager import SerialManager
class MainWindow(MainWindowUI):
"""主窗口"""
@ -159,6 +160,9 @@ class MainWindow(MainWindowUI):
self.statusBar().addPermanentWidget(QLabel(" "))
logging.info(f"主窗口已创建,用户: {user_name}")
# 初始化串口管理器
self.serial_manager = SerialManager()
def add_pallet_type_selectors(self):
"""添加托盘类型选择下拉框"""
# 创建上料托盘类型选择下拉框
@ -288,15 +292,29 @@ class MainWindow(MainWindowUI):
def show_settings_page(self):
"""显示设置页面"""
# 延迟创建设置组件
if not hasattr(self, 'settings_widget'):
from widgets.settings_widget import SettingsWidget
self.settings_widget = SettingsWidget(self)
self.stacked_widget.addWidget(self.settings_widget)
# 创建设置窗口
if not hasattr(self, 'settings_window'):
from widgets.settings_window import SettingsWindow
self.settings_window = SettingsWindow(self)
# 连接设置改变信号
self.settings_window.settings_changed.connect(self.on_settings_changed)
# 切换到设置页面
self.stacked_widget.setCurrentWidget(self.settings_widget)
logging.info("显示设置页面")
# 显示设置窗口
self.settings_window.show()
logging.info("显示设置窗口")
def on_settings_changed(self):
"""设置改变时触发"""
# 重新加载配置
from utils.config_loader import ConfigLoader
config_loader = ConfigLoader.get_instance()
config_loader.load_config()
self.config = self.load_config() # 重新加载配置到 self.config
# 更新串口管理器配置
self.serial_manager.reload_config()
logging.info("设置已更新,重新加载配置")
def handle_input(self):
"""处理上料按钮点击事件"""
@ -498,6 +516,9 @@ class MainWindow(MainWindowUI):
# 启动Modbus监控
self.setup_modbus_monitor()
# 启动串口监听
self.serial_manager.auto_open_configured_ports()
success0 = modbus.write_register_until_success(client, 0, int(stow_num))
success1 = modbus.write_register_until_success(client, 1, int(pallet_type))
success2 = modbus.write_register_until_success(client, 2, 1)
@ -515,6 +536,7 @@ class MainWindow(MainWindowUI):
finally:
modbus.close_client(client)
def handle_stop(self):
"""处理停止按钮点击事件,并关闭 modbus 监控"""
modbus = ModbusUtils()
@ -534,6 +556,9 @@ class MainWindow(MainWindowUI):
if hasattr(self, 'modbus_monitor'):
logging.info("停止Modbus监控")
self.modbus_monitor.stop()
# 停止串口监听
self.serial_manager.stop_keyboard_listener()
self.serial_manager.close_all_ports()
def handle_camera_status(self, is_connected, message):
"""处理相机状态变化"""
@ -579,9 +604,14 @@ class MainWindow(MainWindowUI):
# 停止相机显示
self.camera_display.stop_display()
# 停止串口监听
self.serial_manager.stop_keyboard_listener()
self.serial_manager.close_all_ports()
# 接受关闭事件
event.accept()
def handle_order_enter(self):
"""处理工程号输入框按下回车事件"""
logging.info("工程号输入框按下回车事件")
@ -1729,8 +1759,14 @@ class MainWindow(MainWindowUI):
"""处理NG信号, 删除当前在处理的数据(也就是第一条数据)"""
if ng == 1:
# 获取当前选中的行或第一个数据行,并删除
order_id = self.process_table.item(2, 1).text().strip()
tray_id = self.tray_edit.currentText()
try:
order_id = self.process_table.item(2, 1).text().strip()
tray_id = self.tray_edit.currentText()
except Exception as e:
logging.error(f"处理NG信号时发生错误: {str(e)}")
order_id = ""
tray_id = ""
self.inspection_manager.delete_inspection_data(order_id, tray_id)
# 触发重新查询,更新数据
self.load_finished_inspection_data()

View File

@ -9,6 +9,7 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
# 定义信号
signal_pallet_types_changed = Signal()
settings_changed = Signal() # 设置变更信号,与 SettingsWindow 兼容
def __init__(self, parent=None):
super().__init__(parent)
@ -29,22 +30,6 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
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"))
@ -67,9 +52,6 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
# 重新加载数据
self.pallet_type_manager.reload_pallet_types()
# 加载上料类型
self.load_operation_pallet_types("input")
# 加载下料类型
self.load_operation_pallet_types("output")
@ -82,7 +64,7 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""加载指定操作类型的托盘类型数据
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
"""
# 获取表格
table = self.get_table_by_operation_type(operation_type)
@ -126,14 +108,12 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""根据操作类型获取表格
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
Returns:
QTableWidget: 表格部件
"""
if operation_type == "input":
return self.input_widget.findChild(QTableWidget, "input_table")
elif operation_type == "output":
if operation_type == "output":
return self.output_widget.findChild(QTableWidget, "output_table")
return None
@ -141,12 +121,12 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""获取表单值
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
Returns:
dict: 表单值
"""
widget = self.input_widget if operation_type == "input" else self.output_widget
widget = 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")
@ -165,10 +145,10 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""设置表单值
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
values: 表单值
"""
widget = self.input_widget if operation_type == "input" else self.output_widget
widget = 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")
@ -184,9 +164,9 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""重置表单
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
"""
widget = self.input_widget if operation_type == "input" else self.output_widget
widget = self.output_widget
# 重置表单值
self.set_form_values(operation_type, {
@ -199,35 +179,31 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
# 重置当前编辑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)
operation_type: 操作类型 (output)
"""
# 获取表格
table = self.get_table_by_operation_type(operation_type)
if not table:
return
# 获取选中行
selected_rows = table.selectionModel().selectedRows()
if not selected_rows:
selected_items = table.selectedItems()
if not selected_items:
return
# 获取选中行数据
row = selected_rows[0].row()
pallet_type_id = table.item(row, 0).data(Qt.UserRole)
# 获取行数据
row = selected_items[0].row()
# 获取ID
id_item = table.item(row, 0)
if not id_item:
return
pallet_type_id = id_item.data(Qt.UserRole)
# 获取托盘类型数据
pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id)
@ -238,33 +214,22 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
self.set_form_values(operation_type, pallet_type)
# 设置当前编辑ID
widget = self.input_widget if operation_type == "input" else self.output_widget
widget = 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)
operation_type: 操作类型 (output)
Returns:
bool: 表单是否有效
bool: 验证是否通过
"""
values = self.get_form_values(operation_type)
if not values['type_name']:
QMessageBox.warning(self, "警告", "托盘类型名称不能为空")
QMessageBox.warning(self, "警告", "请输入类型名称")
return False
return True
@ -273,148 +238,172 @@ class PalletTypeSettingsWidget(PalletTypeSettingsUI):
"""添加托盘类型
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
"""
# 验证表单
if not self.validate_form(operation_type):
return
try:
# 获取表单值
values = self.get_form_values(operation_type)
# 获取表单值
values = self.get_form_values(operation_type)
# 创建托盘类型
result = self.pallet_type_manager.create_pallet_type(values)
if result:
logging.info(f"添加托盘类型成功: {values['type_name']}")
# 添加托盘类型
success = self.pallet_type_manager.add_pallet_type(values)
# 重新加载数据
self.load_operation_pallet_types(operation_type)
if success:
# 重新加载数据
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)}")
# 重置表单
self.reset_form(operation_type)
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"已添加{operation_type}托盘类型: {values['type_name']}")
else:
QMessageBox.critical(self, "错误", f"添加{operation_type}托盘类型失败")
logging.error(f"添加{operation_type}托盘类型失败: {values['type_name']}")
def update_pallet_type(self, operation_type):
"""更新托盘类型
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (output)
"""
# 获取当前编辑ID
widget = self.output_widget
pallet_type_id = widget.property("current_edit_id")
if pallet_type_id < 0:
QMessageBox.warning(self, "警告", "请先选择要编辑的托盘类型")
return
# 验证表单
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)
# 获取表单值
values = self.get_form_values(operation_type)
# 更新托盘类型
success = self.pallet_type_manager.update_pallet_type(pallet_type_id, values)
# 更新托盘类型
result = self.pallet_type_manager.update_pallet_type(pallet_type_id, values)
if result:
logging.info(f"更新托盘类型成功: {values['type_name']}")
if success:
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重置表单
self.reset_form(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)}")
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"已更新{operation_type}托盘类型: {values['type_name']}")
else:
QMessageBox.critical(self, "错误", f"更新{operation_type}托盘类型失败")
logging.error(f"更新{operation_type}托盘类型失败: {values['type_name']}")
def delete_pallet_type(self, operation_type):
"""删除托盘类型
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (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
# 获取选中行
table = self.get_table_by_operation_type(operation_type)
if not table:
return
# 确认删除
pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id)
if not pallet_type:
QMessageBox.warning(self, "警告", "找不到要删除的托盘类型")
return
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "警告", "请先选择要删除的托盘类型")
return
reply = QMessageBox.question(self, "确认删除",
f"确定要删除托盘类型 '{pallet_type['type_name']}' 吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply != QMessageBox.Yes:
return
# 获取托盘类型ID
row = selected_items[0].row()
id_item = table.item(row, 0)
if not id_item:
return
# 删除托盘类型
result = self.pallet_type_manager.delete_pallet_type(pallet_type_id)
if result:
logging.info(f"删除托盘类型成功: {pallet_type['type_name']}")
pallet_type_id = id_item.data(Qt.UserRole)
type_name = id_item.text()
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 确认删除
reply = QMessageBox.question(self, "确认删除", f"确定要删除{operation_type}托盘类型 [{type_name}] 吗?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# 发送信号
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)}")
if reply != QMessageBox.Yes:
return
# 删除托盘类型
success = self.pallet_type_manager.delete_pallet_type(pallet_type_id)
if success:
# 重新加载数据
self.load_operation_pallet_types(operation_type)
# 重置表单
self.reset_form(operation_type)
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"已删除{operation_type}托盘类型: {type_name}")
else:
QMessageBox.critical(self, "错误", f"删除{operation_type}托盘类型失败")
logging.error(f"删除{operation_type}托盘类型失败: {type_name}")
def cancel_edit(self, operation_type):
"""取消编辑
Args:
operation_type: 操作类型 (input/output)
operation_type: 操作类型 (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}")
# 更新托盘类型启用状态
success = self.pallet_type_manager.update_pallet_type_status(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)}")
if success:
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info(f"{('启用' if enabled else '禁用')}托盘类型: {pallet_type_id}")
else:
QMessageBox.critical(self, "错误", f"更新托盘类型状态失败")
logging.error(f"更新托盘类型状态失败: {pallet_type_id}")
def save_all_pallet_types(self):
"""保存所有托盘类型"""
# 目前没有需要批量保存的操作,所有操作都是实时保存的
QMessageBox.information(self, "提示", "托盘类型配置已保存")
# 保存所有托盘类型
success = self.pallet_type_manager.save_all_pallet_types()
# 发送信号
self.signal_pallet_types_changed.emit()
if success:
QMessageBox.information(self, "成功", "所有托盘类型已保存")
# 发送信号
self.signal_pallet_types_changed.emit()
self.settings_changed.emit() # 发送设置变更信号
logging.info("已保存所有托盘类型")
else:
QMessageBox.critical(self, "错误", "保存托盘类型失败")
logging.error("保存托盘类型失败")

View File

@ -0,0 +1,324 @@
import os
import json
import logging
import serial.tools.list_ports
import time
from PySide6.QtWidgets import QMessageBox
from ui.serial_settings_ui import SerialSettingsUI
from utils.config_loader import ConfigLoader
from utils.serial_manager import SerialManager
class SerialSettingsWidget(SerialSettingsUI):
"""串口设置组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.config = ConfigLoader.get_instance()
self.serial_manager = SerialManager()
# 连接信号
self.mdz_refresh_btn.clicked.connect(self.refresh_ports)
self.cz_refresh_btn.clicked.connect(self.refresh_ports)
self.test_mdz_btn.clicked.connect(self.test_mdz_port)
self.test_cz_btn.clicked.connect(self.test_cz_port)
self.save_btn.clicked.connect(self.save_settings)
# 初始化串口列表
self.refresh_ports()
# 加载设置
self.load_settings()
def refresh_ports(self):
"""刷新串口列表"""
try:
# 保存当前选择
current_mdz_port = self.mdz_port_combo.currentData()
current_cz_port = self.cz_port_combo.currentData()
# 清空列表
self.mdz_port_combo.clear()
self.cz_port_combo.clear()
# 获取可用串口
ports = list(serial.tools.list_ports.comports())
for port in ports:
port_name = port.device
port_desc = f"{port_name} ({port.description})"
self.mdz_port_combo.addItem(port_desc, port_name)
self.cz_port_combo.addItem(port_desc, port_name)
# 恢复之前的选择
if current_mdz_port:
index = self.mdz_port_combo.findData(current_mdz_port)
if index >= 0:
self.mdz_port_combo.setCurrentIndex(index)
if current_cz_port:
index = self.cz_port_combo.findData(current_cz_port)
if index >= 0:
self.cz_port_combo.setCurrentIndex(index)
logging.info(f"已刷新串口列表,找到 {len(ports)} 个串口")
except Exception as e:
logging.error(f"刷新串口列表失败: {e}")
QMessageBox.warning(self, "刷新失败", f"刷新串口列表失败: {e}")
def load_settings(self):
"""加载设置"""
try:
# 加载全局设置
enable_serial = self.config.get_value('app.features.enable_serial_ports', False)
enable_keyboard = self.config.get_value('app.features.enable_keyboard_listener', False)
self.enable_serial_checkbox.setChecked(enable_serial)
self.enable_keyboard_checkbox.setChecked(enable_keyboard)
# 加载米电阻设置
mdz_config = self.config.get_config('mdz')
if mdz_config:
# 设置串口
mdz_port = mdz_config.get('ser', '')
index = self.mdz_port_combo.findData(mdz_port)
if index >= 0:
self.mdz_port_combo.setCurrentIndex(index)
# 设置波特率
mdz_baud = str(mdz_config.get('port', '9600'))
index = self.mdz_baud_combo.findText(mdz_baud)
if index >= 0:
self.mdz_baud_combo.setCurrentIndex(index)
# 设置数据位
mdz_data_bits = str(mdz_config.get('data_bits', '8'))
index = self.mdz_data_bits_combo.findText(mdz_data_bits)
if index >= 0:
self.mdz_data_bits_combo.setCurrentIndex(index)
# 设置停止位
mdz_stop_bits = str(mdz_config.get('stop_bits', '1'))
index = self.mdz_stop_bits_combo.findText(mdz_stop_bits)
if index >= 0:
self.mdz_stop_bits_combo.setCurrentIndex(index)
# 设置校验位
mdz_parity = mdz_config.get('parity', 'N')
index = self.mdz_parity_combo.findData(mdz_parity)
if index >= 0:
self.mdz_parity_combo.setCurrentIndex(index)
# 设置查询指令
mdz_query_cmd = mdz_config.get('query_cmd', '01 03 00 01 00 07 55 C8')
self.mdz_query_cmd.setText(mdz_query_cmd)
# 设置查询间隔
mdz_query_interval = mdz_config.get('query_interval', 5)
self.mdz_query_interval.setValue(mdz_query_interval)
# 加载称重设置
cz_config = self.config.get_config('cz')
if cz_config:
# 设置串口
cz_port = cz_config.get('ser', '')
index = self.cz_port_combo.findData(cz_port)
if index >= 0:
self.cz_port_combo.setCurrentIndex(index)
# 设置波特率
cz_baud = str(cz_config.get('port', '9600'))
index = self.cz_baud_combo.findText(cz_baud)
if index >= 0:
self.cz_baud_combo.setCurrentIndex(index)
# 设置数据位
cz_data_bits = str(cz_config.get('data_bits', '8'))
index = self.cz_data_bits_combo.findText(cz_data_bits)
if index >= 0:
self.cz_data_bits_combo.setCurrentIndex(index)
# 设置停止位
cz_stop_bits = str(cz_config.get('stop_bits', '1'))
index = self.cz_stop_bits_combo.findText(cz_stop_bits)
if index >= 0:
self.cz_stop_bits_combo.setCurrentIndex(index)
# 设置校验位
cz_parity = cz_config.get('parity', 'N')
index = self.cz_parity_combo.findData(cz_parity)
if index >= 0:
self.cz_parity_combo.setCurrentIndex(index)
# 设置稳定阈值
cz_stable_threshold = cz_config.get('stable_threshold', 10)
self.cz_stable_threshold.setValue(cz_stable_threshold)
logging.info("已加载串口设置")
except Exception as e:
logging.error(f"加载串口设置失败: {e}")
QMessageBox.warning(self, "加载失败", f"加载串口设置失败: {e}")
def save_settings(self):
"""保存设置"""
try:
# 保存全局设置
enable_serial = self.enable_serial_checkbox.isChecked()
enable_keyboard = self.enable_keyboard_checkbox.isChecked()
self.config.set_value('app.features.enable_serial_ports', enable_serial)
self.config.set_value('app.features.enable_keyboard_listener', enable_keyboard)
# 保存米电阻设置
mdz_config = {}
mdz_config['ser'] = self.mdz_port_combo.currentData()
mdz_config['port'] = int(self.mdz_baud_combo.currentText())
mdz_config['data_bits'] = int(self.mdz_data_bits_combo.currentText())
mdz_config['stop_bits'] = float(self.mdz_stop_bits_combo.currentText())
mdz_config['parity'] = self.mdz_parity_combo.currentData()
mdz_config['query_cmd'] = self.mdz_query_cmd.text()
mdz_config['query_interval'] = self.mdz_query_interval.value()
mdz_config['code'] = 'mdz'
mdz_config['bit'] = 10
mdz_config['timeout'] = 1
self.config.set_config('mdz', mdz_config)
# 保存称重设置
cz_config = {}
cz_config['ser'] = self.cz_port_combo.currentData()
cz_config['port'] = int(self.cz_baud_combo.currentText())
cz_config['data_bits'] = int(self.cz_data_bits_combo.currentText())
cz_config['stop_bits'] = float(self.cz_stop_bits_combo.currentText())
cz_config['parity'] = self.cz_parity_combo.currentData()
cz_config['stable_threshold'] = self.cz_stable_threshold.value()
cz_config['code'] = 'cz'
cz_config['bit'] = 10
cz_config['timeout'] = 1
self.config.set_config('cz', cz_config)
# 保存键盘设置
keyboard_config = {}
keyboard_config['enabled'] = enable_keyboard
keyboard_config['trigger_key'] = 'Key.page_up'
self.config.set_config('keyboard', keyboard_config)
# 保存配置文件
self.config.save_config()
# 重新加载串口管理器的配置
self.serial_manager.reload_config()
# 根据键盘监听设置立即启动或停止键盘监听
if enable_keyboard:
logging.info("键盘监听已启用,正在启动键盘监听...")
self.serial_manager.start_keyboard_listener()
# 添加测试触发,确认键盘监听是否正常工作
if hasattr(self.serial_manager, 'keyboard_listener') and self.serial_manager.keyboard_listener:
if self.serial_manager.keyboard_listener.is_active():
logging.info("键盘监听已成功启动,尝试手动触发测试事件")
self.serial_manager.keyboard_listener.trigger_test_event()
else:
logging.warning("键盘监听启动失败,未处于活动状态")
else:
logging.info("键盘监听已禁用,正在停止键盘监听...")
self.serial_manager.stop_keyboard_listener()
# 发送设置改变信号
self.settings_changed.emit()
logging.info("已保存串口设置")
QMessageBox.information(self, "保存成功", "串口设置已保存")
except Exception as e:
logging.error(f"保存串口设置失败: {e}")
QMessageBox.warning(self, "保存失败", f"保存串口设置失败: {e}")
def test_mdz_port(self):
"""测试米电阻串口"""
try:
port = self.mdz_port_combo.currentData()
baud = int(self.mdz_baud_combo.currentText())
data_bits = int(self.mdz_data_bits_combo.currentText())
stop_bits = float(self.mdz_stop_bits_combo.currentText())
parity = self.mdz_parity_combo.currentData()
if not port:
QMessageBox.warning(self, "测试失败", "请选择串口")
return
# 关闭可能已经打开的串口
if self.serial_manager.is_port_open(port):
self.serial_manager.close_port(port)
# 尝试打开串口
success = self.serial_manager.open_port(
port, 'mdz', baud, data_bits, stop_bits, parity, 1.0
)
if success:
# 尝试发送查询指令
query_cmd = self.mdz_query_cmd.text()
if query_cmd:
try:
# 转换查询指令为字节
query_bytes = bytes.fromhex(query_cmd.replace(' ', ''))
# 发送查询指令
self.serial_manager.write_data(port, query_bytes)
# 等待一段时间
time.sleep(0.5)
# 关闭串口
self.serial_manager.close_port(port)
QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 测试成功,已发送查询指令")
except Exception as e:
self.serial_manager.close_port(port)
QMessageBox.warning(self, "测试失败", f"发送查询指令失败: {e}")
else:
self.serial_manager.close_port(port)
QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 打开成功,但未发送查询指令")
else:
QMessageBox.warning(self, "测试失败", f"无法打开米电阻串口 {port}")
except Exception as e:
logging.error(f"测试米电阻串口失败: {e}")
QMessageBox.warning(self, "测试失败", f"测试米电阻串口失败: {e}")
def test_cz_port(self):
"""测试线径串口"""
try:
port = self.cz_port_combo.currentData()
baud = int(self.cz_baud_combo.currentText())
data_bits = int(self.cz_data_bits_combo.currentText())
stop_bits = float(self.cz_stop_bits_combo.currentText())
parity = self.cz_parity_combo.currentData()
if not port:
QMessageBox.warning(self, "测试失败", "请选择串口")
return
# 关闭可能已经打开的串口
if self.serial_manager.is_port_open(port):
self.serial_manager.close_port(port)
# 尝试打开串口
success = self.serial_manager.open_port(
port, 'cz', baud, data_bits, stop_bits, parity, 1.0
)
if success:
# 等待一段时间
time.sleep(2)
# 关闭串口
self.serial_manager.close_port(port)
QMessageBox.information(self, "测试成功", f"线径串口 {port} 测试成功")
else:
QMessageBox.warning(self, "测试失败", f"无法打开线径串口 {port}")
except Exception as e:
logging.error(f"测试线径串口失败: {e}")
QMessageBox.warning(self, "测试失败", f"测试线径串口失败: {e}")

View File

@ -0,0 +1,51 @@
import logging
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
from widgets.serial_settings_widget import SerialSettingsWidget
from widgets.settings_widget import SettingsWidget
class SettingsWindow(QDialog):
"""设置窗口直接使用SettingsWidget中的标签页"""
# 定义信号
settings_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
logging.info("正在初始化SettingsWindow")
# 设置窗口标题和大小
self.setWindowTitle("系统设置")
self.resize(900, 700)
self.setModal(True)
# 创建主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
# 创建设置部件
self.settings_widget = SettingsWidget(self)
main_layout.addWidget(self.settings_widget)
# 添加串口设置到标签页
self.serial_settings = SerialSettingsWidget(self)
self.settings_widget.tab_widget.addTab(self.serial_settings, "串口设置")
# 添加按钮
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
main_layout.addWidget(self.button_box)
# 连接信号
self.serial_settings.settings_changed.connect(self.settings_changed.emit)
logging.info("SettingsWindow初始化完成")
def accept(self):
"""确认按钮处理,保存所有设置并发送设置变更信号"""
# 通知设置已变更
self.settings_changed.emit()
# 调用父类方法关闭对话框
super().accept()