diff --git a/camera/__pycache__/CamOperation_class.cpython-310.pyc b/camera/__pycache__/CamOperation_class.cpython-310.pyc index 12e5fea..8323c12 100644 Binary files a/camera/__pycache__/CamOperation_class.cpython-310.pyc and b/camera/__pycache__/CamOperation_class.cpython-310.pyc differ diff --git a/camera/__pycache__/CameraParams_const.cpython-310.pyc b/camera/__pycache__/CameraParams_const.cpython-310.pyc index 7ab9127..4f01d68 100644 Binary files a/camera/__pycache__/CameraParams_const.cpython-310.pyc and b/camera/__pycache__/CameraParams_const.cpython-310.pyc differ diff --git a/camera/__pycache__/CameraParams_header.cpython-310.pyc b/camera/__pycache__/CameraParams_header.cpython-310.pyc index 4643dbf..ff7ea12 100644 Binary files a/camera/__pycache__/CameraParams_header.cpython-310.pyc and b/camera/__pycache__/CameraParams_header.cpython-310.pyc differ diff --git a/camera/__pycache__/MvCameraControl_class.cpython-310.pyc b/camera/__pycache__/MvCameraControl_class.cpython-310.pyc index 0308dfe..c5fcd32 100644 Binary files a/camera/__pycache__/MvCameraControl_class.cpython-310.pyc and b/camera/__pycache__/MvCameraControl_class.cpython-310.pyc differ diff --git a/camera/__pycache__/MvErrorDefine_const.cpython-310.pyc b/camera/__pycache__/MvErrorDefine_const.cpython-310.pyc index 83da1bb..c2cda85 100644 Binary files a/camera/__pycache__/MvErrorDefine_const.cpython-310.pyc and b/camera/__pycache__/MvErrorDefine_const.cpython-310.pyc differ diff --git a/camera/__pycache__/PixelType_header.cpython-310.pyc b/camera/__pycache__/PixelType_header.cpython-310.pyc index 3472dd5..76af58f 100644 Binary files a/camera/__pycache__/PixelType_header.cpython-310.pyc and b/camera/__pycache__/PixelType_header.cpython-310.pyc differ diff --git a/config/app_config.json b/config/app_config.json index d777b81..e96b4c6 100644 --- a/config/app_config.json +++ b/config/app_config.json @@ -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 + } } } \ No newline at end of file diff --git a/db/jtDB.db b/db/jtDB.db index 43d2da0..6dda420 100644 Binary files a/db/jtDB.db and b/db/jtDB.db differ diff --git a/from pymodbus.py b/from pymodbus.py index 1ef6265..5dbe8f9 100644 --- a/from pymodbus.py +++ b/from pymodbus.py @@ -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) diff --git a/main.py b/main.py index f8d5d81..59eca21 100644 --- a/main.py +++ b/main.py @@ -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}") # 设置中文翻译器 diff --git a/test_keyboard.py b/test_keyboard.py new file mode 100644 index 0000000..8f1b287 --- /dev/null +++ b/test_keyboard.py @@ -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() \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..b5a0982 --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,2 @@ +# ui 包 +# 包含所有 UI 相关的类 \ No newline at end of file diff --git a/ui/inspection_settings_ui.py b/ui/inspection_settings_ui.py index 8092740..b39cc0b 100644 --- a/ui/inspection_settings_ui.py +++ b/ui/inspection_settings_ui.py @@ -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) diff --git a/ui/pallet_type_settings_ui.py b/ui/pallet_type_settings_ui.py index a8022db..814c20a 100644 --- a/ui/pallet_type_settings_ui.py +++ b/ui/pallet_type_settings_ui.py @@ -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() @@ -161,10 +82,6 @@ class PalletTypeSettingsUI(QWidget): 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): """创建托盘类型配置部件 @@ -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.addLayout(button_layout) - layout.addWidget(form_group) - - # 添加隐藏字段,用于存储当前编辑的ID - widget.setProperty("current_edit_id", -1) - - return widget - - def show_input_types(self): - """显示上料类型""" - self.input_button.setChecked(True) - self.output_button.setChecked(False) - self.stacked_widget.setCurrentIndex(0) - - def show_output_types(self): - """显示下料类型""" - self.input_button.setChecked(False) - self.output_button.setChecked(True) - self.stacked_widget.setCurrentIndex(1) - - def set_form_enabled(self, enabled): - """设置表单是否可编辑""" - self.input_button.setEnabled(enabled) - self.output_button.setEnabled(enabled) - self.save_button.setEnabled(enabled) - self.reset_button.setEnabled(enabled) - - # 禁用所有表格和表单 - for widget in [self.input_widget, self.output_widget]: - for child in widget.findChildren(QWidget): - child.setEnabled(enabled) \ No newline at end of file + return widget \ No newline at end of file diff --git a/ui/serial_settings_ui.py b/ui/serial_settings_ui.py new file mode 100644 index 0000000..ecd47f9 --- /dev/null +++ b/ui/serial_settings_ui.py @@ -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() \ No newline at end of file diff --git a/ui/settings_window_ui.py b/ui/settings_window_ui.py new file mode 100644 index 0000000..c71710d --- /dev/null +++ b/ui/settings_window_ui.py @@ -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) \ No newline at end of file diff --git a/utils/__pycache__/config_loader.cpython-310.pyc b/utils/__pycache__/config_loader.cpython-310.pyc index f6a7b59..924dd84 100644 Binary files a/utils/__pycache__/config_loader.cpython-310.pyc and b/utils/__pycache__/config_loader.cpython-310.pyc differ diff --git a/utils/config_loader.py b/utils/config_loader.py index d668bd9..4e4345d 100644 --- a/utils/config_loader.py +++ b/utils/config_loader.py @@ -127,4 +127,40 @@ class ConfigLoader: config[keys[-1]] = value # 保存配置 - return self.save_config() \ No newline at end of file + 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 \ No newline at end of file diff --git a/utils/inspection_config_manager.py b/utils/inspection_config_manager.py index 886c075..41b0186 100644 --- a/utils/inspection_config_manager.py +++ b/utils/inspection_config_manager.py @@ -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): """获取检验配置列表 diff --git a/utils/keyboard_listener.py b/utils/keyboard_listener.py new file mode 100644 index 0000000..36ff2bd --- /dev/null +++ b/utils/keyboard_listener.py @@ -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 '' 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('') + + if page_up_callback: + hotkey_mapping[''] = page_up_callback + else: + logging.warning("未找到PageUp回调 (Key.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' 或 '' (用于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[''] = callback + logging.info(f"已为 PageUp 键额外注册 '' (用于全局热键)") + + 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('') + if page_up_callback: + logging.info("[Normal Listener] 使用 'Key.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('') + + 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 或 )") + 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操作完成") \ No newline at end of file diff --git a/utils/pallet_type_manager.py b/utils/pallet_type_manager.py index 4b4ee32..ad2bd23 100644 --- a/utils/pallet_type_manager.py +++ b/utils/pallet_type_manager.py @@ -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'): """更新托盘类型 @@ -134,7 +151,38 @@ class PalletTypeManager: result = self.dao.toggle_pallet_type(pallet_type_id, enabled, username) if result: self.reload_pallet_types() - return result + 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): """根据托盘号获取托盘类型 diff --git a/utils/serial_manager.py b/utils/serial_manager.py new file mode 100644 index 0000000..f48061d --- /dev/null +++ b/utils/serial_manager.py @@ -0,0 +1,1140 @@ +import serial +import threading +import time +import logging +import os +import json +from typing import Dict, Optional, Callable +from utils.config_loader import ConfigLoader +from utils.keyboard_listener import KeyboardListener +from pynput.keyboard import Key +import re +import platform + +class SerialManager: + """串口管理器,单例模式""" + _instance = None + _lock = threading.Lock() + + def __new__(cls): + with cls._lock: + if cls._instance is None: + cls._instance = super(SerialManager, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + self._initialized = True + self.serial_ports: Dict[str, serial.Serial] = {} # 存储打开的串口对象 + self.read_threads: Dict[str, threading.Thread] = {} # 存储读取线程 + self.running_flags: Dict[str, bool] = {} # 存储线程运行标志 + self.callbacks: Dict[str, Callable] = {} # 存储数据回调函数 + + # 添加文件操作暂停控制 + self._file_operations_suspended = False + self._file_operations_lock = threading.Lock() + + # 添加称重稳定性检测变量 + self.last_weight = 0 + self.stable_count = 0 + self.weight_written = False # 初始化为False,确保首次称重能够正确处理 + + # 添加抗抖动变量 + self.last_weights = [0] * 3 # 存储最近3个重量值 + self.weight_changed_time = time.time() # 上次重量变化的时间 + self.last_write_time = 0 # 最后写入时间 + + # 稳定性时间跟踪 + self.stability_start_time = 0 # 开始检测稳定性的时间 + + # 数据存储 + self.data = { + 'mdz': 0, + 'cz': 0 + } + + # 是否自动查询米电阻数据,默认为False,只通过PageUp键触发 + self.auto_query_mdz = False + + logging.info("初始化 SerialManager") + + # 加载配置 + self._load_config() + + # 检查是否启用键盘监听功能 + enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False) + if enable_keyboard_listener: + try: + # 初始化键盘监听器 + self.keyboard_listener = KeyboardListener() + # 从配置中获取触发键 + trigger_key = self.config.get_value('serial.keyboard.trigger_key', 'Key.page_up') + # 注册触发键回调 + self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query) + logging.info(f"已注册{trigger_key}按键回调用于触发米电阻查询") + except Exception as e: + logging.error(f"初始化键盘监听器失败: {e}") + # 创建一个空的键盘监听器对象,以避免后续代码出现NoneType错误 + self.keyboard_listener = None + else: + logging.info("键盘监听功能已在配置中禁用,跳过初始化键盘监听器") + self.keyboard_listener = None + + def _load_config(self): + """加载配置""" + try: + config_loader = ConfigLoader.get_instance() # Renamed for clarity + self.config = config_loader # Assign the whole config object + + # 获取数据文件路径 + self.data_file = self.config.get_value('serial.data_file', 'data.txt') + # 确保是绝对路径 + if not os.path.isabs(self.data_file): + self.data_file = os.path.abspath(self.data_file) + logging.info(f"最终确定的 data_file 绝对路径: {self.data_file}") + + # 获取串口配置 + self.mdz_config = self.config.get_config('mdz') + self.cz_config = self.config.get_config('cz') + + # 检查操作系统类型,在macOS上处理COM端口名称问题 + os_type = platform.system() + if os_type == "Darwin": + # 检查是否需要自动检测macOS上的串口 + macos_autodetect = self.config.get_value('serial.os_config.macos_autodetect', False) + if macos_autodetect: + logging.info("在macOS上启用了串口自动检测") + self._detect_macos_ports() + + # 获取默认的稳定阈值 + self.stable_threshold = self.cz_config.get('stable_threshold', 10) if self.cz_config else 10 + + # 检查是否自动查询米电阻数据 + self.auto_query_mdz = self.config.get_value('serial.keyboard.auto_query', False) + + logging.info(f"已加载串口配置:mdz={self.mdz_config}, cz={self.cz_config}, data_file={self.data_file}") + logging.info(f"米电阻自动查询: {'开启' if self.auto_query_mdz else '关闭'}") + except Exception as e: + logging.error(f"加载配置出错: {e}") + # 设置默认值 + self.data_file = os.path.abspath('data.txt') + self.mdz_config = {'port': 9600, 'ser': 'COM5'} + self.cz_config = {'port': 9600, 'ser': 'COM2', 'stable_threshold': 10} + self.stable_threshold = 10 + self.auto_query_mdz = False + logging.info(f"使用默认配置,数据文件: {self.data_file}") + + def _detect_macos_ports(self): + """在macOS上检测可用的串口""" + try: + if platform.system() != "Darwin": + logging.info("不是macOS系统,跳过串口检测") + return + + logging.info("正在检测macOS可用串口...") + + # 检查/dev目录下的tty.*和cu.*设备 + import glob + tty_ports = glob.glob('/dev/tty.*') + cu_ports = glob.glob('/dev/cu.*') + all_ports = tty_ports + cu_ports + + if not all_ports: + logging.warning("未检测到macOS上的串口设备") + return + + logging.info(f"检测到以下串口设备: {all_ports}") + + # 如果mdz_config中的串口是COM格式,尝试替换为检测到的第一个串口 + if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'].startswith('COM'): + # 优先选择包含"usb"的设备 + usb_ports = [port for port in all_ports if 'usb' in port.lower()] + if usb_ports: + self.mdz_config['ser'] = usb_ports[0] + logging.info(f"自动将米电阻串口从COM格式替换为: {usb_ports[0]}") + elif all_ports: + self.mdz_config['ser'] = all_ports[0] + logging.info(f"自动将米电阻串口从COM格式替换为: {all_ports[0]}") + + # 如果cz_config中的串口是COM格式,尝试替换为检测到的第二个串口 + if self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'].startswith('COM'): + # 优先选择包含"usb"的设备,并且不是已分配给mdz的设备 + usb_ports = [port for port in all_ports if 'usb' in port.lower() + and (not self.mdz_config or port != self.mdz_config.get('ser'))] + if usb_ports: + self.cz_config['ser'] = usb_ports[0] + logging.info(f"自动将线径串口从COM格式替换为: {usb_ports[0]}") + elif len(all_ports) > 1: + # 选择不是mdz_config已使用的第一个端口 + for port in all_ports: + if not self.mdz_config or port != self.mdz_config.get('ser'): + self.cz_config['ser'] = port + logging.info(f"自动将线径串口从COM格式替换为: {port}") + break + elif all_ports and (not self.mdz_config or all_ports[0] != self.mdz_config.get('ser')): + self.cz_config['ser'] = all_ports[0] + logging.info(f"自动将线径串口从COM格式替换为: {all_ports[0]}") + + except Exception as e: + logging.error(f"检测macOS串口时发生错误: {e}") + logging.info("将继续使用配置文件中的串口设置") + + def open_port(self, port_name: str, port_type: str, baud_rate: Optional[int] = None, + data_bits: int = 8, stop_bits: int = 1, + parity: str = 'N', timeout: float = 1.0, + callback: Optional[Callable] = None) -> bool: + """ + 打开串口 + + Args: + port_name: 串口名称,如COM1 + port_type: 串口类型,'cz'表示称重,'mdz'表示米电阻 + baud_rate: 波特率,如果为None则从配置文件读取 + data_bits: 数据位 + stop_bits: 停止位 + parity: 校验位,N-无校验,E-偶校验,O-奇校验 + timeout: 超时时间,单位秒 + callback: 数据回调函数,接收参数为(port_name, data) + + Returns: + 是否成功打开 + """ + try: + # 如果波特率为None,从配置文件读取 + if baud_rate is None: + if port_type == 'cz' and self.cz_config: + baud_rate = self.cz_config.get('port', 9600) + elif port_type == 'mdz' and self.mdz_config: + baud_rate = self.mdz_config.get('port', 9600) + else: + baud_rate = 9600 # 默认波特率 + + # 如果串口已经打开,先关闭 + if port_name in self.serial_ports: + self.close_port(port_name) + + # 打开串口 + ser = serial.Serial( + port=port_name, + baudrate=baud_rate, + bytesize=data_bits, + stopbits=stop_bits, + parity=parity, + timeout=timeout + ) + + if not ser.is_open: + ser.open() + + # 存储串口对象 + self.serial_ports[port_name] = ser + logging.info(f"串行对象 for {port_name} 存储在 self.serial_ports 中. 当前活跃端口: {list(self.serial_ports.keys())}") + + # 设置回调 + if callback: + self.callbacks[port_name] = callback + + # 启动读取线程 + self.running_flags[port_name] = True + + # 根据串口类型选择不同的读取线程 + if port_type == 'cz': + thread = threading.Thread(target=self._read_weight_thread, args=(port_name, self.stable_threshold)) + thread.daemon = True + thread.start() + self.read_threads[port_name] = thread + elif port_type == 'mdz': + thread = threading.Thread(target=self._read_resistance_thread, args=(port_name,)) + thread.daemon = True + thread.start() + self.read_threads[port_name] = thread + else: + # 默认读取线程 + thread = threading.Thread(target=self._read_thread, args=(port_name,)) + thread.daemon = True + thread.start() + self.read_threads[port_name] = thread + + logging.info(f"串口 {port_name} ({port_type}) 已打开,波特率={baud_rate}") + return True + + except Exception as e: + logging.error(f"打开串口 {port_name} 失败: {str(e)}") + if port_name in self.serial_ports: # 清理,以防部分成功 + del self.serial_ports[port_name] + logging.info(f"打开 {port_name} 失败后, 当前活跃端口: {list(self.serial_ports.keys())}") + return False + + def close_port(self, port_name: str) -> bool: + """ + 关闭串口 + + Args: + port_name: 串口名称 + + Returns: + 是否成功关闭 + """ + try: + # 停止读取线程 + if port_name in self.running_flags: + self.running_flags[port_name] = False + + # 等待线程结束 + if port_name in self.read_threads: + if self.read_threads[port_name].is_alive(): + self.read_threads[port_name].join(1.0) # 最多等待1秒 + del self.read_threads[port_name] + + # 关闭串口 + if port_name in self.serial_ports: + if self.serial_ports[port_name].is_open: + self.serial_ports[port_name].close() + del self.serial_ports[port_name] + logging.info(f"串行对象 for {port_name} 从 self.serial_ports 中删除. 当前活跃端口: {list(self.serial_ports.keys())}") + + # 删除回调 + if port_name in self.callbacks: + del self.callbacks[port_name] + + logging.info(f"串口 {port_name} 已关闭") + return True + + except Exception as e: + logging.error(f"关闭串口 {port_name} 失败: {str(e)}") + return False + + def is_port_open(self, port_name: str) -> bool: + """ + 检查串口是否打开 + + Args: + port_name: 串口名称 + + Returns: + 是否打开 + """ + return port_name in self.serial_ports and self.serial_ports[port_name].is_open + + def write_data(self, port_name: str, data: bytes) -> bool: + """ + 向串口写入数据 + + Args: + port_name: 串口名称 + data: 要写入的数据 + + Returns: + 是否成功写入 + """ + try: + if not self.is_port_open(port_name): + return False + + self.serial_ports[port_name].write(data) + return True + + except Exception as e: + logging.error(f"向串口 {port_name} 写入数据失败: {str(e)}") + return False + + def _read_thread(self, port_name: str): + """ + 串口读取线程 + + Args: + port_name: 串口名称 + """ + try: + while self.running_flags.get(port_name, False): + if not self.is_port_open(port_name): + time.sleep(0.1) + continue + + # 读取数据 + if self.serial_ports[port_name].in_waiting: + data = self.serial_ports[port_name].readline() + + # 调用回调函数 + if port_name in self.callbacks and data: + try: + self.callbacks[port_name](port_name, data) + except Exception as e: + logging.error(f"调用串口 {port_name} 回调函数失败: {str(e)}") + + time.sleep(0.01) # 短暂休眠,避免CPU占用过高 + + except Exception as e: + logging.error(f"串口 {port_name} 读取线程异常: {str(e)}") + + def _read_weight_thread(self, port_name: str, stable_threshold: int = 10): + logging.info(f"[{port_name}] 称重线程启动") + # 重置状态变量,确保线程重启时能正确处理称重数据 + self.weight_written = False + self.stable_count = 0 + self.last_weight = 0 + self.last_weights = [0] * 3 + self.weight_changed_time = time.time() + self.last_write_time = 0 # 添加一个变量来跟踪最后一次写入时间 + self.stability_start_time = 0 # 重置稳定性检测开始时间 + + # 定义处理重量更新的函数,避免代码重复 + def process_weight_update(current_weight, source_text): + # 更新最近重量记录用于抗抖动 + self.last_weights.pop(0) # 移除最旧的记录 + self.last_weights.append(current_weight) # 添加新记录 + + # --- 稳定性及后续处理 --- + # 检查重量是否稳定,容忍0.01以内的微小波动 + is_stable = abs(current_weight - self.last_weight) < 0.01 + + # 添加重量检查,确保重量大于1 + if is_stable and current_weight < 1: + is_stable = False # 重量小于1时,不认为是稳定的 + + # 检查重量是否有重大变化(超过0.05)或者长时间未更新(>30秒) + now = time.time() + significant_change = abs(current_weight - self.last_weight) >= 0.05 + time_since_last_change = now - self.weight_changed_time + force_update = time_since_last_change > 30 and not self.weight_written + + if is_stable: + # 如果这是第一次检测到稳定,记录开始时间 + if self.stable_count == 0: + self.stability_start_time = now + + self.stable_count += 1 + + # 检查时间要求 - 确保10次稳定读数在2秒内发生 + time_in_stability = now - self.stability_start_time + meets_time_requirement = time_in_stability <= 2.0 or self.stable_count < stable_threshold + + # 如果稳定时间超过2秒但还没达到稳定阈值,重置计数 + if not meets_time_requirement: + logging.warning(f"[{port_name}] 稳定检测超时 - {time_in_stability:.2f}秒内只有{self.stable_count}次稳定读数,重置计数") + self.stable_count = 1 # 重置为1而不是0,因为当前读数是稳定的 + self.stability_start_time = now # 重置开始时间 + + # 仅在达到特定阈值时记录日志 + if self.stable_count % 5 == 0 or self.stable_count >= stable_threshold: + logging.info(f"[{port_name}] 称重稳定计数: {self.stable_count}/{stable_threshold}, 经过时间: {time_in_stability:.2f}秒") + + # 添加"ST"标记到数据中,模拟PB代码中的稳定标志 + st_marker = "" + if self.stable_count >= stable_threshold: + st_marker = " ST" # 添加ST标记,表示完全稳定 + + if port_name in self.callbacks: + try: + # 将稳定标记添加到回调数据中 + callback_data = f"称重数据: {current_weight}, 稳定计数: {self.stable_count}/{stable_threshold}{st_marker}".encode() + self.callbacks[port_name](port_name, callback_data) + except Exception as e: + logging.error(f"[{port_name}] 调用串口回调失败: {str(e)}") + + if self.stable_count >= stable_threshold and not self.weight_written: + logging.info(f"[{port_name}] 称重数据稳定,写入: {current_weight}") + self.data['cz'] = current_weight + + # 添加写入文件的限流,确保最少间隔0.5秒才写入一次 + current_time = time.time() + if current_time - self.last_write_time > 0.5: + try: + self._write_data_to_file() + self.last_write_time = current_time + except Exception as e: + logging.error(f"写入数据文件失败: {str(e)}") + + self.weight_written = True + elif force_update: + logging.info(f"[{port_name}] 长时间未更新,强制更新重量: {current_weight}") + self.data['cz'] = current_weight + + # 同样对强制更新添加限流 + current_time = time.time() + if current_time - self.last_write_time > 0.5: + try: + self._write_data_to_file() + self.last_write_time = current_time + except Exception as e: + logging.error(f"写入数据文件失败: {str(e)}") + + self.weight_written = True + self.weight_changed_time = now + elif significant_change: + # 记录旧值,防止日志中的循环引用 + old_weight = self.last_weight + self.stable_count = 0 + self.weight_written = False # 确保在重量变化时重置写入标志 + self.last_weight = current_weight + self.weight_changed_time = now + logging.info(f"[{port_name}] 重量变化: {old_weight} -> {current_weight}") + + # 重量变化时也调用回调,但不添加ST标记 + if port_name in self.callbacks: + try: + callback_data = f"称重数据: {current_weight}, 稳定计数: 0/{stable_threshold}".encode() + self.callbacks[port_name](port_name, callback_data) + except Exception as e: + logging.error(f"[{port_name}] 调用串口回调失败: {str(e)}") + else: + # 微小波动,保持当前状态,不重置计数器 + # 即使有微小波动,也要更新UI显示,但不影响稳定计数 + if port_name in self.callbacks: + try: + # 如果接近稳定阈值但还未完全稳定,添加不同的标记 + st_partial = "" + if self.stable_count >= stable_threshold * 0.7: # 达到阈值的70% + st_partial = " ST_PARTIAL" + + callback_data = f"称重数据: {current_weight}, 稳定计数: {self.stable_count}/{stable_threshold}{st_partial}".encode() + self.callbacks[port_name](port_name, callback_data) + except Exception as e: + logging.error(f"[{port_name}] 调用串口回调失败: {str(e)}") + # --- 结束稳定性处理 --- + return + + try: + read_count = 0 # 添加读取计数器,用于调整睡眠时间 + last_read_time = time.time() # 记录上次读取时间 + + while self.running_flags.get(port_name, False): + if not self.is_port_open(port_name): + time.sleep(0.1) + continue + + read_count += 1 + current_time = time.time() + + try: + if self.serial_ports[port_name].in_waiting > 0: + # 等待一小段时间让数据完整接收,避免数据帧被拆分 + time.sleep(0.05) # 减少等待时间,提高响应速度 + + # 直接读取所有可用数据 + available_bytes = self.serial_ports[port_name].in_waiting + raw_data = self.serial_ports[port_name].read(available_bytes) + + # 有数据读取时重置计数器 + read_count = 0 + last_read_time = current_time + + # 尝试解码为字符串 + try: + data_str = raw_data.decode('ascii', errors='replace') + + # 基于截图中的数据格式,尝试直接用正则表达式搜索"ST,NT,+ X.XXkg"格式 + import re + # 完整格式的正则表达式,匹配称重显示格式 + full_pattern = r"ST,NT,\+\s+(\d+\.\d+)kg" + matches = re.findall(full_pattern, data_str) + + if matches: + for weight_str in matches: + # 计算重量 - 直接使用原始值,无需除以分度值 + raw_value = float(weight_str) + current_weight = raw_value # 直接使用原始值,不再除以 bit_value + + # 处理重量更新 + process_weight_update(current_weight, f"完整格式: '{weight_str}kg'") + else: + # 如果没有找到完整格式,尝试按行分割处理 + # 按行分割,处理每一行数据 + lines = data_str.split('\r\n') + for line in lines: + if not line.strip(): + continue + + # 提取重量值 - 查找任何包含数字的部分 + weight_pattern = r"([+-]?)\s*(\d+\.?\d*|\.\d+)" + match = re.search(weight_pattern, line) + + if match: + sign = match.group(1) or '+' # 如果没有符号,默认为正 + number = match.group(2) + cleaned_weight_str = sign + number + + # 计算重量 - 直接使用原始值,无需除以分度值 + raw_value = float(cleaned_weight_str) + current_weight = raw_value # 直接使用原始值,不再除以 bit_value + + # 处理重量更新 + process_weight_update(current_weight, f"行处理: '{line}'") + else: + logging.warning(f"[{port_name}] 无法从行数据中提取有效的数字") + except UnicodeDecodeError: + logging.warning(f"[{port_name}] 无法解码数据为ASCII") + except Exception as e: + logging.error(f"[{port_name}] 处理数据时出错: {str(e)}") + + except Exception as e: + logging.error(f"[{port_name}] 读取处理异常: {str(e)}") + + # 动态调整睡眠时间,减少CPU占用 + # 如果长时间没有读取到数据,增加睡眠时间 + sleep_time = 0.01 # 基础睡眠时间 + + if read_count > 100: # 连续多次无数据读取 + time_since_last_read = current_time - last_read_time + if time_since_last_read > 5: # 如果超过5秒没有数据 + sleep_time = 0.2 # 增加到较长的睡眠时间 + elif time_since_last_read > 2: # 如果超过2秒没有数据 + sleep_time = 0.1 # 增加到中等睡眠时间 + elif read_count > 1000: # 非常长时间无数据 + sleep_time = 0.3 # 更长的睡眠时间 + + time.sleep(sleep_time) + + except Exception as e: + logging.error(f"[{port_name}] 主循环异常: {str(e)}") + logging.info(f"[{port_name}] 称重线程结束") + + def _process_weight_data(self, data_bytes): + """ + 处理原始称重数据 + + Args: + data_bytes: 原始字节数据 + + Returns: + 解析后的重量值或None + """ + try: + # 尝试多种编码方式解析 + weight = None + + # 记录原始数据以便调试 + logging.debug(f"称重原始数据: {data_bytes.hex()}") + + # 方法1: 尝试ASCII解码 + try: + ascii_str = data_bytes.decode('ascii', errors='replace') + # 仅提取数字部分 + import re + numbers = re.findall(r'\d+', ascii_str) + if numbers: + weight = float(numbers[0]) / 10.0 + logging.debug(f"ASCII解码成功: {weight}") + except Exception as e: + logging.debug(f"ASCII解码失败: {e}") + + # 方法2: 尝试直接从二进制解析 (根据具体协议) + if weight is None and len(data_bytes) >= 8: + try: + # 假设重量在特定位置,具体需根据实际协议调整 + weight_bytes = data_bytes[2:6] + weight = int.from_bytes(weight_bytes, byteorder='big') / 10.0 + logging.debug(f"二进制解码成功: {weight}") + except Exception as e: + logging.debug(f"二进制解码失败: {e}") + + return weight + except Exception as e: + logging.error(f"处理称重数据失败: {e}") + return None + + def _read_resistance_thread(self, port_name: str): + """ + 米电阻串口读取线程 + + Args: + port_name: 串口名称 + """ + try: + while self.running_flags.get(port_name, False): + if not self.is_port_open(port_name): + time.sleep(0.1) + continue + + # 如果不是自动查询模式,则只监听串口数据而不主动发送查询 + if not self.auto_query_mdz: + # 检查是否有数据可读 + if self.serial_ports[port_name].in_waiting > 0: + response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting) + self._process_mdz_response(port_name, response) + + time.sleep(0.1) + continue + + # 以下代码只在自动查询模式下执行 + try: + # 发送查询指令 + hex_data = '01 03 00 01 00 07 55 C8' + byte_data = bytes.fromhex(hex_data.replace(' ', '')) + self.serial_ports[port_name].write(byte_data) + + # 等待响应 + time.sleep(1) + + if self.serial_ports[port_name].in_waiting > 0: + response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting) + self._process_mdz_response(port_name, response) + + except Exception as e: + logging.error(f"米电阻数据处理异常: {e}") + + # 每5秒查询一次 + for i in range(50): + if not self.running_flags.get(port_name, False): + break + time.sleep(0.1) + + except Exception as e: + logging.error(f"米电阻串口 {port_name} 读取线程异常: {e}") + + def _process_mdz_response(self, port_name, response_bytes: bytes): + """处理米电阻响应数据""" + try: + if response_bytes: # 确保有响应数据 + try: + # 转换为字符串用于日志记录 + response_str = str(response_bytes) + logging.warning(f"[{port_name}] 米电阻数据: {response_str}") + + # 使用正则表达式直接提取数字 + # 查找格式为11.58201这样的浮点数 + match = re.search(r'(\d+\.\d+)', response_str) + if match: + number_str = match.group(1) + try: + # 转换为浮点数 + mdz_value = float(number_str) + logging.info(f"米电阻数据: {mdz_value}") + + # 更新数据 + self.data['mdz'] = mdz_value + self._write_data_to_file() + self._notify_callbacks('mdz_data', {"type": "mdz", "value": self.data['mdz'], "source": f"serial ({port_name})"}) + return True + except ValueError: + logging.warning(f"米电阻数据字符串 '{number_str}' 无法转换为浮点数") + else: + logging.warning(f"米电阻数据中未找到有效的浮点数") + except Exception as e: + logging.error(f"处理米电阻数据异常: {e}") + else: + logging.warning("米电阻响应数据为空") + + # 如果无法解析,则不再使用模拟数据,直接返回失败 + return False + + except Exception as e: + logging.error(f"米电阻数据处理关键异常: {e}") + return False + + def _write_data_to_file(self): + """将数据写入文件""" + try: + # 检查文件操作是否已暂停 + with self._file_operations_lock: + if self._file_operations_suspended: + logging.info("文件操作已暂停,跳过写入") + return + + # 构建数据字符串 + data_str = f"mdz:{self.data['mdz']}|cz:{self.data['cz']}|" + + # 确保目录存在 + data_dir = os.path.dirname(self.data_file) + if data_dir and not os.path.exists(data_dir): + logging.info(f"创建目录: {data_dir}") + os.makedirs(data_dir, exist_ok=True) + + # 写入文件 - 使用临时文件写入然后重命名,避免文件锁定问题 + # 创建临时文件 + temp_file = f"{self.data_file}.tmp" + try: + with open(temp_file, 'w', encoding='utf-8') as f: + f.write(data_str) + f.flush() + os.fsync(f.fileno()) # 确保数据写入磁盘 + + # 再次检查文件操作是否已暂停 + with self._file_operations_lock: + if self._file_operations_suspended: + logging.info("文件操作已暂停,已写入临时文件但取消重命名操作") + return + + # 原子性地重命名文件,替换原有文件(在大多数操作系统上是原子操作) + if os.path.exists(self.data_file): + try: + os.remove(self.data_file) + except Exception as e: + logging.warning(f"无法删除旧数据文件: {e}, 尝试直接覆盖") + + os.rename(temp_file, self.data_file) + logging.info(f"数据已写入文件: {self.data_file}") + except Exception as e: + logging.error(f"写入临时文件失败: {e}") + # 清理临时文件 + if os.path.exists(temp_file): + try: + os.remove(temp_file) + except: + pass + raise # 重新抛出异常 + + except Exception as e: + logging.error(f"写入数据文件失败: {e}, 文件路径: {self.data_file}") + + def get_current_data(self): + """获取当前数据""" + return self.data.copy() + + def close_all_ports(self): + """关闭所有串口""" + port_names = list(self.serial_ports.keys()) + for port_name in port_names: + self.close_port(port_name) + + def reload_config(self): + """重新加载配置""" + self._load_config() + logging.info("已重新加载串口配置") + + def start_keyboard_listener(self): + """启动键盘监听""" + try: + # 检查是否启用键盘监听功能 + enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False) + if not enable_keyboard_listener: + logging.info("键盘监听功能已在配置中禁用,跳过启动键盘监听") + return False + + # 检查键盘监听器是否已初始化 + if self.keyboard_listener is None: + logging.warning("键盘监听器未初始化,无法启动") + return False + + # 从配置中获取触发键 + config = ConfigLoader.get_instance() + trigger_key = config.get_value('serial.keyboard.trigger_key', 'Key.page_up') + + # 确保已注册触发键回调 + if trigger_key not in self.keyboard_listener.callbacks: + self.keyboard_listener.register_callback(trigger_key, self.trigger_resistance_query) + logging.info(f"已注册{trigger_key}按键回调用于触发米电阻数据查询") + + # 启动键盘监听 + result = self.keyboard_listener.start() + if result: + logging.info(f"已启动键盘监听,按 {trigger_key} 键可触发米电阻数据查询") + + # 检查监听器状态 + if self.keyboard_listener.is_active(): + logging.info("键盘监听器处于活动状态") + else: + logging.warning("键盘监听器启动完成,但状态检查显示不活动") + else: + logging.error("启动键盘监听失败") + return result + except Exception as e: + logging.error(f"启动键盘监听失败: {e}") + return False + + def stop_keyboard_listener(self, join_thread=False): + """停止键盘监听 + + Args: + join_thread: 是否等待键盘监听线程结束 + """ + try: + # 检查键盘监听器是否已初始化 + if self.keyboard_listener is None: + logging.info("键盘监听器未初始化,无需停止") + return + + self.keyboard_listener.stop() + logging.info("已停止键盘监听") + + # 如果需要等待线程结束 + if join_thread: + try: + self.keyboard_listener.join(timeout=1.0) # 最多等待1秒 + except Exception as e: + logging.error(f"等待键盘监听线程结束时出错: {e}") + except Exception as e: + logging.error(f"停止键盘监听失败: {e}") + + def register_callback(self, key, callback): + """ + 注册数据回调函数 + + Args: + key: 回调标识,如 'mdz_data' + callback: 回调函数,参数为 (port_name, data) + """ + try: + if key in self.callbacks: + logging.warning(f"覆盖已存在的回调函数: {key}") + self.callbacks[key] = callback + logging.info(f"已注册回调函数: {key}") + except Exception as e: + logging.error(f"注册回调失败: {e}") + + def trigger_resistance_query(self): + """触发米电阻数据查询,如果串口未打开,则尝试临时打开并查询""" + # 直接打印到控制台,确保可见 + print("\n[米电阻查询] PageUp键被按下,正在触发米电阻数据查询...\n") + + # 检查是否启用串口功能 + enable_serial_ports = self.config.get_value('app.features.enable_serial_ports', False) + if not enable_serial_ports: + logging.info("串口功能已在配置中禁用,跳过米电阻数据查询") + print("\n[米电阻查询] 串口功能已禁用,无法查询\n") + return + + # 检查是否启用键盘监听功能 - 如果这个按键是通过键盘触发的,应该尊重键盘监听器配置 + enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False) + if not enable_keyboard_listener: + logging.info("键盘监听功能已在配置中禁用,但收到了Page Up触发,检查是否为其他来源的调用") + print("\n[米电阻查询] 键盘监听功能已禁用,但收到了触发\n") + # 这里我们仍然继续执行,因为该方法可能由其他非键盘源调用 + + # 从配置中获取触发键 + config = ConfigLoader.get_instance() + trigger_key = config.get_value('serial.keyboard.trigger_key', 'Key.page_up') + logging.info(f"[SerialManager] {trigger_key}键按下,正在触发米电阻数据查询...") + print(f"\n[米电阻查询] {trigger_key}键按下,正在处理...\n") + + if not self.mdz_config: + logging.error("[SerialManager] 米电阻配置 (mdz_config) 未加载,无法查询。") + print("\n[米电阻查询] 配置未加载,无法查询\n") + return + + mdz_port_name = self.mdz_config.get('ser') + query_cmd_hex = self.mdz_config.get('query_cmd', '01030001000755C8') + baud_rate = self.mdz_config.get('port', 9600) + timeout = self.mdz_config.get('timeout', 1.0) + data_bits = self.mdz_config.get('data_bits', 8) + stop_bits = self.mdz_config.get('stop_bits', 1) + parity_char = self.mdz_config.get('parity', 'N') + parity = parity_char # Use the character directly ('N', 'E', or 'O') + logging.info(f"[SerialManager] 使用校验位设置: {parity}") + print(f"\n[米电阻查询] 串口配置: {mdz_port_name}, {baud_rate}, {data_bits}, {stop_bits}, {parity}\n") + + temp_ser = None + try: + byte_data = bytes.fromhex(query_cmd_hex.replace(' ', '')) + logging.info(f"[SerialManager] 准备发送米电阻查询指令: {byte_data.hex(' ').upper()} 到端口 {mdz_port_name}") + print(f"\n[米电阻查询] 准备发送查询指令: {byte_data.hex(' ').upper()}\n") + + # 检查 SerialManager 是否已管理此端口且已打开 + if self.is_port_open(mdz_port_name): + logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 已由 SerialManager 管理并打开,直接发送指令。") + print(f"\n[米电阻查询] 串口 {mdz_port_name} 已打开,直接发送指令\n") + if self.write_data(mdz_port_name, byte_data): + logging.info(f"[SerialManager] 指令已发送到 {mdz_port_name} (通过已打开的串口)。响应将由读取线程处理。") + print("\n[米电阻查询] 指令发送成功,等待响应\n") + # 当串口已打开时,指令发送后,响应会由 _read_resistance_thread 捕获并处理。 + # _read_resistance_thread 内部的 _process_mdz_response 会负责更新 self.data, + # 调用 _write_data_to_file 和 _notify_callbacks。 + # 因此,这里不需要再显式地 sleep 后调用 _write_data_to_file 和 _notify_callbacks, + # 以避免数据竞争或重复通知。 + return # 指令已发送,等待线程处理 + else: + logging.warning(f"[SerialManager] 向已打开的串口 {mdz_port_name} 发送指令失败。将尝试临时打开。") + print("\n[米电阻查询] 指令发送失败,尝试临时打开串口\n") + + # 如果串口未被 SerialManager 管理或发送失败,则尝试临时打开 + logging.info(f"[SerialManager] 米电阻串口 {mdz_port_name} 未打开或发送失败。尝试临时打开并查询...") + print(f"\n[米电阻查询] 串口 {mdz_port_name} 未打开,尝试临时打开\n") + temp_ser = serial.Serial( + port=mdz_port_name, + baudrate=baud_rate, + bytesize=data_bits, + stopbits=stop_bits, + parity=parity, + timeout=timeout + ) + if not temp_ser.is_open: + temp_ser.open() + + temp_ser.write(byte_data) + logging.info(f"[SerialManager] 指令已通过临时串口发送到 {mdz_port_name}。等待响应...") + print("\n[米电阻查询] 指令已通过临时串口发送,等待响应\n") + time.sleep(0.1) # 等待设备响应 + response_bytes = b'' + if temp_ser.in_waiting > 0: + response_bytes = temp_ser.read(temp_ser.in_waiting) + + if response_bytes: + logging.info(f"[SerialManager] 收到来自 {mdz_port_name} (临时串口) 的响应: {response_bytes.hex(' ').upper()}") + print(f"\n[米电阻查询] 收到响应: {response_bytes.hex(' ').upper()}\n") + + # 将响应交给标准的处理函数 + parse_success = self._process_mdz_response(mdz_port_name, response_bytes) + if not parse_success: + logging.warning(f"[SerialManager] _process_mdz_response未能成功处理来自临时串口{mdz_port_name}的响应。将依赖其内部的mock/old data逻辑。") + print("\n[米电阻查询] 响应解析失败\n") + # _process_mdz_response 内部在失败时会处理 mock/old data 及文件写入和通知,这里无需额外操作。 + else: + logging.warning(f"[SerialManager] 未收到来自 {mdz_port_name} (临时串口) 的响应。") + print("\n[米电阻查询] 未收到响应\n") + except serial.SerialException as se: + logging.error(f"[SerialManager] 临时打开或操作串口 {mdz_port_name} 失败: {se}") + print(f"\n[米电阻查询] 串口操作失败: {se}\n") + except ValueError as ve: + logging.error(f"[SerialManager] 指令转换错误或响应解析错误 (临时查询): {ve}") + print(f"\n[米电阻查询] 指令转换或响应解析错误: {ve}\n") + except Exception as e: + logging.error(f"[SerialManager] 触发米电阻查询时发生未知错误 (临时查询): {e}", exc_info=True) + print(f"\n[米电阻查询] 未知错误: {e}\n") + finally: + if temp_ser and temp_ser.is_open: + temp_ser.close() + logging.info("[SerialManager] 米电阻数据查询流程结束。") + print("\n[米电阻查询] 查询流程结束\n") + + def _notify_callbacks(self, port_name, value): + """通知所有相关回调函数""" + try: + # 端口特定回调 (通常用于原始串口数据) + if port_name in self.callbacks and port_name != 'mdz_data': # 避免重复处理 mdz_data + try: + # 假设这种回调期望原始的 value (可能是字节串,也可能是其他类型) + self.callbacks[port_name](port_name, value) + logging.debug(f"Notified port-specific callback for {port_name}") + except Exception as e: + logging.error(f"调用端口回调 {port_name} 失败: {e}") + + # 全局回调, 特别处理 'mdz_data' + if 'mdz_data' in self.callbacks: + actual_mdz_numeric_value = None + source_info = "unknown" + + if isinstance(value, dict): + actual_mdz_numeric_value = value.get('value') + source_info = value.get('source', source_info) + elif isinstance(value, (str, float, int)): + # 如果直接传递了数值 (例如来自旧的 _use_mock_data) + actual_mdz_numeric_value = str(value) + else: + # 尝试从可能是字节串的value中解码 (不太可能走到这里了,因为上游会处理好) + try: + decoded_value = value.decode('utf-8') + if "米电阻数据:" in decoded_value: + actual_mdz_numeric_value = decoded_value.split("米电阻数据:")[1].strip() + else: + actual_mdz_numeric_value = decoded_value # best guess + except: # noqa + pass # 无法解码或解析,保持 None + + if actual_mdz_numeric_value is not None: + # 构建 PackageInboundDialog.on_mdz_data_received 期望的格式 + callback_data_str = f"米电阻数据: {actual_mdz_numeric_value}" + try: + # port_name 对于 mdz_data 回调,可以传递触发源的串口名,或者一个通用标识 + # trigger_resistance_query 知道 mdz_port_name,它应该作为 port_name 传给 _notify_callbacks + # 如果是从模拟数据来,port_name 可能是 'mdz' 或 'mock' + triggering_port = port_name if port_name not in ['mdz_data', 'mdz'] else self.mdz_config.get('ser', 'N/A') if self.mdz_config else 'N/A' + if source_info.startswith("mock"): # 如果源是模拟数据 + triggering_port = f"mock_{port_name}" # e.g. mock_mdz + + self.callbacks['mdz_data'](triggering_port, callback_data_str.encode('utf-8')) + logging.info(f"通知 'mdz_data' 回调. 值: {actual_mdz_numeric_value}, 源: {source_info}, 触发源端口: {triggering_port}") + except Exception as e: + logging.error(f"调用全局回调 'mdz_data' 失败: {e}", exc_info=True) + else: + logging.warning(f"回调失败: mdz_data 中 实际值为None. 初始 value: {value}") + + except Exception as e: + logging.error(f"通知回调失败: {e}", exc_info=True) + + def auto_open_configured_ports(self): + """自动打开已配置的串口""" + logging.info("尝试自动打开已配置的串口...") + + # 首先检查是否启用串口功能 + enable_serial_ports = self.config.get_value('app.features.enable_serial_ports', False) + if not enable_serial_ports: + logging.info("串口功能已在配置中禁用,跳过自动打开串口") + return False + + success = True + + # 检查操作系统类型并提供合适的警告 + os_type = platform.system() + if os_type == "Darwin" and ( + (self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'] and self.mdz_config['ser'].startswith('COM')) or + (self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].startswith('COM')) + ): + logging.warning("检测到在macOS系统上配置了Windows格式的COM端口,这些端口将无法正常打开") + logging.warning("macOS上的串口通常是/dev/tty.*或/dev/cu.*格式") + # 继续尝试打开,但不影响程序流程 + + # 尝试打开线径串口 + if self.cz_config and 'ser' in self.cz_config and self.cz_config['ser']: + port_name = self.cz_config['ser'] + baud_rate = self.cz_config.get('port', 2400) + + if not self.is_port_open(port_name): + try: + if self.open_port(port_name, 'cz', baud_rate): + logging.info(f"自动打开线径串口 {port_name} 成功") + else: + logging.error(f"自动打开线径串口 {port_name} 失败") + success = False + except Exception as e: + logging.error(f"自动打开线径串口 {port_name} 时发生异常: {e}") + success = False + else: + logging.info(f"线径串口 {port_name} 已经打开,无需重新打开") + else: + logging.warning("线径串口未配置,跳过自动打开") + + # 尝试打开米电阻串口 + if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser']: + port_name = self.mdz_config['ser'] + baud_rate = self.mdz_config.get('port', 9600) + + if not self.is_port_open(port_name): + try: + if self.open_port(port_name, 'mdz', baud_rate): + logging.info(f"自动打开米电阻串口 {port_name} 成功") + else: + logging.error(f"自动打开米电阻串口 {port_name} 失败") + success = False + except Exception as e: + logging.error(f"自动打开米电阻串口 {port_name} 时发生异常: {e}") + success = False + else: + logging.info(f"米电阻串口 {port_name} 已经打开,无需重新打开") + else: + logging.warning("米电阻串口未配置,跳过自动打开") + + # 检查是否启用键盘监听功能 + enable_keyboard_listener = self.config.get_value('app.features.enable_keyboard_listener', False) + if enable_keyboard_listener: + # 启动键盘监听 + try: + self.start_keyboard_listener() + except Exception as e: + logging.error(f"启动键盘监听失败: {e}") + # 键盘监听启动失败不影响串口打开的整体状态 + else: + logging.info("键盘监听功能已在配置中禁用,跳过启动") + + if not success: + logging.warning("部分串口自动打开失败,请检查设备连接或在参数配置中手动打开") + + return True # 总是返回True,防止应用程序因串口问题而终止 + + def suspend_file_operations(self, suspend: bool): + """暂停或恢复文件操作 + + Args: + suspend: True表示暂停,False表示恢复 + """ + with self._file_operations_lock: + old_state = self._file_operations_suspended + self._file_operations_suspended = suspend + + if old_state != suspend: + if suspend: + logging.info("已暂停SerialManager文件操作") + else: + logging.info("已恢复SerialManager文件操作") \ No newline at end of file diff --git a/widgets/__pycache__/camera_manager.cpython-310.pyc b/widgets/__pycache__/camera_manager.cpython-310.pyc index 9fdf963..4834915 100644 Binary files a/widgets/__pycache__/camera_manager.cpython-310.pyc and b/widgets/__pycache__/camera_manager.cpython-310.pyc differ diff --git a/widgets/camera_settings_widget.py b/widgets/camera_settings_widget.py index a96f055..1ece866 100644 --- a/widgets/camera_settings_widget.py +++ b/widgets/camera_settings_widget.py @@ -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 + + # 获取当前参数 + exposure = self.exposure_slider.value() + gain = self.gain_slider.value() + frame_rate = self.framerate_slider.value() - # 实现保存参数到配置文件的功能 - # TODO: 待实现 - QMessageBox.information(self, "提示", "参数保存功能尚未实现") + # 保存到配置文件 + 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): """窗口关闭事件""" diff --git a/widgets/inspection_settings_widget.py b/widgets/inspection_settings_widget.py index 47b7988..3821009 100644 --- a/widgets/inspection_settings_widget.py +++ b/widgets/inspection_settings_widget.py @@ -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] - - # 获取表单数据 - 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} 不存在,需要在初始化时创建") + # 保存配置 + success = self.inspection_manager.save_configs(configs) - # 重新加载配置 - 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) \ No newline at end of file + QMessageBox.critical(self, "错误", f"保存检验配置失败: {str(e)}") \ No newline at end of file diff --git a/widgets/main_window.py b/widgets/main_window.py index 7757979..38e88c7 100644 --- a/widgets/main_window.py +++ b/widgets/main_window.py @@ -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): """主窗口""" @@ -158,6 +159,9 @@ class MainWindow(MainWindowUI): self.statusBar().addPermanentWidget(self.error_status_label) 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,8 +604,13 @@ 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): """处理工程号输入框按下回车事件""" @@ -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() diff --git a/widgets/pallet_type_settings_widget.py b/widgets/pallet_type_settings_widget.py index 73289c8..8c6ab11 100644 --- a/widgets/pallet_type_settings_widget.py +++ b/widgets/pallet_type_settings_widget.py @@ -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, { @@ -198,36 +178,32 @@ 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) + + # 添加托盘类型 + success = self.pallet_type_manager.add_pallet_type(values) + + 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}托盘类型: {values['type_name']}") + else: + QMessageBox.critical(self, "错误", f"添加{operation_type}托盘类型失败") + logging.error(f"添加{operation_type}托盘类型失败: {values['type_name']}") - # 创建托盘类型 - 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) + 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) + + # 更新托盘类型 + success = self.pallet_type_manager.update_pallet_type(pallet_type_id, values) + + if success: + # 重新加载数据 + self.load_operation_pallet_types(operation_type) - # 获取表单值 - values = self.get_form_values(operation_type) + # 重置表单 + 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']}") - # 更新托盘类型 - 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) + 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 + + selected_items = table.selectedItems() + if not selected_items: + QMessageBox.warning(self, "警告", "请先选择要删除的托盘类型") + return + + # 获取托盘类型ID + row = selected_items[0].row() + id_item = table.item(row, 0) + if not id_item: + return + + pallet_type_id = id_item.data(Qt.UserRole) + type_name = id_item.text() + + # 确认删除 + reply = QMessageBox.question(self, "确认删除", f"确定要删除{operation_type}托盘类型 [{type_name}] 吗?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + + 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) - # 确认删除 - pallet_type = self.pallet_type_manager.get_pallet_type_by_id(pallet_type_id) - if not pallet_type: - QMessageBox.warning(self, "警告", "找不到要删除的托盘类型") - return + # 重置表单 + self.reset_form(operation_type) - reply = QMessageBox.question(self, "确认删除", - f"确定要删除托盘类型 '{pallet_type['type_name']}' 吗?", - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - if reply != QMessageBox.Yes: - return + # 发送信号 + 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}") - # 删除托盘类型 - 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) + 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}") - - # 发送信号 - 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)}") - + # 更新托盘类型启用状态 + success = self.pallet_type_manager.update_pallet_type_status(pallet_type_id, enabled) + + 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() \ No newline at end of file + if success: + QMessageBox.information(self, "成功", "所有托盘类型已保存") + + # 发送信号 + self.signal_pallet_types_changed.emit() + self.settings_changed.emit() # 发送设置变更信号 + + logging.info("已保存所有托盘类型") + else: + QMessageBox.critical(self, "错误", "保存托盘类型失败") + logging.error("保存托盘类型失败") \ No newline at end of file diff --git a/widgets/serial_settings_widget.py b/widgets/serial_settings_widget.py new file mode 100644 index 0000000..29ab877 --- /dev/null +++ b/widgets/serial_settings_widget.py @@ -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}") \ No newline at end of file diff --git a/widgets/settings_window.py b/widgets/settings_window.py new file mode 100644 index 0000000..f8ad4ca --- /dev/null +++ b/widgets/settings_window.py @@ -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() \ No newline at end of file