From 6d60e777432222693ea486bb5f8e5c1ddc08f959 Mon Sep 17 00:00:00 2001 From: zhu-mengmeng <15588200382@163.com> Date: Mon, 30 Jun 2025 09:58:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/app_config.json | 15 +- db/jtDB.db | Bin 106496 -> 118784 bytes from pymodbus.py | 12 +- ui/serial_settings_ui.py | 43 ++++- ui/settings_ui.py | 10 +- utils/serial_manager.py | 272 ++++++++++++++++++++++++------ widgets/main_window.py | 162 +++++++++++++++--- widgets/serial_settings_widget.py | 224 ++++++++++++++++++++---- widgets/settings_widget.py | 20 +-- widgets/settings_window.py | 83 ++------- 10 files changed, 636 insertions(+), 205 deletions(-) diff --git a/config/app_config.json b/config/app_config.json index c284556..d348eb8 100644 --- a/config/app_config.json +++ b/config/app_config.json @@ -3,7 +3,7 @@ "name": "腾智微丝产线包装系统", "version": "1.0.0", "features": { - "enable_serial_ports": false, + "enable_serial_ports": true, "enable_keyboard_listener": false, "enable_camera": false }, @@ -49,8 +49,8 @@ "default_framerate": 30 }, "modbus": { - "host": "192.168.2.88", - "port": "502" + "host": "localhost", + "port": "5020" }, "serial": { "keyboard": { @@ -78,6 +78,15 @@ "stable_threshold": 10, "stop_bits": 1, "timeout": 1 + }, + "scanner": { + "code": "scanner", + "data_bits": 8, + "parity": "N", + "port": "9600", + "ser": "COM3", + "stop_bits": 1, + "timeout": 1 } }, "electricity": { diff --git a/db/jtDB.db b/db/jtDB.db index e0c99697c4ecd3d4a4310ad36f6e40089af287f4..f7cd1ace7a15819de505f76c0f4a10e4965c0f1e 100644 GIT binary patch delta 10752 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 IBIzvs7XZ)NH2?qr delta 664 zcmZozz}|3xZGsdV_bCPj2C0b(c1+x-HYWVBV`6+gInKU~O-_=R*_3JW1U8Y)|LoZc zWY~DmG4L80XHjbn99u;5@2L%U}j)qYR;4_BrrK{w_Lrkm8p@Q zp`nGDiGf#0yrYY+r=O95k*ThMnXZwEf}x3(2^JMzAeaI_G_^3ce6n}*i`IS5*DZWLq3!9!8AK~Y_Lrd%a|o|MJ#v7lGV;TXGE@M1$JiVg zJ23acrGU;cv@)_p3QMn$02gq~8W@3^_0W8J;i~kG%yUl_M_xZPf%x4r bool: """ 关闭串口 @@ -351,6 +372,35 @@ class SerialManager: logging.error(f"向串口 {port_name} 写入数据失败: {str(e)}") return False + def read_data(self, port_name: str, size: int = None) -> bytes: + """ + 从串口读取数据 + + Args: + port_name: 串口名称 + size: 要读取的字节数,如果为None则读取所有可用数据 + + Returns: + 读取的数据,如果失败则返回空字节 + """ + try: + if not self.is_port_open(port_name): + logging.error(f"尝试从未打开的串口 {port_name} 读取数据") + return b'' + + if size is None: + # 读取所有可用数据 + if self.serial_ports[port_name].in_waiting > 0: + return self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting) + return b'' + else: + # 读取指定数量的字节 + return self.serial_ports[port_name].read(size) + + except Exception as e: + logging.error(f"从串口 {port_name} 读取数据失败: {str(e)}") + return b'' + def _read_thread(self, port_name: str): """ 串口读取线程 @@ -534,7 +584,7 @@ class SerialManager: return # 构建数据字符串 - data_str = f"mdz:{self.data['mdz']}|cz:{self.data['cz']}|" + data_str = f"mdz:{self.data['mdz']}|cz:{self.data['cz']}|scanner:{self.data['scanner']}|" # 确保目录存在 data_dir = os.path.dirname(self.data_file) @@ -792,7 +842,7 @@ class SerialManager: """通知所有相关回调函数""" try: # 端口特定回调 (通常用于原始串口数据) - if port_name in self.callbacks and port_name not in ['mdz_data', 'xj_data']: # 避免重复处理 + if port_name in self.callbacks and port_name not in ['mdz_data', 'xj_data', 'scanner_data']: # 避免重复处理 try: # 假设这种回调期望原始的 value (可能是字节串,也可能是其他类型) self.callbacks[port_name](port_name, value) @@ -874,6 +924,37 @@ class SerialManager: else: logging.warning(f"回调失败: xj_data 中实际值为None. 初始 value: {value}") + # 全局回调, 特别处理 'scanner_data' + if 'scanner_data' in self.callbacks and port_name == 'scanner_data': + actual_scanner_value = None + source_info = "unknown" + + if isinstance(value, dict): + actual_scanner_value = value.get('value') + source_info = value.get('source', source_info) + elif isinstance(value, (str, bytes)): + if isinstance(value, bytes): + try: + actual_scanner_value = value.decode('utf-8').strip() + except: + actual_scanner_value = str(value) + else: + actual_scanner_value = value + + if actual_scanner_value is not None: + callback_data_str = f"扫码数据: {actual_scanner_value}" + try: + triggering_port = port_name if port_name not in ['scanner_data', 'scanner'] else self.scanner_config.get('ser', 'N/A') if self.scanner_config else 'N/A' + if source_info.startswith("mock"): + triggering_port = f"mock_{port_name}" + + self.callbacks['scanner_data'](triggering_port, callback_data_str.encode('utf-8')) + logging.info(f"通知 'scanner_data' 回调. 值: {actual_scanner_value}, 源: {source_info}, 触发源端口: {triggering_port}") + except Exception as e: + logging.error(f"调用全局回调 'scanner_data' 失败: {e}", exc_info=True) + else: + logging.warning(f"回调失败: scanner_data 中实际值为None. 初始 value: {value}") + except Exception as e: logging.error(f"通知回调失败: {e}", exc_info=True) @@ -893,14 +974,15 @@ class SerialManager: 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')) + (self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].startswith('COM')) or + (self.scanner_config and 'ser' in self.scanner_config and self.scanner_config['ser'] and self.scanner_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']: + if self.cz_config and 'ser' in self.cz_config and self.cz_config['ser'] and self.cz_config['ser'].strip(): port_name = self.cz_config['ser'] baud_rate = self.cz_config.get('port', 2400) @@ -917,10 +999,10 @@ class SerialManager: else: logging.info(f"线径串口 {port_name} 已经打开,无需重新打开") else: - logging.warning("线径串口未配置,跳过自动打开") + logging.warning("线径串口未配置或设置为不使用,跳过自动打开") # 尝试打开米电阻串口 - if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser']: + if self.mdz_config and 'ser' in self.mdz_config and self.mdz_config['ser'] and self.mdz_config['ser'].strip(): port_name = self.mdz_config['ser'] baud_rate = self.mdz_config.get('port', 9600) @@ -937,7 +1019,27 @@ class SerialManager: else: logging.info(f"米电阻串口 {port_name} 已经打开,无需重新打开") else: - logging.warning("米电阻串口未配置,跳过自动打开") + logging.warning("米电阻串口未配置或设置为不使用,跳过自动打开") + + # 尝试打开扫码器串口 + if self.scanner_config and 'ser' in self.scanner_config and self.scanner_config['ser'] and self.scanner_config['ser'].strip(): + port_name = self.scanner_config['ser'] + baud_rate = self.scanner_config.get('port', 9600) + + if not self.is_port_open(port_name): + try: + if self.open_port(port_name, 'scanner', 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("扫码器串口未配置或设置为不使用,跳过自动打开") # 注意:不在这里启动键盘监听器,而是在MainWindow的handle_start方法中显式调用start_keyboard_listener @@ -1022,4 +1124,74 @@ class SerialManager: return False except Exception as e: logging.error(f"处理线径数据总体异常: {e}") + return False + + def _read_scanner_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 > 0: + response = self.serial_ports[port_name].read(self.serial_ports[port_name].in_waiting) + self._process_scanner_response(port_name, response) + + time.sleep(0.1) + + except Exception as e: + logging.error(f"扫码器串口 {port_name} 读取线程异常: {e}") + + def _process_scanner_response(self, port_name, response_bytes: bytes): + """处理扫码器响应数据""" + try: + if response_bytes: # 确保有响应数据 + try: + # 尝试解码为字符串 + scanner_value = response_bytes.decode('utf-8').strip() + + # 记录日志 + logging.info(f"[{port_name}] 扫码数据: {scanner_value}") + + # 更新数据 + self.data['scanner'] = scanner_value + + # 写入文件并通知回调 + self._write_data_to_file() + # 使用"扫码数据: xxx"格式通知回调 + callback_data = f"扫码数据: {scanner_value}".encode('utf-8') + if 'scanner_data' in self.callbacks: + self.callbacks['scanner_data'](port_name, callback_data) + return True + except Exception as e: + logging.error(f"处理扫码数据异常: {e}") + # 解码失败,尝试直接使用字节数据 + # 记录日志(十六进制字符串) + hex_str = ' '.join(f'{b:02X}' for b in response_bytes) + logging.warning(f"[{port_name}] 扫码数据(十六进制): {hex_str}") + + # 更新数据(使用十六进制字符串) + self.data['scanner'] = hex_str + + # 写入文件并通知回调 + self._write_data_to_file() + # 使用"扫码数据: xxx"格式通知回调 + callback_data = f"扫码数据: {hex_str}".encode('utf-8') + if 'scanner_data' in self.callbacks: + self.callbacks['scanner_data'](port_name, callback_data) + return True + else: + logging.warning("扫码响应数据为空") + + # 如果无法解析,则直接返回失败 + return False + except Exception as e: + logging.error(f"处理扫码数据总体异常: {e}") return False \ No newline at end of file diff --git a/widgets/main_window.py b/widgets/main_window.py index 848452b..36caf7b 100644 --- a/widgets/main_window.py +++ b/widgets/main_window.py @@ -205,6 +205,10 @@ class MainWindow(MainWindowUI): # 加载托盘号列表 self.load_pallet_codes() + + # 恢复开始按钮原始样式 + self.restore_start_button_style() + def get_axios_num(self,tray_id): """获取托盘号对应的轴号""" from dao.inspection_dao import InspectionDAO @@ -369,10 +373,13 @@ class MainWindow(MainWindowUI): # 更新串口管理器配置 self.serial_manager.reload_config() + # 重新打开已配置的串口 + self.serial_manager.auto_open_configured_ports() + # 重新加载托盘号 self.load_pallet_codes() - logging.info("设置已更新,重新加载配置") + logging.info("设置已更新,重新加载配置并重新打开串口") def handle_input(self): """处理上料按钮点击事件""" @@ -508,6 +515,49 @@ class MainWindow(MainWindowUI): logging.error(f"处理下料操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"处理下料操作失败: {str(e)}") + def restore_start_button_style(self): + """恢复开始按钮的原始样式""" + try: + self.start_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: black; + border: 1px solid #4caf50; + border-radius: 4px; + padding: 2px 10px; + min-height: 29px; + max-height: 29px; + } + QPushButton:hover { + background-color: #f0f0f0; + } + """) + except Exception as e: + logging.error(f"{str(e)}") + + def fill_start_button_style(self): + """填充开始按钮样式 - 绿色背景,白色字体""" + try: + # 填充按钮样式 - 绿色背景,白色字体,高度与下料按钮一致 + self.start_button.setStyleSheet(""" + QPushButton { + background-color: #4caf50; + color: white; + border: 1px solid #4caf50; + border-radius: 4px; + padding: 2px 10px; + min-height: 24px; + max-height: 24px; + } + QPushButton:hover { + background-color: #4caf50; + color: white; + } + """) + logging.info("已填充开始按钮样式") + except Exception as e: + logging.error(f"填充开始按钮样式失败: {str(e)}") + def handle_start(self): """ 处理开始按钮点击事件 @@ -522,11 +572,14 @@ class MainWindow(MainWindowUI): if self._current_unload_info and self._current_unload_num > 0: # 下料模式 - 开始下料操作 # 确保寄存器3(下料启动)设为1,寄存器4已在handle_output中设置了当前层数 + success2 = modbus.write_register_until_success(client, 2, 1) success3 = modbus.write_register_until_success(client, 3, 1) - if success3: + if success2 and success3: logging.info(f"开始下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") QMessageBox.information(self, "操作提示", f"开始下料操作:当前第{self._current_unload_num}层") + # 填充按钮样式 + self.fill_start_button_style() else: QMessageBox.warning(self, "错误", "开始下料操作失败") else: @@ -538,8 +591,11 @@ class MainWindow(MainWindowUI): if success0 and success2: self._is_loading_active = True # 标记上料任务已开始 logging.info(f"开始上料操作:当前层数 {self._current_stow_num}") + # 填充按钮样式 + self.fill_start_button_style() else: QMessageBox.warning(self, "错误", "开始上料操作失败") + except Exception as e: logging.error(f"开始操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"开始操作失败: {str(e)}") @@ -559,6 +615,8 @@ class MainWindow(MainWindowUI): if success3: logging.info(f"停止下料操作:当前层数 {self._current_unload_num}/{self._total_unload_num}") QMessageBox.information(self, "操作提示", "已停止下料操作") + # 恢复按钮原始样式 + self.restore_start_button_style() else: QMessageBox.warning(self, "错误", "停止下料操作失败") else: @@ -569,8 +627,11 @@ class MainWindow(MainWindowUI): self._is_loading_active = False # 标记上料任务已停止 logging.info("停止上料操作") QMessageBox.information(self, "操作提示", "已停止上料操作") + # 恢复按钮原始样式 + self.restore_start_button_style() else: QMessageBox.warning(self, "错误", "停止上料操作失败") + except Exception as e: logging.error(f"停止操作失败: {str(e)}") QMessageBox.critical(self, "错误", f"停止操作失败: {str(e)}") @@ -1943,34 +2004,45 @@ class MainWindow(MainWindowUI): def handle_register_change(self, address, value): """处理寄存器变化""" logging.info(f"[处理] 寄存器D{address}变化: {value}") - # 在这里可以添加通用寄存器变化处理逻辑 - pass - + # 这里可以添加通用寄存器变化处理逻辑 + @Slot(int, str) def handle_loading_feedback(self, status, desc): """处理上料信息反馈""" - message = desc # Default message - if status == 1: - modbus = ModbusUtils() - client = modbus.get_client() - # 睡 0.5 秒,用于延缓modbus 监听 - time.sleep(0.5) - modbus.write_register_until_success(client, 2, 0) + message = desc + try: + if status == 1: + modbus = ModbusUtils() + client = modbus.get_client() + # 睡 0.5 秒,用于延缓modbus 监听 + time.sleep(0.5) + modbus.write_register_until_success(client, 2, 0) + if self._current_stow_num > 0: + completed_layer_num = self._current_stow_num + self._current_stow_num -= 1 + if self._current_stow_num == 0: + self._is_loading_active = False # 任务完成,标记为非活动 + self._loading_info = None + logging.info("所有层拆垛完成,清空上料信息") + message = f"第 {completed_layer_num} 层(最后一层)拆垛完成!" + # 重置寄存器 0 和 2 为 0 + modbus.write_register_until_success(client, 0, 0) + modbus.write_register_until_success(client, 2, 0) + self.loading_feedback_signal.emit("input", message) + # 恢复开始按钮原始样式 + self.restore_start_button_style() + else: + logging.info(f"当前层拆垛完成,剩余层数: {self._current_stow_num}") + message = f"第 {completed_layer_num} 层拆垛完成。" + self.loading_feedback_signal.emit("input", message) + #通知寄存器,进行第几层拆垛 + modbus.write_register_until_success(client,0 ,self._current_stow_num) + except Exception as e: + logging.error(f"处理上料信息反馈失败: {str(e)}") + # 不在这里显示对话框,而是通过信号传递错误信息 + self.loading_feedback_signal.emit("error", f"处理上料信息反馈失败: {str(e)}") + finally: modbus.close_client(client) - - if self._current_stow_num > 0: - completed_layer_num = self._current_stow_num - self._current_stow_num -= 1 - if self._current_stow_num == 0: - self._is_loading_active = False # 任务完成,标记为非活动 - self._loading_info = None - logging.info("所有层拆垛完成,清空上料信息") - message = f"第 {completed_layer_num} 层(最后一层)拆垛完成!" - else: - logging.info(f"当前层拆垛完成,剩余层数: {self._current_stow_num}") - message = f"第 {completed_layer_num} 层拆垛完成。" - - self.loading_feedback_signal.emit("input", message) def _handle_loading_feedback_ui(self, status_type, desc): """在主线程中处理上料UI更新""" @@ -1996,6 +2068,8 @@ class MainWindow(MainWindowUI): client = modbus.get_client() try: + # 睡 0.5 秒,用于延缓modbus 监听 + time.sleep(0.5) # 临时重置寄存器3(下料启动)为0,等待用户下一次启动 modbus.write_register_until_success(client, 3, 0) @@ -2013,6 +2087,9 @@ class MainWindow(MainWindowUI): # 通过信号触发UI更新 - 显示前一层完成的消息 message = f"第{self._current_unload_num-1}层下料完成,请启动第{self._current_unload_num}层下料" self.unloading_feedback_signal.emit("output", message) + + # 恢复开始按钮原始样式 + self.restore_start_button_style() else: # 所有层都下料完成,重置寄存器和计数器 modbus.write_register_until_success(client, 3, 0) # 确保下料启动寄存器为0 @@ -2034,6 +2111,9 @@ class MainWindow(MainWindowUI): # 通过信号触发UI更新,而不是直接操作UI message = f"托盘 {tray_code} 的所有 {total_tier} 层下料已全部完成" self.unloading_feedback_signal.emit("output", message) + + # 恢复开始按钮原始样式 + self.restore_start_button_style() except Exception as e: logging.error(f"处理下料反馈时发生错误: {str(e)}") # 不在这里显示对话框,而是通过信号传递错误信息 @@ -2292,6 +2372,9 @@ class MainWindow(MainWindowUI): # 注册线径数据回调 self.serial_manager.callbacks['xj_data'] = self.on_diameter_data_received + # 注册扫码器数据回调 + self.serial_manager.callbacks['scanner_data'] = self.on_scanner_data_received + # 自动打开已配置的串口 self.serial_manager.auto_open_configured_ports() @@ -2402,6 +2485,33 @@ class MainWindow(MainWindowUI): except Exception as e: logging.error(f"处理线径数据失败: {str(e)}") + def on_scanner_data_received(self, port_name, data): + """扫码器数据接收回调函数 + + Args: + port_name: 串口名称 + data: 接收到的数据 + """ + try: + # 解析数据 + data_str = data.decode('utf-8') if isinstance(data, bytes) else str(data) + logging.info(f"收到扫码器数据: {data_str} 来自 {port_name}") + + # 提取扫码数据,格式为"扫码数据: xxx" + if "扫码数据:" in data_str: + gc_note = data_str.split("扫码数据:")[1].strip() + logging.info(f"提取到工程号: {gc_note}") + + # 设置工程号到输入框 + self.order_edit.setText(gc_note) + + # 模拟按下回车键,触发handle_order_enter方法 + self.handle_order_enter() + else: + logging.warning(f"收到的数据不包含扫码数据标记: {data_str}") + except Exception as e: + logging.error(f"处理扫码器数据失败: {str(e)}") + def set_inspection_value(self, data_type, config, value): """设置检验项目值到表格中 diff --git a/widgets/serial_settings_widget.py b/widgets/serial_settings_widget.py index c9cbd9e..5e5f632 100644 --- a/widgets/serial_settings_widget.py +++ b/widgets/serial_settings_widget.py @@ -32,6 +32,10 @@ class SerialSettingsWidget(SerialSettingsUI): self.xj_refresh_btn.clicked.connect(self.refresh_ports) self.test_xj_btn.clicked.connect(self.test_xj_port) + # 扫码器串口 + self.scanner_refresh_btn.clicked.connect(self.refresh_ports) + self.test_scanner_btn.clicked.connect(self.test_scanner_port) + # 保存按钮 self.save_btn.clicked.connect(self.save_settings) @@ -46,9 +50,18 @@ class SerialSettingsWidget(SerialSettingsUI): try: # 保存当前选择 current_mdz_port = self.mdz_port_combo.currentData() + current_xj_port = self.xj_port_combo.currentData() + current_scanner_port = self.scanner_port_combo.currentData() # 清空列表 self.mdz_port_combo.clear() + self.xj_port_combo.clear() + self.scanner_port_combo.clear() + + # 添加"不使用"选项 + self.mdz_port_combo.addItem("不使用", "") + self.xj_port_combo.addItem("不使用", "") + self.scanner_port_combo.addItem("不使用", "") # 获取可用串口 ports = list(serial.tools.list_ports.comports()) @@ -56,16 +69,41 @@ class SerialSettingsWidget(SerialSettingsUI): 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.xj_port_combo.addItem(port_desc, port_name) + + # 添加到扫码器下拉框 + self.scanner_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) + else: + # 如果之前没有选择,则设为"不使用" + self.mdz_port_combo.setCurrentIndex(0) + + if current_xj_port: + index = self.xj_port_combo.findData(current_xj_port) + if index >= 0: + self.xj_port_combo.setCurrentIndex(index) + else: + # 如果之前没有选择,则设为"不使用" + self.xj_port_combo.setCurrentIndex(0) + + if current_scanner_port: + index = self.scanner_port_combo.findData(current_scanner_port) + if index >= 0: + self.scanner_port_combo.setCurrentIndex(index) + else: + # 如果之前没有选择,则设为"不使用" + self.scanner_port_combo.setCurrentIndex(0) - - logging.info(f"已刷新串口列表,找到 {len(ports)} 个串口") except Exception as e: logging.error(f"刷新串口列表失败: {e}") @@ -155,6 +193,39 @@ class SerialSettingsWidget(SerialSettingsUI): if index >= 0: self.xj_parity_combo.setCurrentIndex(index) + # 加载扫码器设置 + scanner_config = self.config.get_config('scanner') + if scanner_config: + # 设置串口 + scanner_port = scanner_config.get('ser', '') + index = self.scanner_port_combo.findData(scanner_port) + if index >= 0: + self.scanner_port_combo.setCurrentIndex(index) + + # 设置波特率 + scanner_baud = str(scanner_config.get('port', '9600')) + index = self.scanner_baud_combo.findText(scanner_baud) + if index >= 0: + self.scanner_baud_combo.setCurrentIndex(index) + + # 设置数据位 + scanner_data_bits = str(scanner_config.get('data_bits', '8')) + index = self.scanner_data_bits_combo.findText(scanner_data_bits) + if index >= 0: + self.scanner_data_bits_combo.setCurrentIndex(index) + + # 设置停止位 + scanner_stop_bits = str(scanner_config.get('stop_bits', '1')) + index = self.scanner_stop_bits_combo.findText(scanner_stop_bits) + if index >= 0: + self.scanner_stop_bits_combo.setCurrentIndex(index) + + # 设置校验位 + scanner_parity = scanner_config.get('parity', 'N') + index = self.scanner_parity_combo.findData(scanner_parity) + if index >= 0: + self.scanner_parity_combo.setCurrentIndex(index) + logging.info("已加载串口设置") except Exception as e: logging.error(f"加载串口设置失败: {e}") @@ -180,7 +251,6 @@ class SerialSettingsWidget(SerialSettingsUI): mdz_query_interval = self.mdz_query_interval.value() mdz_config = { - 'ser': mdz_port, 'port': mdz_baud, 'data_bits': mdz_data_bits, 'stop_bits': mdz_stop_bits, @@ -189,6 +259,10 @@ class SerialSettingsWidget(SerialSettingsUI): 'query_interval': mdz_query_interval } + # 只有当用户选择了串口时才保存串口配置 + if mdz_port: + mdz_config['ser'] = mdz_port + self.config.set_config('mdz', mdz_config) # 保存线径设置 @@ -199,17 +273,37 @@ class SerialSettingsWidget(SerialSettingsUI): xj_parity = self.xj_parity_combo.currentData() xj_config = { - 'ser': xj_port, 'port': xj_baud, 'data_bits': xj_data_bits, 'stop_bits': xj_stop_bits, 'parity': xj_parity } + # 只有当用户选择了串口时才保存串口配置 + if xj_port: + xj_config['ser'] = xj_port + self.config.set_config('xj', xj_config) - + # 保存扫码器设置 + scanner_port = self.scanner_port_combo.currentData() + scanner_baud = int(self.scanner_baud_combo.currentText()) + scanner_data_bits = int(self.scanner_data_bits_combo.currentText()) + scanner_stop_bits = float(self.scanner_stop_bits_combo.currentText()) + scanner_parity = self.scanner_parity_combo.currentData() + scanner_config = { + 'port': scanner_baud, + 'data_bits': scanner_data_bits, + 'stop_bits': scanner_stop_bits, + 'parity': scanner_parity + } + + # 只有当用户选择了串口时才保存串口配置 + if scanner_port: + scanner_config['ser'] = scanner_port + + self.config.set_config('scanner', scanner_config) # 发送设置变更信号 self.settings_changed.emit() @@ -224,15 +318,16 @@ class SerialSettingsWidget(SerialSettingsUI): """测试米电阻串口""" try: port = self.mdz_port_combo.currentData() + + if not port: + QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"") + return + 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) @@ -248,44 +343,46 @@ class SerialSettingsWidget(SerialSettingsUI): if query_cmd: try: # 转换查询指令为字节 - query_bytes = bytes.fromhex(query_cmd.replace(' ', '')) + cmd_bytes = bytes.fromhex(query_cmd.replace(' ', '')) + self.serial_manager.write_data(port, cmd_bytes) + time.sleep(0.1) # 等待响应 - # 发送查询指令 - self.serial_manager.write_data(port, query_bytes) - - # 等待一段时间 - time.sleep(0.5) - - # 关闭串口 - self.serial_manager.close_port(port) - - QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 测试成功,已发送查询指令") + # 读取响应 + response = self.serial_manager.read_data(port) + if response: + # 将字节转换为十六进制字符串 + hex_str = ' '.join(f'{b:02X}' for b in response) + QMessageBox.information(self, "测试成功", f"串口打开成功,收到响应:\n{hex_str}") + else: + QMessageBox.information(self, "测试成功", "串口打开成功,但未收到响应") except Exception as e: - self.serial_manager.close_port(port) - QMessageBox.warning(self, "测试失败", f"发送查询指令失败: {e}") + QMessageBox.warning(self, "测试结果", f"串口打开成功,但发送指令失败: {e}") else: - self.serial_manager.close_port(port) - QMessageBox.information(self, "测试成功", f"米电阻串口 {port} 打开成功,但未发送查询指令") + QMessageBox.information(self, "测试成功", "串口打开成功") + + # 关闭串口 + self.serial_manager.close_port(port) else: - QMessageBox.warning(self, "测试失败", f"无法打开米电阻串口 {port}") + QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}") + except Exception as e: logging.error(f"测试米电阻串口失败: {e}") - QMessageBox.warning(self, "测试失败", f"测试米电阻串口失败: {e}") - + QMessageBox.critical(self, "测试失败", f"测试米电阻串口失败: {e}") def test_xj_port(self): """测试线径串口""" try: port = self.xj_port_combo.currentData() + + if not port: + QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"") + return + baud = int(self.xj_baud_combo.currentText()) data_bits = int(self.xj_data_bits_combo.currentText()) stop_bits = float(self.xj_stop_bits_combo.currentText()) parity = self.xj_parity_combo.currentData() - if not port: - QMessageBox.warning(self, "测试失败", "请选择串口") - return - # 关闭可能已经打开的串口 if self.serial_manager.is_port_open(port): self.serial_manager.close_port(port) @@ -296,15 +393,68 @@ class SerialSettingsWidget(SerialSettingsUI): ) if success: - # 等待一段时间 - time.sleep(2) + QMessageBox.information(self, "测试成功", f"串口 {port} 打开成功") # 关闭串口 self.serial_manager.close_port(port) - - QMessageBox.information(self, "测试成功", f"线径串口 {port} 测试成功") else: - QMessageBox.warning(self, "测试失败", f"无法打开线径串口 {port}") + QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}") + except Exception as e: logging.error(f"测试线径串口失败: {e}") - QMessageBox.warning(self, "测试失败", f"测试线径串口失败: {e}") \ No newline at end of file + QMessageBox.critical(self, "测试失败", f"测试线径串口失败: {e}") + + def test_scanner_port(self): + """测试扫码器串口""" + try: + port = self.scanner_port_combo.currentData() + + if not port: + QMessageBox.warning(self, "测试失败", "请先选择串口,当前设置为\"不使用\"") + return + + baud = int(self.scanner_baud_combo.currentText()) + data_bits = int(self.scanner_data_bits_combo.currentText()) + stop_bits = float(self.scanner_stop_bits_combo.currentText()) + parity = self.scanner_parity_combo.currentData() + + # 关闭可能已经打开的串口 + if self.serial_manager.is_port_open(port): + self.serial_manager.close_port(port) + + # 尝试打开串口 + success = self.serial_manager.open_port( + port, 'scanner', baud, data_bits, stop_bits, parity, 1.0 + ) + + if success: + QMessageBox.information(self, "测试成功", f"串口 {port} 打开成功\n请触发扫码器进行扫描测试") + + # 尝试读取数据(短暂等待扫码器输入) + start_time = time.time() + timeout = 5.0 # 5秒超时 + + while time.time() - start_time < timeout: + response = self.serial_manager.read_data(port) + if response: + # 尝试将字节解码为字符串 + try: + text = response.decode('utf-8').strip() + QMessageBox.information(self, "测试成功", f"收到扫码数据:\n{text}") + break + except: + # 如果解码失败,显示十六进制 + hex_str = ' '.join(f'{b:02X}' for b in response) + QMessageBox.information(self, "测试成功", f"收到扫码数据 (十六进制):\n{hex_str}") + break + + time.sleep(0.1) # 短暂休眠,减少CPU占用 + + # 关闭串口 + self.serial_manager.close_port(port) + else: + QMessageBox.critical(self, "测试失败", f"无法打开串口 {port}") + + except Exception as e: + logging.error(f"测试扫码器串口失败: {e}") + QMessageBox.critical(self, "测试失败", f"测试扫码器串口失败: {e}") \ No newline at end of file diff --git a/widgets/settings_widget.py b/widgets/settings_widget.py index 6d00ee4..640dd8c 100644 --- a/widgets/settings_widget.py +++ b/widgets/settings_widget.py @@ -85,28 +85,28 @@ class SettingsWidget(SettingsUI): self.pallet_type_settings = PalletTypeSettingsWidget(self) self.plc_settings = PLCSettingsWidget(self) - # 创建电量监控设置组件 + # 创建电力监控设置组件 try: self.electricity_settings = ElectricitySettingsUI(self) - logging.info("电量监控设置组件创建成功") + logging.info("电力监控设置组件创建成功") - # 移除临时占位符标签并添加电量监控设置部件 + # 移除临时占位符标签并添加电力监控设置部件 if hasattr(self, 'electricity_placeholder'): - logging.info("移除电量监控临时占位符") + logging.info("移除电力监控临时占位符") self.electricity_layout.removeWidget(self.electricity_placeholder) self.electricity_placeholder.hide() self.electricity_placeholder.deleteLater() else: - logging.warning("未找到电量监控临时占位符标签") + logging.warning("未找到电力监控临时占位符标签") # 检查布局是否可用 if hasattr(self, 'electricity_layout'): - logging.info("添加电量监控设置部件到布局") + logging.info("添加电力监控设置部件到布局") self.electricity_layout.addWidget(self.electricity_settings) else: logging.error("无法找到electricity_layout布局") except Exception as e: - logging.error(f"初始化电量监控设置组件失败: {e}") + logging.error(f"初始化电力监控设置组件失败: {e}") # 移除临时占位符标签并添加检验设置部件 if hasattr(self, 'inspection_placeholder'): @@ -172,13 +172,13 @@ class SettingsWidget(SettingsUI): self.pallet_type_settings.settings_changed.connect(self.on_settings_changed) self.plc_settings.settings_changed.connect(self.on_settings_changed) - # 连接电量监控设置信号 + # 连接电力监控设置信号 if hasattr(self, 'electricity_settings'): try: self.electricity_settings.settings_changed.connect(self.on_settings_changed) - logging.info("已连接电量监控设置信号") + logging.info("已连接电力监控设置信号") except Exception as e: - logging.error(f"连接电量监控设置信号时出错: {e}") + logging.error(f"连接电力监控设置信号时出错: {e}") # 不再在这里连接刷新按钮,避免重复连接 logging.info("刷新按钮已在初始化时连接,不再重复连接") diff --git a/widgets/settings_window.py b/widgets/settings_window.py index 7e70ee6..7d8e5b7 100644 --- a/widgets/settings_window.py +++ b/widgets/settings_window.py @@ -28,54 +28,23 @@ class SettingsWindow(QDialog): 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, "串口设置") + # 应用相机刷新按钮修复 - try: - # 先尝试直接找到刷新按钮并添加事件处理 - logging.info("尝试直接查找刷新按钮并添加事件处理...") - from PySide6.QtWidgets import QPushButton - - # 在整个UI层次中查找刷新设备按钮 - refresh_buttons = [] - for widget in self.findChildren(QPushButton): - if widget.text() == "刷新设备": - refresh_buttons.append(widget) - - if refresh_buttons: - logging.info(f"找到 {len(refresh_buttons)} 个刷新设备按钮") - for i, button in enumerate(refresh_buttons): - try: - # 断开现有连接 - button.clicked.disconnect() - except Exception: - pass - - # 连接到相机设置的刷新方法 - if hasattr(self.settings_widget, 'camera_settings') and hasattr(self.settings_widget.camera_settings, 'refresh_devices'): - button.clicked.connect(self.settings_widget.camera_settings.refresh_devices) - logging.info(f"已将刷新按钮 #{i+1} 连接到refresh_devices方法") - - # 尝试调用一次 - self.settings_widget.camera_settings.refresh_devices() - logging.info("已手动调用refresh_devices方法初始化设备列表") - - # 使用导入的修复模块作为备选方案 - try: - # 使用相对导入,更可靠 - from . import refresh_devices_fix - refresh_devices_fix.fix_camera_refresh_button(self.settings_widget) - logging.info("已应用相机刷新按钮修复(通过相对导入)") - except ImportError: - # 如果相对导入失败,尝试绝对导入 - try: - import refresh_devices_fix - refresh_devices_fix.fix_camera_refresh_button(self.settings_widget) - logging.info("已应用相机刷新按钮修复(通过绝对导入)") - except ImportError: - # 如果绝对导入也失败,直接内联实现修复逻辑 - logging.warning("导入refresh_devices_fix失败,使用内联修复") - self._fix_camera_refresh_button() - except Exception as e: - logging.error(f"应用相机刷新按钮修复失败: {str(e)}") + self._fix_camera_refresh_button() + + # 添加按钮 + 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 _fix_camera_refresh_button(self): """内联实现的刷新按钮修复逻辑,用于导入模块失败的情况""" @@ -134,26 +103,6 @@ class SettingsWindow(QDialog): except Exception as e: logging.error(f"内联修复相机刷新按钮时发生错误: {str(e)}") - - # 添加串口设置到标签页 - self.serial_settings = SerialSettingsWidget(self) - self.settings_widget.tab_widget.addTab(self.serial_settings, "串口设置") - - # 添加电力监控设置到标签页 - self.electricity_settings = ElectricitySettingsUI(self) - self.settings_widget.tab_widget.addTab(self.electricity_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) - self.electricity_settings.settings_changed.connect(self.settings_changed.emit) - - logging.info("SettingsWindow初始化完成") def accept(self): """确认按钮处理,保存所有设置并发送设置变更信号"""