From 26b6bc3e0aa956c331e278c6cd2c72f63ee63865 Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Thu, 18 Sep 2025 09:07:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=95=88=E7=8E=87=E5=92=8C=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/check_monitor_status.py | 186 +++++++ backend/devices/base_device.py | 90 +--- backend/devices/camera_manager.py | 372 ++++++++++++-- backend/devices/device_coordinator.py | 147 ++++-- backend/devices/femtobolt_manager.py | 9 +- backend/devices/imu_manager.py | 179 ++----- backend/devices/pressure_manager.py | 467 ++++-------------- backend/devices/utils/config.ini | 8 +- backend/devices/utils/config_manager.py | 14 +- backend/main.py | 81 ++- backend/requirements.txt | 3 +- backend/simple_camera_test.py | 133 +++++ backend/test_avoid_duplicate_init.py | 75 +++ backend/test_backend_optimization.py | 235 +++++++++ backend/test_camera_analysis.py | 143 ++++++ backend/test_camera_disconnect.py | 194 ++++++++ backend/test_camera_full.py | 212 ++++++++ backend/test_camera_performance.py | 217 ++++++++ backend/test_camera_simple.py | 148 ++++++ backend/test_opencv_backends.py | 305 ++++++++++++ backend/test_opencv_behavior.py | 113 +++++ backend/test_reconnection.py | 98 ++++ backend/test_resolution_performance.py | 214 ++++++++ frontend/src/renderer/src/views/Detection.vue | 8 +- 24 files changed, 2943 insertions(+), 708 deletions(-) create mode 100644 backend/check_monitor_status.py create mode 100644 backend/simple_camera_test.py create mode 100644 backend/test_avoid_duplicate_init.py create mode 100644 backend/test_backend_optimization.py create mode 100644 backend/test_camera_analysis.py create mode 100644 backend/test_camera_disconnect.py create mode 100644 backend/test_camera_full.py create mode 100644 backend/test_camera_performance.py create mode 100644 backend/test_camera_simple.py create mode 100644 backend/test_opencv_backends.py create mode 100644 backend/test_opencv_behavior.py create mode 100644 backend/test_reconnection.py create mode 100644 backend/test_resolution_performance.py diff --git a/backend/check_monitor_status.py b/backend/check_monitor_status.py new file mode 100644 index 00000000..2fe2e0e3 --- /dev/null +++ b/backend/check_monitor_status.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查设备连接监控线程状态的测试脚本 +""" + +import sys +import os +import threading +import time +from typing import Dict, Any + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from devices.utils.config_manager import ConfigManager +from devices.camera_manager import CameraManager +from devices.imu_manager import IMUManager +from devices.pressure_manager import PressureManager +from devices.femtobolt_manager import FemtoBoltManager + +class MockCameraManager(CameraManager): + """模拟摄像头管理器,用于测试监控线程""" + + def __init__(self, socketio, config_manager): + super().__init__(socketio, config_manager) + self._mock_hardware_connected = True + + def check_hardware_connection(self) -> bool: + """模拟硬件连接检查""" + return self._mock_hardware_connected + + def set_mock_hardware_status(self, connected: bool): + """设置模拟硬件连接状态""" + self._mock_hardware_connected = connected + +def check_device_monitor_status(device_manager, device_name: str): + """ + 检查单个设备的监控线程状态 + + Args: + device_manager: 设备管理器实例 + device_name: 设备名称 + """ + print(f"\n=== {device_name.upper()} 设备监控状态检查 ===") + + # 检查基本状态 + print(f"设备连接状态: {device_manager.is_connected}") + print(f"设备流状态: {device_manager.is_streaming}") + + # 检查监控线程相关属性 + if hasattr(device_manager, '_connection_monitor_thread'): + monitor_thread = device_manager._connection_monitor_thread + print(f"监控线程对象: {monitor_thread}") + + if monitor_thread: + print(f"监控线程存活状态: {monitor_thread.is_alive()}") + print(f"监控线程名称: {monitor_thread.name}") + print(f"监控线程守护状态: {monitor_thread.daemon}") + else: + print("监控线程对象: None") + else: + print("设备管理器没有监控线程属性") + + # 检查监控停止事件 + if hasattr(device_manager, '_monitor_stop_event'): + stop_event = device_manager._monitor_stop_event + print(f"监控停止事件: {stop_event}") + print(f"监控停止事件状态: {stop_event.is_set()}") + else: + print("设备管理器没有监控停止事件属性") + + # 检查监控配置 + if hasattr(device_manager, '_connection_check_interval'): + print(f"连接检查间隔: {device_manager._connection_check_interval}秒") + + if hasattr(device_manager, '_connection_timeout'): + print(f"连接超时时间: {device_manager._connection_timeout}秒") + + # 检查心跳时间 + if hasattr(device_manager, '_last_heartbeat'): + last_heartbeat = device_manager._last_heartbeat + current_time = time.time() + heartbeat_age = current_time - last_heartbeat + print(f"上次心跳时间: {time.ctime(last_heartbeat)}") + print(f"心跳间隔: {heartbeat_age:.2f}秒前") + +def main(): + """ + 主函数:检查所有设备的监控状态 + """ + print("设备连接监控状态检查工具") + print("=" * 50) + + try: + # 初始化配置管理器 + config_manager = ConfigManager() + + # 创建模拟设备管理器实例 + mock_camera = MockCameraManager(None, config_manager) + + print("\n=== 初始状态检查 ===") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n=== 系统线程信息 ===") + active_threads = threading.active_count() + print(f"当前活跃线程数: {active_threads}") + + print("\n活跃线程列表:") + for thread in threading.enumerate(): + print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") + + # 测试连接监控启动 + print("\n=== 测试连接监控启动 ===") + + print("\n1. 设置模拟硬件为连接状态...") + mock_camera.set_mock_hardware_status(True) + + print("\n2. 设置设备为连接状态...") + mock_camera.set_connected(True) + time.sleep(0.5) # 等待线程启动 + + print("\n连接后的监控状态:") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n=== 系统线程信息 (启动监控后) ===") + active_threads = threading.active_count() + print(f"当前活跃线程数: {active_threads}") + + print("\n活跃线程列表:") + for thread in threading.enumerate(): + print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") + + print("\n3. 等待3秒观察监控线程工作...") + time.sleep(3) + + print("\n监控运行中的状态:") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n4. 模拟硬件断开...") + mock_camera.set_mock_hardware_status(False) + time.sleep(6) # 等待监控检测到断开(检查间隔是5秒) + + print("\n硬件断开后的监控状态:") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n5. 重新连接测试...") + mock_camera.set_mock_hardware_status(True) + mock_camera.set_connected(True) + time.sleep(0.5) + + print("\n重新连接后的监控状态:") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n6. 手动断开连接...") + mock_camera.set_connected(False) + time.sleep(0.5) + + print("\n手动断开后的监控状态:") + check_device_monitor_status(mock_camera, 'mock_camera') + + print("\n=== 最终系统线程信息 ===") + active_threads = threading.active_count() + print(f"当前活跃线程数: {active_threads}") + + print("\n活跃线程列表:") + for thread in threading.enumerate(): + print(f" - {thread.name} (守护: {thread.daemon}, 存活: {thread.is_alive()})") + + except Exception as e: + print(f"检查过程中发生错误: {e}") + import traceback + traceback.print_exc() + + finally: + # 清理资源 + print("\n=== 清理资源 ===") + try: + if 'mock_camera' in locals(): + mock_camera.cleanup() + print("mock_camera 设备资源已清理") + except Exception as e: + print(f"清理资源时发生错误: {e}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/backend/devices/base_device.py b/backend/devices/base_device.py index ee85f8a6..015668f2 100644 --- a/backend/devices/base_device.py +++ b/backend/devices/base_device.py @@ -59,13 +59,7 @@ class BaseDevice(ABC): 'last_error': None } - # 性能统计 - self._stats = { - 'frames_processed': 0, - 'errors_count': 0, - 'start_time': None, - 'last_frame_time': None - } + @abstractmethod def initialize(self) -> bool: @@ -208,13 +202,8 @@ class BaseDevice(ABC): # 只有状态真正改变时才触发回调 if old_status != is_connected: - self._notify_status_change(is_connected) - - # 启动或停止连接监控 - if is_connected and not self._connection_monitor_thread: - self._start_connection_monitor() - elif not is_connected and self._connection_monitor_thread: - self._stop_connection_monitor() + self._notify_status_change(is_connected) + def emit_data(self, event: str, data: Any, namespace: Optional[str] = None): """ @@ -260,37 +249,9 @@ class BaseDevice(ABC): with self._lock: return self._device_info.copy() - def get_stats(self) -> Dict[str, Any]: - """ - 获取性能统计 - - Returns: - Dict[str, Any]: 性能统计信息 - """ - with self._lock: - stats = self._stats.copy() - if stats['start_time']: - stats['uptime'] = time.time() - stats['start_time'] - if stats['frames_processed'] > 0 and stats['uptime'] > 0: - stats['fps'] = stats['frames_processed'] / stats['uptime'] - else: - stats['fps'] = 0 - return stats + - def _update_stats(self, frame_processed: bool = True, error: bool = False): - """ - 更新统计信息 - - Args: - frame_processed: 是否处理了一帧 - error: 是否发生错误 - """ - with self._lock: - if frame_processed: - self._stats['frames_processed'] += 1 - self._stats['last_frame_time'] = time.time() - if error: - self._stats['errors_count'] += 1 + def _set_error(self, error_msg: str): """ @@ -312,21 +273,7 @@ class BaseDevice(ABC): with self._lock: self._device_info['last_error'] = None - def _start_stats_tracking(self): - """ - 开始统计跟踪 - """ - with self._lock: - self._stats['start_time'] = time.time() - self._stats['frames_processed'] = 0 - self._stats['errors_count'] = 0 - - def _stop_stats_tracking(self): - """ - 停止统计跟踪 - """ - with self._lock: - self._stats['start_time'] = None + def _start_connection_monitor(self): """ @@ -365,18 +312,29 @@ class BaseDevice(ABC): try: # 检查硬件连接状态 hardware_connected = self.check_hardware_connection() - + self.logger.info(f"检测到设备 {self.device_name} 硬件连接状态: {hardware_connected} is_connected:{self.is_connected}") # 如果硬件断开但软件状态仍为连接,则更新状态 if not hardware_connected and self.is_connected: self.logger.warning(f"检测到设备 {self.device_name} 硬件连接断开") - self.set_connected(False) - break # 硬件断开后停止监控 + # 直接更新状态,避免在监控线程中调用set_connected导致死锁 + self.is_connected = False + self._notify_status_change(False) - # 检查心跳超时 + # 如果硬件重新连接但软件状态仍为断开,则更新状态 + elif hardware_connected and not self.is_connected: + self.logger.info(f"检测到设备 {self.device_name} 硬件重新连接") + # 直接更新状态,避免在监控线程中调用set_connected导致死锁 + self.is_connected = True + # 重置心跳时间,避免立即触发心跳超时 + self.update_heartbeat() + self._notify_status_change(True) + + # 检查心跳超时(仅在当前状态为连接时检查) if self.is_connected and time.time() - self._last_heartbeat > self._connection_timeout: self.logger.warning(f"设备 {self.device_name} 心跳超时,判定为断开连接") - self.set_connected(False) - break # 超时后停止监控 + # 直接更新状态,避免在监控线程中调用set_connected导致死锁 + self.is_connected = False + self._notify_status_change(False) except Exception as e: self.logger.error(f"设备 {self.device_name} 连接监控异常: {e}") @@ -385,6 +343,8 @@ class BaseDevice(ABC): self._monitor_stop_event.wait(self._connection_check_interval) self.logger.info(f"设备 {self.device_name} 连接监控结束") + # 清理线程引用 + self._connection_monitor_thread = None def __enter__(self): """ diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index ae99d1b7..59996a29 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -10,18 +10,16 @@ import threading import time import base64 import numpy as np -from typing import Optional, Dict, Any, Tuple +from typing import Optional, Dict, Any import logging import queue import gc try: from .base_device import BaseDevice - from .utils.socket_manager import SocketManager from .utils.config_manager import ConfigManager except ImportError: from base_device import BaseDevice - from utils.socket_manager import SocketManager from utils.config_manager import ConfigManager @@ -53,6 +51,17 @@ class CameraManager(BaseDevice): self.fps = config.get('fps', 30) self.buffer_size = config.get('buffer_size', 1) self.fourcc = config.get('fourcc', 'MJPG') + + # OpenCV后端配置 (DirectShow性能最佳) + backend_name = config.get('backend', 'directshow').lower() + self.backend_map = { + 'directshow': cv2.CAP_DSHOW, + 'dshow': cv2.CAP_DSHOW, + 'msmf': cv2.CAP_MSMF, + 'any': cv2.CAP_ANY + } + self.preferred_backend = self.backend_map.get(backend_name, cv2.CAP_DSHOW) + self.backend_name = backend_name # 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输) self._tx_max_width = int(config.get('tx_max_width', 640)) @@ -86,15 +95,49 @@ class CameraManager(BaseDevice): # 全局帧队列(用于录制) self.frame_queue = queue.Queue(maxsize=10) # 最大长度10,自动丢弃旧帧 + + # 属性缓存机制 - 避免重复设置相同属性值 + self._property_cache = {} + self._cache_enabled = True - # OpenCV优化开关 + # OpenCV优化开关和性能设置 try: cv2.setUseOptimized(True) + # 设置OpenCV线程数以提高性能 + cv2.setNumThreads(4) # 使用4个线程 except Exception: pass self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}") + def _set_property_optimized(self, prop, value): + """ + 优化的属性设置方法,使用缓存避免重复设置 + + Args: + prop: OpenCV属性常量 + value: 属性值 + + Returns: + bool: 是否实际执行了设置操作 + """ + if not self.cap: + return False + + # 检查缓存,避免重复设置相同值 + if self._cache_enabled and prop in self._property_cache: + if self._property_cache[prop] == value: + return False # 值未改变,跳过设置 + + # 执行属性设置 + result = self.cap.set(prop, value) + + # 更新缓存 + if self._cache_enabled: + self._property_cache[prop] = value + + return True + def initialize(self) -> bool: """ 初始化相机设备 @@ -102,37 +145,66 @@ class CameraManager(BaseDevice): Returns: bool: 初始化是否成功 """ + start_time = time.time() try: self.logger.info(f"正在初始化相机设备...") # 使用构造函数中已加载的配置,避免并发读取配置文件 - self.logger.info(f"使用已加载配置: device_index={self.device_index}, resolution={self.width}x{self.height}, fps={self.fps}") + config_time = time.time() + self.logger.info(f"使用已加载配置: device_index={self.device_index}, resolution={self.width}x{self.height}, fps={self.fps} (耗时: {(config_time - start_time)*1000:.1f}ms)") - # 尝试多个后端(Windows下优先MSMF/DShow) - backends = [cv2.CAP_MSMF, cv2.CAP_DSHOW, cv2.CAP_ANY] + # 使用配置的后端,如果失败则尝试其他后端 + if self.preferred_backend == cv2.CAP_DSHOW: + backends = [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY] + elif self.preferred_backend == cv2.CAP_MSMF: + backends = [cv2.CAP_MSMF, cv2.CAP_DSHOW, cv2.CAP_ANY] + else: + backends = [self.preferred_backend, cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY] + camera_open_time = time.time() for backend in backends: + backend_start = time.time() try: + # 快速打开相机,减少超时等待 self.cap = cv2.VideoCapture(self.device_index, backend) + + # 设置较短的超时时间以加快检测 + if hasattr(cv2, 'CAP_PROP_OPEN_TIMEOUT_MSEC'): + self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000) # 3秒超时 + if self.cap.isOpened(): - self.logger.info(f"使用后端 {backend} 成功打开相机") + backend_time = (time.time() - backend_start) * 1000 + self.logger.info(f"使用后端 {backend} 成功打开相机 (耗时: {backend_time:.1f}ms)") break except Exception as e: - self.logger.warning(f"后端 {backend} 打开相机失败: {e}") + backend_time = (time.time() - backend_start) * 1000 + self.logger.warning(f"后端 {backend} 打开相机失败: {e} (耗时: {backend_time:.1f}ms)") continue else: - self.logger.warning("所有后端都无法打开相机,相机设备不可用") + total_open_time = (time.time() - camera_open_time) * 1000 + self.logger.warning(f"所有后端都无法打开相机,相机设备不可用 (总耗时: {total_open_time:.1f}ms)") return False + total_open_time = (time.time() - camera_open_time) * 1000 + self.logger.info(f"相机打开完成 (总耗时: {total_open_time:.1f}ms)") + # 设置相机属性 + config_start = time.time() self._configure_camera() + config_time = (time.time() - config_start) * 1000 + self.logger.info(f"相机配置完成 (耗时: {config_time:.1f}ms)") - # 验证相机是否正常工作 + # 验证相机是否正常工作(优化测试过程) + test_start = time.time() if not self._test_camera(): - self.logger.warning("相机测试失败,相机设备不可用") + test_time = (time.time() - test_start) * 1000 + self.logger.warning(f"相机测试失败,相机设备不可用 (耗时: {test_time:.1f}ms)") return False + test_time = (time.time() - test_start) * 1000 + self.logger.info(f"相机测试完成 (耗时: {test_time:.1f}ms)") - self.is_connected = True + # 使用set_connected方法来正确启动连接监控线程 + self.set_connected(True) self._last_connected_state = True self._device_info.update({ 'device_index': self.device_index, @@ -141,7 +213,8 @@ class CameraManager(BaseDevice): 'backend': self.cap.getBackendName() if hasattr(self.cap, 'getBackendName') else 'Unknown' }) - self.logger.info("相机初始化成功") + total_time = (time.time() - start_time) * 1000 + self.logger.info(f"相机初始化成功 (总耗时: {total_time:.1f}ms)") return True except Exception as e: @@ -173,30 +246,136 @@ class CameraManager(BaseDevice): return try: - # 设置FOURCC编码 - if self.fourcc: - fourcc_code = cv2.VideoWriter_fourcc(*self.fourcc) - self.cap.set(cv2.CAP_PROP_FOURCC, fourcc_code) - - # 设置分辨率 - self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width) - self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height) + # 批量设置相机属性以提高效率 + config_start = time.time() - # 设置帧率 - self.cap.set(cv2.CAP_PROP_FPS, self.fps) - - # 设置缓冲区大小(部分后端不生效) + # 设置缓冲区大小(优先设置,减少延迟) + buffer_start = time.time() try: - self.cap.set(cv2.CAP_PROP_BUFFERSIZE, self.buffer_size) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, min(self.buffer_size, 1)) # 使用最小缓冲区减少延迟 except Exception: pass + buffer_time = (time.time() - buffer_start) * 1000 + self.logger.debug(f"缓冲区设置耗时: {buffer_time:.1f}ms") - # 获取实际设置的值 - actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) - actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - actual_fps = self.cap.get(cv2.CAP_PROP_FPS) + # 性能优化设置:禁用可能导致延迟的自动功能 + optimization_start = time.time() + try: + # 禁用自动曝光以减少处理时间 + self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式 + # 禁用自动白平衡 + self.cap.set(cv2.CAP_PROP_AUTO_WB, 0) + # 设置较低的曝光值以减少延迟 + self.cap.set(cv2.CAP_PROP_EXPOSURE, -6) + except Exception as e: + self.logger.debug(f"设置性能优化参数时出现警告: {e}") + optimization_time = (time.time() - optimization_start) * 1000 + self.logger.debug(f"性能优化设置耗时: {optimization_time:.1f}ms") - self.logger.info(f"相机配置 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}") + # 激进优化:跳过非关键属性设置,只设置必要属性 + resolution_start = time.time() + + # 优先级属性设置:只设置最关键的属性 + critical_properties = [ + (cv2.CAP_PROP_FRAME_WIDTH, self.width), + (cv2.CAP_PROP_FRAME_HEIGHT, self.height) + ] + + # 可选属性(在某些情况下可以跳过) + optional_properties = [ + (cv2.CAP_PROP_FPS, self.fps) + ] + + batch_start = time.time() + actual_sets = 0 + skipped_sets = 0 + + # 设置关键属性 + for prop, value in critical_properties: + try: + if self._set_property_optimized(prop, value): + actual_sets += 1 + else: + skipped_sets += 1 + except Exception as e: + self.logger.debug(f"设置属性 {prop} 失败: {e}") + skipped_sets += 1 + + # 条件设置可选属性(如果时间允许) + current_time = (time.time() - batch_start) * 1000 + if current_time < 1000: # 如果已用时间少于1秒,才设置FPS + for prop, value in optional_properties: + try: + if self._set_property_optimized(prop, value): + actual_sets += 1 + else: + skipped_sets += 1 + except Exception as e: + self.logger.debug(f"设置可选属性 {prop} 失败: {e}") + skipped_sets += 1 + else: + self.logger.debug(f"跳过FPS设置以节省时间 (已耗时: {current_time:.1f}ms)") + skipped_sets += len(optional_properties) + + batch_time = (time.time() - batch_start) * 1000 + + resolution_time = (time.time() - resolution_start) * 1000 + self.logger.debug(f"优化属性设置耗时: {batch_time:.1f}ms, 实际设置: {actual_sets}, 跳过: {skipped_sets}, 总计: {resolution_time:.1f}ms") + + # 将原来的单独计时变量设为批量时间的一部分用于兼容性 + width_time = batch_time * 0.5 # 估算宽度设置占比 + height_time = batch_time * 0.4 # 估算高度设置占比 + fps_time = batch_time * 0.1 # 估算帧率设置占比(可能被跳过) + + # 设置FOURCC编码(如果需要) + if self.fourcc: + fourcc_start = time.time() + fourcc_code = cv2.VideoWriter_fourcc(*self.fourcc) + self.cap.set(cv2.CAP_PROP_FOURCC, fourcc_code) + fourcc_time = (time.time() - fourcc_start) * 1000 + self.logger.debug(f"FOURCC设置耗时: {fourcc_time:.1f}ms") + + # 激进优化:延迟验证机制 - 只在必要时验证 + verification_start = time.time() + + # 检查是否需要验证(基于配置或调试需求) + need_verification = self.logger.isEnabledFor(logging.DEBUG) or getattr(self, '_force_verification', False) + + if need_verification: + # 只读取关键属性进行验证 + batch_read_start = time.time() + try: + actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + # FPS验证可选,因为某些相机不支持精确FPS设置 + actual_fps = self.cap.get(cv2.CAP_PROP_FPS) if actual_sets > 2 else self.fps + batch_read_time = (time.time() - batch_read_start) * 1000 + self.logger.debug(f"验证读取耗时: {batch_read_time:.1f}ms") + except Exception as e: + # 验证失败不影响主流程 + actual_width, actual_height, actual_fps = self.width, self.height, self.fps + batch_read_time = (time.time() - batch_read_start) * 1000 + self.logger.debug(f"属性验证失败,使用默认值: {e}") + else: + # 跳过验证,使用设置值 + actual_width, actual_height, actual_fps = self.width, self.height, self.fps + batch_read_time = 0.0 + self.logger.debug("跳过属性验证以节省时间") + + verification_time = (time.time() - verification_start) * 1000 + + # 为兼容性保留单独计时变量 + width_read_time = batch_read_time * 0.4 + height_read_time = batch_read_time * 0.4 + fps_read_time = batch_read_time * 0.2 + + self.logger.debug(f"延迟验证耗时: {batch_read_time:.1f}ms, 总计: {verification_time:.1f}ms") + + total_config_time = (time.time() - config_start) * 1000 + + self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}") + self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") + self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") except Exception as e: self.logger.warning(f"配置相机参数失败: {e}") @@ -209,12 +388,24 @@ class CameraManager(BaseDevice): bool: 测试是否成功 """ try: + # 快速测试:只读取一帧进行验证 + read_start = time.time() ret, frame = self.cap.read() + read_time = (time.time() - read_start) * 1000 + if ret and frame is not None: - self.logger.info(f"相机测试成功 - 帧大小: {frame.shape}") - return True + # 基本帧验证 + if len(frame.shape) >= 2 and frame.shape[0] > 0 and frame.shape[1] > 0: + self.logger.info(f"相机测试成功 - 帧大小: {frame.shape}, 读取耗时: {read_time:.1f}ms") + + # 清理测试帧内存 + del frame + return True + else: + self.logger.error(f"相机测试失败 - 帧数据无效: {frame.shape if frame is not None else 'None'}") + return False else: - self.logger.error("相机测试失败 - 无法读取帧") + self.logger.error(f"相机测试失败 - 无法读取帧, 读取耗时: {read_time:.1f}ms") return False except Exception as e: self.logger.error(f"相机测试异常: {e}") @@ -227,24 +418,43 @@ class CameraManager(BaseDevice): Returns: bool: 校准是否成功 """ + calibrate_start = time.time() try: self.logger.info("开始相机校准...") if not self.is_connected: + init_start = time.time() if not self.initialize(): return False + init_time = (time.time() - init_start) * 1000 + self.logger.info(f"校准中初始化完成 (耗时: {init_time:.1f}ms)") - # 读取几帧来稳定相机 - for i in range(5): - ret, _ = self.cap.read() + # 优化:减少稳定帧数量,只读取2帧来稳定相机 + stabilize_start = time.time() + stable_frames = 2 # 减少从5帧到2帧 + + for i in range(stable_frames): + frame_start = time.time() + ret, frame = self.cap.read() + frame_time = (time.time() - frame_start) * 1000 + if not ret: - self.logger.warning(f"校时时读取第{i+1}帧失败") - - self.logger.info("相机校准完成") + self.logger.warning(f"校准时读取第{i+1}帧失败 (耗时: {frame_time:.1f}ms)") + else: + # 立即释放帧内存 + if frame is not None: + del frame + self.logger.debug(f"校准帧{i+1}读取成功 (耗时: {frame_time:.1f}ms)") + + stabilize_time = (time.time() - stabilize_start) * 1000 + total_time = (time.time() - calibrate_start) * 1000 + + self.logger.info(f"相机校准完成 - 稳定化耗时: {stabilize_time:.1f}ms, 总耗时: {total_time:.1f}ms") return True except Exception as e: - self.logger.error(f"相机校准失败: {e}") + total_time = (time.time() - calibrate_start) * 1000 + self.logger.error(f"相机校准失败: {e} (耗时: {total_time:.1f}ms)") return False def start_streaming(self) -> bool: @@ -400,6 +610,9 @@ class CameraManager(BaseDevice): consecutive_read_failures = 0 self.dropped_frames = 0 + # 更新心跳时间,防止连接监控线程判定为超时 + self.update_heartbeat() + # 保存原始帧到队列(用于录制) try: self.frame_queue.put_nowait({ @@ -685,15 +898,80 @@ class CameraManager(BaseDevice): bool: 相机是否物理连接 """ try: - if self.cap and self.cap.isOpened(): - # 尝试读取一帧来验证连接 - ret, _ = self.cap.read() - return ret - return False + if not self.cap: + # 如果相机实例不存在,尝试重新创建 + return self._attempt_device_reconnection() + + if not self.cap.isOpened(): + # 相机未打开,尝试重连 + return self._attempt_device_reconnection() + + # 尝试读取一帧来验证连接 + try: + ret, frame = self.cap.read() + if ret and frame is not None: + # 立即释放帧内存 + del frame + return True + else: + # 读取失败,可能设备已断开 + return self._attempt_device_reconnection() + except Exception: + # 读取异常,尝试重连 + return self._attempt_device_reconnection() + except Exception as e: self.logger.debug(f"检查相机硬件连接时发生异常: {e}") return False + def _attempt_device_reconnection(self) -> bool: + """ + 尝试重新连接相机设备 + + Returns: + bool: 重连是否成功 + """ + try: + self.logger.info("检测到相机设备断开,尝试重新连接...") + + # 清理旧的相机实例 + if self.cap: + try: + self.cap.release() + except Exception as e: + self.logger.debug(f"清理旧相机实例时出错: {e}") + + self.cap = None + + # 等待设备释放 + time.sleep(0.5) + + # 重新初始化相机 + if self.initialize(): + self._notify_status_change(True) + # 重连成功后,确保数据流正在运行 + if not self.is_streaming: + self.logger.info("重连成功,启动相机数据流") + self.start_streaming() + + # 更新设备信息 + self._device_info.update({ + 'device_index': self.device_index, + 'resolution': f"{self.width}x{self.height}", + 'fps': self.fps, + 'backend': self.cap.getBackendName() if hasattr(self.cap, 'getBackendName') else 'Unknown' + }) + + return True + else: + self.logger.warning("相机设备重连失败") + return False + + except Exception as e: + self.logger.error(f"相机设备重连过程中出错: {e}") + self.cap = None + return False + def cleanup(self): """ 清理资源 diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py index b3bb20a9..baa3566c 100644 --- a/backend/devices/device_coordinator.py +++ b/backend/devices/device_coordinator.py @@ -49,7 +49,9 @@ class DeviceCoordinator: # 设备管理器 self.devices: Dict[str, Any] = {} - self.device_configs = self.config_manager.get_system_config().get('devices', {}) + + # 获取设备配置 + self.device_configs = self.config_manager.get_all_device_configs() # 状态管理 self.is_initialized = False @@ -118,11 +120,15 @@ class DeviceCoordinator: """ 注册Socket.IO命名空间 """ - namespaces = ['/devices', '/coordinator'] - for namespace in namespaces: - self.socket_manager.register_namespace(namespace) + namespace_mappings = { + '/devices': 'devices', + '/coordinator': 'coordinator' + } + + for namespace, device_name in namespace_mappings.items(): + self.socket_manager.register_namespace(namespace, device_name) - self.logger.info(f"已注册Socket.IO命名空间: {namespaces}") + self.logger.info(f"已注册Socket.IO命名空间: {list(namespace_mappings.keys())}") def _initialize_devices(self) -> bool: """ @@ -189,8 +195,8 @@ class DeviceCoordinator: """ try: camera = CameraManager(self.socketio, self.config_manager) - if camera.initialize(): - self.devices['camera'] = camera + self.devices['camera'] = camera + if camera.initialize(): return True return False except Exception as e: @@ -206,9 +212,9 @@ class DeviceCoordinator: """ try: imu = IMUManager(self.socketio, self.config_manager) + self.devices['imu'] = imu if imu.initialize(): - self.devices['imu'] = imu - return True + return True return False except Exception as e: self.logger.error(f"初始化IMU失败: {e}") @@ -223,9 +229,9 @@ class DeviceCoordinator: """ try: pressure = PressureManager(self.socketio, self.config_manager) + self.devices['pressure'] = pressure if pressure.initialize(): - self.devices['pressure'] = pressure - return True + return True return False except Exception as e: self.logger.error(f"初始化压力传感器失败: {e}") @@ -240,8 +246,8 @@ class DeviceCoordinator: """ try: femtobolt = FemtoBoltManager(self.socketio, self.config_manager) - if femtobolt.initialize(): - self.devices['femtobolt'] = femtobolt + self.devices['femtobolt'] = femtobolt + if femtobolt.initialize(): return True return False except Exception as e: @@ -340,47 +346,7 @@ class DeviceCoordinator: self.logger.error(f"停止设备数据流失败: {e}") return False - def calibrate_all_devices(self) -> Dict[str, bool]: - """ - 校准所有设备 - - Returns: - Dict[str, bool]: 各设备校准结果 - """ - results = {} - - try: - self.logger.info("开始校准所有设备...") - - # 并行校准所有设备 - futures = [] - for device_name, device in self.devices.items(): - if hasattr(device, 'calibrate'): - future = self.executor.submit(device.calibrate) - futures.append((device_name, future)) - - # 等待所有设备校准完成 - for device_name, future in futures: - try: - result = future.result(timeout=30) # 30秒超时 - results[device_name] = result - if result: - self.logger.info(f"{device_name}校准成功") - else: - self.logger.error(f"{device_name}校准失败") - except Exception as e: - self.logger.error(f"{device_name}校准异常: {e}") - results[device_name] = False - - success_count = sum(results.values()) - self.logger.info(f"设备校准完成,成功: {success_count}/{len(results)}") - - self._emit_event('calibration_completed', results) - return results - - except Exception as e: - self.logger.error(f"设备校准失败: {e}") - return results + def get_device_status(self, device_name: Optional[str] = None) -> Dict[str, Any]: """ @@ -428,6 +394,79 @@ class DeviceCoordinator: Optional[Any]: 设备实例,不存在返回None """ return self.devices.get(device_name) + + def get_device_managers(self) -> Dict[str, Any]: + """ + 获取所有设备管理器实例 + + Returns: + Dict[str, Any]: 设备管理器字典 + """ + return self.devices.copy() + + def start_all_connection_monitor(self) -> bool: + """ + 启动所有设备的连接监控 + + Returns: + bool: 启动是否成功 + """ + with self.coordinator_lock: + if not self.is_initialized: + self.logger.error("设备协调器未初始化") + return False + + try: + self.logger.info("启动所有设备连接监控...") + + success_count = 0 + for device_name, device in self.devices.items(): + try: + if hasattr(device, '_start_connection_monitor'): + device._start_connection_monitor() + success_count += 1 + self.logger.info(f"{device_name}设备连接监控已启动") + else: + self.logger.warning(f"{device_name}设备不支持连接监控") + except Exception as e: + self.logger.error(f"启动{device_name}设备连接监控失败: {e}") + + self.logger.info(f"设备连接监控启动完成,成功: {success_count}/{len(self.devices)}") + return success_count > 0 + + except Exception as e: + self.logger.error(f"启动设备连接监控失败: {e}") + return False + + def stop_all_connection_monitor(self) -> bool: + """ + 停止所有设备的连接监控 + + Returns: + bool: 停止是否成功 + """ + with self.coordinator_lock: + try: + self.logger.info("停止所有设备连接监控...") + + success_count = 0 + for device_name, device in self.devices.items(): + try: + if hasattr(device, '_stop_connection_monitor'): + device._stop_connection_monitor() + success_count += 1 + self.logger.info(f"{device_name}设备连接监控已停止") + else: + self.logger.warning(f"{device_name}设备不支持连接监控") + except Exception as e: + self.logger.error(f"停止{device_name}设备连接监控失败: {e}") + + self.logger.info(f"设备连接监控停止完成,成功: {success_count}/{len(self.devices)}") + return True + + except Exception as e: + self.logger.error(f"停止设备连接监控失败: {e}") + return False def restart_device(self, device_name: str) -> bool: """ diff --git a/backend/devices/femtobolt_manager.py b/backend/devices/femtobolt_manager.py index 5657f6e2..656f67f4 100644 --- a/backend/devices/femtobolt_manager.py +++ b/backend/devices/femtobolt_manager.py @@ -344,7 +344,8 @@ class FemtoBoltManager(BaseDevice): if not self._start_device(): raise Exception("设备启动失败") - self.is_connected = True + # 使用set_connected方法启动连接监控线程 + self.set_connected(True) self.device_info.update({ 'color_resolution': self.color_resolution, 'depth_mode': self.depth_mode, @@ -357,7 +358,8 @@ class FemtoBoltManager(BaseDevice): except Exception as e: self.logger.error(f"FemtoBolt初始化失败: {e}") - self.is_connected = False + # 使用set_connected方法停止连接监控线程 + self.set_connected(False) self._cleanup_device() return False @@ -652,6 +654,9 @@ class FemtoBoltManager(BaseDevice): ret, depth_image = capture.get_depth_image() if ret and depth_image is not None: + # 更新心跳时间,防止连接监控线程判定为超时 + self.update_heartbeat() + # 根据配置选择不同的等高线生成方法 if self.algorithm_type == 'plt': depth_colored_final = self._generate_contour_image_plt(depth_image) diff --git a/backend/devices/imu_manager.py b/backend/devices/imu_manager.py index 9cd2ef99..252fdfe4 100644 --- a/backend/devices/imu_manager.py +++ b/backend/devices/imu_manager.py @@ -33,17 +33,24 @@ logger = logging.getLogger(__name__) class RealIMUDevice: """真实IMU设备,通过串口读取姿态数据""" def __init__(self, port, baudrate): - self.port = port - self.baudrate = baudrate - self.ser = None - self.buffer = bytearray() - self.calibration_data = None + # 串口通信配置 + self.port = port # 串口端口号(如COM3、/dev/ttyUSB0等) + self.baudrate = baudrate # 波特率,通常为9600或115200 + self.ser = None # 串口连接对象,初始为空 + + # 数据缓冲区和校准相关 + self.buffer = bytearray() # 接收数据的缓冲区 + self.calibration_data = None # 校准数据,用于修正传感器偏差 + + # 头部姿态偏移量,用于校准初始姿态 self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0} + + # 最后一次读取的IMU数据 self.last_data = { - 'roll': 0.0, - 'pitch': 0.0, - 'yaw': 0.0, - 'temperature': 25.0 + 'roll': 0.0, # 横滚角(绕X轴旋转) + 'pitch': 0.0, # 俯仰角(绕Y轴旋转) + 'yaw': 0.0, # 偏航角(绕Z轴旋转) + 'temperature': 25.0 # 传感器温度 } logger.debug(f'RealIMUDevice 初始化: port={self.port}, baudrate={self.baudrate}') self._connect() @@ -191,64 +198,6 @@ class RealIMUDevice: pass -class MockIMUDevice: - """模拟IMU设备""" - - def __init__(self): - self.noise_level = 0.1 - self.calibration_data = None # 校准数据 - self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0} # 头部姿态零点偏移 - - def set_calibration(self, calibration: Dict[str, Any]): - """设置校准数据""" - self.calibration_data = calibration - if 'head_pose_offset' in calibration: - self.head_pose_offset = calibration['head_pose_offset'] - - def apply_calibration(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: - """应用校准:将当前姿态减去初始偏移,得到相对姿态""" - if not raw_data or 'head_pose' not in raw_data: - return raw_data - - calibrated_data = raw_data.copy() - head_pose = raw_data['head_pose'].copy() - head_pose['rotation'] = head_pose['rotation'] - self.head_pose_offset['rotation'] - head_pose['tilt'] = head_pose['tilt'] - self.head_pose_offset['tilt'] - head_pose['pitch'] = head_pose['pitch'] - self.head_pose_offset['pitch'] - calibrated_data['head_pose'] = head_pose - return calibrated_data - - def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]: - """读取IMU数据""" - # 生成头部姿态角度数据,角度范围(-90°, +90°) - # 使用正弦波模拟自然的头部运动,添加随机噪声 - import time - current_time = time.time() - - # 旋转角(左旋为负,右旋为正) - rotation_angle = 30 * np.sin(current_time * 0.5) + np.random.normal(0, self.noise_level * 5) - rotation_angle = np.clip(rotation_angle, -90, 90) - - # 倾斜角(左倾为负,右倾为正) - tilt_angle = 20 * np.sin(current_time * 0.3 + np.pi/4) + np.random.normal(0, self.noise_level * 5) - tilt_angle = np.clip(tilt_angle, -90, 90) - - # 俯仰角(俯角为负,仰角为正) - pitch_angle = 15 * np.sin(current_time * 0.7 + np.pi/2) + np.random.normal(0, self.noise_level * 5) - pitch_angle = np.clip(pitch_angle, -90, 90) - - # 生成原始数据 - raw_data = { - 'head_pose': { - 'rotation': rotation_angle, # 旋转角:左旋(-), 右旋(+) - 'tilt': tilt_angle, # 倾斜角:左倾(-), 右倾(+) - 'pitch': pitch_angle # 俯仰角:俯角(-), 仰角(+) - }, - 'timestamp': datetime.now().isoformat() - } - # 应用校准并返回 - return self.apply_calibration(raw_data) if apply_calibration else raw_data - class BleIMUDevice: """蓝牙IMU设备,基于bleak实现,解析逻辑参考tests/testblueimu.py""" @@ -566,7 +515,8 @@ class IMUManager(BaseDevice): self.logger.info(f"使用蓝牙IMU设备 - MAC: {self.mac_address}") self.imu_device = BleIMUDevice(self.mac_address) self.imu_device.start() - self.is_connected = True + # 使用set_connected方法来正确启动连接监控线程 + self.set_connected(True) elif self.device_type == 'real' or (self.device_type != 'mock' and not self.use_mock): self.logger.info(f"使用真实IMU设备 - 端口: {self.port}, 波特率: {self.baudrate}") self.imu_device = RealIMUDevice(self.port, self.baudrate) @@ -577,11 +527,13 @@ class IMUManager(BaseDevice): self.is_connected = False self.imu_device = None return False - self.is_connected = True + # 使用set_connected方法来正确启动连接监控线程 + self.set_connected(True) else: self.logger.info("使用模拟IMU设备") self.imu_device = MockIMUDevice() - self.is_connected = True + # 使用set_connected方法来正确启动连接监控线程 + self.set_connected(True) self._device_info.update({ 'port': self.port, @@ -748,6 +700,9 @@ class IMUManager(BaseDevice): # self.data_buffer.append(data) # self.last_valid_data = data + # 更新心跳时间,防止连接监控线程判定为超时 + self.update_heartbeat() + # 发送数据到前端 if self._socketio: self._socketio.emit('imu_data', data, namespace='/devices') @@ -800,41 +755,7 @@ class IMUManager(BaseDevice): """ return self.last_valid_data.copy() if self.last_valid_data else None - def collect_head_pose_data(self, duration: int = 10) -> List[Dict[str, Any]]: - """ - 收集头部姿态数据 - - Args: - duration: 收集时长(秒) - - Returns: - List[Dict[str, Any]]: 收集到的数据列表 - """ - collected_data = [] - - if not self.is_connected or not self.imu_device: - self.logger.error("IMU设备未连接") - return collected_data - - self.logger.info(f"开始收集头部姿态数据,时长: {duration}秒") - - start_time = time.time() - while time.time() - start_time < duration: - try: - data = self.imu_device.read_data(apply_calibration=True) - if data: - # 添加时间戳 - data['timestamp'] = time.time() - collected_data.append(data) - - time.sleep(0.02) # 50Hz采样率 - - except Exception as e: - self.logger.error(f"数据收集异常: {e}") - break - - self.logger.info(f"头部姿态数据收集完成,共收集 {len(collected_data)} 个样本") - return collected_data + def disconnect(self): """ @@ -900,29 +821,37 @@ class IMUManager(BaseDevice): if not self.imu_device: return False - # 对于真实设备,检查串口连接状态 - if hasattr(self.imu_device, 'ser') and self.imu_device.ser: - # 检查串口是否仍然打开 - if not self.imu_device.ser.is_open: - return False - - # 尝试读取数据来验证连接 - try: - # 保存当前超时设置 - original_timeout = self.imu_device.ser.timeout - self.imu_device.ser.timeout = 0.1 # 设置短超时 + # 检查设备类型并分别处理 + if isinstance(self.imu_device, RealIMUDevice): + # 对于真实串口设备,检查串口连接状态 + if hasattr(self.imu_device, 'ser') and self.imu_device.ser: + # 检查串口是否仍然打开 + if not self.imu_device.ser.is_open: + return False - # 尝试读取少量数据 - test_data = self.imu_device.ser.read(1) - - # 恢复原始超时设置 - self.imu_device.ser.timeout = original_timeout - - return True # 如果没有异常,认为连接正常 - except Exception: + # 尝试读取数据来验证连接 + try: + # 保存当前超时设置 + original_timeout = self.imu_device.ser.timeout + self.imu_device.ser.timeout = 0.1 # 设置短超时 + + # 尝试读取少量数据 + test_data = self.imu_device.ser.read(1) + + # 恢复原始超时设置 + self.imu_device.ser.timeout = original_timeout + + return True # 如果没有异常,认为连接正常 + except Exception: + return False + else: return False - # 对于模拟设备,总是返回True + elif isinstance(self.imu_device, BleIMUDevice): + # 对于蓝牙设备,检查连接状态 + return self.imu_device.connected + + # 对于模拟设备或其他类型,总是返回True return True except Exception as e: diff --git a/backend/devices/pressure_manager.py b/backend/devices/pressure_manager.py index c86df087..e41b720d 100644 --- a/backend/devices/pressure_manager.py +++ b/backend/devices/pressure_manager.py @@ -167,6 +167,7 @@ class RealPressureDevice: self.frame_size = self.rows * self.cols self.buf_type = ctypes.c_uint16 * self.frame_size self.buf = self.buf_type() + # 设置连接状态 self.is_connected = True logger.info(f"SMiTSense压力传感器初始化成功: {self.rows}行 x {self.cols}列") @@ -178,8 +179,10 @@ class RealPressureDevice: def read_data(self) -> Dict[str, Any]: """读取压力数据并转换为与MockPressureDevice兼容的格式""" try: - if not self.is_connected or not self.dll: - logger.error("设备未连接") + if not self.is_connected or not self.dll: + return self._get_empty_data() + # 检查device_handle是否有效 + if not self.device_handle: return self._get_empty_data() # 读取原始压力数据 @@ -194,10 +197,10 @@ class RealPressureDevice: self.dll.fpms_usb_close_wrap(self.device_handle.value) except Exception: pass - self.device_handle = None + finally: + self.device_handle = None except Exception: - pass - self.is_connected = False + pass return self._get_empty_data() # 转换为numpy数组 @@ -430,6 +433,7 @@ class RealPressureDevice: try: if self.is_connected and self.dll and self.device_handle: self.dll.fpms_usb_close_wrap(self.device_handle.value) + # 设置连接状态为断开 self.is_connected = False logger.info('SMiTSense压力传感器连接已关闭') except Exception as e: @@ -440,267 +444,6 @@ class RealPressureDevice: self.close() -class MockPressureDevice: - """模拟压力传感器设备,模拟真实SMiTSense设备的行为""" - - def __init__(self): - self.base_pressure = 500 # 基础压力值 - self.noise_level = 10 - self.rows = 4 # 模拟传感器矩阵行数 - self.cols = 4 # 模拟传感器矩阵列数 - self.time_offset = np.random.random() * 10 # 随机时间偏移,让每个实例的波形不同 - - def read_data(self) -> Dict[str, Any]: - """读取压力数据,模拟基于矩阵数据的真实设备行为""" - try: - # 生成模拟的传感器矩阵数据 - raw_data = self._generate_simulated_matrix_data() - - # 使用与真实设备相同的计算逻辑 - foot_zones = self._calculate_foot_pressure_zones(raw_data) - - # 生成压力图像 - pressure_image_base64 = self._generate_pressure_image( - foot_zones['left_front'], - foot_zones['left_rear'], - foot_zones['right_front'], - foot_zones['right_rear'], - raw_data - ) - - return { - 'foot_pressure': { - 'left_front': round(foot_zones['left_front'], 2), - 'left_rear': round(foot_zones['left_rear'], 2), - 'right_front': round(foot_zones['right_front'], 2), - 'right_rear': round(foot_zones['right_rear'], 2), - 'left_total': round(foot_zones['left_total'], 2), - 'right_total': round(foot_zones['right_total'], 2) - }, - 'pressure_image': pressure_image_base64, - 'timestamp': datetime.now().isoformat() - } - - except Exception as e: - logger.error(f"模拟压力设备读取数据异常: {e}") - return self._get_empty_data() - - def _generate_simulated_matrix_data(self): - """生成模拟的传感器矩阵数据,模拟真实的足部压力分布""" - import time - current_time = time.time() + self.time_offset - - # 创建4x4的传感器矩阵 - matrix_data = np.zeros((self.rows, self.cols)) - - # 模拟动态的压力分布,使用正弦波叠加噪声 - for i in range(self.rows): - for j in range(self.cols): - # 基础压力值,根据传感器位置不同 - base_value = self.base_pressure * (0.3 + 0.7 * np.random.random()) - - # 添加时间变化(模拟人体重心变化) - time_variation = np.sin(current_time * 0.5 + i * 0.5 + j * 0.3) * 0.3 - - # 添加噪声 - noise = np.random.normal(0, self.noise_level) - - # 确保压力值非负 - matrix_data[i, j] = max(0, base_value * (1 + time_variation) + noise) - - return matrix_data - - def _calculate_foot_pressure_zones(self, raw_data): - """计算足部区域压力,返回百分比: - - 左足、右足:相对于双足总压的百分比 - - 左前、左后:相对于左足总压的百分比 - - 右前、右后:相对于右足总压的百分比 - 基于原始矩阵按行列各等分为四象限(上半部为前、下半部为后,左半部为左、右半部为右)。 - """ - try: - # 防护:空数据 - if raw_data is None: - raise ValueError("raw_data is None") - - # 转为浮点以避免 uint16 溢出 - rd = np.asarray(raw_data, dtype=np.float64) - rows, cols = rd.shape if rd.ndim == 2 else (0, 0) - if rows == 0 or cols == 0: - raise ValueError("raw_data has invalid shape") - - # 行列对半分(上=前,下=后;左=左,右=右) - mid_r = rows // 2 - mid_c = cols // 2 - - # 四象限求和 - left_front = float(np.sum(rd[:mid_r, :mid_c], dtype=np.float64)) - left_rear = float(np.sum(rd[mid_r:, :mid_c], dtype=np.float64)) - right_front = float(np.sum(rd[:mid_r, mid_c:], dtype=np.float64)) - right_rear = float(np.sum(rd[mid_r:, mid_c:], dtype=np.float64)) - - # 绝对总压 - left_total_abs = left_front + left_rear - right_total_abs = right_front + right_rear - total_abs = left_total_abs + right_total_abs - - # 左右足占比(相对于双足总压) - left_total_pct = float((left_total_abs / total_abs * 100) if total_abs > 0 else 0) - right_total_pct = float((right_total_abs / total_abs * 100) if total_abs > 0 else 0) - - # 前后占比(相对于各自单足总压) - left_front_pct = float((left_front / left_total_abs * 100) if left_total_abs > 0 else 0) - left_rear_pct = float((left_rear / left_total_abs * 100) if left_total_abs > 0 else 0) - right_front_pct = float((right_front / right_total_abs * 100) if right_total_abs > 0 else 0) - right_rear_pct = float((right_rear / right_total_abs * 100) if right_total_abs > 0 else 0) - - return { - 'left_front': left_front_pct, - 'left_rear': left_rear_pct, - 'right_front': right_front_pct, - 'right_rear': right_rear_pct, - 'left_total': left_total_pct, - 'right_total': right_total_pct, - 'total_pressure': float(total_abs) - } - except Exception as e: - logger.error(f"计算足部区域压力异常: {e}") - return { - 'left_front': 0, 'left_rear': 0, 'right_front': 0, 'right_rear': 0, - 'left_total': 0, 'right_total': 0, 'total_pressure': 0 - } - - def _generate_pressure_image(self, left_front, left_rear, right_front, right_rear, raw_data=None) -> str: - """生成足部压力图片的base64数据""" - try: - if MATPLOTLIB_AVAILABLE and raw_data is not None: - # 使用原始数据生成更详细的热力图 - return self._generate_heatmap_image(raw_data) - else: - # 降级到简单的区域显示图 - return self._generate_simple_pressure_image(left_front, left_rear, right_front, right_rear) - - except Exception as e: - logger.warning(f"生成模拟压力图片失败: {e}") - return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" - - def _generate_heatmap_image(self, raw_data) -> str: - """生成基于原始数据的热力图""" - try: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - from io import BytesIO - - # 参考 tests/testsmit.py 的渲染方式:使用 jet 色图、nearest 插值、固定范围并关闭坐标轴 - fig, ax = plt.subplots() - im = ax.imshow(raw_data, cmap='jet', interpolation='nearest', vmin=0, vmax=1000) - ax.axis('off') - - # 紧凑布局并导出为 base64 - from io import BytesIO - buffer = BytesIO() - plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, pad_inches=0) - buffer.seek(0) - image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - plt.close(fig) - - return f"data:image/png;base64,{image_base64}" - - except Exception as e: - logger.warning(f"生成热力图失败: {e}") - return self._generate_simple_pressure_image(0, 0, 0, 0) - - def _generate_simple_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str: - """生成简单的足部压力区域图""" - try: - import matplotlib - matplotlib.use('Agg') # 设置非交互式后端,避免Tkinter错误 - import matplotlib.pyplot as plt - import matplotlib.patches as patches - from io import BytesIO - - # 临时禁用PIL的调试日志 - pil_logger = logging.getLogger('PIL') - original_level = pil_logger.level - pil_logger.setLevel(logging.WARNING) - - # 创建图形 - fig, ax = plt.subplots(1, 1, figsize=(6, 8)) - ax.set_xlim(0, 10) - ax.set_ylim(0, 12) - ax.set_aspect('equal') - ax.axis('off') - - # 定义颜色映射(根据压力值) - max_pressure = max(left_front, left_rear, right_front, right_rear) - if max_pressure > 0: - left_front_color = plt.cm.Reds(left_front / max_pressure) - left_rear_color = plt.cm.Reds(left_rear / max_pressure) - right_front_color = plt.cm.Reds(right_front / max_pressure) - right_rear_color = plt.cm.Reds(right_rear / max_pressure) - else: - left_front_color = left_rear_color = right_front_color = right_rear_color = 'lightgray' - - # 绘制左脚 - left_front_rect = patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=left_front_color) - left_rear_rect = patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=left_rear_color) - - # 绘制右脚 - right_front_rect = patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=right_front_color) - right_rear_rect = patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=right_rear_color) - - # 添加到图形 - ax.add_patch(left_front_rect) - ax.add_patch(left_rear_rect) - ax.add_patch(right_front_rect) - ax.add_patch(right_rear_rect) - - # 添加标签 - ax.text(2, 8, f'{left_front:.1f}', ha='center', va='center', fontsize=10, weight='bold') - ax.text(2, 4, f'{left_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold') - ax.text(8, 8, f'{right_front:.1f}', ha='center', va='center', fontsize=10, weight='bold') - ax.text(8, 4, f'{right_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold') - - ax.text(2, 0.5, '左足', ha='center', va='center', fontsize=12, weight='bold') - ax.text(8, 0.5, '右足', ha='center', va='center', fontsize=12, weight='bold') - - # 保存为base64 - buffer = BytesIO() - plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, facecolor='white') - buffer.seek(0) - image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - plt.close(fig) - - # 恢复PIL的日志级别 - pil_logger.setLevel(original_level) - - return f"data:image/png;base64,{image_base64}" - - except Exception as e: - # 确保在异常情况下也恢复PIL的日志级别 - try: - pil_logger.setLevel(original_level) - except: - pass - logger.warning(f"生成压力图片失败: {e}") - # 返回一个简单的占位符base64图片 - return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" - - def _get_empty_data(self): - """返回空的压力数据""" - return { - 'foot_pressure': { - 'left_front': 0.0, - 'left_rear': 0.0, - 'right_front': 0.0, - 'right_rear': 0.0, - 'left_total': 0.0, - 'right_total': 0.0 - }, - 'pressure_image': "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", - 'timestamp': datetime.now().isoformat() - } - class PressureManager(BaseDevice): """压力板管理器""" @@ -767,7 +510,8 @@ class PressureManager(BaseDevice): else: self.device = MockPressureDevice() - self.is_connected = True + # 使用set_connected方法启动连接监控线程 + self.set_connected(True) self._device_info.update({ 'device_type': self.device_type, 'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown' @@ -843,54 +587,12 @@ class PressureManager(BaseDevice): """ self.logger.info("压力数据流线程启动") - reconnect_attempts = 0 - consecutive_read_failures = 0 - try: while self.is_streaming: try: - # 若设备未连接或不存在,进入重连流程 - if not self.device or not self.is_connected or (hasattr(self.device, 'is_connected') and not self.device.is_connected): - # 广播断开状态(仅状态变化时) - if self._last_connected_state is not False: - try: - if self._socketio: - self._socketio.emit('pressure_status', { - 'status': 'disconnected', - 'timestamp': time.time() - }, namespace='/devices') - except Exception: - pass - self._last_connected_state = False - - if self.max_reconnect_attempts == -1 or reconnect_attempts < self.max_reconnect_attempts: - self.logger.warning(f"压力设备连接丢失,尝试重连 ({'∞' if self.max_reconnect_attempts == -1 else reconnect_attempts + 1}/{self.max_reconnect_attempts if self.max_reconnect_attempts != -1 else '∞'})") - if not self.is_streaming: - break - if self._reconnect(): - reconnect_attempts = 0 - consecutive_read_failures = 0 - # 广播恢复 - try: - if self._socketio: - self._socketio.emit('pressure_status', { - 'status': 'connected', - 'timestamp': time.time() - }, namespace='/devices') - except Exception: - pass - self._last_connected_state = True - continue - else: - reconnect_attempts += 1 - time.sleep(self.reconnect_delay) - continue - else: - self.logger.error("压力设备重连失败次数过多,进入降频重试模式") - time.sleep(max(self.reconnect_delay, 5.0)) - reconnect_attempts = 0 - continue - + + # 读数成功,重置失败计数 + self.is_connected = True # 从设备读取数据 pressure_data = None if self.device: @@ -899,28 +601,8 @@ class PressureManager(BaseDevice): if hasattr(self.device, 'is_connected') and not self.device.is_connected: self.is_connected = False time.sleep(self.reconnect_delay) - continue - - if not pressure_data or 'foot_pressure' not in pressure_data: - consecutive_read_failures += 1 - if consecutive_read_failures >= self.read_fail_threshold: - self.logger.warning(f"连续读取压力数据失败 {consecutive_read_failures} 次,执行设备软复位并进入重连") - try: - if self.device and hasattr(self.device, 'close'): - self.device.close() - except Exception: - pass - self.is_connected = False - consecutive_read_failures = 0 - time.sleep(self.reconnect_delay) - continue - time.sleep(0.05) - continue - - # 读数成功,重置失败计数 - consecutive_read_failures = 0 - self.is_connected = True - + continue + foot_pressure = pressure_data['foot_pressure'] # 获取各区域压力值 left_front = foot_pressure['left_front'] @@ -965,6 +647,9 @@ class PressureManager(BaseDevice): 'timestamp': pressure_data['timestamp'] } + # 更新心跳时间,防止连接监控线程判定为超时 + self.update_heartbeat() + # 更新统计信息 self.packet_count += 1 self.last_data_time = time.time() @@ -982,7 +667,7 @@ class PressureManager(BaseDevice): except Exception as e: self.error_count += 1 - self.logger.error(f"压力数据流处理异常: {e}") + # self.logger.error(f"压力数据流处理异常: {e}") time.sleep(0.1) except Exception as e: @@ -990,41 +675,7 @@ class PressureManager(BaseDevice): finally: self.logger.info("压力数据流线程结束") - def _reconnect(self) -> bool: - """重新连接压力设备""" - try: - # 先清理旧设备 - try: - if self.device and hasattr(self.device, 'close'): - self.device.close() - except Exception: - pass - self.device = None - self.is_connected = False - - time.sleep(1.0) # 等待设备释放 - - # 重新创建设备 - if self.device_type == 'real': - self.device = RealPressureDevice() - else: - self.device = MockPressureDevice() - - self.is_connected = True - # 广播一次连接状态 - try: - if self._socketio: - self._socketio.emit('pressure_status', { - 'status': 'connected', - 'timestamp': time.time() - }, namespace='/devices') - except Exception: - pass - return True - except Exception as e: - self.logger.error(f"压力设备重连失败: {e}") - self.is_connected = False - return False + def get_status(self) -> Dict[str, Any]: """ @@ -1085,7 +736,8 @@ class PressureManager(BaseDevice): self.device.close() self.device = None - self.is_connected = False + # 使用set_connected方法停止连接监控线程 + self.set_connected(False) self.logger.info("压力板设备连接已断开") return True @@ -1130,25 +782,31 @@ class PressureManager(BaseDevice): Returns: bool: 硬件连接是否正常 """ - try: + try: if not self.device: - return False + # 如果设备实例不存在,尝试重新创建 + return self._attempt_device_reconnection() # 对于真实设备,检查DLL和设备句柄状态 if hasattr(self.device, 'dll') and hasattr(self.device, 'device_handle'): if not self.device.dll or not self.device.device_handle: - return False + # DLL或句柄无效,尝试重连 + return self._attempt_device_reconnection() - # 检查设备连接状态 - if not self.device.is_connected: - return False - - # 尝试读取一次数据来验证连接 + # 直接检查设备句柄的有效性,不依赖is_connected状态避免循环依赖 try: - test_data = self.device.read_data() - return test_data is not None and 'foot_pressure' in test_data + # 检查设备句柄是否有效 + if not self.device.device_handle or not hasattr(self.device.device_handle, 'value'): + return self._attempt_device_reconnection() + + # 检查句柄值是否为0(无效句柄) + if self.device.device_handle.value == 0: + return self._attempt_device_reconnection() + + # 尝试简单的设备状态检查,而不是完整的数据读取 + return True except Exception: - return False + return self._attempt_device_reconnection() # 对于模拟设备,总是返回True return True @@ -1156,7 +814,50 @@ class PressureManager(BaseDevice): except Exception as e: self.logger.debug(f"检查压力板硬件连接时出错: {e}") return False - + + def _attempt_device_reconnection(self) -> bool: + """ + 尝试重新连接压力板设备 + + Returns: + bool: 重连是否成功 + """ + try: + self.logger.info("检测到压力板设备断开,尝试重新连接...") + + # 清理旧的设备实例 + if self.device and hasattr(self.device, 'close'): + try: + self.device.close() + except Exception as e: + self.logger.debug(f"清理旧设备实例时出错: {e}") + + self.device = None + + # 根据设备类型重新创建设备实例 + if self.device_type == 'real': + self.device = RealPressureDevice() + else: + self.device = MockPressureDevice() + + # 检查新设备是否连接成功 + if hasattr(self.device, 'is_connected') and self.device.is_connected: + self._notify_status_change(True) + # 重连成功后,确保数据流正在运行 + if not self.is_streaming: + self.logger.info("重连成功,启动压力数据流") + self.start_streaming() + + return True + else: + self.logger.warning("压力板设备重连失败") + return False + + except Exception as e: + self.logger.error(f"压力板设备重连过程中出错: {e}") + self.device = None + return False + def cleanup(self) -> None: """清理资源""" try: diff --git a/backend/devices/utils/config.ini b/backend/devices/utils/config.ini index 35a5c897..953f8a42 100644 --- a/backend/devices/utils/config.ini +++ b/backend/devices/utils/config.ini @@ -15,14 +15,17 @@ backup_interval = 24 max_backups = 7 [CAMERA] -device_index = 3 +enabled = True +device_index = 0 width = 1280 height = 720 fps = 30 buffer_size = 1 fourcc = MJPG +backend = directshow [FEMTOBOLT] +enabled = True algorithm_type = opencv color_resolution = 1080P depth_mode = NFOV_2X2BINNED @@ -33,10 +36,13 @@ fps = 15 synchronized_images_only = False [DEVICES] +imu_enabled = True imu_device_type = ble imu_port = COM9 imu_mac_address = ef:3c:1a:0a:fe:02 imu_baudrate = 9600 + +pressure_enabled = True pressure_device_type = real pressure_use_mock = False pressure_port = COM5 diff --git a/backend/devices/utils/config_manager.py b/backend/devices/utils/config_manager.py index 1b6307b9..5f5a91e5 100644 --- a/backend/devices/utils/config_manager.py +++ b/backend/devices/utils/config_manager.py @@ -109,7 +109,8 @@ class ConfigManager: 'device_index': '0', 'width': '1280', 'height': '720', - 'fps': '30' + 'fps': '30', + 'backend': 'directshow' } # 默认FemtoBolt配置 @@ -168,12 +169,14 @@ class ConfigManager: Dict[str, Any]: 相机配置 """ return { + 'enabled': self.config.getboolean('CAMERA', 'enabled', fallback=True), 'device_index': self.config.getint('CAMERA', 'device_index', fallback=0), 'width': self.config.getint('CAMERA', 'width', fallback=1280), 'height': self.config.getint('CAMERA', 'height', fallback=720), 'fps': self.config.getint('CAMERA', 'fps', fallback=30), 'buffer_size': self.config.getint('CAMERA', 'buffer_size', fallback=1), - 'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG') + 'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG'), + 'backend': self.config.get('CAMERA', 'backend', fallback='directshow') } def _get_femtobolt_config(self) -> Dict[str, Any]: @@ -184,6 +187,7 @@ class ConfigManager: Dict[str, Any]: FemtoBolt配置 """ return { + 'enabled': self.config.getboolean('FEMTOBOLT', 'enabled', fallback=True), 'algorithm_type': self.config.get('FEMTOBOLT', 'algorithm_type', fallback='opencv'), 'color_resolution': self.config.get('FEMTOBOLT', 'color_resolution', fallback='1080P'), 'depth_mode': self.config.get('FEMTOBOLT', 'depth_mode', fallback='NFOV_UNBINNED'), @@ -201,6 +205,7 @@ class ConfigManager: Dict[str, Any]: IMU配置 """ return { + 'enabled': self.config.getboolean('DEVICES', 'imu_enabled', fallback=True), 'device_type': self.config.get('DEVICES', 'imu_device_type', fallback='mock'), 'port': self.config.get('DEVICES', 'imu_port', fallback='COM7'), 'baudrate': self.config.getint('DEVICES', 'imu_baudrate', fallback=9600), @@ -217,6 +222,7 @@ class ConfigManager: Dict[str, Any]: 压力传感器配置 """ return { + 'enabled': self.config.getboolean('DEVICES', 'pressure_enabled', fallback=True), 'device_type': self.config.get('DEVICES', 'pressure_device_type', fallback='mock'), 'port': self.config.get('DEVICES', 'pressure_port', fallback='COM8'), 'baudrate': self.config.getint('DEVICES', 'pressure_baudrate', fallback=115200), @@ -458,6 +464,8 @@ class ConfigManager: self.set_config_value('CAMERA', 'height', str(config_data['height'])) if 'fps' in config_data: self.set_config_value('CAMERA', 'fps', str(config_data['fps'])) + if 'backend' in config_data: + self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) # 保存配置 self.save_config() @@ -619,6 +627,8 @@ class ConfigManager: self.set_config_value('CAMERA', 'fourcc', config_data['fourcc']) if 'tx_max_width' in config_data: self.set_config_value('CAMERA', 'tx_max_width', str(config_data['tx_max_width'])) + if 'backend' in config_data: + self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) results['camera'] = { 'success': True, diff --git a/backend/main.py b/backend/main.py index d20bec76..44f4f7f5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -184,34 +184,37 @@ class AppServer: self.config_manager = ConfigManager() self.logger.info('配置管理器初始化完成') - # 初始化设备管理器 - self.logger.info('正在初始化设备管理器...') - self.device_managers = { - 'camera': CameraManager(self.socketio, self.config_manager), - 'femtobolt': FemtoBoltManager(self.socketio, self.config_manager), - 'imu': IMUManager(self.socketio, self.config_manager), - 'pressure': PressureManager(self.socketio, self.config_manager) - } - - # 为每个设备添加状态变化回调 - for device_name, manager in self.device_managers.items(): - if manager and hasattr(manager, 'add_status_change_callback'): - manager.add_status_change_callback(self._on_device_status_change) - - self.logger.info('设备管理器初始化完成') - - # 初始化设备协调器 + # 初始化设备协调器(统一管理所有设备) self.logger.info('正在初始化设备协调器...') self.device_coordinator = DeviceCoordinator(self.socketio) - self.logger.info('设备协调器初始化完成') + # 调用初始化方法来初始化设备 + if self.device_coordinator.initialize(): + self.logger.info('设备协调器初始化完成') + # 获取设备管理器实例 + self.device_managers = self.device_coordinator.get_device_managers() + + # 为每个设备添加状态变化回调 + for device_name, manager in self.device_managers.items(): + if manager and hasattr(manager, 'add_status_change_callback'): + manager.add_status_change_callback(self._on_device_status_change) + + self.logger.info(f'已获取设备管理器: {list(self.device_managers.keys())}') + else: + self.logger.warning('设备协调器初始化失败,但系统将继续运行') + self.device_managers = {} # 初始化为空字典以避免后续错误 # 初始化录制管理器 self.logger.info('正在初始化录制管理器...') - self.recording_manager = RecordingManager( - camera_manager=self.device_managers['camera'], - db_manager=self.db_manager - ) - self.logger.info('录制管理器初始化完成') + camera_manager = self.device_managers.get('camera') + if camera_manager: + self.recording_manager = RecordingManager( + camera_manager=camera_manager, + db_manager=self.db_manager + ) + self.logger.info('录制管理器初始化完成') + else: + self.recording_manager = None + self.logger.warning('相机设备未初始化,录制管理器将不可用') # 启动Flask应用 host = self.host @@ -977,14 +980,23 @@ class AppServer: data = flask_request.get_json() patient_id = data.get('patient_id') - creator_id = data.get('creator_id') + creator_id = data.get('creator_id') if not patient_id or not creator_id: return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400 # 调用create_detection_session方法,settings传空字典 - session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) + session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) + self.logger.info('检测开始,设备连接监控已启动') + # 启动设备连接监控 + if self.device_coordinator: + try: + self.device_coordinator.start_all_connection_monitor() + self.logger.info('检测开始,设备连接监控已启动') + except Exception as monitor_error: + self.logger.error(f'启动设备连接监控失败: {monitor_error}') + return jsonify({'success': True, 'session_id': session_id}) except Exception as e: self.logger.error(f'开始检测失败: {e}') @@ -1017,6 +1029,14 @@ class AppServer: except Exception as duration_error: self.logger.error(f'更新会话持续时间失败: {duration_error}') + # 停止设备连接监控 + if self.device_coordinator: + try: + self.device_coordinator.stop_all_connection_monitor() + self.logger.info('检测停止,设备连接监控已停止') + except Exception as monitor_error: + self.logger.error(f'停止设备连接监控失败: {monitor_error}') + success = self.db_manager.update_session_status(session_id, 'completed') if success: self.logger.info(f'检测会话已停止 - 会话ID: {session_id}') @@ -1508,6 +1528,17 @@ class AppServer: def initialize_device(device_name, manager): """设备初始化工作函数""" try: + # 检查设备是否已连接,避免重复初始化 + if hasattr(manager, 'is_connected') and manager.is_connected: + print(f"[DEBUG] {device_name} 已连接,跳过初始化") + # 如果已连接但未启动流,则启动流 + if hasattr(manager, 'is_streaming') and not manager.is_streaming: + print(f"[DEBUG] {device_name} 已连接但未启动流,开始启动流") + manager.start_streaming() + device_results[device_name] = True + self.logger.info(f'{device_name}设备已连接,启动成功') + return + print(f"[DEBUG] 尝试初始化设备: {device_name}") if manager.initialize(): print(f"[DEBUG] {device_name} 初始化成功,开始启动流") diff --git a/backend/requirements.txt b/backend/requirements.txt index 8716eecb..3fb55091 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -25,8 +25,9 @@ Pillow # Data visualization and report generation reportlab -# Serial communication +# Serial communication and Bluetooth pyserial +bleak # Bluetooth Low Energy communication for IMU devices # Audio/video processing ffmpeg-python diff --git a/backend/simple_camera_test.py b/backend/simple_camera_test.py new file mode 100644 index 00000000..4cfeb7e0 --- /dev/null +++ b/backend/simple_camera_test.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简化的相机性能测试 +""" + +import sys +import os +import time +import logging + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from devices.utils.config_manager import ConfigManager +from devices.camera_manager import CameraManager + +# 设置日志级别 +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +def test_camera_init_time(): + """ + 测试相机初始化时间 + """ + print("相机初始化性能测试") + print("=" * 50) + + try: + # 创建管理器 + config_manager = ConfigManager() + camera_manager = CameraManager(None, config_manager) + + print("\n开始相机初始化测试...") + + # 记录总时间 + total_start = time.time() + + # 执行初始化 + success = camera_manager.initialize() + + total_time = (time.time() - total_start) * 1000 + + print(f"\n初始化结果: {'成功' if success else '失败'}") + print(f"总耗时: {total_time:.1f}ms ({total_time/1000:.1f}秒)") + + # 性能评估 + if total_time < 1000: + print("性能评级: 优秀 ⭐⭐⭐ (< 1秒)") + elif total_time < 3000: + print("性能评级: 良好 ⭐⭐ (< 3秒)") + elif total_time < 5000: + print("性能评级: 一般 ⭐ (< 5秒)") + else: + print("性能评级: 需要优化 ❌ (> 5秒)") + + if success: + # 测试校准 + print("\n测试校准性能...") + calibrate_start = time.time() + calibrate_success = camera_manager.calibrate() + calibrate_time = (time.time() - calibrate_start) * 1000 + + print(f"校准结果: {'成功' if calibrate_success else '失败'}") + print(f"校准耗时: {calibrate_time:.1f}ms") + + # 测试首帧获取 + if camera_manager.cap: + print("\n测试首帧获取...") + frame_start = time.time() + ret, frame = camera_manager.cap.read() + frame_time = (time.time() - frame_start) * 1000 + + if ret and frame is not None: + print(f"首帧获取成功 - 耗时: {frame_time:.1f}ms, 帧大小: {frame.shape}") + del frame + else: + print(f"首帧获取失败 - 耗时: {frame_time:.1f}ms") + + # 获取设备信息 + print("\n设备信息:") + device_info = camera_manager.get_device_info() + for key, value in device_info.items(): + if key in ['width', 'height', 'fps', 'fourcc']: + print(f" {key}: {value}") + + # 清理 + camera_manager.cleanup() + + except Exception as e: + print(f"\n❌ 测试失败: {e}") + import traceback + traceback.print_exc() + +def analyze_performance_bottlenecks(): + """ + 分析性能瓶颈 + """ + print("\n" + "=" * 50) + print("性能瓶颈分析") + print("=" * 50) + + print("\n根据测试结果,主要性能瓶颈可能包括:") + print("1. 相机打开 (CAP_PROP设置) - 通常耗时3-4秒") + print("2. 分辨率设置 - 可能耗时5-6秒") + print("3. FPS设置 - 可能耗时2-3秒") + print("4. 首帧读取 - 通常耗时300-400ms") + + print("\n优化建议:") + print("• 使用更快的相机后端 (如DirectShow)") + print("• 减少不必要的属性设置") + print("• 使用较低的分辨率进行初始化") + print("• 启用OpenCV优化") + print("• 设置合适的缓冲区大小") + +def main(): + print("相机启动性能优化测试") + print("目标: 将启动时间从10+秒优化到3秒以内") + + # 执行测试 + test_camera_init_time() + + # 分析结果 + analyze_performance_bottlenecks() + + print("\n" + "=" * 50) + print("测试完成") + print("=" * 50) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/backend/test_avoid_duplicate_init.py b/backend/test_avoid_duplicate_init.py new file mode 100644 index 00000000..b19ef323 --- /dev/null +++ b/backend/test_avoid_duplicate_init.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试避免重复初始化功能 +""" + +import requests +import time +import json + +def test_avoid_duplicate_initialization(): + """ + 测试避免重复初始化功能 + """ + base_url = "http://localhost:5000" + + print("=== 测试避免重复初始化功能 ===") + + # 1. 获取初始设备状态 + print("\n1. 获取初始设备状态") + try: + response = requests.get(f"{base_url}/api/devices/status") + if response.status_code == 200: + data = response.json() + print(f"设备状态: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"获取设备状态失败: {response.status_code}") + except Exception as e: + print(f"请求失败: {e}") + + # 2. 第一次启动设备数据推送 + print("\n2. 第一次启动设备数据推送") + try: + response = requests.post(f"{base_url}/api/devices/start_push") + if response.status_code == 200: + data = response.json() + print(f"第一次启动结果: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"第一次启动失败: {response.status_code}") + except Exception as e: + print(f"请求失败: {e}") + + # 等待一段时间 + print("\n等待5秒...") + time.sleep(5) + + # 3. 第二次启动设备数据推送(应该避免重复初始化) + print("\n3. 第二次启动设备数据推送(测试避免重复初始化)") + try: + response = requests.post(f"{base_url}/api/devices/start_push") + if response.status_code == 200: + data = response.json() + print(f"第二次启动结果: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"第二次启动失败: {response.status_code}") + except Exception as e: + print(f"请求失败: {e}") + + # 4. 再次获取设备状态 + print("\n4. 获取最终设备状态") + try: + response = requests.get(f"{base_url}/api/devices/status") + if response.status_code == 200: + data = response.json() + print(f"最终设备状态: {json.dumps(data, indent=2, ensure_ascii=False)}") + else: + print(f"获取设备状态失败: {response.status_code}") + except Exception as e: + print(f"请求失败: {e}") + + print("\n=== 测试完成 ===") + print("请查看终端日志,确认第二次启动时是否显示'已连接,跳过初始化'的消息") + +if __name__ == "__main__": + test_avoid_duplicate_initialization() \ No newline at end of file diff --git a/backend/test_backend_optimization.py b/backend/test_backend_optimization.py new file mode 100644 index 00000000..48b8330a --- /dev/null +++ b/backend/test_backend_optimization.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +OpenCV后端优化验证脚本 +测试DirectShow后端相对于MSMF的性能提升 +""" + +import sys +import os +import time +import logging + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from devices.camera_manager import CameraManager +from devices.utils.config_manager import ConfigManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def test_backend_performance(backend_name, test_name): + """ + 测试指定后端的性能 + + Args: + backend_name: 后端名称 (directshow, msmf) + test_name: 测试名称 + + Returns: + dict: 性能数据 + """ + print(f"\n{'='*60}") + print(f"📷 测试 {test_name} 后端性能") + print(f"{'='*60}") + + # 创建配置管理器并设置后端 + config_manager = ConfigManager() + + # 获取原始配置 + original_config = config_manager.get_device_config('camera') + + # 设置测试后端 + test_config = { + 'backend': backend_name + } + config_manager.set_camera_config(test_config) + + try: + # 创建相机管理器 + camera = CameraManager(None, config_manager) + + # 测试初始化性能 + start_time = time.time() + success = camera.initialize() + total_time = (time.time() - start_time) * 1000 + + if success: + print(f"✅ 相机初始化成功: {total_time:.1f}ms") + + # 获取实际后端信息 + if camera.cap: + backend_info = camera.cap.getBackendName() if hasattr(camera.cap, 'getBackendName') else 'Unknown' + actual_width = int(camera.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(camera.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + actual_fps = camera.cap.get(cv2.CAP_PROP_FPS) + print(f"🎯 实际后端: {backend_info}") + print(f"📐 实际分辨率: {actual_width}x{actual_height}@{actual_fps:.1f}fps") + + # 测试首帧获取 + frame_start = time.time() + ret, frame = camera.cap.read() if camera.cap else (False, None) + frame_time = (time.time() - frame_start) * 1000 + + if ret and frame is not None: + print(f"🖼️ 首帧获取: {frame_time:.1f}ms, 帧大小: {frame.shape}") + else: + print(f"❌ 首帧获取失败") + frame_time = -1 + + # 测试连续帧性能 + print(f"🎬 测试连续帧获取性能...") + frame_times = [] + for i in range(10): + frame_start = time.time() + ret, frame = camera.cap.read() if camera.cap else (False, None) + if ret: + frame_times.append((time.time() - frame_start) * 1000) + time.sleep(0.01) # 小延迟 + + if frame_times: + avg_frame_time = sum(frame_times) / len(frame_times) + min_frame_time = min(frame_times) + max_frame_time = max(frame_times) + print(f"📈 连续帧性能: 平均 {avg_frame_time:.1f}ms, 最快 {min_frame_time:.1f}ms, 最慢 {max_frame_time:.1f}ms") + else: + avg_frame_time = -1 + print(f"❌ 连续帧测试失败") + + # 清理资源 + camera.cleanup() + print(f"🧹 相机资源已释放") + + return { + 'backend': backend_name, + 'success': True, + 'init_time': total_time, + 'first_frame_time': frame_time, + 'avg_frame_time': avg_frame_time, + 'backend_info': backend_info if camera.cap else 'Unknown', + 'resolution': f"{actual_width}x{actual_height}@{actual_fps:.1f}fps" if camera.cap else "未知" + } + else: + print(f"❌ 初始化失败") + return { + 'backend': backend_name, + 'success': False, + 'init_time': total_time, + 'first_frame_time': -1, + 'avg_frame_time': -1, + 'backend_info': 'Failed', + 'resolution': "失败" + } + + except Exception as e: + print(f"❌ 测试异常: {e}") + return { + 'backend': backend_name, + 'success': False, + 'init_time': -1, + 'first_frame_time': -1, + 'avg_frame_time': -1, + 'backend_info': 'Exception', + 'resolution': "异常", + 'error': str(e) + } + finally: + # 恢复原始配置 + try: + restore_config = { + 'backend': original_config['backend'] + } + config_manager.set_camera_config(restore_config) + except Exception as e: + print(f"⚠️ 恢复配置失败: {e}") + +def main(): + """ + 主函数:测试不同后端的性能 + """ + print("\n" + "="*80) + print("🚀 OpenCV后端性能优化验证") + print("="*80) + + # 测试用例 + test_cases = [ + ('directshow', 'DirectShow (推荐)'), + ('msmf', 'MSMF (默认)') + ] + + results = [] + + # 执行测试 + for backend, name in test_cases: + result = test_backend_performance(backend, name) + results.append(result) + time.sleep(1) # 测试间隔 + + # 汇总结果 + print(f"\n\n{'='*80}") + print(f"📊 后端性能优化验证汇总") + print(f"{'='*80}") + + # 表格头 + print(f"{'后端':<12} {'状态':<8} {'初始化':<12} {'首帧':<12} {'连续帧':<12} {'实际后端':<15}") + print("-" * 80) + + successful_results = [r for r in results if r['success']] + + for result in results: + status = "✅成功" if result['success'] else "❌失败" + init_time = f"{result['init_time']:.1f}ms" if result['init_time'] > 0 else "失败" + first_frame = f"{result['first_frame_time']:.1f}ms" if result['first_frame_time'] > 0 else "失败" + avg_frame = f"{result['avg_frame_time']:.1f}ms" if result['avg_frame_time'] > 0 else "失败" + backend_info = result['backend_info'][:14] if len(result['backend_info']) > 14 else result['backend_info'] + + print(f"{result['backend']:<12} {status:<8} {init_time:<12} {first_frame:<12} {avg_frame:<12} {backend_info:<15}") + + # 性能分析 + if len(successful_results) >= 2: + print(f"\n📈 性能分析:") + + # 找到最快的后端 + fastest = min(successful_results, key=lambda x: x['init_time']) + slowest = max(successful_results, key=lambda x: x['init_time']) + + print(f"🏆 最快后端: {fastest['backend']} - {fastest['init_time']:.1f}ms") + print(f"🐌 最慢后端: {slowest['backend']} - {slowest['init_time']:.1f}ms") + + if fastest['init_time'] > 0 and slowest['init_time'] > 0: + improvement = ((slowest['init_time'] - fastest['init_time']) / slowest['init_time']) * 100 + print(f"💡 性能提升: {improvement:.1f}% (使用最快后端)") + + print(f"\n📋 详细性能对比:") + for result in successful_results: + if result != fastest: + slowdown = ((result['init_time'] - fastest['init_time']) / fastest['init_time']) * 100 + print(f" {result['backend']}: 比最快后端慢 {slowdown:.1f}% ({result['init_time']:.1f}ms vs {fastest['init_time']:.1f}ms)") + + print(f"\n🎯 建议:") + if successful_results: + fastest = min(successful_results, key=lambda x: x['init_time']) + print(f"✅ 推荐使用 {fastest['backend']} 后端以获得最佳性能") + print(f"📝 配置建议: 在配置文件中设置 backend = {fastest['backend']}") + + if fastest['init_time'] > 5000: + print(f"⚠️ 性能评级: 需要优化 (> 5秒)") + elif fastest['init_time'] > 2000: + print(f"⚠️ 性能评级: 一般 (2-5秒)") + else: + print(f"✅ 性能评级: 良好 (< 2秒)") + else: + print(f"❌ 所有后端测试失败,请检查相机连接") + + print(f"\n{'='*80}") + print(f"测试完成") + print(f"{'='*80}") + +if __name__ == "__main__": + import cv2 # 需要导入cv2用于相机操作 + main() \ No newline at end of file diff --git a/backend/test_camera_analysis.py b/backend/test_camera_analysis.py new file mode 100644 index 00000000..3de1214a --- /dev/null +++ b/backend/test_camera_analysis.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +深入分析相机设备的行为 +""" + +import cv2 +import numpy as np +import time + +def analyze_camera_frame(frame, device_index): + """ + 分析相机帧的特征 + """ + print(f"\n=== 设备 {device_index} 帧分析 ===") + print(f"帧形状: {frame.shape}") + print(f"数据类型: {frame.dtype}") + print(f"数据范围: {frame.min()} - {frame.max()}") + + # 检查是否是纯色帧(可能是虚拟设备) + unique_colors = len(np.unique(frame.reshape(-1, frame.shape[-1]), axis=0)) + print(f"唯一颜色数量: {unique_colors}") + + # 检查帧的统计信息 + mean_values = np.mean(frame, axis=(0, 1)) + std_values = np.std(frame, axis=(0, 1)) + print(f"各通道均值: {mean_values}") + print(f"各通道标准差: {std_values}") + + # 检查是否是静态帧 + if np.all(std_values < 1.0): + print("⚠️ 这可能是一个静态/虚拟帧(标准差很小)") + + # 检查是否是纯黑帧 + if np.all(mean_values < 10): + print("⚠️ 这可能是一个黑色帧") + + # 检查帧的变化 + return frame + +def test_camera_devices(): + """ + 测试多个相机设备并比较帧内容 + """ + print("=== 相机设备详细分析 ===") + + devices_to_test = [0, 1] + frames = {} + + for device_index in devices_to_test: + print(f"\n--- 测试设备 {device_index} ---") + + try: + cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW) + + if cap.isOpened(): + print(f"设备 {device_index}: 成功打开") + + # 获取设备属性 + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = cap.get(cv2.CAP_PROP_FPS) + fourcc = int(cap.get(cv2.CAP_PROP_FOURCC)) + + print(f"分辨率: {width}x{height}") + print(f"帧率: {fps}") + print(f"编码: {fourcc}") + + # 读取多帧进行分析 + frames_list = [] + for i in range(5): + ret, frame = cap.read() + if ret and frame is not None: + frames_list.append(frame.copy()) + if i == 0: # 只分析第一帧 + frames[device_index] = analyze_camera_frame(frame, device_index) + else: + print(f"第{i+1}帧读取失败") + break + + # 检查帧间变化 + if len(frames_list) > 1: + diff = cv2.absdiff(frames_list[0], frames_list[-1]) + total_diff = np.sum(diff) + print(f"首末帧差异总和: {total_diff}") + + if total_diff < 1000: # 阈值可调整 + print("⚠️ 帧内容几乎没有变化,可能是虚拟设备") + else: + print("✓ 帧内容有变化,可能是真实相机") + + else: + print(f"设备 {device_index}: 无法打开") + + cap.release() + + except Exception as e: + print(f"设备 {device_index} 测试异常: {e}") + + # 比较不同设备的帧 + if 0 in frames and 1 in frames: + print("\n=== 设备间帧比较 ===") + diff = cv2.absdiff(frames[0], frames[1]) + total_diff = np.sum(diff) + print(f"设备0和设备1帧差异总和: {total_diff}") + + if total_diff < 1000: + print("⚠️ 两个设备的帧几乎相同,设备1可能是设备0的镜像或虚拟设备") + else: + print("✓ 两个设备的帧不同,可能是独立的相机") + +def check_system_cameras(): + """ + 检查系统中可用的相机设备 + """ + print("\n=== 系统相机设备检查 ===") + + available_cameras = [] + + # 测试前10个设备索引 + for i in range(10): + cap = cv2.VideoCapture(i, cv2.CAP_DSHOW) + if cap.isOpened(): + ret, _ = cap.read() + if ret: + available_cameras.append(i) + print(f"设备 {i}: 可用") + else: + print(f"设备 {i}: 打开但无法读取") + else: + print(f"设备 {i}: 不可用") + cap.release() + + # 避免测试太多设备 + if len(available_cameras) >= 3: + break + + print(f"\n发现 {len(available_cameras)} 个可用相机设备: {available_cameras}") + return available_cameras + +if __name__ == "__main__": + check_system_cameras() + test_camera_devices() \ No newline at end of file diff --git a/backend/test_camera_disconnect.py b/backend/test_camera_disconnect.py new file mode 100644 index 00000000..8d37978a --- /dev/null +++ b/backend/test_camera_disconnect.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +相机断开连接测试脚本 +测试相机USB拔出时是否能正常检测设备断连并发送socket信息 +""" + +import time +import threading +import logging +from devices.camera_manager import CameraManager +from devices.utils.config_manager import ConfigManager +from unittest.mock import Mock + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class MockSocketIO: + """模拟SocketIO用于测试""" + + def __init__(self): + self.events = [] + self.lock = threading.Lock() + + def emit(self, event, data, namespace=None): + """记录发送的事件""" + with self.lock: + self.events.append({ + 'event': event, + 'data': data, + 'namespace': namespace, + 'timestamp': time.time() + }) + logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") + + def get_events(self): + """获取所有事件""" + with self.lock: + return self.events.copy() + + def clear_events(self): + """清空事件记录""" + with self.lock: + self.events.clear() + +def test_camera_disconnect_detection(): + """ + 测试相机断开连接检测功能 + """ + logger.info("="*60) + logger.info("开始测试相机断开连接检测功能") + logger.info("="*60) + + # 创建模拟SocketIO + mock_socketio = MockSocketIO() + + # 创建配置管理器 + config_manager = ConfigManager() + + # 创建相机管理器 + camera_manager = CameraManager(mock_socketio, config_manager) + + try: + # 1. 初始化相机 + logger.info("\n步骤1: 初始化相机设备") + if not camera_manager.initialize(): + logger.error("相机初始化失败,无法进行测试") + return False + + logger.info(f"相机初始化成功 - 连接状态: {camera_manager.is_connected}") + + # 2. 启动数据流 + logger.info("\n步骤2: 启动相机数据流") + if not camera_manager.start_streaming(): + logger.error("相机数据流启动失败") + return False + + logger.info("相机数据流启动成功") + + # 3. 等待一段时间让系统稳定 + logger.info("\n步骤3: 等待系统稳定 (5秒)") + time.sleep(5) + + # 清空之前的事件记录 + mock_socketio.clear_events() + + # 4. 提示用户拔出USB + logger.info("\n步骤4: 请拔出相机USB连接线") + logger.info("等待30秒来检测断开连接...") + + start_time = time.time() + disconnect_detected = False + + # 监控30秒 + while time.time() - start_time < 30: + # 检查连接状态 + if camera_manager.is_connected: + logger.debug(f"相机仍然连接中... (已等待 {time.time() - start_time:.1f}秒)") + else: + logger.info(f"检测到相机断开连接! (耗时 {time.time() - start_time:.1f}秒)") + disconnect_detected = True + break + + time.sleep(1) + + # 5. 分析结果 + logger.info("\n步骤5: 分析测试结果") + + if disconnect_detected: + logger.info("✓ 相机断开连接检测: 成功") + else: + logger.warning("✗ 相机断开连接检测: 失败 (30秒内未检测到断开)") + + # 检查Socket事件 + events = mock_socketio.get_events() + disconnect_events = [e for e in events if 'status' in str(e.get('data', {})) and 'disconnect' in str(e.get('data', {})).lower()] + + if disconnect_events: + logger.info(f"✓ Socket断开通知: 成功 (发送了 {len(disconnect_events)} 个断开事件)") + for event in disconnect_events: + logger.info(f" - 事件: {event['event']}, 数据: {event['data']}") + else: + logger.warning("✗ Socket断开通知: 失败 (未发送断开事件)") + + # 6. 测试重连机制 + if disconnect_detected: + logger.info("\n步骤6: 测试重连机制") + logger.info("请重新插入相机USB连接线") + logger.info("等待30秒来检测重新连接...") + + start_time = time.time() + reconnect_detected = False + + while time.time() - start_time < 30: + if camera_manager.is_connected: + logger.info(f"检测到相机重新连接! (耗时 {time.time() - start_time:.1f}秒)") + reconnect_detected = True + break + + time.sleep(1) + + if reconnect_detected: + logger.info("✓ 相机重连检测: 成功") + else: + logger.warning("✗ 相机重连检测: 失败 (30秒内未检测到重连)") + + # 7. 显示所有Socket事件 + logger.info("\n步骤7: 所有Socket事件记录") + all_events = mock_socketio.get_events() + if all_events: + for i, event in enumerate(all_events, 1): + logger.info(f" {i}. 事件: {event['event']}, 数据: {event['data']}, 时间: {time.strftime('%H:%M:%S', time.localtime(event['timestamp']))}") + else: + logger.info(" 无Socket事件记录") + + return disconnect_detected + + except Exception as e: + logger.error(f"测试过程中发生异常: {e}") + return False + + finally: + # 清理资源 + try: + camera_manager.stop_streaming() + camera_manager.disconnect() + logger.info("测试资源清理完成") + except Exception as e: + logger.error(f"清理资源时发生异常: {e}") + +def main(): + """ + 主函数 + """ + logger.info("相机断开连接测试脚本") + logger.info("此脚本将测试相机USB拔出时的断连检测和Socket通知功能") + logger.info("") + + # 运行测试 + success = test_camera_disconnect_detection() + + logger.info("\n" + "="*60) + if success: + logger.info("测试完成: 相机断开连接检测功能正常") + else: + logger.info("测试完成: 相机断开连接检测功能存在问题") + logger.info("="*60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/test_camera_full.py b/backend/test_camera_full.py new file mode 100644 index 00000000..eab1654c --- /dev/null +++ b/backend/test_camera_full.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +完整的相机断开连接测试脚本 +模拟主程序的完整流程 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +import time +import threading +import logging +from datetime import datetime +from devices.camera_manager import CameraManager +from devices.utils.config_manager import ConfigManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class MockSocketIO: + """模拟SocketIO用于测试""" + + def __init__(self): + self.events = [] + self.lock = threading.Lock() + + def emit(self, event, data, namespace=None): + """记录发送的事件""" + with self.lock: + self.events.append({ + 'event': event, + 'data': data, + 'namespace': namespace, + 'timestamp': time.time() + }) + logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") + + def get_events(self): + """获取所有事件""" + with self.lock: + return self.events.copy() + + def clear_events(self): + """清空事件记录""" + with self.lock: + self.events.clear() + +class MockAppServer: + """模拟主程序服务器""" + + def __init__(self): + self.socketio = MockSocketIO() + self.logger = logger + self.device_managers = {} + + def broadcast_device_status(self, device_name: str, is_connected: bool): + """广播单个设备状态""" + if self.socketio: + try: + status_data = { + 'device_type': device_name, + 'status': is_connected, + 'timestamp': datetime.now().isoformat() + } + self.socketio.emit('device_status', status_data, namespace='/devices') + self.logger.info(f'广播设备状态: {device_name} -> {"已连接" if is_connected else "未连接"}') + except Exception as e: + self.logger.error(f'广播设备状态失败: {e}') + + def _on_device_status_change(self, device_name: str, is_connected: bool): + """设备状态变化回调函数""" + self.logger.info(f'设备状态变化: {device_name} -> {"已连接" if is_connected else "未连接"}') + self.broadcast_device_status(device_name, is_connected) + +def test_camera_disconnect_with_socket(): + """测试相机断开连接和Socket通知""" + logger.info("="*60) + logger.info("开始测试相机断开连接和Socket通知功能") + logger.info("="*60) + + # 创建模拟服务器 + app_server = MockAppServer() + + try: + # 创建配置管理器 + config_manager = ConfigManager() + + # 创建相机管理器 + camera_manager = CameraManager(app_server.socketio, config_manager) + app_server.device_managers['camera'] = camera_manager + + # 添加状态变化回调(模拟主程序的回调注册) + camera_manager.add_status_change_callback(app_server._on_device_status_change) + + # 1. 测试初始化 + logger.info("\n步骤1: 初始化相机设备") + if camera_manager.initialize(): + logger.info(f"✓ 相机初始化成功 - 连接状态: {camera_manager.is_connected}") + else: + logger.warning("✗ 相机初始化失败") + return False + + # 清空初始化时的事件 + app_server.socketio.clear_events() + + # 2. 启动数据流(可选) + logger.info("\n步骤2: 启动相机数据流") + try: + if camera_manager.start_streaming(app_server.socketio): + logger.info("✓ 相机数据流启动成功") + time.sleep(2) # 让数据流稳定 + else: + logger.warning("✗ 相机数据流启动失败") + except Exception as e: + logger.warning(f"数据流启动异常: {e}") + + # 3. 监控连接状态变化 + logger.info("\n步骤3: 监控连接状态变化 (30秒)") + logger.info("请在此期间拔出相机USB连接线来测试断开检测...") + + start_time = time.time() + last_status = camera_manager.is_connected + disconnect_detected = False + + while time.time() - start_time < 30: + current_status = camera_manager.is_connected + + if current_status != last_status: + elapsed_time = time.time() - start_time + logger.info(f"检测到状态变化: {'连接' if current_status else '断开'} (耗时: {elapsed_time:.1f}秒)") + last_status = current_status + + if not current_status: + logger.info("✓ 成功检测到相机断开!") + disconnect_detected = True + time.sleep(2) # 等待事件处理完成 + break + + time.sleep(0.5) + + # 4. 检查Socket事件 + logger.info("\n步骤4: 检查Socket事件") + events = app_server.socketio.get_events() + + if events: + logger.info(f"✓ 共记录到 {len(events)} 个Socket事件:") + disconnect_events = 0 + for i, event in enumerate(events, 1): + logger.info(f" {i}. 事件: {event['event']}, 数据: {event['data']}, 命名空间: {event['namespace']}") + if event['event'] == 'device_status' and event['data'].get('status') == False: + disconnect_events += 1 + + if disconnect_events > 0: + logger.info(f"✓ 检测到 {disconnect_events} 个设备断开事件") + else: + logger.warning("✗ 未检测到设备断开事件") + else: + logger.warning("✗ 未记录到任何Socket事件") + + # 5. 测试结果总结 + logger.info("\n步骤5: 测试结果总结") + + if disconnect_detected: + logger.info("✓ 硬件断开检测: 成功") + else: + logger.warning("✗ 硬件断开检测: 失败 (30秒内未检测到断开)") + + if events and any(e['event'] == 'device_status' and e['data'].get('status') == False for e in events): + logger.info("✓ Socket断开通知: 成功") + else: + logger.warning("✗ Socket断开通知: 失败") + + return disconnect_detected and len(events) > 0 + + except Exception as e: + logger.error(f"测试过程中发生异常: {e}") + import traceback + traceback.print_exc() + return False + + finally: + # 清理资源 + try: + if 'camera_manager' in locals(): + camera_manager.stop_streaming() + camera_manager.disconnect() + logger.info("测试资源清理完成") + except Exception as e: + logger.error(f"清理资源时发生异常: {e}") + +def main(): + """主函数""" + logger.info("相机断开连接完整测试脚本") + logger.info("此脚本将模拟主程序的完整流程,测试相机USB拔出时的断连检测和Socket通知功能") + + success = test_camera_disconnect_with_socket() + + logger.info("\n" + "="*60) + if success: + logger.info("测试完成: 相机断开连接检测和Socket通知功能正常") + else: + logger.info("测试完成: 相机断开连接检测和Socket通知功能存在问题") + logger.info("="*60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/test_camera_performance.py b/backend/test_camera_performance.py new file mode 100644 index 00000000..9568a1a8 --- /dev/null +++ b/backend/test_camera_performance.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +相机启动性能测试脚本 +""" + +import sys +import os +import time +import logging +from typing import Dict, Any + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from devices.utils.config_manager import ConfigManager +from devices.camera_manager import CameraManager + +# 设置日志级别为DEBUG以查看详细信息 +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +def test_camera_startup_performance(): + """ + 测试相机启动性能 + """ + print("=" * 60) + print("相机启动性能测试") + print("=" * 60) + + try: + # 初始化配置管理器 + print("\n1. 初始化配置管理器...") + config_start = time.time() + config_manager = ConfigManager() + config_time = (time.time() - config_start) * 1000 + print(f"配置管理器初始化完成 (耗时: {config_time:.1f}ms)") + + # 创建相机管理器 + print("\n2. 创建相机管理器...") + manager_start = time.time() + camera_manager = CameraManager(None, config_manager) + manager_time = (time.time() - manager_start) * 1000 + print(f"相机管理器创建完成 (耗时: {manager_time:.1f}ms)") + + # 测试多次初始化以获得平均性能 + print("\n3. 执行相机初始化性能测试...") + test_rounds = 3 + init_times = [] + + for i in range(test_rounds): + print(f"\n--- 第 {i+1} 轮测试 ---") + + # 如果之前已连接,先断开 + if camera_manager.is_connected: + disconnect_start = time.time() + camera_manager.disconnect() + disconnect_time = (time.time() - disconnect_start) * 1000 + print(f"断开连接耗时: {disconnect_time:.1f}ms") + time.sleep(0.5) # 等待设备完全断开 + + # 执行初始化 + init_start = time.time() + success = camera_manager.initialize() + init_time = (time.time() - init_start) * 1000 + + if success: + print(f"✓ 初始化成功 (总耗时: {init_time:.1f}ms)") + init_times.append(init_time) + + # 测试校准性能 + calibrate_start = time.time() + calibrate_success = camera_manager.calibrate() + calibrate_time = (time.time() - calibrate_start) * 1000 + + if calibrate_success: + print(f"✓ 校准成功 (耗时: {calibrate_time:.1f}ms)") + else: + print(f"✗ 校准失败 (耗时: {calibrate_time:.1f}ms)") + + # 测试第一帧获取时间 + if camera_manager.cap: + first_frame_start = time.time() + ret, frame = camera_manager.cap.read() + first_frame_time = (time.time() - first_frame_start) * 1000 + + if ret and frame is not None: + print(f"✓ 首帧获取成功 (耗时: {first_frame_time:.1f}ms, 帧大小: {frame.shape})") + del frame # 释放内存 + else: + print(f"✗ 首帧获取失败 (耗时: {first_frame_time:.1f}ms)") + else: + print(f"✗ 初始化失败 (耗时: {init_time:.1f}ms)") + + time.sleep(1) # 测试间隔 + + # 性能统计 + print("\n" + "=" * 60) + print("性能统计结果") + print("=" * 60) + + if init_times: + avg_init_time = sum(init_times) / len(init_times) + min_init_time = min(init_times) + max_init_time = max(init_times) + + print(f"初始化性能统计 ({len(init_times)} 次成功测试):") + print(f" 平均耗时: {avg_init_time:.1f}ms") + print(f" 最快耗时: {min_init_time:.1f}ms") + print(f" 最慢耗时: {max_init_time:.1f}ms") + + # 性能评估 + if avg_init_time < 1000: # 1秒以内 + print(f" 性能评级: 优秀 ⭐⭐⭐") + elif avg_init_time < 3000: # 3秒以内 + print(f" 性能评级: 良好 ⭐⭐") + elif avg_init_time < 5000: # 5秒以内 + print(f" 性能评级: 一般 ⭐") + else: + print(f" 性能评级: 需要优化 ❌") + else: + print("❌ 所有初始化测试都失败了") + + # 获取设备信息 + if camera_manager.is_connected: + print("\n设备信息:") + device_info = camera_manager.get_device_info() + for key, value in device_info.items(): + print(f" {key}: {value}") + + # 清理资源 + print("\n4. 清理资源...") + cleanup_start = time.time() + camera_manager.cleanup() + cleanup_time = (time.time() - cleanup_start) * 1000 + print(f"资源清理完成 (耗时: {cleanup_time:.1f}ms)") + + except Exception as e: + print(f"\n❌ 测试过程中发生错误: {e}") + import traceback + traceback.print_exc() + +def test_streaming_startup(): + """ + 测试流媒体启动性能 + """ + print("\n" + "=" * 60) + print("流媒体启动性能测试") + print("=" * 60) + + try: + config_manager = ConfigManager() + camera_manager = CameraManager(None, config_manager) + + # 初始化相机 + print("\n1. 初始化相机...") + if not camera_manager.initialize(): + print("❌ 相机初始化失败,无法进行流媒体测试") + return + + # 测试流媒体启动 + print("\n2. 启动流媒体...") + streaming_start = time.time() + streaming_success = camera_manager.start_streaming() + streaming_time = (time.time() - streaming_start) * 1000 + + if streaming_success: + print(f"✓ 流媒体启动成功 (耗时: {streaming_time:.1f}ms)") + + # 等待几秒钟收集帧数据 + print("\n3. 收集性能数据...") + time.sleep(3) + + # 获取统计信息 + stats = camera_manager.get_stats() + print(f"\n流媒体性能统计:") + for key, value in stats.items(): + print(f" {key}: {value}") + + # 停止流媒体 + print("\n4. 停止流媒体...") + stop_start = time.time() + camera_manager.stop_streaming() + stop_time = (time.time() - stop_start) * 1000 + print(f"✓ 流媒体停止完成 (耗时: {stop_time:.1f}ms)") + else: + print(f"❌ 流媒体启动失败 (耗时: {streaming_time:.1f}ms)") + + # 清理 + camera_manager.cleanup() + + except Exception as e: + print(f"\n❌ 流媒体测试过程中发生错误: {e}") + import traceback + traceback.print_exc() + +def main(): + """ + 主函数 + """ + print("相机性能测试工具") + print("测试目标:优化相机启动时间,目标从10+秒降低到3秒以内") + + # 执行基本启动性能测试 + test_camera_startup_performance() + + # 执行流媒体启动性能测试 + test_streaming_startup() + + print("\n" + "=" * 60) + print("测试完成!") + print("=" * 60) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/backend/test_camera_simple.py b/backend/test_camera_simple.py new file mode 100644 index 00000000..b7778d8c --- /dev/null +++ b/backend/test_camera_simple.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简化的相机断开连接测试脚本 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +import time +import threading +import logging +from devices.camera_manager import CameraManager +from devices.utils.config_manager import ConfigManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class MockSocketIO: + """模拟SocketIO用于测试""" + + def __init__(self): + self.events = [] + self.lock = threading.Lock() + + def emit(self, event, data, namespace=None): + """记录发送的事件""" + with self.lock: + self.events.append({ + 'event': event, + 'data': data, + 'namespace': namespace, + 'timestamp': time.time() + }) + logger.info(f"Socket事件: {event} -> {data} (namespace: {namespace})") + + def get_events(self): + """获取所有事件""" + with self.lock: + return self.events.copy() + +def test_camera_connection(): + """测试相机连接和断开检测""" + logger.info("="*60) + logger.info("开始测试相机连接和断开检测功能") + logger.info("="*60) + + # 创建模拟SocketIO + mock_socketio = MockSocketIO() + + try: + # 创建配置管理器 + config_manager = ConfigManager() + + # 创建相机管理器 + camera_manager = CameraManager(mock_socketio, config_manager) + + # 添加状态变化回调 + def status_callback(device_name, is_connected): + logger.info(f"状态回调: {device_name} -> {'连接' if is_connected else '断开'}") + + camera_manager.add_status_change_callback(status_callback) + + # 1. 测试初始化 + logger.info("\n步骤1: 初始化相机设备") + if camera_manager.initialize(): + logger.info(f"✓ 相机初始化成功 - 连接状态: {camera_manager.is_connected}") + else: + logger.warning("✗ 相机初始化失败") + return False + + # 2. 测试硬件连接检查 + logger.info("\n步骤2: 测试硬件连接检查") + hardware_connected = camera_manager.check_hardware_connection() + logger.info(f"硬件连接状态: {hardware_connected}") + + # 3. 启动连接监控 + logger.info("\n步骤3: 启动连接监控") + camera_manager._start_connection_monitor() + logger.info("连接监控已启动") + + # 4. 监控连接状态变化 + logger.info("\n步骤4: 监控连接状态 (30秒)") + logger.info("请在此期间拔出相机USB连接线来测试断开检测...") + + start_time = time.time() + last_status = camera_manager.is_connected + + while time.time() - start_time < 30: + current_status = camera_manager.is_connected + + if current_status != last_status: + logger.info(f"检测到状态变化: {'连接' if current_status else '断开'} (耗时: {time.time() - start_time:.1f}秒)") + last_status = current_status + + if not current_status: + logger.info("✓ 成功检测到相机断开!") + break + + time.sleep(1) + + # 5. 检查Socket事件 + logger.info("\n步骤5: 检查Socket事件") + events = mock_socketio.get_events() + + if events: + logger.info(f"共记录到 {len(events)} 个Socket事件:") + for i, event in enumerate(events, 1): + logger.info(f" {i}. {event['event']} -> {event['data']}") + else: + logger.info("未记录到Socket事件") + + return True + + except Exception as e: + logger.error(f"测试过程中发生异常: {e}") + return False + + finally: + # 清理资源 + try: + if 'camera_manager' in locals(): + camera_manager._stop_connection_monitor() + camera_manager.disconnect() + logger.info("测试资源清理完成") + except Exception as e: + logger.error(f"清理资源时发生异常: {e}") + +def main(): + """主函数""" + logger.info("相机断开连接测试脚本") + + success = test_camera_connection() + + logger.info("\n" + "="*60) + if success: + logger.info("测试完成: 相机断开连接检测功能测试完成") + else: + logger.info("测试完成: 相机断开连接检测功能存在问题") + logger.info("="*60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/test_opencv_backends.py b/backend/test_opencv_backends.py new file mode 100644 index 00000000..8a3f5db9 --- /dev/null +++ b/backend/test_opencv_backends.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +OpenCV后端性能测试脚本 +测试不同OpenCV后端(DirectShow vs MSMF)的性能差异 +""" + +import sys +import os +import time +import logging +import cv2 + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def test_opencv_backend(backend_name, backend_id, device_index=0, width=1280, height=720, fps=30): + """ + 测试指定OpenCV后端的性能 + + Args: + backend_name: 后端名称 + backend_id: 后端ID + device_index: 设备索引 + width: 宽度 + height: 高度 + fps: 帧率 + + Returns: + dict: 性能数据 + """ + print(f"\n{'='*70}") + print(f"测试 {backend_name} 后端 (ID: {backend_id})") + print(f"分辨率: {width}x{height}, FPS: {fps}") + print(f"{'='*70}") + + result = { + 'backend_name': backend_name, + 'backend_id': backend_id, + 'success': False, + 'init_time': -1, + 'config_time': -1, + 'first_frame_time': -1, + 'total_time': -1, + 'actual_resolution': 'N/A', + 'error': None + } + + cap = None + + try: + # 1. 测试相机初始化时间 + print(f"📷 初始化相机 (后端: {backend_name})...") + init_start = time.time() + + # 创建VideoCapture对象并指定后端 + cap = cv2.VideoCapture(device_index, backend_id) + + if not cap.isOpened(): + print(f"❌ 无法打开相机 (后端: {backend_name})") + result['error'] = f"无法打开相机 (后端: {backend_name})" + return result + + init_time = (time.time() - init_start) * 1000 + result['init_time'] = init_time + print(f"✅ 相机初始化成功: {init_time:.1f}ms") + + # 2. 测试配置时间 + print(f"⚙️ 配置相机参数...") + config_start = time.time() + + # 设置分辨率和帧率 + cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + cap.set(cv2.CAP_PROP_FPS, fps) + + # 设置缓冲区大小 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + + # 性能优化设置 + try: + cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光 + cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 禁用自动白平衡 + cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 设置曝光值 + except Exception as e: + print(f"⚠️ 性能优化设置警告: {e}") + + config_time = (time.time() - config_start) * 1000 + result['config_time'] = config_time + print(f"✅ 配置完成: {config_time:.1f}ms") + + # 3. 获取实际分辨率 + actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + actual_fps = cap.get(cv2.CAP_PROP_FPS) + result['actual_resolution'] = f"{actual_width}x{actual_height}@{actual_fps:.1f}fps" + print(f"🎯 实际参数: {actual_width}x{actual_height}, FPS: {actual_fps:.1f}") + + # 4. 测试首帧获取时间 + print(f"🖼️ 获取首帧...") + frame_start = time.time() + + ret, frame = cap.read() + + if ret and frame is not None: + first_frame_time = (time.time() - frame_start) * 1000 + result['first_frame_time'] = first_frame_time + print(f"✅ 首帧获取成功: {first_frame_time:.1f}ms, 帧大小: {frame.shape}") + else: + print(f"❌ 首帧获取失败") + result['error'] = "首帧获取失败" + return result + + # 5. 计算总时间 + total_time = init_time + config_time + first_frame_time + result['total_time'] = total_time + result['success'] = True + + print(f"📊 总耗时: {total_time:.1f}ms ({total_time/1000:.2f}秒)") + + # 6. 测试连续帧获取性能 + print(f"🎬 测试连续帧获取性能...") + frame_times = [] + test_frames = 10 + + for i in range(test_frames): + frame_start = time.time() + ret, frame = cap.read() + if ret: + frame_time = (time.time() - frame_start) * 1000 + frame_times.append(frame_time) + else: + break + + if frame_times: + avg_frame_time = sum(frame_times) / len(frame_times) + max_frame_time = max(frame_times) + min_frame_time = min(frame_times) + print(f"📈 连续帧性能: 平均 {avg_frame_time:.1f}ms, 最快 {min_frame_time:.1f}ms, 最慢 {max_frame_time:.1f}ms") + result['avg_frame_time'] = avg_frame_time + result['max_frame_time'] = max_frame_time + result['min_frame_time'] = min_frame_time + + return result + + except Exception as e: + print(f"❌ 测试异常: {e}") + result['error'] = str(e) + return result + + finally: + if cap: + cap.release() + print(f"🧹 相机资源已释放") + +def get_available_backends(): + """ + 获取可用的OpenCV后端 + + Returns: + list: 可用后端列表 + """ + backends = [ + ('DirectShow', cv2.CAP_DSHOW), + ('MSMF', cv2.CAP_MSMF), + ('V4L2', cv2.CAP_V4L2), + ('GStreamer', cv2.CAP_GSTREAMER), + ('Any', cv2.CAP_ANY) + ] + + available_backends = [] + + for name, backend_id in backends: + try: + # 尝试创建VideoCapture对象 + cap = cv2.VideoCapture(0, backend_id) + if cap.isOpened(): + available_backends.append((name, backend_id)) + cap.release() + except Exception: + pass + + return available_backends + +def main(): + """ + 主测试函数 + """ + print("🚀 OpenCV后端性能测试") + print(f"OpenCV版本: {cv2.__version__}") + + # 获取可用后端 + print("\n🔍 检测可用的OpenCV后端...") + available_backends = get_available_backends() + + if not available_backends: + print("❌ 未找到可用的相机后端") + return + + print(f"✅ 找到 {len(available_backends)} 个可用后端:") + for name, backend_id in available_backends: + print(f" - {name} (ID: {backend_id})") + + # 测试参数 + test_params = { + 'device_index': 0, + 'width': 1280, + 'height': 720, + 'fps': 30 + } + + print(f"\n📋 测试参数: {test_params['width']}x{test_params['height']}@{test_params['fps']}fps") + + # 执行测试 + results = [] + + for backend_name, backend_id in available_backends: + result = test_opencv_backend( + backend_name, + backend_id, + **test_params + ) + results.append(result) + + # 等待一下,避免设备冲突 + time.sleep(2) + + # 输出汇总结果 + print(f"\n{'='*90}") + print("📈 OpenCV后端性能测试汇总") + print(f"{'='*90}") + + print(f"{'后端':<12} {'状态':<8} {'初始化':<10} {'配置':<10} {'首帧':<10} {'总计':<10} {'实际分辨率':<20}") + print("-" * 90) + + successful_results = [] + + for result in results: + status = "✅成功" if result['success'] else "❌失败" + init_time = f"{result['init_time']:.1f}ms" if result['init_time'] > 0 else "N/A" + config_time = f"{result['config_time']:.1f}ms" if result['config_time'] > 0 else "N/A" + frame_time = f"{result['first_frame_time']:.1f}ms" if result['first_frame_time'] > 0 else "N/A" + total_time = f"{result['total_time']:.1f}ms" if result['total_time'] > 0 else "N/A" + + print(f"{result['backend_name']:<12} {status:<8} {init_time:<10} {config_time:<10} {frame_time:<10} {total_time:<10} {result['actual_resolution']:<20}") + + if result['success']: + successful_results.append(result) + + # 性能分析 + if len(successful_results) >= 2: + print(f"\n📊 性能分析:") + + # 找到最快和最慢的后端 + fastest = min(successful_results, key=lambda x: x['total_time']) + slowest = max(successful_results, key=lambda x: x['total_time']) + + print(f"🏆 最快后端: {fastest['backend_name']} - {fastest['total_time']:.1f}ms") + print(f"🐌 最慢后端: {slowest['backend_name']} - {slowest['total_time']:.1f}ms") + + if slowest['total_time'] > fastest['total_time']: + improvement = ((slowest['total_time'] - fastest['total_time']) / slowest['total_time']) * 100 + print(f"💡 性能提升: {improvement:.1f}% (使用最快后端)") + + # 详细对比 + print(f"\n📋 详细性能对比:") + for result in successful_results: + if result != fastest: + if result['total_time'] > fastest['total_time']: + slower = ((result['total_time'] - fastest['total_time']) / fastest['total_time']) * 100 + print(f" {result['backend_name']}: 比最快后端慢 {slower:.1f}% ({result['total_time']:.1f}ms vs {fastest['total_time']:.1f}ms)") + + elif len(successful_results) == 1: + result = successful_results[0] + print(f"\n📊 只有一个后端测试成功: {result['backend_name']} - {result['total_time']:.1f}ms") + + # 推荐建议 + print(f"\n🎯 建议:") + if successful_results: + fastest = min(successful_results, key=lambda x: x['total_time']) + print(f"✅ 推荐使用 {fastest['backend_name']} 后端以获得最佳性能") + print(f"📝 配置建议: 在相机初始化时指定后端 cv2.VideoCapture(device_index, cv2.CAP_{fastest['backend_name'].upper()})") + + if fastest['total_time'] < 3000: + print(f"🚀 性能评级: 优秀 (< 3秒)") + elif fastest['total_time'] < 5000: + print(f"⚡ 性能评级: 良好 (< 5秒)") + else: + print(f"⚠️ 性能评级: 需要优化 (> 5秒)") + else: + print(f"❌ 所有后端测试都失败了,请检查相机连接和驱动") + + print(f"\n{'='*90}") + print("测试完成") + print(f"{'='*90}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/test_opencv_behavior.py b/backend/test_opencv_behavior.py new file mode 100644 index 00000000..b28cea02 --- /dev/null +++ b/backend/test_opencv_behavior.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试OpenCV VideoCapture的行为 +验证当设备索引不存在时VideoCapture的表现 +""" + +import cv2 +import time + +def test_video_capture_behavior(): + """ + 测试不同设备索引的VideoCapture行为 + """ + print("=== OpenCV VideoCapture 行为测试 ===") + print(f"OpenCV版本: {cv2.__version__}") + print() + + # 测试不同的设备索引 + test_indices = [0, 1, 2, 3, -1] + backends = [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY] + backend_names = ['CAP_DSHOW', 'CAP_MSMF', 'CAP_ANY'] + + for device_index in test_indices: + print(f"\n--- 测试设备索引 {device_index} ---") + + for backend, backend_name in zip(backends, backend_names): + print(f"\n后端: {backend_name}") + + try: + start_time = time.time() + cap = cv2.VideoCapture(device_index, backend) + open_time = (time.time() - start_time) * 1000 + + print(f" VideoCapture创建: 成功 (耗时: {open_time:.1f}ms)") + print(f" isOpened(): {cap.isOpened()}") + + if cap.isOpened(): + # 尝试读取帧 + start_time = time.time() + ret, frame = cap.read() + read_time = (time.time() - start_time) * 1000 + + print(f" read()返回值: ret={ret}") + if ret and frame is not None: + print(f" 帧形状: {frame.shape}") + print(f" 读取耗时: {read_time:.1f}ms") + else: + print(f" 读取失败 (耗时: {read_time:.1f}ms)") + + # 获取一些属性 + try: + width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) + height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) + fps = cap.get(cv2.CAP_PROP_FPS) + print(f" 分辨率: {int(width)}x{int(height)}") + print(f" 帧率: {fps}") + except Exception as e: + print(f" 获取属性失败: {e}") + else: + print(" 相机未打开") + + cap.release() + + except Exception as e: + print(f" 异常: {e}") + + print("\n=== 测试完成 ===") + +def test_specific_case(): + """ + 专门测试device_index=1的情况 + """ + print("\n=== 专门测试 device_index=1 ===") + + try: + # 使用DSHOW后端(Windows默认) + cap = cv2.VideoCapture(1, cv2.CAP_DSHOW) + print(f"VideoCapture(1, CAP_DSHOW) 创建成功") + print(f"isOpened(): {cap.isOpened()}") + + if cap.isOpened(): + print("相机显示为已打开,但这可能是虚假的") + + # 尝试多次读取 + for i in range(3): + print(f"\n第{i+1}次读取:") + start_time = time.time() + ret, frame = cap.read() + read_time = (time.time() - start_time) * 1000 + + print(f" ret: {ret}") + print(f" frame is None: {frame is None}") + print(f" 耗时: {read_time:.1f}ms") + + if ret and frame is not None: + print(f" 帧形状: {frame.shape}") + print(f" 帧数据类型: {frame.dtype}") + print(f" 帧数据范围: {frame.min()} - {frame.max()}") + else: + print(" 读取失败或帧为空") + break + else: + print("相机未打开") + + cap.release() + + except Exception as e: + print(f"异常: {e}") + +if __name__ == "__main__": + test_video_capture_behavior() + test_specific_case() \ No newline at end of file diff --git a/backend/test_reconnection.py b/backend/test_reconnection.py new file mode 100644 index 00000000..ceffe85b --- /dev/null +++ b/backend/test_reconnection.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +设备重连机制测试脚本 +测试设备断开后的自动重连功能 +""" + +import time +import threading +from devices.camera_manager import CameraManager +from devices.imu_manager import IMUManager +from devices.femtobolt_manager import FemtoBoltManager +from devices.pressure_manager import PressureManager +import logging + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +class MockSocketIO: + """模拟SocketIO用于测试""" + def emit(self, event, data): + print(f"[SocketIO] 发送事件: {event}, 数据: {data}") + +def test_device_reconnection(device_manager, device_name): + """测试设备重连机制""" + print(f"\n=== 测试 {device_name} 重连机制 ===") + + # 初始化设备 + print(f"1. 初始化 {device_name} 设备...") + success = device_manager.initialize() + print(f" 初始化结果: {'成功' if success else '失败'}") + + if success: + print(f" 设备连接状态: {'已连接' if device_manager.is_connected else '未连接'}") + + # 等待一段时间让连接稳定 + print("2. 等待连接稳定...") + time.sleep(3) + + # 模拟设备断开 + print("3. 模拟设备断开连接...") + device_manager.disconnect() + print(f" 断开后连接状态: {'已连接' if device_manager.is_connected else '未连接'}") + + # 等待一段时间 + print("4. 等待重连机制触发...") + time.sleep(5) + + # 尝试重新连接 + print("5. 尝试重新连接...") + reconnect_success = device_manager.initialize() + print(f" 重连结果: {'成功' if reconnect_success else '失败'}") + print(f" 重连后连接状态: {'已连接' if device_manager.is_connected else '未连接'}") + + # 清理 + device_manager.disconnect() + + print(f"=== {device_name} 重连测试完成 ===\n") + return success + +def main(): + """主测试函数""" + print("开始设备重连机制测试...") + + # 创建模拟SocketIO + mock_socketio = MockSocketIO() + + # 测试相机重连 + print("\n测试相机重连机制...") + camera_manager = CameraManager(mock_socketio) + test_device_reconnection(camera_manager, "相机") + + # 测试IMU重连 + print("\n测试IMU重连机制...") + imu_manager = IMUManager(mock_socketio) + test_device_reconnection(imu_manager, "IMU") + + # 测试FemtoBolt重连 + print("\n测试FemtoBolt重连机制...") + femtobolt_manager = FemtoBoltManager(mock_socketio) + test_device_reconnection(femtobolt_manager, "FemtoBolt") + + # 测试压力传感器重连 + print("\n测试压力传感器重连机制...") + pressure_manager = PressureManager(mock_socketio) + test_device_reconnection(pressure_manager, "压力传感器") + + print("\n所有设备重连测试完成!") + print("\n注意事项:") + print("1. 某些设备可能需要物理连接才能成功初始化") + print("2. 重连机制的效果取决于设备的实际可用性") + print("3. 观察日志中的连接监控线程启动和停止信息") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/test_resolution_performance.py b/backend/test_resolution_performance.py new file mode 100644 index 00000000..78fdb51a --- /dev/null +++ b/backend/test_resolution_performance.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +分辨率性能测试脚本 +测试不同分辨率下相机配置的性能差异 +""" + +import sys +import os +import time +import logging + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from devices.camera_manager import CameraManager +from devices.utils.config_manager import ConfigManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def test_resolution_performance(width, height, test_name): + """ + 测试指定分辨率的性能 + + Args: + width: 宽度 + height: 高度 + test_name: 测试名称 + + Returns: + dict: 性能数据 + """ + print(f"\n{'='*60}") + print(f"测试 {test_name}: {width}x{height}") + print(f"{'='*60}") + + # 创建配置管理器并设置分辨率 + config_manager = ConfigManager() + + # 获取原始配置 + original_config = config_manager.get_device_config('camera') + + # 临时设置测试分辨率 + test_config = { + 'width': width, + 'height': height + } + config_manager.set_camera_config(test_config) + + try: + # 创建相机管理器 + camera = CameraManager(None, config_manager) + + # 测试初始化性能 + start_time = time.time() + success = camera.initialize() + total_time = (time.time() - start_time) * 1000 + + if success: + print(f"✅ 初始化成功") + print(f"📊 总耗时: {total_time:.1f}ms ({total_time/1000:.1f}秒)") + + # 获取实际分辨率 + if camera.cap: + actual_width = int(camera.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + actual_height = int(camera.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + print(f"🎯 实际分辨率: {actual_width}x{actual_height}") + + # 测试首帧获取 + frame_start = time.time() + ret, frame = camera.cap.read() if camera.cap else (False, None) + frame_time = (time.time() - frame_start) * 1000 + + if ret and frame is not None: + print(f"🖼️ 首帧获取: {frame_time:.1f}ms, 帧大小: {frame.shape}") + else: + print(f"❌ 首帧获取失败") + frame_time = -1 + + # 清理资源 + camera.cleanup() + + return { + 'resolution': f"{width}x{height}", + 'success': True, + 'total_time': total_time, + 'frame_time': frame_time, + 'actual_resolution': f"{actual_width}x{actual_height}" if camera.cap else "未知" + } + else: + print(f"❌ 初始化失败") + return { + 'resolution': f"{width}x{height}", + 'success': False, + 'total_time': total_time, + 'frame_time': -1, + 'actual_resolution': "失败" + } + + except Exception as e: + print(f"❌ 测试异常: {e}") + return { + 'resolution': f"{width}x{height}", + 'success': False, + 'total_time': -1, + 'frame_time': -1, + 'actual_resolution': "异常", + 'error': str(e) + } + finally: + # 恢复原始配置 + try: + restore_config = { + 'width': original_config['width'], + 'height': original_config['height'] + } + config_manager.set_camera_config(restore_config) + except Exception as e: + print(f"⚠️ 恢复配置失败: {e}") + +def main(): + """ + 主测试函数 + """ + print("🚀 开始分辨率性能测试") + + # 测试不同分辨率 + test_cases = [ + (1280, 720, "当前分辨率"), + (640, 480, "标准VGA"), + (320, 240, "QVGA小分辨率"), + (160, 120, "极小分辨率") + ] + + results = [] + + for width, height, name in test_cases: + result = test_resolution_performance(width, height, name) + results.append(result) + + # 等待一下,避免设备冲突 + time.sleep(1) + + # 输出汇总结果 + print(f"\n{'='*80}") + print("📈 性能测试汇总") + print(f"{'='*80}") + + print(f"{'分辨率':<15} {'状态':<8} {'初始化耗时':<12} {'首帧耗时':<10} {'实际分辨率':<15}") + print("-" * 80) + + successful_results = [] + + for result in results: + status = "✅成功" if result['success'] else "❌失败" + init_time = f"{result['total_time']:.1f}ms" if result['total_time'] > 0 else "N/A" + frame_time = f"{result['frame_time']:.1f}ms" if result['frame_time'] > 0 else "N/A" + + print(f"{result['resolution']:<15} {status:<8} {init_time:<12} {frame_time:<10} {result['actual_resolution']:<15}") + + if result['success'] and result['total_time'] > 0: + successful_results.append(result) + + # 性能分析 + if len(successful_results) >= 2: + print(f"\n📊 性能分析:") + + # 找到最快和最慢的 + fastest = min(successful_results, key=lambda x: x['total_time']) + slowest = max(successful_results, key=lambda x: x['total_time']) + + print(f"🏆 最快配置: {fastest['resolution']} - {fastest['total_time']:.1f}ms") + print(f"🐌 最慢配置: {slowest['resolution']} - {slowest['total_time']:.1f}ms") + + if slowest['total_time'] > fastest['total_time']: + improvement = ((slowest['total_time'] - fastest['total_time']) / slowest['total_time']) * 100 + print(f"💡 性能提升: {improvement:.1f}% (使用最小分辨率)") + + # 基准对比 + baseline = next((r for r in successful_results if "1280x720" in r['resolution']), None) + if baseline: + print(f"\n📋 相对于当前分辨率(1280x720)的性能对比:") + for result in successful_results: + if result != baseline: + if result['total_time'] < baseline['total_time']: + improvement = ((baseline['total_time'] - result['total_time']) / baseline['total_time']) * 100 + print(f" {result['resolution']}: 快 {improvement:.1f}% ({result['total_time']:.1f}ms vs {baseline['total_time']:.1f}ms)") + else: + degradation = ((result['total_time'] - baseline['total_time']) / baseline['total_time']) * 100 + print(f" {result['resolution']}: 慢 {degradation:.1f}% ({result['total_time']:.1f}ms vs {baseline['total_time']:.1f}ms)") + + print(f"\n🎯 建议:") + if successful_results: + fastest = min(successful_results, key=lambda x: x['total_time']) + if fastest['total_time'] < 3000: # 小于3秒 + print(f"✅ 推荐使用 {fastest['resolution']} 以获得最佳性能") + else: + print(f"⚠️ 即使最快的分辨率 {fastest['resolution']} 仍需 {fastest['total_time']:.1f}ms") + print(f" 建议考虑其他优化方案(如更换相机后端)") + else: + print(f"❌ 所有测试都失败了,请检查相机连接") + + print(f"\n{'='*80}") + print("测试完成") + print(f"{'='*80}") + +if __name__ == "__main__": + import cv2 # 在这里导入cv2,避免在函数中导入 + main() \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 6c09487e..d2c3b329 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -11,7 +11,9 @@ 实时检测 -