From 1d0d6d0b9f0f6f9cdb65c02183d36f80c9a261a6 Mon Sep 17 00:00:00 2001 From: ray <1416431931@qq.com> Date: Thu, 6 Nov 2025 09:48:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=85=8D=E7=BD=AE=E5=AF=B9?= =?UTF-8?q?=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui_config.py | 96 ++++++++++++++++---- main_single.py | 58 +++++++----- utils/device_scanner.py | 197 ++++++++++++++++++++++++++++++++++++++++ utils/mouse.py | 34 +++++-- yolotest2.py | 1 + 5 files changed, 338 insertions(+), 48 deletions(-) create mode 100644 utils/device_scanner.py diff --git a/gui_config.py b/gui_config.py index 9bdf9e7..92b4321 100644 --- a/gui_config.py +++ b/gui_config.py @@ -214,13 +214,28 @@ class ConfigGUI: # 确保串口下拉框列表与当前值一致 if group and 'serial_port' in group: - # 如果当前串口不在选项里,追加 values = list(self.serial_port_cb.cget('values')) if self.serial_port_cb.cget('values') else [] port_value = group['serial_port'] - if port_value not in values: - values.append(port_value) - self.serial_port_cb['values'] = values - self.serial_port_cb.set(port_value) + + # 如果当前值不在列表中,尝试通过设备名匹配 + matched = False + if hasattr(self, 'port_display_map'): + for display_text, device in self.port_display_map.items(): + if device == port_value: + if display_text not in values: + values.append(display_text) + self.serial_port_cb.set(display_text) + matched = True + break + + if not matched: + # 如果找不到匹配,直接添加原始值 + if port_value not in values: + values.append(port_value) + if not hasattr(self, 'port_display_map'): + self.port_display_map = {} + self.port_display_map[port_value] = port_value + self.serial_port_cb.set(port_value) def scan_cameras(self, max_index: int = 10): """扫描系统可用的采集卡索引,并填充下拉框""" @@ -302,23 +317,59 @@ class ConfigGUI: self.camera_index_cb.set(found[0]) def scan_ports(self): - """扫描系统可用的串口,并填充下拉框""" + """扫描系统可用的串口,并填充下拉框(显示设备描述)""" + from utils.device_scanner import scan_serial_ports_with_info + found_real = [] + port_display_map = {} # 显示文本 -> 实际设备名 + try: - ports = serial.tools.list_ports.comports() - found_real = [port.device for port in ports] - # 按端口名排序 - found_real.sort(key=lambda x: int(x.replace('COM', '')) if x.replace('COM', '').isdigit() else 999) + ports_info = scan_serial_ports_with_info() + for port_info in ports_info: + device = port_info['device'] + description = port_info.get('description', '') + hwid = port_info.get('hwid', '') + + # 显示格式:COM3 (设备描述) [HWID] + if description: + display_text = f"{device} ({description})" + else: + display_text = device + + # 如果有HWID,添加到显示中(较短版本) + if hwid: + # 提取VID/PID部分(如果存在) + if 'VID_' in hwid and 'PID_' in hwid: + import re + vid_match = re.search(r'VID_([0-9A-F]+)', hwid) + pid_match = re.search(r'PID_([0-9A-F]+)', hwid) + if vid_match and pid_match: + display_text += f" [{vid_match.group(1)}:{pid_match.group(1)}]" + + found_real.append(display_text) + port_display_map[display_text] = device except Exception as e: print(f"扫描串口错误: {e}") + # 回退到简单扫描 + try: + ports = serial.tools.list_ports.comports() + found_real = [port.device for port in ports] + found_real.sort(key=lambda x: int(x.replace('COM', '')) if x.replace('COM', '').isdigit() else 999) + port_display_map = {p: p for p in found_real} + except: + pass # 如果没有发现实际端口,使用默认端口列表 if not found_real: - found = ["COM1", "COM2", "COM3", "COM4", "COM5", "COM6"] # 默认给一些常见端口 + found = ["COM1", "COM2", "COM3", "COM4", "COM5", "COM6"] + port_display_map = {p: p for p in found} messagebox.showwarning("警告", "未发现可用串口设备,已添加常用默认选项") else: found = found_real - messagebox.showinfo("扫描完成", f"发现可用串口: {', '.join(found)}") + messagebox.showinfo("扫描完成", f"发现 {len(found)} 个串口设备\n\n设备信息已包含描述和硬件ID") + + # 保存映射关系,用于后续获取实际设备名 + self.port_display_map = port_display_map self.serial_port_cb['values'] = found # 若当前无选择,则选择第一项 @@ -390,6 +441,11 @@ class ConfigGUI: else: # 字符串字段 value = value_str if value_str else group.get(key, '') + # 特殊处理串口:从显示文本中提取实际设备名 + if key == 'serial_port' and hasattr(self, 'port_display_map'): + # 如果值是显示文本,转换为实际设备名 + if value in self.port_display_map: + value = self.port_display_map[value] group[key] = value except Exception as e: @@ -467,6 +523,11 @@ class ConfigGUI: else: # 字符串字段 value = value_str if value_str else group.get(key, '') + # 特殊处理串口:从显示文本中提取实际设备名 + if key == 'serial_port' and hasattr(self, 'port_display_map'): + # 如果值是显示文本,转换为实际设备名 + if value in self.port_display_map: + value = self.port_display_map[value] group[key] = value except Exception as e: @@ -500,7 +561,7 @@ class ConfigGUI: return result def start_program(self): - """启动单个配置组的主程序""" + """启动单个配置组的主程序并自动弹出采集卡预览窗口""" # 保存配置(静默) if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败") @@ -527,11 +588,13 @@ class ConfigGUI: ], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0) messagebox.showinfo("成功", f"已启动配置组: {active_group['name']}\n\n请在控制台查看运行状态") + # 成功后弹出预览 + self.start_preview() except Exception as e: messagebox.showerror("错误", f"启动失败: {e}") def start_multi_program(self): - """启动多个配置组的主程序""" + """启动多个配置组的主程序并自动弹出采集卡预览窗口""" # 保存配置(静默) if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败") @@ -547,6 +610,8 @@ class ConfigGUI: ], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0) messagebox.showinfo("提示", "多配置组启动器已打开\n\n请在控制台中选择要启动的配置组") + # 成功后弹出预览 + self.start_preview() except Exception as e: messagebox.showerror("错误", f"启动失败: {e}") @@ -556,5 +621,4 @@ class ConfigGUI: if __name__ == "__main__": app = ConfigGUI() - app.run() - + app.run() \ No newline at end of file diff --git a/main_single.py b/main_single.py index edc6320..892a4fe 100644 --- a/main_single.py +++ b/main_single.py @@ -69,28 +69,42 @@ def run_automation_for_group(group_index): v = group['move_velocity'] # 从配置读取移动速度 def yolo_shibie(im_PIL, detections, model): - results = model(im_PIL) - for result in results: - for i in range(len(result.boxes.xyxy)): - left, top, right, bottom = result.boxes.xyxy[i] - scalar_tensor = result.boxes.cls[i] - value = scalar_tensor.item() - label = result.names[int(value)] - if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi': - player_x = int(left + (right - left) / 2) - player_y = int(top + (bottom - top) / 2) + 30 - RW = [player_x, player_y] - detections[label] = RW - elif label == 'daojv' or label == 'gw': - player_x = int(left + (right - left) / 2) - player_y = int(top + (bottom - top) / 2) + 30 - RW = [player_x, player_y] - detections[label].append(RW) - elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4': - player_x = int(left + (right - left) / 2) - player_y = int(bottom) + 30 - RW = [player_x, player_y] - detections[label] = RW + try: + results = model(im_PIL) + for result in results: + if result.boxes is None or len(result.boxes.xyxy) == 0: + continue + for i in range(len(result.boxes.xyxy)): + try: + left = float(result.boxes.xyxy[i][0]) + top = float(result.boxes.xyxy[i][1]) + right = float(result.boxes.xyxy[i][2]) + bottom = float(result.boxes.xyxy[i][3]) + cls_id = int(result.boxes.cls[i]) + label = result.names[cls_id] + + if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi': + player_x = int(left + (right - left) / 2) + 3 + player_y = int(top + (bottom - top) / 2) + 40 + RW = [player_x, player_y] + detections[label] = RW + elif label == 'daojv' or label == 'gw': + player_x = int(left + (right - left) / 2) + 3 + player_y = int(top + (bottom - top) / 2) + 40 + RW = [player_x, player_y] + if label not in detections: + detections[label] = [] + detections[label].append(RW) + elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4': + player_x = int(left + (right - left) / 2) + player_y = int(bottom) + 30 + RW = [player_x, player_y] + detections[label] = RW + except Exception as e: + print(f"⚠️ 处理检测框时出错: {e}") + continue + except Exception as e: + print(f"⚠️ YOLO检测出错: {e}") return detections def sq(p1, p2): diff --git a/utils/device_scanner.py b/utils/device_scanner.py new file mode 100644 index 0000000..bb1ed59 --- /dev/null +++ b/utils/device_scanner.py @@ -0,0 +1,197 @@ +""" +设备扫描工具 - 通过唯一标识符而非索引来识别设备 +解决重启后设备序号混乱的问题 +""" +import cv2 +import serial.tools.list_ports +import warnings +import sys +import io +from typing import List, Dict, Optional + +def get_camera_info(cam_index: int) -> Optional[Dict]: + """ + 获取采集卡的详细信息(包括设备名称) + :param cam_index: 采集卡索引 + :return: 包含设备信息的字典,如果失败返回None + """ + old_stderr = sys.stderr + suppressed_output = io.StringIO() + + try: + sys.stderr = suppressed_output + + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + + # 尝试多种后端 + backends = [ + (cam_index, cv2.CAP_DSHOW), + (cam_index, cv2.CAP_ANY), + (cam_index, None), + ] + + cap = None + for idx, backend in backends: + try: + if backend is not None: + cap = cv2.VideoCapture(idx, backend) + else: + cap = cv2.VideoCapture(idx) + + if cap.isOpened(): + ret, frame = cap.read() + if ret and frame is not None: + # 尝试获取设备名称(不同后端可能支持不同) + device_name = None + backend_name = None + + if backend == cv2.CAP_DSHOW: + backend_name = "DirectShow" + # DirectShow可能支持获取设备名称 + try: + # 某些情况下可以通过属性获取 + pass # OpenCV限制,无法直接获取设备名称 + except: + pass + elif backend == cv2.CAP_ANY: + backend_name = "Any" + else: + backend_name = "Default" + + # 获取分辨率信息作为辅助标识 + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + cap.release() + + return { + 'index': cam_index, + 'backend': backend_name, + 'width': width, + 'height': height, + 'name': f"采集卡-{cam_index}", + 'available': True + } + else: + if cap: + cap.release() + cap = None + except Exception: + if cap: + try: + cap.release() + except: + pass + cap = None + continue + + return None + finally: + sys.stderr = old_stderr + +def scan_cameras_with_info(max_index: int = 10) -> List[Dict]: + """ + 扫描所有可用采集卡,返回详细信息列表 + :param max_index: 最大扫描索引 + :return: 采集卡信息列表 + """ + cameras = [] + for i in range(max_index + 1): + info = get_camera_info(i) + if info: + cameras.append(info) + return cameras + +def get_serial_port_info(port_name: str) -> Optional[Dict]: + """ + 获取串口的详细信息(包括硬件ID和描述) + :param port_name: 串口名称(如 "COM3") + :return: 包含串口信息的字典 + """ + try: + ports = serial.tools.list_ports.comports() + for port in ports: + if port.device == port_name: + return { + 'device': port.device, + 'description': port.description, + 'hwid': port.hwid, # 硬件ID(唯一标识) + 'vid': port.vid if hasattr(port, 'vid') else None, + 'pid': port.pid if hasattr(port, 'pid') else None, + 'serial_number': port.serial_number if hasattr(port, 'serial_number') else None, + 'manufacturer': port.manufacturer if hasattr(port, 'manufacturer') else None, + 'name': port.description or port.device + } + except Exception as e: + print(f"⚠️ 获取串口信息失败: {e}") + + return None + +def scan_serial_ports_with_info() -> List[Dict]: + """ + 扫描所有可用串口,返回详细信息列表 + :return: 串口信息列表 + """ + ports_info = [] + try: + ports = serial.tools.list_ports.comports() + for port in ports: + info = { + 'device': port.device, + 'description': port.description, + 'hwid': port.hwid, + 'vid': port.vid if hasattr(port, 'vid') else None, + 'pid': port.pid if hasattr(port, 'pid') else None, + 'serial_number': port.serial_number if hasattr(port, 'serial_number') else None, + 'manufacturer': port.manufacturer if hasattr(port, 'manufacturer') else None, + 'name': port.description or port.device + } + ports_info.append(info) + except Exception as e: + print(f"⚠️ 扫描串口失败: {e}") + + # 按设备名排序 + ports_info.sort(key=lambda x: int(x['device'].replace('COM', '')) if x['device'].replace('COM', '').isdigit() else 999) + return ports_info + +def find_camera_by_index(cameras: List[Dict], index: int) -> Optional[Dict]: + """通过索引查找采集卡""" + for cam in cameras: + if cam['index'] == index: + return cam + return None + +def find_serial_by_hwid(ports: List[Dict], hwid: str) -> Optional[Dict]: + """通过硬件ID查找串口(最可靠)""" + for port in ports: + if port['hwid'] == hwid: + return port + return None + +def find_serial_by_device(ports: List[Dict], device: str) -> Optional[Dict]: + """通过设备名查找串口(兼容旧配置)""" + for port in ports: + if port['device'] == device: + return port + return None + +if __name__ == "__main__": + # 测试代码 + print("=" * 60) + print("采集卡扫描:") + print("=" * 60) + cameras = scan_cameras_with_info() + for cam in cameras: + print(f" 索引 {cam['index']}: {cam['name']} ({cam['width']}x{cam['height']})") + + print("\n" + "=" * 60) + print("串口扫描:") + print("=" * 60) + ports = scan_serial_ports_with_info() + for port in ports: + print(f" {port['device']}: {port['name']}") + print(f" 硬件ID: {port['hwid']}") + if port['vid'] and port['pid']: + print(f" VID/PID: {port['vid']:04X}/{port['pid']:04X}") + diff --git a/utils/mouse.py b/utils/mouse.py index c46a5b3..dac9a3e 100644 --- a/utils/mouse.py +++ b/utils/mouse.py @@ -10,18 +10,32 @@ mouse = None def init_mouse_keyboard(config_group): """初始化鼠标和串口""" global serial, mouse + from utils.logger import logger + # 初始化串口 - serial.ser = serial.Serial( - config_group['serial_port'], - config_group['serial_baudrate'] - ) + try: + logger.info(f"🔧 正在打开串口: {config_group['serial_port']} @ {config_group['serial_baudrate']}") + serial.ser = serial.Serial( + config_group['serial_port'], + config_group['serial_baudrate'], + timeout=1 + ) + logger.info(f"✅ 串口已打开: {config_group['serial_port']} @ {config_group['serial_baudrate']}") + except Exception as e: + logger.error(f"❌ 串口打开失败: {e}") + raise + # 初始化鼠标 - mouse = ch9329Comm.mouse.DataComm( - config_group['camera_width'], - config_group['camera_height'] - ) - print(f"✅ 串口已打开: {config_group['serial_port']} {config_group['serial_baudrate']}") - print(f"✅ 鼠标已初始化: {config_group['camera_width']}x{config_group['camera_height']}") + try: + logger.info(f"🔧 正在初始化鼠标: {config_group['camera_width']}x{config_group['camera_height']}") + mouse = ch9329Comm.mouse.DataComm( + config_group['camera_width'], + config_group['camera_height'] + ) + logger.info(f"✅ 鼠标已初始化: {config_group['camera_width']}x{config_group['camera_height']}") + except Exception as e: + logger.error(f"❌ 鼠标初始化失败: {e}") + raise def bezier_point(t, p0, p1, p2, p3): """计算三次贝塞尔曲线上的点""" diff --git a/yolotest2.py b/yolotest2.py index 22f40cd..95a5c6e 100644 --- a/yolotest2.py +++ b/yolotest2.py @@ -224,3 +224,4 @@ def main(): if __name__ == "__main__": main() +