优化系统框架结构,设备独立管理,实现并行数据推送
This commit is contained in:
parent
1db5a5cb33
commit
7053427249
@ -2,7 +2,6 @@
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
debug = True
|
||||
|
||||
log_level = INFO
|
||||
|
||||
[SERVER]
|
||||
@ -15,15 +14,32 @@ path = data/body_balance.db
|
||||
backup_interval = 24
|
||||
max_backups = 7
|
||||
|
||||
[DEVICES]
|
||||
camera_index = 3
|
||||
camera_width = 640
|
||||
camera_height = 480
|
||||
camera_fps = 30
|
||||
imu_port = COM8
|
||||
imu_baudrate = 9600
|
||||
pressure_port = COM4
|
||||
[CAMERA]
|
||||
device_index = 0
|
||||
width = 1280
|
||||
height = 720
|
||||
fps = 30
|
||||
|
||||
[FEMTOBOLT]
|
||||
color_resolution = 1080P
|
||||
depth_mode = NFOV_UNBINNED
|
||||
fps = 30
|
||||
depth_range_min = 1200
|
||||
depth_range_max = 1500
|
||||
|
||||
[DEVICES]
|
||||
imu_device_type = real
|
||||
imu_port = COM6
|
||||
imu_baudrate = 9600
|
||||
pressure_device_type = real
|
||||
pressure_use_mock = False
|
||||
pressure_port = COM5
|
||||
pressure_baudrate = 115200
|
||||
|
||||
[SYSTEM]
|
||||
log_level = INFO
|
||||
max_cache_size = 10
|
||||
cache_timeout = 5.0
|
||||
|
||||
[DETECTION]
|
||||
default_duration = 60
|
||||
@ -42,8 +58,3 @@ secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
[DEFAULT]
|
||||
# FemtoBolt深度相机配置
|
||||
femtobolt_color_resolution = 720P
|
||||
femtobolt_depth_range_min = 1400
|
||||
femtobolt_depth_range_max = 1900
|
||||
|
25
backend/devices/__init__.py
Normal file
25
backend/devices/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备管理模块
|
||||
重构后的设备管理架构,将原有的单一设备管理器拆分为多个独立的设备管理器
|
||||
"""
|
||||
|
||||
from .base_device import BaseDevice
|
||||
from .camera_manager import CameraManager
|
||||
from .imu_manager import IMUManager
|
||||
from .pressure_manager import PressureManager
|
||||
from .femtobolt_manager import FemtoBoltManager
|
||||
from .device_coordinator import DeviceCoordinator
|
||||
|
||||
__all__ = [
|
||||
'BaseDevice',
|
||||
'CameraManager',
|
||||
'IMUManager',
|
||||
'PressureManager',
|
||||
'FemtoBoltManager',
|
||||
'DeviceCoordinator'
|
||||
]
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__author__ = 'Body Balance Detection System'
|
262
backend/devices/base_device.py
Normal file
262
backend/devices/base_device.py
Normal file
@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备抽象基类
|
||||
定义所有设备管理器的通用接口和基础功能
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
import threading
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from .utils.socket_manager import SocketManager
|
||||
except ImportError:
|
||||
from utils.socket_manager import SocketManager
|
||||
|
||||
|
||||
class BaseDevice(ABC):
|
||||
"""设备抽象基类"""
|
||||
|
||||
def __init__(self, device_name: str, config: Dict[str, Any]):
|
||||
"""
|
||||
初始化设备基类
|
||||
|
||||
Args:
|
||||
device_name: 设备名称
|
||||
config: 设备配置字典
|
||||
"""
|
||||
self.device_name = device_name
|
||||
self.config = config
|
||||
self.is_connected = False
|
||||
self.is_streaming = False
|
||||
self.socket_namespace = f"/{device_name}"
|
||||
self.logger = logging.getLogger(f"device.{device_name}")
|
||||
self._lock = threading.RLock() # 可重入锁
|
||||
self._streaming_thread = None
|
||||
self._stop_event = threading.Event()
|
||||
self._socketio = None
|
||||
self._last_heartbeat = time.time()
|
||||
|
||||
# 设备状态信息
|
||||
self._device_info = {
|
||||
'name': device_name,
|
||||
'type': self.__class__.__name__,
|
||||
'version': '1.0.0',
|
||||
'initialized_at': None,
|
||||
'last_error': None
|
||||
}
|
||||
|
||||
# 性能统计
|
||||
self._stats = {
|
||||
'frames_processed': 0,
|
||||
'errors_count': 0,
|
||||
'start_time': None,
|
||||
'last_frame_time': None
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def calibrate(self) -> Dict[str, Any]:
|
||||
"""
|
||||
校准设备
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 校准结果
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def start_streaming(self, socketio) -> bool:
|
||||
"""
|
||||
启动数据推流
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止数据推流
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self) -> None:
|
||||
"""
|
||||
断开设备连接
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
清理资源
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_socketio(self, socketio):
|
||||
"""
|
||||
设置SocketIO实例
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
"""
|
||||
self._socketio = socketio
|
||||
|
||||
def emit_data(self, event: str, data: Any, namespace: Optional[str] = None):
|
||||
"""
|
||||
发送数据到前端
|
||||
|
||||
Args:
|
||||
event: 事件名称
|
||||
data: 数据
|
||||
namespace: 命名空间,默认使用设备命名空间
|
||||
"""
|
||||
if self._socketio:
|
||||
ns = namespace or self.socket_namespace
|
||||
try:
|
||||
self._socketio.emit(event, data, namespace=ns)
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送数据失败: {e}")
|
||||
|
||||
def update_heartbeat(self):
|
||||
"""
|
||||
更新心跳时间
|
||||
"""
|
||||
self._last_heartbeat = time.time()
|
||||
|
||||
def is_alive(self, timeout: float = 30.0) -> bool:
|
||||
"""
|
||||
检查设备是否存活
|
||||
|
||||
Args:
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
bool: 设备是否存活
|
||||
"""
|
||||
return (time.time() - self._last_heartbeat) < timeout
|
||||
|
||||
def get_device_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备信息
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备信息
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
设置错误信息
|
||||
|
||||
Args:
|
||||
error_msg: 错误消息
|
||||
"""
|
||||
with self._lock:
|
||||
self._device_info['last_error'] = {
|
||||
'message': error_msg,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _clear_error(self):
|
||||
"""
|
||||
清除错误信息
|
||||
"""
|
||||
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 __enter__(self):
|
||||
"""
|
||||
上下文管理器入口
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
上下文管理器出口
|
||||
"""
|
||||
self.cleanup()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
|
511
backend/devices/camera_manager.py
Normal file
511
backend/devices/camera_manager.py
Normal file
@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
普通相机管理器
|
||||
负责普通USB摄像头的连接、配置和数据采集
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import threading
|
||||
import time
|
||||
import base64
|
||||
import numpy as np
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import logging
|
||||
from collections import deque
|
||||
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
|
||||
|
||||
|
||||
class CameraManager(BaseDevice):
|
||||
"""普通相机管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
"""
|
||||
初始化相机管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
config = self.config_manager.get_device_config('camera')
|
||||
|
||||
super().__init__("camera", config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 相机相关属性
|
||||
self.cap = None
|
||||
self.device_index = config.get('device_index', 0)
|
||||
self.width = config.get('width', 1280)
|
||||
self.height = config.get('height', 720)
|
||||
self.fps = config.get('fps', 30)
|
||||
self.buffer_size = config.get('buffer_size', 1)
|
||||
self.fourcc = config.get('fourcc', 'MJPG')
|
||||
|
||||
# 流控制
|
||||
self.streaming_thread = None
|
||||
self.frame_cache = deque(maxlen=10)
|
||||
self.last_frame = None
|
||||
self.frame_count = 0
|
||||
self.dropped_frames = 0
|
||||
|
||||
# 性能监控
|
||||
self.fps_counter = 0
|
||||
self.fps_start_time = time.time()
|
||||
self.actual_fps = 0
|
||||
|
||||
# 重连机制
|
||||
self.max_reconnect_attempts = 3
|
||||
self.reconnect_delay = 2.0
|
||||
|
||||
# 设备标识和性能统计
|
||||
self.device_id = f"camera_{self.device_index}"
|
||||
self.performance_stats = {
|
||||
'frames_processed': 0,
|
||||
'actual_fps': 0,
|
||||
'dropped_frames': 0
|
||||
}
|
||||
|
||||
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化相机设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"正在初始化相机设备 {self.device_index}...")
|
||||
|
||||
# 尝试多个后端
|
||||
backends = [cv2.CAP_MSMF, cv2.CAP_DSHOW, cv2.CAP_ANY]
|
||||
|
||||
for backend in backends:
|
||||
try:
|
||||
self.cap = cv2.VideoCapture(self.device_index, backend)
|
||||
if self.cap.isOpened():
|
||||
self.logger.info(f"使用后端 {backend} 成功打开相机")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.warning(f"后端 {backend} 打开相机失败: {e}")
|
||||
continue
|
||||
else:
|
||||
raise Exception("所有后端都无法打开相机")
|
||||
|
||||
# 设置相机属性
|
||||
self._configure_camera()
|
||||
|
||||
# 验证相机是否正常工作
|
||||
if not self._test_camera():
|
||||
raise Exception("相机测试失败")
|
||||
|
||||
self.is_connected = True
|
||||
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'
|
||||
})
|
||||
|
||||
self.logger.info("相机初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"相机初始化失败: {e}")
|
||||
self.is_connected = False
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
return False
|
||||
|
||||
def _configure_camera(self):
|
||||
"""
|
||||
配置相机参数
|
||||
"""
|
||||
if not self.cap:
|
||||
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)
|
||||
|
||||
# 设置帧率
|
||||
self.cap.set(cv2.CAP_PROP_FPS, self.fps)
|
||||
|
||||
# 设置缓冲区大小
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, self.buffer_size)
|
||||
|
||||
# 获取实际设置的值
|
||||
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)
|
||||
|
||||
self.logger.info(f"相机配置 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"配置相机参数失败: {e}")
|
||||
|
||||
def _test_camera(self) -> bool:
|
||||
"""
|
||||
测试相机是否正常工作
|
||||
|
||||
Returns:
|
||||
bool: 测试是否成功
|
||||
"""
|
||||
try:
|
||||
ret, frame = self.cap.read()
|
||||
if ret and frame is not None:
|
||||
self.logger.info(f"相机测试成功 - 帧大小: {frame.shape}")
|
||||
return True
|
||||
else:
|
||||
self.logger.error("相机测试失败 - 无法读取帧")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"相机测试异常: {e}")
|
||||
return False
|
||||
|
||||
def calibrate(self) -> bool:
|
||||
"""
|
||||
校准相机(对于普通相机,主要是验证连接和设置)
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info("开始相机校准...")
|
||||
|
||||
if not self.is_connected:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
# 读取几帧来稳定相机
|
||||
for i in range(5):
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
self.logger.warning(f"校准时读取第{i+1}帧失败")
|
||||
|
||||
self.logger.info("相机校准完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"相机校准失败: {e}")
|
||||
return False
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
"""
|
||||
开始数据流推送
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
if self.is_streaming:
|
||||
self.logger.warning("相机流已在运行")
|
||||
return True
|
||||
|
||||
if not self.is_connected:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.is_streaming = True
|
||||
self.streaming_thread = threading.Thread(
|
||||
target=self._streaming_worker,
|
||||
name=f"Camera-{self.device_index}-Stream",
|
||||
daemon=True
|
||||
)
|
||||
self.streaming_thread.start()
|
||||
|
||||
self.logger.info("相机流启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动相机流失败: {e}")
|
||||
self.is_streaming = False
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止数据流推送
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
try:
|
||||
self.is_streaming = False
|
||||
|
||||
if self.streaming_thread and self.streaming_thread.is_alive():
|
||||
self.streaming_thread.join(timeout=3.0)
|
||||
|
||||
self.logger.info("相机流已停止")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止相机流失败: {e}")
|
||||
return False
|
||||
|
||||
def _streaming_worker(self):
|
||||
"""
|
||||
流处理工作线程
|
||||
"""
|
||||
self.logger.info("相机流工作线程启动")
|
||||
|
||||
reconnect_attempts = 0
|
||||
|
||||
while self.is_streaming:
|
||||
try:
|
||||
if not self.cap or not self.cap.isOpened():
|
||||
if reconnect_attempts < self.max_reconnect_attempts:
|
||||
self.logger.warning(f"相机连接丢失,尝试重连 ({reconnect_attempts + 1}/{self.max_reconnect_attempts})")
|
||||
if self._reconnect():
|
||||
reconnect_attempts = 0
|
||||
continue
|
||||
else:
|
||||
reconnect_attempts += 1
|
||||
time.sleep(self.reconnect_delay)
|
||||
continue
|
||||
else:
|
||||
self.logger.error("相机重连失败次数过多,停止流")
|
||||
break
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
|
||||
if not ret or frame is None:
|
||||
self.dropped_frames += 1
|
||||
if self.dropped_frames > 10:
|
||||
self.logger.warning(f"连续丢帧过多: {self.dropped_frames}")
|
||||
self.dropped_frames = 0
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# 重置丢帧计数
|
||||
self.dropped_frames = 0
|
||||
|
||||
# 处理帧
|
||||
processed_frame = self._process_frame(frame)
|
||||
|
||||
# 缓存帧
|
||||
self.last_frame = processed_frame.copy()
|
||||
self.frame_cache.append(processed_frame)
|
||||
|
||||
# 发送帧数据
|
||||
self._send_frame_data(processed_frame)
|
||||
|
||||
# 更新统计
|
||||
self._update_statistics()
|
||||
|
||||
# 内存管理
|
||||
if self.frame_count % 30 == 0:
|
||||
gc.collect()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"相机流处理异常: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.info("相机流工作线程结束")
|
||||
|
||||
def _process_frame(self, frame: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
处理视频帧
|
||||
|
||||
Args:
|
||||
frame: 原始帧
|
||||
|
||||
Returns:
|
||||
np.ndarray: 处理后的帧
|
||||
"""
|
||||
try:
|
||||
# 调整大小以优化传输
|
||||
if frame.shape[1] > 640:
|
||||
scale_factor = 640 / frame.shape[1]
|
||||
new_width = 640
|
||||
new_height = int(frame.shape[0] * scale_factor)
|
||||
frame = cv2.resize(frame, (new_width, new_height))
|
||||
|
||||
return frame
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理帧失败: {e}")
|
||||
return frame
|
||||
|
||||
def _send_frame_data(self, frame: np.ndarray):
|
||||
"""
|
||||
发送帧数据
|
||||
|
||||
Args:
|
||||
frame: 视频帧
|
||||
"""
|
||||
try:
|
||||
# 编码为JPEG
|
||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 80]
|
||||
_, buffer = cv2.imencode('.jpg', frame, encode_param)
|
||||
|
||||
# 转换为base64
|
||||
frame_data = base64.b64encode(buffer).decode('utf-8')
|
||||
|
||||
# 发送数据
|
||||
data = {
|
||||
'timestamp': time.time(),
|
||||
'frame_count': self.frame_count,
|
||||
'image': frame_data,
|
||||
'fps': self.actual_fps,
|
||||
'device_id': self.device_id
|
||||
}
|
||||
|
||||
self._socketio.emit('camera_frame', data, namespace='/camera')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送帧数据失败: {e}")
|
||||
|
||||
def _update_statistics(self):
|
||||
"""
|
||||
更新性能统计
|
||||
"""
|
||||
self.frame_count += 1
|
||||
self.fps_counter += 1
|
||||
|
||||
# 每秒计算一次实际FPS
|
||||
current_time = time.time()
|
||||
if current_time - self.fps_start_time >= 1.0:
|
||||
self.actual_fps = self.fps_counter / (current_time - self.fps_start_time)
|
||||
self.fps_counter = 0
|
||||
self.fps_start_time = current_time
|
||||
|
||||
# 更新性能统计
|
||||
self.performance_stats.update({
|
||||
'frames_processed': self.frame_count,
|
||||
'actual_fps': round(self.actual_fps, 2),
|
||||
'dropped_frames': self.dropped_frames
|
||||
})
|
||||
|
||||
def _reconnect(self) -> bool:
|
||||
"""
|
||||
重新连接相机
|
||||
|
||||
Returns:
|
||||
bool: 重连是否成功
|
||||
"""
|
||||
try:
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
|
||||
time.sleep(1.0) # 等待设备释放
|
||||
|
||||
return self.initialize()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"相机重连失败: {e}")
|
||||
return False
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
'device_index': self.device_index,
|
||||
'resolution': f"{self.width}x{self.height}",
|
||||
'target_fps': self.fps,
|
||||
'actual_fps': self.actual_fps,
|
||||
'frame_count': self.frame_count,
|
||||
'dropped_frames': self.dropped_frames,
|
||||
'has_frame': self.last_frame is not None
|
||||
})
|
||||
return status
|
||||
|
||||
def capture_image(self, save_path: Optional[str] = None) -> Optional[np.ndarray]:
|
||||
"""
|
||||
捕获单张图像
|
||||
|
||||
Args:
|
||||
save_path: 保存路径(可选)
|
||||
|
||||
Returns:
|
||||
Optional[np.ndarray]: 捕获的图像,失败返回None
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.cap:
|
||||
self.logger.error("相机未连接")
|
||||
return None
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
if not ret or frame is None:
|
||||
self.logger.error("捕获图像失败")
|
||||
return None
|
||||
|
||||
if save_path:
|
||||
cv2.imwrite(save_path, frame)
|
||||
self.logger.info(f"图像已保存到: {save_path}")
|
||||
|
||||
return frame
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"捕获图像异常: {e}")
|
||||
return None
|
||||
|
||||
def get_latest_frame(self) -> Optional[np.ndarray]:
|
||||
"""
|
||||
获取最新帧
|
||||
|
||||
Returns:
|
||||
Optional[np.ndarray]: 最新帧,无帧返回None
|
||||
"""
|
||||
return self.last_frame.copy() if self.last_frame is not None else None
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
断开相机连接
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
|
||||
self.is_connected = False
|
||||
self.logger.info("相机已断开连接")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开相机连接失败: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
清理资源
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
|
||||
self.frame_cache.clear()
|
||||
self.last_frame = None
|
||||
|
||||
super().cleanup()
|
||||
self.logger.info("相机资源清理完成")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理相机资源失败: {e}")
|
242
backend/devices/config_api.py
Normal file
242
backend/devices/config_api.py
Normal file
@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备配置HTTP API接口
|
||||
提供通过HTTP方式设置设备参数的功能
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from flask import Flask, request, jsonify
|
||||
from typing import Dict, Any
|
||||
|
||||
# 添加路径以支持导入
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
from utils.config_manager import ConfigManager
|
||||
|
||||
|
||||
class ConfigAPI:
|
||||
"""配置API类"""
|
||||
|
||||
def __init__(self, app: Flask = None):
|
||||
"""
|
||||
初始化配置API
|
||||
|
||||
Args:
|
||||
app: Flask应用实例
|
||||
"""
|
||||
self.logger = logging.getLogger(f"{__name__}.ConfigAPI")
|
||||
self.config_manager = ConfigManager()
|
||||
|
||||
if app:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app: Flask):
|
||||
"""
|
||||
初始化Flask应用
|
||||
|
||||
Args:
|
||||
app: Flask应用实例
|
||||
"""
|
||||
self.app = app
|
||||
self._register_routes()
|
||||
|
||||
def _register_routes(self):
|
||||
"""
|
||||
注册路由
|
||||
"""
|
||||
# 获取所有设备配置
|
||||
@self.app.route('/api/config/devices', methods=['GET'])
|
||||
def get_all_device_configs():
|
||||
"""获取所有设备配置"""
|
||||
try:
|
||||
configs = self.config_manager.get_all_device_configs()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': configs
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取设备配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'获取设备配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 获取单个设备配置
|
||||
@self.app.route('/api/config/devices/<device_name>', methods=['GET'])
|
||||
def get_device_config(device_name: str):
|
||||
"""获取单个设备配置"""
|
||||
try:
|
||||
if device_name not in ['imu', 'pressure', 'camera', 'femtobolt']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'不支持的设备类型: {device_name}'
|
||||
}), 400
|
||||
|
||||
config = self.config_manager.get_device_config(device_name)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': config
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取{device_name}配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'获取{device_name}配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 设置IMU配置
|
||||
@self.app.route('/api/config/devices/imu', methods=['POST'])
|
||||
def set_imu_config():
|
||||
"""设置IMU配置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请求数据不能为空'
|
||||
}), 400
|
||||
|
||||
result = self.config_manager.set_imu_config(data)
|
||||
status_code = 200 if result['success'] else 400
|
||||
return jsonify(result), status_code
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置IMU配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'设置IMU配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 设置压力板配置
|
||||
@self.app.route('/api/config/devices/pressure', methods=['POST'])
|
||||
def set_pressure_config():
|
||||
"""设置压力板配置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请求数据不能为空'
|
||||
}), 400
|
||||
|
||||
result = self.config_manager.set_pressure_config(data)
|
||||
status_code = 200 if result['success'] else 400
|
||||
return jsonify(result), status_code
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置压力板配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'设置压力板配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 设置相机配置
|
||||
@self.app.route('/api/config/devices/camera', methods=['POST'])
|
||||
def set_camera_config():
|
||||
"""设置相机配置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请求数据不能为空'
|
||||
}), 400
|
||||
|
||||
result = self.config_manager.set_camera_config(data)
|
||||
status_code = 200 if result['success'] else 400
|
||||
return jsonify(result), status_code
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置相机配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'设置相机配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 设置FemtoBolt配置
|
||||
@self.app.route('/api/config/devices/femtobolt', methods=['POST'])
|
||||
def set_femtobolt_config():
|
||||
"""设置FemtoBolt配置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请求数据不能为空'
|
||||
}), 400
|
||||
|
||||
result = self.config_manager.set_femtobolt_config(data)
|
||||
status_code = 200 if result['success'] else 400
|
||||
return jsonify(result), status_code
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置FemtoBolt配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'设置FemtoBolt配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 重新加载配置
|
||||
@self.app.route('/api/config/reload', methods=['POST'])
|
||||
def reload_config():
|
||||
"""重新加载配置"""
|
||||
try:
|
||||
self.config_manager.reload_config()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '配置重新加载成功'
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"重新加载配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'重新加载配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 验证配置
|
||||
@self.app.route('/api/config/validate', methods=['GET'])
|
||||
def validate_config():
|
||||
"""验证配置"""
|
||||
try:
|
||||
result = self.config_manager.validate_config()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"验证配置失败: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'验证配置失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
# 创建独立的Flask应用用于测试
|
||||
def create_config_app():
|
||||
"""
|
||||
创建配置API应用
|
||||
|
||||
Returns:
|
||||
Flask: Flask应用实例
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# 初始化配置API
|
||||
config_api = ConfigAPI(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 创建并运行应用
|
||||
app = create_config_app()
|
||||
app.run(host='0.0.0.0', port=5002, debug=True)
|
260
backend/devices/config_api_examples.md
Normal file
260
backend/devices/config_api_examples.md
Normal file
@ -0,0 +1,260 @@
|
||||
# 设备配置HTTP API使用示例
|
||||
|
||||
本文档展示如何通过HTTP API来设置和获取设备参数。
|
||||
|
||||
## API端点
|
||||
|
||||
基础URL: `http://localhost:5002/api/config`
|
||||
|
||||
## 1. 获取所有设备配置
|
||||
|
||||
**GET** `/devices`
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:5002/api/config/devices
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"imu": {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600,
|
||||
"timeout": 1.0,
|
||||
"calibration_samples": 100
|
||||
},
|
||||
"pressure": {
|
||||
"device_type": "real",
|
||||
"port": "COM5",
|
||||
"baudrate": 115200,
|
||||
"timeout": 1.0,
|
||||
"calibration_samples": 50
|
||||
},
|
||||
"camera": {
|
||||
"device_index": 1,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 30,
|
||||
"buffer_size": 1,
|
||||
"fourcc": "MJPG"
|
||||
},
|
||||
"femtobolt": {
|
||||
"color_resolution": "1080P",
|
||||
"depth_mode": "NFOV_UNBINNED",
|
||||
"fps": 30,
|
||||
"depth_range_min": 1200,
|
||||
"depth_range_max": 1500,
|
||||
"synchronized_images_only": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 获取单个设备配置
|
||||
|
||||
**GET** `/devices/{device_name}`
|
||||
|
||||
支持的设备名称: `imu`, `pressure`, `camera`, `femtobolt`
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:5002/api/config/devices/imu
|
||||
```
|
||||
|
||||
## 3. 设置IMU配置
|
||||
|
||||
**POST** `/devices/imu`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5002/api/config/devices/imu \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
- `device_type`: 设备类型 ("real" 或 "mock")
|
||||
- `port`: 串口号 (如 "COM6")
|
||||
- `baudrate`: 波特率 (如 9600)
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "IMU配置更新成功",
|
||||
"config": {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600,
|
||||
"timeout": 1.0,
|
||||
"calibration_samples": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 设置压力板配置
|
||||
|
||||
**POST** `/devices/pressure`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5002/api/config/devices/pressure \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_type": "real",
|
||||
"use_mock": false,
|
||||
"port": "COM5",
|
||||
"baudrate": 115200
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
- `device_type`: 设备类型 ("real" 或 "mock")
|
||||
- `use_mock`: 是否使用模拟数据 (true 或 false)
|
||||
- `port`: 串口号 (如 "COM5")
|
||||
- `baudrate`: 波特率 (如 115200)
|
||||
|
||||
## 5. 设置相机配置
|
||||
|
||||
**POST** `/devices/camera`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5002/api/config/devices/camera \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_index": 1,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 30
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
- `device_index`: 相机设备索引 (如 1)
|
||||
- `width`: 图像宽度 (如 1280)
|
||||
- `height`: 图像高度 (如 720)
|
||||
- `fps`: 帧率 (如 30)
|
||||
|
||||
## 6. 设置FemtoBolt配置
|
||||
|
||||
**POST** `/devices/femtobolt`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5002/api/config/devices/femtobolt \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"color_resolution": "1080P",
|
||||
"depth_mode": "NFOV_UNBINNED",
|
||||
"fps": 30,
|
||||
"depth_range_min": 1200,
|
||||
"depth_range_max": 1500
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
- `color_resolution`: 颜色分辨率 (如 "1080P")
|
||||
- `depth_mode`: 深度模式 (如 "NFOV_UNBINNED")
|
||||
- `fps`: 帧率 (如 30)
|
||||
- `depth_range_min`: 最小深度范围 (如 1200)
|
||||
- `depth_range_max`: 最大深度范围 (如 1500)
|
||||
|
||||
## 7. 重新加载配置
|
||||
|
||||
**POST** `/reload`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5002/api/config/reload
|
||||
```
|
||||
|
||||
## 8. 验证配置
|
||||
|
||||
**GET** `/validate`
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:5002/api/config/validate
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
当请求失败时,API会返回错误信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述"
|
||||
}
|
||||
```
|
||||
|
||||
常见的HTTP状态码:
|
||||
- `200`: 成功
|
||||
- `400`: 请求参数错误
|
||||
- `500`: 服务器内部错误
|
||||
|
||||
## 启动配置API服务
|
||||
|
||||
```bash
|
||||
# 进入设备目录
|
||||
cd backend/devices
|
||||
|
||||
# 运行配置API服务
|
||||
python config_api.py
|
||||
```
|
||||
|
||||
服务将在 `http://localhost:5002` 启动。
|
||||
|
||||
## Python客户端示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
# 设置IMU配置
|
||||
def set_imu_config():
|
||||
url = "http://localhost:5002/api/config/devices/imu"
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("IMU配置设置成功")
|
||||
print(json.dumps(result['config'], indent=2))
|
||||
else:
|
||||
print(f"设置失败: {result['message']}")
|
||||
|
||||
# 获取所有设备配置
|
||||
def get_all_configs():
|
||||
url = "http://localhost:5002/api/config/devices"
|
||||
response = requests.get(url)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("所有设备配置:")
|
||||
print(json.dumps(result['data'], indent=2))
|
||||
else:
|
||||
print(f"获取失败: {result['message']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
set_imu_config()
|
||||
get_all_configs()
|
||||
```
|
606
backend/devices/device_coordinator.py
Normal file
606
backend/devices/device_coordinator.py
Normal file
@ -0,0 +1,606 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备协调器
|
||||
负责统一管理和协调所有设备的生命周期、数据流和状态同步
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any, Callable
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
try:
|
||||
from .camera_manager import CameraManager
|
||||
from .imu_manager import IMUManager
|
||||
from .pressure_manager import PressureManager
|
||||
from .femtobolt_manager import FemtoBoltManager
|
||||
from .utils.config_manager import ConfigManager
|
||||
from .utils.socket_manager import SocketManager
|
||||
except ImportError:
|
||||
from camera_manager import CameraManager
|
||||
from imu_manager import IMUManager
|
||||
from pressure_manager import PressureManager
|
||||
from femtobolt_manager import FemtoBoltManager
|
||||
from utils.config_manager import ConfigManager
|
||||
from utils.socket_manager import SocketManager
|
||||
|
||||
|
||||
class DeviceCoordinator:
|
||||
"""设备协调器 - 统一管理所有设备"""
|
||||
|
||||
def __init__(self, socketio, config_path: Optional[str] = None):
|
||||
"""
|
||||
初始化设备协调器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_path: 配置文件路径
|
||||
"""
|
||||
self.socketio = socketio
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
# 配置管理
|
||||
self.config_manager = ConfigManager(config_path)
|
||||
self.socket_manager = SocketManager(socketio)
|
||||
|
||||
# 设备管理器
|
||||
self.devices: Dict[str, Any] = {}
|
||||
self.device_configs = self.config_manager.get_system_config().get('devices', {})
|
||||
|
||||
# 状态管理
|
||||
self.is_initialized = False
|
||||
self.is_running = False
|
||||
self.coordinator_lock = threading.RLock()
|
||||
|
||||
# 监控线程
|
||||
self.monitor_thread = None
|
||||
self.health_check_interval = 5.0 # 健康检查间隔(秒)
|
||||
|
||||
# 事件回调
|
||||
self.event_callbacks: Dict[str, List[Callable]] = defaultdict(list)
|
||||
|
||||
# 性能统计
|
||||
self.stats = {
|
||||
'start_time': None,
|
||||
'total_frames': 0,
|
||||
'device_errors': defaultdict(int),
|
||||
'reconnect_attempts': defaultdict(int)
|
||||
}
|
||||
|
||||
# 线程池
|
||||
self.executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="DeviceCoord")
|
||||
|
||||
self.logger.info("设备协调器初始化完成")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化所有设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
with self.coordinator_lock:
|
||||
if self.is_initialized:
|
||||
self.logger.warning("设备协调器已初始化")
|
||||
return True
|
||||
|
||||
try:
|
||||
self.logger.info("开始初始化设备协调器...")
|
||||
|
||||
# 注册Socket.IO命名空间
|
||||
self._register_namespaces()
|
||||
|
||||
# 初始化设备
|
||||
if not self._initialize_devices():
|
||||
raise Exception("设备初始化失败")
|
||||
|
||||
# 启动监控线程
|
||||
self._start_monitor()
|
||||
|
||||
self.is_initialized = True
|
||||
self.stats['start_time'] = time.time()
|
||||
|
||||
self.logger.info("设备协调器初始化成功")
|
||||
self._emit_event('coordinator_initialized', {'devices': list(self.devices.keys())})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设备协调器初始化失败: {e}")
|
||||
self._cleanup_devices()
|
||||
return False
|
||||
|
||||
def _register_namespaces(self):
|
||||
"""
|
||||
注册Socket.IO命名空间
|
||||
"""
|
||||
namespaces = ['/camera', '/imu', '/pressure', '/femtobolt', '/coordinator']
|
||||
for namespace in namespaces:
|
||||
self.socket_manager.register_namespace(namespace)
|
||||
|
||||
self.logger.info(f"已注册Socket.IO命名空间: {namespaces}")
|
||||
|
||||
def _initialize_devices(self) -> bool:
|
||||
"""
|
||||
初始化所有设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
# 并行初始化设备
|
||||
futures = []
|
||||
|
||||
# 普通相机
|
||||
if self.device_configs.get('camera', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_camera)
|
||||
futures.append(('camera', future))
|
||||
|
||||
# IMU传感器
|
||||
if self.device_configs.get('imu', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_imu)
|
||||
futures.append(('imu', future))
|
||||
|
||||
# 压力传感器
|
||||
if self.device_configs.get('pressure', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_pressure)
|
||||
futures.append(('pressure', future))
|
||||
|
||||
# FemtoBolt深度相机
|
||||
if self.device_configs.get('femtobolt', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_femtobolt)
|
||||
futures.append(('femtobolt', future))
|
||||
|
||||
# 等待所有设备初始化完成
|
||||
success_count = 0
|
||||
for device_name, future in futures:
|
||||
try:
|
||||
result = future.result(timeout=30) # 30秒超时
|
||||
if result:
|
||||
success_count += 1
|
||||
self.logger.info(f"{device_name}设备初始化成功")
|
||||
else:
|
||||
self.logger.error(f"{device_name}设备初始化失败")
|
||||
except Exception as e:
|
||||
self.logger.error(f"{device_name}设备初始化异常: {e}")
|
||||
|
||||
# 至少需要一个设备初始化成功
|
||||
if success_count == 0:
|
||||
raise Exception("没有设备初始化成功")
|
||||
|
||||
self.logger.info(f"设备初始化完成,成功: {success_count}/{len(futures)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"设备初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_camera(self) -> bool:
|
||||
"""
|
||||
初始化普通相机
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
camera = CameraManager(self.socketio, self.config_manager)
|
||||
if camera.initialize():
|
||||
self.devices['camera'] = camera
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化相机失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_imu(self) -> bool:
|
||||
"""
|
||||
初始化IMU传感器
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
imu = IMUManager(self.socketio, self.config_manager)
|
||||
if imu.initialize():
|
||||
self.devices['imu'] = imu
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化IMU失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_pressure(self) -> bool:
|
||||
"""
|
||||
初始化压力传感器
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
pressure = PressureManager(self.socketio, self.config_manager)
|
||||
if pressure.initialize():
|
||||
self.devices['pressure'] = pressure
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化压力传感器失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_femtobolt(self) -> bool:
|
||||
"""
|
||||
初始化FemtoBolt深度相机
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
femtobolt = FemtoBoltManager(self.socketio, self.config_manager)
|
||||
if femtobolt.initialize():
|
||||
self.devices['femtobolt'] = femtobolt
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化FemtoBolt失败: {e}")
|
||||
return False
|
||||
|
||||
def start_all_streaming(self) -> bool:
|
||||
"""
|
||||
启动所有设备的数据流
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
with self.coordinator_lock:
|
||||
if not self.is_initialized:
|
||||
self.logger.error("设备协调器未初始化")
|
||||
return False
|
||||
|
||||
if self.is_running:
|
||||
self.logger.warning("设备流已在运行")
|
||||
return True
|
||||
|
||||
try:
|
||||
self.logger.info("启动所有设备数据流...")
|
||||
|
||||
# 并行启动所有设备流
|
||||
futures = []
|
||||
for device_name, device in self.devices.items():
|
||||
future = self.executor.submit(device.start_streaming)
|
||||
futures.append((device_name, future))
|
||||
|
||||
# 等待所有设备启动完成
|
||||
success_count = 0
|
||||
for device_name, future in futures:
|
||||
try:
|
||||
result = future.result(timeout=10) # 10秒超时
|
||||
if result:
|
||||
success_count += 1
|
||||
self.logger.info(f"{device_name}数据流启动成功")
|
||||
else:
|
||||
self.logger.error(f"{device_name}数据流启动失败")
|
||||
except Exception as e:
|
||||
self.logger.error(f"{device_name}数据流启动异常: {e}")
|
||||
|
||||
self.is_running = success_count > 0
|
||||
|
||||
if self.is_running:
|
||||
self.logger.info(f"设备数据流启动完成,成功: {success_count}/{len(futures)}")
|
||||
self._emit_event('streaming_started', {'active_devices': success_count})
|
||||
else:
|
||||
self.logger.error("没有设备数据流启动成功")
|
||||
|
||||
return self.is_running
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动设备数据流失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_all_streaming(self) -> bool:
|
||||
"""
|
||||
停止所有设备的数据流
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
with self.coordinator_lock:
|
||||
if not self.is_running:
|
||||
self.logger.warning("设备流未运行")
|
||||
return True
|
||||
|
||||
try:
|
||||
self.logger.info("停止所有设备数据流...")
|
||||
|
||||
# 并行停止所有设备流
|
||||
futures = []
|
||||
for device_name, device in self.devices.items():
|
||||
if hasattr(device, 'stop_streaming'):
|
||||
future = self.executor.submit(device.stop_streaming)
|
||||
futures.append((device_name, future))
|
||||
|
||||
# 等待所有设备停止完成
|
||||
for device_name, future in futures:
|
||||
try:
|
||||
future.result(timeout=5) # 5秒超时
|
||||
self.logger.info(f"{device_name}数据流已停止")
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止{device_name}数据流异常: {e}")
|
||||
|
||||
self.is_running = False
|
||||
self.logger.info("所有设备数据流已停止")
|
||||
self._emit_event('streaming_stopped', {})
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
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]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Args:
|
||||
device_name: 设备名称,None表示获取所有设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
if device_name:
|
||||
if device_name in self.devices:
|
||||
return self.devices[device_name].get_status()
|
||||
else:
|
||||
return {'error': f'设备 {device_name} 不存在'}
|
||||
else:
|
||||
# 获取所有设备状态
|
||||
status = {
|
||||
'coordinator': {
|
||||
'is_initialized': self.is_initialized,
|
||||
'is_running': self.is_running,
|
||||
'device_count': len(self.devices),
|
||||
'uptime': time.time() - self.stats['start_time'] if self.stats['start_time'] else 0
|
||||
},
|
||||
'devices': {}
|
||||
}
|
||||
|
||||
for name, device in self.devices.items():
|
||||
try:
|
||||
status['devices'][name] = device.get_status()
|
||||
except Exception as e:
|
||||
status['devices'][name] = {'error': str(e)}
|
||||
|
||||
return status
|
||||
|
||||
def get_device(self, device_name: str) -> Optional[Any]:
|
||||
"""
|
||||
获取指定设备实例
|
||||
|
||||
Args:
|
||||
device_name: 设备名称
|
||||
|
||||
Returns:
|
||||
Optional[Any]: 设备实例,不存在返回None
|
||||
"""
|
||||
return self.devices.get(device_name)
|
||||
|
||||
def restart_device(self, device_name: str) -> bool:
|
||||
"""
|
||||
重启指定设备
|
||||
|
||||
Args:
|
||||
device_name: 设备名称
|
||||
|
||||
Returns:
|
||||
bool: 重启是否成功
|
||||
"""
|
||||
if device_name not in self.devices:
|
||||
self.logger.error(f"设备 {device_name} 不存在")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.logger.info(f"重启设备: {device_name}")
|
||||
|
||||
device = self.devices[device_name]
|
||||
|
||||
# 停止数据流
|
||||
if hasattr(device, 'stop_streaming'):
|
||||
device.stop_streaming()
|
||||
|
||||
# 清理资源
|
||||
if hasattr(device, 'cleanup'):
|
||||
device.cleanup()
|
||||
|
||||
# 重新初始化
|
||||
if device.initialize():
|
||||
self.logger.info(f"设备 {device_name} 重启成功")
|
||||
self._emit_event('device_restarted', {'device': device_name})
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"设备 {device_name} 重启失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"重启设备 {device_name} 异常: {e}")
|
||||
return False
|
||||
|
||||
def _start_monitor(self):
|
||||
"""
|
||||
启动监控线程
|
||||
"""
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
return
|
||||
|
||||
self.monitor_thread = threading.Thread(
|
||||
target=self._monitor_worker,
|
||||
name="DeviceMonitor",
|
||||
daemon=True
|
||||
)
|
||||
self.monitor_thread.start()
|
||||
self.logger.info("设备监控线程已启动")
|
||||
|
||||
def _monitor_worker(self):
|
||||
"""
|
||||
监控工作线程
|
||||
"""
|
||||
self.logger.info("设备监控线程开始运行")
|
||||
|
||||
while self.is_initialized:
|
||||
try:
|
||||
# 检查设备健康状态
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
status = device.get_status()
|
||||
if not status.get('is_connected', False):
|
||||
self.logger.warning(f"设备 {device_name} 连接丢失")
|
||||
self.stats['device_errors'][device_name] += 1
|
||||
|
||||
# 尝试重连
|
||||
if self.stats['device_errors'][device_name] <= 3:
|
||||
self.logger.info(f"尝试重连设备: {device_name}")
|
||||
if self.restart_device(device_name):
|
||||
self.stats['device_errors'][device_name] = 0
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查设备 {device_name} 状态异常: {e}")
|
||||
|
||||
# 发送状态更新
|
||||
if self.is_running:
|
||||
status = self.get_device_status()
|
||||
self.socket_manager.emit_to_namespace(
|
||||
'/coordinator', 'status_update', status
|
||||
)
|
||||
|
||||
time.sleep(self.health_check_interval)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"监控线程异常: {e}")
|
||||
time.sleep(1.0)
|
||||
|
||||
self.logger.info("设备监控线程结束")
|
||||
|
||||
def register_event_callback(self, event_name: str, callback: Callable):
|
||||
"""
|
||||
注册事件回调
|
||||
|
||||
Args:
|
||||
event_name: 事件名称
|
||||
callback: 回调函数
|
||||
"""
|
||||
self.event_callbacks[event_name].append(callback)
|
||||
|
||||
def _emit_event(self, event_name: str, data: Dict[str, Any]):
|
||||
"""
|
||||
触发事件
|
||||
|
||||
Args:
|
||||
event_name: 事件名称
|
||||
data: 事件数据
|
||||
"""
|
||||
# 调用注册的回调
|
||||
for callback in self.event_callbacks[event_name]:
|
||||
try:
|
||||
callback(data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"事件回调异常 {event_name}: {e}")
|
||||
|
||||
# 发送到Socket.IO
|
||||
self.socket_manager.emit_to_namespace(
|
||||
'/coordinator', event_name, data
|
||||
)
|
||||
|
||||
def _cleanup_devices(self):
|
||||
"""
|
||||
清理所有设备
|
||||
"""
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
if hasattr(device, 'cleanup'):
|
||||
device.cleanup()
|
||||
self.logger.info(f"设备 {device_name} 清理完成")
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理设备 {device_name} 失败: {e}")
|
||||
|
||||
self.devices.clear()
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
关闭设备协调器
|
||||
"""
|
||||
with self.coordinator_lock:
|
||||
try:
|
||||
self.logger.info("关闭设备协调器...")
|
||||
|
||||
# 停止数据流
|
||||
self.stop_all_streaming()
|
||||
|
||||
# 停止监控
|
||||
self.is_initialized = False
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
self.monitor_thread.join(timeout=5.0)
|
||||
|
||||
# 清理设备
|
||||
self._cleanup_devices()
|
||||
|
||||
# 关闭线程池
|
||||
self.executor.shutdown(wait=True)
|
||||
|
||||
# 清理Socket管理器
|
||||
self.socket_manager.cleanup()
|
||||
|
||||
self.logger.info("设备协调器已关闭")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"关闭设备协调器失败: {e}")
|
||||
|
||||
def __enter__(self):
|
||||
"""上下文管理器入口"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""上下文管理器出口"""
|
||||
self.shutdown()
|
851
backend/devices/femtobolt_manager.py
Normal file
851
backend/devices/femtobolt_manager.py
Normal file
@ -0,0 +1,851 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FemtoBolt深度相机管理器
|
||||
负责FemtoBolt深度相机的连接、配置和深度图像数据采集
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import base64
|
||||
import numpy as np
|
||||
import cv2
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import logging
|
||||
from collections import deque
|
||||
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
|
||||
|
||||
|
||||
class FemtoBoltManager(BaseDevice):
|
||||
"""FemtoBolt深度相机管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
"""
|
||||
初始化FemtoBolt管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
self.config = self.config_manager.get_device_config('femtobolt')
|
||||
|
||||
# 调用父类初始化
|
||||
super().__init__("femtobolt", self.config)
|
||||
|
||||
# 设置SocketIO实例
|
||||
self.set_socketio(socketio)
|
||||
|
||||
# 设备信息字典
|
||||
self.device_info = {}
|
||||
|
||||
# 设备ID
|
||||
self.device_id = "femtobolt_001"
|
||||
|
||||
# 性能统计
|
||||
self.performance_stats = {
|
||||
'fps': 0.0,
|
||||
'frame_count': 0,
|
||||
'dropped_frames': 0,
|
||||
'processing_time': 0.0
|
||||
}
|
||||
|
||||
# FemtoBolt SDK相关
|
||||
self.femtobolt = None
|
||||
self.device_handle = None
|
||||
self.sdk_initialized = False
|
||||
|
||||
# 设备配置
|
||||
self.color_resolution = self.config.get('color_resolution', '1080P')
|
||||
self.depth_mode = self.config.get('depth_mode', 'NFOV_UNBINNED')
|
||||
self.fps = self.config.get('fps', 15)
|
||||
self.depth_range_min = self.config.get('depth_range_min', 500)
|
||||
self.depth_range_max = self.config.get('depth_range_max', 4500)
|
||||
self.synchronized_images_only = self.config.get('synchronized_images_only', False)
|
||||
|
||||
# 数据处理
|
||||
self.streaming_thread = None
|
||||
self.depth_frame_cache = deque(maxlen=10)
|
||||
self.color_frame_cache = deque(maxlen=10)
|
||||
self.last_depth_frame = None
|
||||
self.last_color_frame = None
|
||||
self.frame_count = 0
|
||||
|
||||
# 图像处理参数
|
||||
self.contrast_factor = 1.2
|
||||
self.gamma_value = 0.8
|
||||
self.use_pseudo_color = True
|
||||
|
||||
# 性能监控
|
||||
self.fps_counter = 0
|
||||
self.fps_start_time = time.time()
|
||||
self.actual_fps = 0
|
||||
self.dropped_frames = 0
|
||||
|
||||
# 重连机制
|
||||
self.max_reconnect_attempts = 3
|
||||
self.reconnect_delay = 3.0
|
||||
|
||||
self.logger.info("FemtoBolt管理器初始化完成")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化FemtoBolt设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info("正在初始化FemtoBolt设备...")
|
||||
|
||||
# 初始化SDK
|
||||
if not self._initialize_sdk():
|
||||
raise Exception("SDK初始化失败")
|
||||
|
||||
# 配置设备
|
||||
if not self._configure_device():
|
||||
raise Exception("设备配置失败")
|
||||
|
||||
# 启动设备
|
||||
if not self._start_device():
|
||||
raise Exception("设备启动失败")
|
||||
|
||||
self.is_connected = True
|
||||
self.device_info.update({
|
||||
'color_resolution': self.color_resolution,
|
||||
'depth_mode': self.depth_mode,
|
||||
'fps': self.fps,
|
||||
'depth_range': f"{self.depth_range_min}-{self.depth_range_max}mm"
|
||||
})
|
||||
|
||||
self.logger.info("FemtoBolt初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt初始化失败: {e}")
|
||||
self.is_connected = False
|
||||
self._cleanup_device()
|
||||
return False
|
||||
|
||||
def _initialize_sdk(self) -> bool:
|
||||
"""
|
||||
初始化FemtoBolt SDK (使用pykinect_azure)
|
||||
|
||||
Returns:
|
||||
bool: SDK初始化是否成功
|
||||
"""
|
||||
try:
|
||||
# 尝试导入pykinect_azure
|
||||
real_pykinect = None
|
||||
try:
|
||||
import pykinect_azure as pykinect
|
||||
real_pykinect = pykinect
|
||||
self.logger.info("成功导入pykinect_azure库")
|
||||
except ImportError as e:
|
||||
self.logger.warning(f"无法导入pykinect_azure库,使用模拟模式: {e}")
|
||||
self.pykinect = self._create_mock_pykinect()
|
||||
self.sdk_initialized = True
|
||||
return True
|
||||
|
||||
# 查找并初始化SDK路径
|
||||
sdk_initialized = False
|
||||
if real_pykinect and hasattr(real_pykinect, 'initialize_libraries'):
|
||||
sdk_paths = self._get_femtobolt_sdk_paths()
|
||||
for sdk_path in sdk_paths:
|
||||
if os.path.exists(sdk_path):
|
||||
try:
|
||||
real_pykinect.initialize_libraries(track_body=False, module_k4a_path=sdk_path)
|
||||
self.logger.info(f'✓ 成功使用FemtoBolt SDK: {sdk_path}')
|
||||
self.pykinect = real_pykinect
|
||||
sdk_initialized = True
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.warning(f'✗ FemtoBolt SDK路径失败: {sdk_path} - {e}')
|
||||
continue
|
||||
|
||||
if not sdk_initialized:
|
||||
self.logger.info('未找到真实SDK,使用模拟模式')
|
||||
self.pykinect = self._create_mock_pykinect()
|
||||
|
||||
self.sdk_initialized = True
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"SDK初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_femtobolt_sdk_paths(self) -> list:
|
||||
import platform
|
||||
sdk_paths = []
|
||||
if platform.system() == "Windows":
|
||||
# 优先使用Orbbec SDK K4A Wrapper(与azure_kinect_image_example.py一致)
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
dll_path = os.path.join(base_dir,"..", "dll","femtobolt","bin", "k4a.dll")
|
||||
self.logger.info(f"FemtoBolt SDK路径: {dll_path}")
|
||||
sdk_paths.append(dll_path)
|
||||
return sdk_paths
|
||||
|
||||
def _create_mock_pykinect(self):
|
||||
"""
|
||||
创建模拟pykinect_azure(用于测试)
|
||||
|
||||
Returns:
|
||||
Mock pykinect对象
|
||||
"""
|
||||
class MockPyKinect:
|
||||
def __init__(self):
|
||||
self.default_configuration = self._create_mock_config()
|
||||
|
||||
def initialize_libraries(self, track_body=False, module_k4a_path=None):
|
||||
pass
|
||||
|
||||
def start_device(self, config=None):
|
||||
return MockDevice()
|
||||
|
||||
def _create_mock_config(self):
|
||||
class MockConfig:
|
||||
def __init__(self):
|
||||
self.depth_mode = 'NFOV_UNBINNED'
|
||||
self.camera_fps = 15
|
||||
self.synchronized_images_only = False
|
||||
self.color_resolution = 0
|
||||
return MockConfig()
|
||||
|
||||
# 添加常量
|
||||
K4A_DEPTH_MODE_NFOV_UNBINNED = 'NFOV_UNBINNED'
|
||||
K4A_FRAMES_PER_SECOND_15 = 15
|
||||
|
||||
class MockDevice:
|
||||
def __init__(self):
|
||||
self.is_started = True
|
||||
|
||||
def update(self):
|
||||
return MockCapture()
|
||||
|
||||
def stop(self):
|
||||
self.is_started = False
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class MockCapture:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_depth_image(self):
|
||||
# 生成模拟深度图像
|
||||
height, width = 480, 640
|
||||
depth_image = np.full((height, width), 2000, dtype=np.uint16)
|
||||
|
||||
# 添加人体轮廓
|
||||
center_x = width // 2
|
||||
center_y = height // 2
|
||||
|
||||
# 头部
|
||||
cv2.circle(depth_image, (center_x, center_y - 100), 40, 1500, -1)
|
||||
# 身体
|
||||
cv2.rectangle(depth_image, (center_x - 50, center_y - 60),
|
||||
(center_x + 50, center_y + 100), 1600, -1)
|
||||
# 手臂
|
||||
cv2.rectangle(depth_image, (center_x - 80, center_y - 40),
|
||||
(center_x - 50, center_y + 20), 1700, -1)
|
||||
cv2.rectangle(depth_image, (center_x + 50, center_y - 40),
|
||||
(center_x + 80, center_y + 20), 1700, -1)
|
||||
|
||||
return True, depth_image
|
||||
|
||||
def get_color_image(self):
|
||||
return None
|
||||
|
||||
return MockPyKinect()
|
||||
|
||||
def _configure_device(self) -> bool:
|
||||
"""
|
||||
配置FemtoBolt设备
|
||||
|
||||
Returns:
|
||||
bool: 配置是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.pykinect:
|
||||
return False
|
||||
|
||||
# 配置FemtoBolt设备参数
|
||||
self.femtobolt_config = self.pykinect.default_configuration
|
||||
self.femtobolt_config.depth_mode = self.pykinect.K4A_DEPTH_MODE_NFOV_UNBINNED
|
||||
self.femtobolt_config.camera_fps = self.pykinect.K4A_FRAMES_PER_SECOND_15
|
||||
self.femtobolt_config.synchronized_images_only = False
|
||||
self.femtobolt_config.color_resolution = 0
|
||||
|
||||
self.logger.info(f"FemtoBolt设备配置完成 - 深度模式: {self.depth_mode}, FPS: {self.fps}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt设备配置失败: {e}")
|
||||
return False
|
||||
|
||||
def _start_device(self) -> bool:
|
||||
"""
|
||||
启动FemtoBolt设备
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
try:
|
||||
# 启动FemtoBolt设备
|
||||
self.logger.info(f'尝试启动FemtoBolt设备...')
|
||||
|
||||
if hasattr(self.pykinect, 'start_device'):
|
||||
# 真实设备模式
|
||||
self.device_handle = self.pykinect.start_device(config=self.femtobolt_config)
|
||||
if self.device_handle:
|
||||
self.logger.info('✓ FemtoBolt深度相机初始化成功!')
|
||||
else:
|
||||
raise Exception('设备启动返回None')
|
||||
else:
|
||||
# 模拟设备模式
|
||||
self.device_handle = self.pykinect.start_device(config=self.femtobolt_config)
|
||||
self.logger.info('✓ FemtoBolt深度相机模拟模式启动成功!')
|
||||
|
||||
# 等待设备稳定
|
||||
time.sleep(1.0)
|
||||
|
||||
# 测试捕获
|
||||
if not self._test_capture():
|
||||
raise Exception("设备捕获测试失败")
|
||||
|
||||
self.logger.info("FemtoBolt设备启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt设备启动失败: {e}")
|
||||
return False
|
||||
|
||||
def _test_capture(self) -> bool:
|
||||
"""
|
||||
测试设备捕获
|
||||
|
||||
Returns:
|
||||
bool: 测试是否成功
|
||||
"""
|
||||
try:
|
||||
for i in range(3):
|
||||
capture = self.device_handle.update()
|
||||
if capture:
|
||||
ret, depth_image = capture.get_depth_image()
|
||||
if ret and depth_image is not None:
|
||||
self.logger.info(f"FemtoBolt捕获测试成功 - 深度图像大小: {depth_image.shape}")
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.error("FemtoBolt捕获测试失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt捕获测试异常: {e}")
|
||||
return False
|
||||
|
||||
def calibrate(self) -> bool:
|
||||
"""
|
||||
校准FemtoBolt设备
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info("开始FemtoBolt校准...")
|
||||
|
||||
if not self.is_connected:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
# 对于FemtoBolt,校准主要是验证设备工作状态
|
||||
# 捕获几帧来确保设备稳定
|
||||
for i in range(10):
|
||||
capture = self.device_handle.get_capture()
|
||||
if capture:
|
||||
depth_image = capture.get_depth_image()
|
||||
if depth_image is not None:
|
||||
# 检查深度图像质量
|
||||
valid_pixels = np.sum((depth_image >= self.depth_range_min) &
|
||||
(depth_image <= self.depth_range_max))
|
||||
total_pixels = depth_image.size
|
||||
valid_ratio = valid_pixels / total_pixels
|
||||
|
||||
if valid_ratio > 0.1: # 至少10%的像素有效
|
||||
self.logger.info(f"校准帧 {i+1}: 有效像素比例 {valid_ratio:.2%}")
|
||||
else:
|
||||
self.logger.warning(f"校准帧 {i+1}: 有效像素比例过低 {valid_ratio:.2%}")
|
||||
|
||||
capture.release()
|
||||
else:
|
||||
self.logger.warning(f"校准时无法获取第{i+1}帧")
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.info("FemtoBolt校准完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt校准失败: {e}")
|
||||
return False
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
"""
|
||||
开始数据流推送
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
if self.is_streaming:
|
||||
self.logger.warning("FemtoBolt流已在运行")
|
||||
return True
|
||||
|
||||
if not self.is_connected:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.is_streaming = True
|
||||
self.streaming_thread = threading.Thread(
|
||||
target=self._streaming_worker,
|
||||
name="FemtoBolt-Stream",
|
||||
daemon=True
|
||||
)
|
||||
self.streaming_thread.start()
|
||||
|
||||
self.logger.info("FemtoBolt流启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动FemtoBolt流失败: {e}")
|
||||
self.is_streaming = False
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止数据流推送
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
try:
|
||||
self.is_streaming = False
|
||||
|
||||
if self.streaming_thread and self.streaming_thread.is_alive():
|
||||
self.streaming_thread.join(timeout=5.0)
|
||||
|
||||
self.logger.info("FemtoBolt流已停止")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止FemtoBolt流失败: {e}")
|
||||
return False
|
||||
|
||||
def _streaming_worker(self):
|
||||
"""
|
||||
流处理工作线程
|
||||
"""
|
||||
self.logger.info("FemtoBolt流工作线程启动")
|
||||
|
||||
frame_count = 0
|
||||
|
||||
try:
|
||||
while self.is_streaming:
|
||||
if self.device_handle and self._socketio:
|
||||
try:
|
||||
capture = self.device_handle.update()
|
||||
if capture is not None:
|
||||
ret, depth_image = capture.get_depth_image()
|
||||
if ret and depth_image is not None:
|
||||
|
||||
# 使用与device_manager.py相同的处理逻辑
|
||||
depth_image = depth_image.copy()
|
||||
|
||||
# === 生成灰色背景 + 白色网格 ===
|
||||
rows, cols = depth_image.shape[:2]
|
||||
background = np.ones((rows, cols, 3), dtype=np.uint8) * 128
|
||||
cell_size = 50
|
||||
grid_color = (255, 255, 255)
|
||||
grid_bg = np.zeros_like(background)
|
||||
for x in range(0, cols, cell_size):
|
||||
cv2.line(grid_bg, (x, 0), (x, rows), grid_color, 1)
|
||||
for y in range(0, rows, cell_size):
|
||||
cv2.line(grid_bg, (0, y), (cols, y), grid_color, 1)
|
||||
mask_grid = (grid_bg.sum(axis=2) > 0)
|
||||
background[mask_grid] = grid_bg[mask_grid]
|
||||
|
||||
# === 处理深度图满足区间的部分 ===
|
||||
depth_clipped = depth_image.copy()
|
||||
depth_clipped[depth_clipped < self.depth_range_min] = 0
|
||||
depth_clipped[depth_clipped > self.depth_range_max] = 0
|
||||
depth_normalized = np.clip(depth_clipped, self.depth_range_min, self.depth_range_max)
|
||||
depth_normalized = ((depth_normalized - self.depth_range_min) / (self.depth_range_max - self.depth_range_min) * 255).astype(np.uint8)
|
||||
|
||||
# 对比度和伽马校正
|
||||
alpha, beta, gamma = 1.5, 0, 0.8
|
||||
depth_normalized = cv2.convertScaleAbs(depth_normalized, alpha=alpha, beta=beta)
|
||||
lut = np.array([((i / 255.0) ** gamma) * 255 for i in range(256)]).astype("uint8")
|
||||
depth_normalized = cv2.LUT(depth_normalized, lut)
|
||||
|
||||
# 伪彩色
|
||||
depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET)
|
||||
|
||||
# 将有效深度覆盖到灰色背景上
|
||||
mask_valid = (depth_clipped > 0)
|
||||
for c in range(3):
|
||||
background[:, :, c][mask_valid] = depth_colored[:, :, c][mask_valid]
|
||||
|
||||
depth_colored_final = background
|
||||
|
||||
# 裁剪宽度
|
||||
height, width = depth_colored_final.shape[:2]
|
||||
target_width = height // 2
|
||||
if width > target_width:
|
||||
left = (width - target_width) // 2
|
||||
right = left + target_width
|
||||
depth_colored_final = depth_colored_final[:, left:right]
|
||||
|
||||
# 缓存图像
|
||||
self.last_depth_frame = depth_colored_final.copy()
|
||||
self.depth_frame_cache.append(depth_colored_final.copy())
|
||||
|
||||
# 推送SocketIO
|
||||
success, buffer = cv2.imencode('.jpg', depth_colored_final, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
|
||||
if success and self._socketio:
|
||||
jpg_as_text = base64.b64encode(buffer).decode('utf-8')
|
||||
# 发送到femtobolt命名空间,使用前端期望的事件名和数据格式
|
||||
self._socketio.emit('femtobolt_frame', {
|
||||
'depth_image': jpg_as_text,
|
||||
'frame_count': frame_count,
|
||||
'timestamp': time.time(),
|
||||
'fps': self.actual_fps,
|
||||
'device_id': self.device_id,
|
||||
'depth_range': {
|
||||
'min': self.depth_range_min,
|
||||
'max': self.depth_range_max
|
||||
}
|
||||
}, namespace='/femtobolt')
|
||||
frame_count += 1
|
||||
|
||||
# 更新统计
|
||||
self._update_statistics()
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'FemtoBolt帧推送失败: {e}')
|
||||
time.sleep(0.1)
|
||||
|
||||
time.sleep(1/30) # 30 FPS
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt流处理异常: {e}")
|
||||
finally:
|
||||
self.is_streaming = False
|
||||
self.logger.info("FemtoBolt流工作线程结束")
|
||||
|
||||
def _process_depth_image(self, depth_image) -> np.ndarray:
|
||||
"""
|
||||
处理深度图像
|
||||
|
||||
Args:
|
||||
depth_image: 原始深度图像
|
||||
|
||||
Returns:
|
||||
np.ndarray: 处理后的深度图像
|
||||
"""
|
||||
try:
|
||||
# 确保输入是numpy数组
|
||||
if not isinstance(depth_image, np.ndarray):
|
||||
self.logger.error(f"输入的深度图像不是numpy数组: {type(depth_image)}")
|
||||
return np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
|
||||
# 深度范围过滤
|
||||
mask = (depth_image >= self.depth_range_min) & (depth_image <= self.depth_range_max)
|
||||
filtered_depth = np.where(mask, depth_image, 0)
|
||||
|
||||
# 归一化到0-255
|
||||
if np.max(filtered_depth) > 0:
|
||||
normalized = ((filtered_depth - self.depth_range_min) /
|
||||
(self.depth_range_max - self.depth_range_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
normalized = np.zeros_like(filtered_depth, dtype=np.uint8)
|
||||
|
||||
# 对比度增强
|
||||
enhanced = cv2.convertScaleAbs(normalized, alpha=self.contrast_factor, beta=0)
|
||||
|
||||
# 伽马校正
|
||||
gamma_corrected = np.power(enhanced / 255.0, self.gamma_value) * 255
|
||||
gamma_corrected = gamma_corrected.astype(np.uint8)
|
||||
|
||||
# 伪彩色映射
|
||||
if self.use_pseudo_color:
|
||||
colored = cv2.applyColorMap(gamma_corrected, cv2.COLORMAP_JET)
|
||||
else:
|
||||
colored = cv2.cvtColor(gamma_corrected, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
return colored
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理深度图像失败: {e}")
|
||||
return np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
|
||||
def _send_depth_data(self, depth_image: np.ndarray, color_image: Optional[np.ndarray] = None):
|
||||
"""
|
||||
发送深度数据
|
||||
|
||||
Args:
|
||||
depth_image: 深度图像
|
||||
color_image: 彩色图像(可选)
|
||||
"""
|
||||
try:
|
||||
# 压缩深度图像
|
||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
|
||||
_, depth_buffer = cv2.imencode('.jpg', depth_image, encode_param)
|
||||
depth_data = base64.b64encode(depth_buffer).decode('utf-8')
|
||||
|
||||
# 准备发送数据
|
||||
send_data = {
|
||||
'timestamp': time.time(),
|
||||
'frame_count': self.frame_count,
|
||||
'depth_image': depth_data,
|
||||
'fps': self.actual_fps,
|
||||
'device_id': self.device_id,
|
||||
'depth_range': {
|
||||
'min': self.depth_range_min,
|
||||
'max': self.depth_range_max
|
||||
},
|
||||
'last_update': time.strftime('%H:%M:%S')
|
||||
}
|
||||
|
||||
# 添加彩色图像(如果有)
|
||||
if color_image is not None:
|
||||
_, color_buffer = cv2.imencode('.jpg', color_image, encode_param)
|
||||
color_data = base64.b64encode(color_buffer).decode('utf-8')
|
||||
send_data['color_image'] = color_data
|
||||
|
||||
# 发送到SocketIO
|
||||
self._socketio.emit('femtobolt_frame', send_data, namespace='/femtobolt')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送深度数据失败: {e}")
|
||||
|
||||
def _update_statistics(self):
|
||||
"""
|
||||
更新性能统计
|
||||
"""
|
||||
self.frame_count += 1
|
||||
self.fps_counter += 1
|
||||
|
||||
# 每秒计算一次实际FPS
|
||||
current_time = time.time()
|
||||
if current_time - self.fps_start_time >= 1.0:
|
||||
self.actual_fps = self.fps_counter / (current_time - self.fps_start_time)
|
||||
self.fps_counter = 0
|
||||
self.fps_start_time = current_time
|
||||
|
||||
# 更新性能统计
|
||||
self.performance_stats.update({
|
||||
'frames_processed': self.frame_count,
|
||||
'actual_fps': round(self.actual_fps, 2),
|
||||
'dropped_frames': self.dropped_frames
|
||||
})
|
||||
|
||||
def _reconnect(self) -> bool:
|
||||
"""
|
||||
重新连接FemtoBolt设备
|
||||
|
||||
Returns:
|
||||
bool: 重连是否成功
|
||||
"""
|
||||
try:
|
||||
self._cleanup_device()
|
||||
time.sleep(2.0) # 等待设备释放
|
||||
return self.initialize()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"FemtoBolt重连失败: {e}")
|
||||
return False
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
'color_resolution': self.color_resolution,
|
||||
'depth_mode': self.depth_mode,
|
||||
'target_fps': self.fps,
|
||||
'actual_fps': self.actual_fps,
|
||||
'frame_count': self.frame_count,
|
||||
'dropped_frames': self.dropped_frames,
|
||||
'depth_range': f"{self.depth_range_min}-{self.depth_range_max}mm",
|
||||
'has_depth_frame': self.last_depth_frame is not None,
|
||||
'has_color_frame': self.last_color_frame is not None
|
||||
})
|
||||
return status
|
||||
|
||||
def capture_body_image(self, save_path: Optional[str] = None) -> Optional[np.ndarray]:
|
||||
"""
|
||||
捕获身体图像
|
||||
|
||||
Args:
|
||||
save_path: 保存路径(可选)
|
||||
|
||||
Returns:
|
||||
Optional[np.ndarray]: 捕获的图像,失败返回None
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.device_handle:
|
||||
self.logger.error("FemtoBolt设备未连接")
|
||||
return None
|
||||
|
||||
capture = self.device_handle.get_capture()
|
||||
if not capture:
|
||||
self.logger.error("无法获取FemtoBolt捕获")
|
||||
return None
|
||||
|
||||
depth_image = capture.get_depth_image()
|
||||
if depth_image is None:
|
||||
self.logger.error("无法获取深度图像")
|
||||
capture.release()
|
||||
return None
|
||||
|
||||
# 处理深度图像
|
||||
processed_image = self._process_depth_image(depth_image)
|
||||
|
||||
if save_path:
|
||||
cv2.imwrite(save_path, processed_image)
|
||||
self.logger.info(f"身体图像已保存到: {save_path}")
|
||||
|
||||
capture.release()
|
||||
return processed_image
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"捕获身体图像异常: {e}")
|
||||
return None
|
||||
|
||||
def get_latest_depth_frame(self) -> Optional[np.ndarray]:
|
||||
"""
|
||||
获取最新深度帧
|
||||
|
||||
Returns:
|
||||
Optional[np.ndarray]: 最新深度帧,无帧返回None
|
||||
"""
|
||||
return self.last_depth_frame.copy() if self.last_depth_frame is not None else None
|
||||
|
||||
def get_latest_color_frame(self) -> Optional[np.ndarray]:
|
||||
"""
|
||||
获取最新彩色帧
|
||||
|
||||
Returns:
|
||||
Optional[np.ndarray]: 最新彩色帧,无帧返回None
|
||||
"""
|
||||
return self.last_color_frame.copy() if self.last_color_frame is not None else None
|
||||
|
||||
def collect_body_pose_data(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
采集身体姿态数据(兼容原接口)
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 身体姿态数据
|
||||
"""
|
||||
# 这里可以集成姿态估计算法
|
||||
# 目前返回模拟数据
|
||||
if not self.last_depth_frame is not None:
|
||||
return None
|
||||
|
||||
# 模拟身体姿态数据
|
||||
mock_keypoints = [
|
||||
{'name': 'head', 'x': 320, 'y': 100, 'confidence': 0.9},
|
||||
{'name': 'neck', 'x': 320, 'y': 150, 'confidence': 0.8},
|
||||
{'name': 'left_shoulder', 'x': 280, 'y': 160, 'confidence': 0.7},
|
||||
{'name': 'right_shoulder', 'x': 360, 'y': 160, 'confidence': 0.7},
|
||||
{'name': 'left_hip', 'x': 300, 'y': 300, 'confidence': 0.6},
|
||||
{'name': 'right_hip', 'x': 340, 'y': 300, 'confidence': 0.6}
|
||||
]
|
||||
|
||||
return {
|
||||
'timestamp': time.time(),
|
||||
'keypoints': mock_keypoints,
|
||||
'balance_score': np.random.uniform(0.7, 0.9),
|
||||
'center_of_mass': {'x': 320, 'y': 240},
|
||||
'device_id': self.device_id
|
||||
}
|
||||
|
||||
def _cleanup_device(self):
|
||||
"""
|
||||
清理设备资源
|
||||
"""
|
||||
try:
|
||||
if self.device_handle:
|
||||
# 尝试停止设备(如果有stop方法)
|
||||
if hasattr(self.device_handle, 'stop'):
|
||||
try:
|
||||
self.device_handle.stop()
|
||||
self.logger.info("FemtoBolt设备已停止")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"停止FemtoBolt设备时出现警告: {e}")
|
||||
|
||||
# 尝试关闭设备(如果有close方法)
|
||||
if hasattr(self.device_handle, 'close'):
|
||||
try:
|
||||
self.device_handle.close()
|
||||
self.logger.info("FemtoBolt设备连接已关闭")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"关闭FemtoBolt设备时出现警告: {e}")
|
||||
|
||||
self.device_handle = None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理FemtoBolt设备失败: {e}")
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
断开FemtoBolt设备连接
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
self._cleanup_device()
|
||||
self.is_connected = False
|
||||
self.logger.info("FemtoBolt设备已断开连接")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开FemtoBolt设备连接失败: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
清理资源
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
self._cleanup_device()
|
||||
|
||||
self.depth_frame_cache.clear()
|
||||
self.color_frame_cache.clear()
|
||||
self.last_depth_frame = None
|
||||
self.last_color_frame = None
|
||||
|
||||
super().cleanup()
|
||||
self.logger.info("FemtoBolt资源清理完成")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理FemtoBolt资源失败: {e}")
|
625
backend/devices/imu_manager.py
Normal file
625
backend/devices/imu_manager.py
Normal file
@ -0,0 +1,625 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
IMU传感器管理器
|
||||
负责IMU传感器的连接、校准和头部姿态数据采集
|
||||
"""
|
||||
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
import numpy as np
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
import logging
|
||||
from collections import deque
|
||||
import struct
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
# 设置日志
|
||||
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.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
self.last_data = {
|
||||
'roll': 0.0,
|
||||
'pitch': 0.0,
|
||||
'yaw': 0.0,
|
||||
'temperature': 25.0
|
||||
}
|
||||
logger.debug(f'RealIMUDevice 初始化: port={self.port}, baudrate={self.baudrate}')
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
logger.debug(f'尝试打开串口: {self.port} @ {self.baudrate}')
|
||||
self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
|
||||
if hasattr(self.ser, 'reset_input_buffer'):
|
||||
try:
|
||||
self.ser.reset_input_buffer()
|
||||
logger.debug('已清空串口输入缓冲区')
|
||||
except Exception as e:
|
||||
logger.debug(f'重置串口输入缓冲区失败: {e}')
|
||||
logger.info(f'IMU设备连接成功: {self.port} @ {self.baudrate}bps')
|
||||
except Exception as e:
|
||||
# logger.error(f'IMU设备连接失败: {e}', exc_info=True)
|
||||
self.ser = None
|
||||
|
||||
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']
|
||||
logger.debug(f'应用IMU校准数据: {self.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
|
||||
|
||||
@staticmethod
|
||||
def _checksum(data: bytes) -> int:
|
||||
return sum(data[:-1]) & 0xFF
|
||||
|
||||
def _parse_packet(self, data: bytes) -> Optional[Dict[str, float]]:
|
||||
if len(data) != 11:
|
||||
logger.debug(f'无效数据包长度: {len(data)}')
|
||||
return None
|
||||
if data[0] != 0x55:
|
||||
logger.debug(f'错误的包头: 0x{data[0]:02X}')
|
||||
return None
|
||||
if self._checksum(data) != data[-1]:
|
||||
logger.debug(f'校验和错误: 期望{self._checksum(data):02X}, 实际{data[-1]:02X}')
|
||||
return None
|
||||
packet_type = data[1]
|
||||
vals = [int.from_bytes(data[i:i+2], 'little', signed=True) for i in range(2, 10, 2)]
|
||||
if packet_type == 0x53: # 姿态角,单位0.01°
|
||||
pitchl, rxl, yawl, temp = vals # 注意这里 vals 已经是有符号整数
|
||||
# 使用第一段代码里的比例系数
|
||||
k_angle = 180.0
|
||||
roll = -round(rxl / 32768.0 * k_angle,2)
|
||||
pitch = -round(pitchl / 32768.0 * k_angle,2)
|
||||
yaw = -round(yawl / 32768.0 * k_angle,2)
|
||||
temp = temp / 100.0
|
||||
self.last_data = {
|
||||
'roll': roll,
|
||||
'pitch': pitch,
|
||||
'yaw': yaw,
|
||||
'temperature': temp
|
||||
}
|
||||
# logger.debug(f'解析姿态角包: roll={roll}, pitch={pitch}, yaw={yaw}, temp={temp}')
|
||||
return self.last_data
|
||||
else:
|
||||
# logger.debug(f'忽略的数据包类型: 0x{packet_type:02X}')
|
||||
return None
|
||||
|
||||
def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]:
|
||||
if not self.ser or not getattr(self.ser, 'is_open', False):
|
||||
# logger.warning('IMU串口未连接,尝试重新连接...')
|
||||
self._connect()
|
||||
return {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
try:
|
||||
bytes_waiting = self.ser.in_waiting
|
||||
if bytes_waiting:
|
||||
# logger.debug(f'串口缓冲区待读字节: {bytes_waiting}')
|
||||
chunk = self.ser.read(bytes_waiting)
|
||||
# logger.debug(f'读取到字节: {len(chunk)}')
|
||||
self.buffer.extend(chunk)
|
||||
while len(self.buffer) >= 11:
|
||||
if self.buffer[0] != 0x55:
|
||||
dropped = self.buffer.pop(0)
|
||||
logger.debug(f'丢弃无效字节: 0x{dropped:02X}')
|
||||
continue
|
||||
packet = bytes(self.buffer[:11])
|
||||
parsed = self._parse_packet(packet)
|
||||
del self.buffer[:11]
|
||||
if parsed is not None:
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': parsed['yaw'], # rotation = roll
|
||||
'tilt': parsed['roll'], # tilt = yaw
|
||||
'pitch': parsed['pitch'] # pitch = pitch
|
||||
},
|
||||
'temperature': parsed['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
# logger.debug(f'映射后的头部姿态: {raw}')
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
except Exception as e:
|
||||
logger.error(f'IMU数据读取异常: {e}', exc_info=True)
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self.ser and getattr(self.ser, 'is_open', False):
|
||||
self.ser.close()
|
||||
logger.info('IMU设备串口已关闭')
|
||||
except Exception:
|
||||
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 IMUManager(BaseDevice):
|
||||
"""IMU传感器管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
"""
|
||||
初始化IMU管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
config = self.config_manager.get_device_config('imu')
|
||||
|
||||
super().__init__("imu", config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 设备配置
|
||||
self.port = config.get('port', 'COM7')
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self.device_type = config.get('device_type', 'mock') # 'real' 或 'mock'
|
||||
self.use_mock = config.get('use_mock', False) # 保持向后兼容
|
||||
# IMU设备实例
|
||||
self.imu_device = None
|
||||
|
||||
# 推流相关
|
||||
self.imu_streaming = False
|
||||
self.imu_thread = None
|
||||
|
||||
# 统计信息
|
||||
self.data_count = 0
|
||||
self.error_count = 0
|
||||
|
||||
# 校准相关
|
||||
self.is_calibrated = False
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
|
||||
# 数据缓存
|
||||
self.data_buffer = deque(maxlen=100)
|
||||
self.last_valid_data = None
|
||||
|
||||
self.logger.info(f"IMU管理器初始化完成 - 端口: {self.port}, 设备类型: {self.device_type}")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化IMU设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"正在初始化IMU设备...")
|
||||
|
||||
# 根据配置选择真实设备或模拟设备
|
||||
# 优先使用device_type配置,如果没有则使用use_mock配置(向后兼容)
|
||||
use_real_device = (self.device_type == 'real') or (not self.use_mock)
|
||||
|
||||
if use_real_device:
|
||||
self.logger.info(f"使用真实IMU设备 - 端口: {self.port}, 波特率: {self.baudrate}")
|
||||
self.imu_device = RealIMUDevice(self.port, self.baudrate)
|
||||
else:
|
||||
self.logger.info("使用模拟IMU设备")
|
||||
self.imu_device = MockIMUDevice()
|
||||
|
||||
self.is_connected = True
|
||||
self._device_info.update({
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'use_mock': self.use_mock
|
||||
})
|
||||
|
||||
self.logger.info("IMU初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU初始化失败: {e}")
|
||||
self.is_connected = False
|
||||
self.imu_device = None
|
||||
return False
|
||||
|
||||
def _quick_calibrate_imu(self) -> Dict[str, Any]:
|
||||
"""
|
||||
快速IMU零点校准(以当前姿态为基准)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 校准结果
|
||||
"""
|
||||
try:
|
||||
if not self.imu_device:
|
||||
return {'status': 'error', 'error': 'IMU设备未初始化'}
|
||||
|
||||
self.logger.info('开始IMU快速零点校准...')
|
||||
|
||||
# 收集校准样本
|
||||
calibration_samples = []
|
||||
sample_count = 50 # 减少样本数量以加快校准速度
|
||||
|
||||
for i in range(sample_count):
|
||||
try:
|
||||
# 读取原始数据(不应用校准)
|
||||
raw_data = self.imu_device.read_data(apply_calibration=False)
|
||||
if raw_data and 'head_pose' in raw_data:
|
||||
calibration_samples.append(raw_data['head_pose'])
|
||||
time.sleep(0.02) # 20ms间隔
|
||||
except Exception as e:
|
||||
self.logger.warning(f'校准样本采集失败: {e}')
|
||||
continue
|
||||
|
||||
if len(calibration_samples) < sample_count * 0.7:
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': f'校准样本不足: {len(calibration_samples)}/{sample_count}'
|
||||
}
|
||||
|
||||
# 计算平均值作为零点偏移
|
||||
rotation_sum = sum(sample['rotation'] for sample in calibration_samples)
|
||||
tilt_sum = sum(sample['tilt'] for sample in calibration_samples)
|
||||
pitch_sum = sum(sample['pitch'] for sample in calibration_samples)
|
||||
|
||||
count = len(calibration_samples)
|
||||
self.head_pose_offset = {
|
||||
'rotation': rotation_sum / count,
|
||||
'tilt': tilt_sum / count,
|
||||
'pitch': pitch_sum / count
|
||||
}
|
||||
|
||||
# 应用校准到设备
|
||||
calibration_data = {'head_pose_offset': self.head_pose_offset}
|
||||
self.imu_device.set_calibration(calibration_data)
|
||||
|
||||
self.logger.info(f'IMU快速校准完成: {self.head_pose_offset}')
|
||||
return {
|
||||
'status': 'success',
|
||||
'head_pose_offset': self.head_pose_offset,
|
||||
'samples_used': count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'IMU快速校准失败: {e}')
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
|
||||
def calibrate(self) -> bool:
|
||||
"""
|
||||
校准IMU传感器
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.imu_device:
|
||||
if not self.initialize():
|
||||
self.logger.error("IMU设备未连接")
|
||||
return False
|
||||
|
||||
# 使用快速校准方法
|
||||
result = self._quick_calibrate_imu()
|
||||
|
||||
if result['status'] == 'success':
|
||||
self.is_calibrated = True
|
||||
self.logger.info("IMU校准成功")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"IMU校准失败: {result.get('error', '未知错误')}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU校准失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
"""
|
||||
开始IMU数据流
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例,用于数据推送
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.imu_device:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
if self.imu_streaming:
|
||||
self.logger.warning("IMU数据流已在运行")
|
||||
return True
|
||||
|
||||
# 启动前进行快速校准
|
||||
if not self.is_calibrated:
|
||||
self.logger.info("启动前进行快速零点校准...")
|
||||
self._quick_calibrate_imu()
|
||||
|
||||
self.imu_streaming = True
|
||||
self.imu_thread = threading.Thread(target=self._imu_streaming_thread, daemon=True)
|
||||
self.imu_thread.start()
|
||||
|
||||
self.logger.info("IMU数据流启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU数据流启动失败: {e}")
|
||||
self.imu_streaming = False
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止IMU数据流
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
try:
|
||||
self.imu_streaming = False
|
||||
|
||||
if self.imu_thread and self.imu_thread.is_alive():
|
||||
self.imu_thread.join(timeout=3.0)
|
||||
|
||||
self.logger.info("IMU数据流已停止")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止IMU数据流失败: {e}")
|
||||
return False
|
||||
|
||||
def _imu_streaming_thread(self):
|
||||
"""
|
||||
IMU数据流工作线程
|
||||
"""
|
||||
self.logger.info("IMU数据流工作线程启动")
|
||||
|
||||
while self.imu_streaming:
|
||||
try:
|
||||
if self.imu_device:
|
||||
# 读取IMU数据
|
||||
data = self.imu_device.read_data(apply_calibration=True)
|
||||
|
||||
if data:
|
||||
# 缓存数据
|
||||
self.data_buffer.append(data)
|
||||
self.last_valid_data = data
|
||||
|
||||
# 发送数据到前端
|
||||
if self._socketio:
|
||||
self._socketio.emit('imu_data', data, namespace='/imu')
|
||||
|
||||
# 更新统计
|
||||
self.data_count += 1
|
||||
else:
|
||||
self.error_count += 1
|
||||
|
||||
time.sleep(0.02) # 50Hz采样率
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU数据流处理异常: {e}")
|
||||
self.error_count += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.info("IMU数据流工作线程结束")
|
||||
|
||||
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'is_streaming': self.imu_streaming,
|
||||
'is_calibrated': self.is_calibrated,
|
||||
'data_count': self.data_count,
|
||||
'error_count': self.error_count,
|
||||
'buffer_size': len(self.data_buffer),
|
||||
'has_data': self.last_valid_data is not None,
|
||||
'head_pose_offset': self.head_pose_offset,
|
||||
'device_type': 'mock' if self.use_mock else 'real'
|
||||
})
|
||||
return status
|
||||
|
||||
def get_latest_data(self) -> Optional[Dict[str, float]]:
|
||||
"""
|
||||
获取最新的IMU数据
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, float]]: 最新数据,无数据返回None
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
断开IMU设备连接
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
|
||||
if self.imu_device:
|
||||
self.imu_device = None
|
||||
|
||||
self.is_connected = False
|
||||
self.logger.info("IMU设备已断开连接")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开IMU设备连接失败: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
清理资源
|
||||
"""
|
||||
try:
|
||||
self.disconnect()
|
||||
|
||||
# 清理缓冲区
|
||||
self.data_buffer.clear()
|
||||
|
||||
# 重置状态
|
||||
self.is_calibrated = False
|
||||
self.last_valid_data = None
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
|
||||
super().cleanup()
|
||||
self.logger.info("IMU资源清理完成")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理IMU资源失败: {e}")
|
908
backend/devices/pressure_manager.py
Normal file
908
backend/devices/pressure_manager.py
Normal file
@ -0,0 +1,908 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
压力板管理器
|
||||
负责压力传感器的连接、校准和足部压力数据采集
|
||||
"""
|
||||
|
||||
import os
|
||||
import ctypes
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
import numpy as np
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
import logging
|
||||
from collections import deque
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.cm as cm
|
||||
from io import BytesIO
|
||||
import base64
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 检查matplotlib可用性
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
MATPLOTLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
MATPLOTLIB_AVAILABLE = False
|
||||
logger.warning("matplotlib不可用,将使用简化的压力图像生成")
|
||||
|
||||
|
||||
class RealPressureDevice:
|
||||
"""真实SMiTSense压力传感器设备"""
|
||||
|
||||
def __init__(self, dll_path=None):
|
||||
"""
|
||||
初始化SMiTSense压力传感器
|
||||
|
||||
Args:
|
||||
dll_path: DLL文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
self.dll = None
|
||||
self.device_handle = None
|
||||
self.is_connected = False
|
||||
self.rows = 0
|
||||
self.cols = 0
|
||||
self.frame_size = 0
|
||||
self.buf = None
|
||||
|
||||
# 设置DLL路径 - 使用正确的DLL文件名
|
||||
if dll_path is None:
|
||||
# 尝试多个可能的DLL文件名
|
||||
dll_candidates = [
|
||||
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'SMiTSenseUsbWrapper.dll'),
|
||||
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'SMiTSenseUsb-F3.0.dll')
|
||||
]
|
||||
dll_path = None
|
||||
for candidate in dll_candidates:
|
||||
if os.path.exists(candidate):
|
||||
dll_path = candidate
|
||||
break
|
||||
|
||||
if dll_path is None:
|
||||
raise FileNotFoundError(f"未找到SMiTSense DLL文件,检查路径: {dll_candidates}")
|
||||
|
||||
self.dll_path = dll_path
|
||||
logger.info(f'初始化真实压力传感器设备,DLL路径: {dll_path}')
|
||||
|
||||
try:
|
||||
self._load_dll()
|
||||
self._initialize_device()
|
||||
except Exception as e:
|
||||
logger.error(f'压力传感器初始化失败: {e}')
|
||||
# 如果真实设备初始化失败,可以选择降级为模拟设备
|
||||
raise
|
||||
|
||||
def _load_dll(self):
|
||||
"""加载SMiTSense DLL并设置函数签名"""
|
||||
try:
|
||||
if not os.path.exists(self.dll_path):
|
||||
raise FileNotFoundError(f"DLL文件未找到: {self.dll_path}")
|
||||
|
||||
# 加载DLL
|
||||
self.dll = ctypes.WinDLL(self.dll_path)
|
||||
logger.info(f"成功加载DLL: {self.dll_path}")
|
||||
|
||||
# 设置函数签名(基于testsmit.py的工作代码)
|
||||
self.dll.SMiTSenseUsb_Init.argtypes = [ctypes.c_int]
|
||||
self.dll.SMiTSenseUsb_Init.restype = ctypes.c_int
|
||||
|
||||
self.dll.SMiTSenseUsb_ScanDevices.argtypes = [ctypes.POINTER(ctypes.c_int)]
|
||||
self.dll.SMiTSenseUsb_ScanDevices.restype = ctypes.c_int
|
||||
|
||||
self.dll.SMiTSenseUsb_OpenAndStart.argtypes = [
|
||||
ctypes.c_int,
|
||||
ctypes.POINTER(ctypes.c_uint16),
|
||||
ctypes.POINTER(ctypes.c_uint16)
|
||||
]
|
||||
self.dll.SMiTSenseUsb_OpenAndStart.restype = ctypes.c_int
|
||||
|
||||
self.dll.SMiTSenseUsb_GetLatestFrame.argtypes = [
|
||||
ctypes.POINTER(ctypes.c_uint16),
|
||||
ctypes.c_int
|
||||
]
|
||||
self.dll.SMiTSenseUsb_GetLatestFrame.restype = ctypes.c_int
|
||||
|
||||
self.dll.SMiTSenseUsb_StopAndClose.argtypes = []
|
||||
self.dll.SMiTSenseUsb_StopAndClose.restype = ctypes.c_int
|
||||
|
||||
logger.info("DLL函数签名设置完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载DLL失败: {e}")
|
||||
raise
|
||||
|
||||
def _initialize_device(self):
|
||||
"""初始化设备连接"""
|
||||
try:
|
||||
# 初始化USB连接
|
||||
ret = self.dll.SMiTSenseUsb_Init(0)
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"USB初始化失败: {ret}")
|
||||
|
||||
# 扫描设备
|
||||
count = ctypes.c_int()
|
||||
ret = self.dll.SMiTSenseUsb_ScanDevices(ctypes.byref(count))
|
||||
if ret != 0 or count.value == 0:
|
||||
raise RuntimeError(f"设备扫描失败或未找到设备: {ret}, count: {count.value}")
|
||||
|
||||
logger.info(f"发现 {count.value} 个SMiTSense设备")
|
||||
|
||||
# 打开并启动第一个设备
|
||||
rows = ctypes.c_uint16()
|
||||
cols = ctypes.c_uint16()
|
||||
ret = self.dll.SMiTSenseUsb_OpenAndStart(0, ctypes.byref(rows), ctypes.byref(cols))
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"设备启动失败: {ret}")
|
||||
|
||||
self.rows = rows.value
|
||||
self.cols = cols.value
|
||||
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}列")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"设备初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def read_data(self) -> Dict[str, Any]:
|
||||
"""读取压力数据并转换为与MockPressureDevice兼容的格式"""
|
||||
try:
|
||||
if not self.is_connected or not self.dll:
|
||||
logger.error("设备未连接")
|
||||
return self._get_empty_data()
|
||||
|
||||
# 读取原始压力数据
|
||||
ret = self.dll.SMiTSenseUsb_GetLatestFrame(self.buf, self.frame_size)
|
||||
if ret != 0:
|
||||
logger.warning(f"读取数据帧失败: {ret}")
|
||||
return self._get_empty_data()
|
||||
|
||||
# 转换为numpy数组
|
||||
raw_data = np.frombuffer(self.buf, dtype=np.uint16).reshape((self.rows, self.cols))
|
||||
|
||||
# 计算足部区域压力 (基于传感器的实际布局)
|
||||
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 _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': round(left_front_pct),
|
||||
'left_rear': round(left_rear_pct),
|
||||
'right_front': round(right_front_pct),
|
||||
'right_rear': round(right_rear_pct),
|
||||
'left_total': round(left_total_pct),
|
||||
'right_total': round(right_total_pct),
|
||||
'total_pressure': round(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:
|
||||
"""生成基于原始数据的热力图(OpenCV实现,固定范围映射,效果与matplotlib一致)"""
|
||||
try:
|
||||
import cv2
|
||||
import numpy as np
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
# 固定映射范围(与 matplotlib vmin/vmax 一致)
|
||||
vmin, vmax = 0, 1000
|
||||
norm_data = np.clip((raw_data - vmin) / (vmax - vmin) * 255, 0, 255).astype(np.uint8)
|
||||
|
||||
# 应用 jet 颜色映射
|
||||
heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET)
|
||||
|
||||
# OpenCV 生成的是 BGR,转成 RGB
|
||||
heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# 转成 Pillow Image
|
||||
img = Image.fromarray(heatmap_rgb)
|
||||
|
||||
# 输出为 Base64 PNG
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
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')
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
from io import BytesIO
|
||||
|
||||
# 创建图形
|
||||
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='black')
|
||||
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 "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()
|
||||
}
|
||||
|
||||
def close(self):
|
||||
"""显式关闭压力传感器连接"""
|
||||
try:
|
||||
if self.is_connected and self.dll:
|
||||
self.dll.SMiTSenseUsb_StopAndClose()
|
||||
self.is_connected = False
|
||||
logger.info('SMiTSense压力传感器连接已关闭')
|
||||
except Exception as e:
|
||||
logger.error(f'关闭压力传感器连接异常: {e}')
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保资源清理"""
|
||||
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):
|
||||
"""压力板管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
"""
|
||||
初始化压力板管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
self.config = self.config_manager.get_device_config('pressure')
|
||||
|
||||
super().__init__("pressure", self.config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 设备实例
|
||||
self.device = None
|
||||
self.device_type = self.config.get('device_type', 'mock') # 'real' 或 'mock'
|
||||
|
||||
# 数据流相关
|
||||
self.streaming_thread = None
|
||||
self.is_streaming = False
|
||||
self.stream_interval = self.config.get('stream_interval', 0.1) # 100ms间隔
|
||||
|
||||
# 校准相关
|
||||
self.is_calibrated = False
|
||||
self.calibration_data = None
|
||||
|
||||
# 性能统计
|
||||
self.packet_count = 0
|
||||
self.error_count = 0
|
||||
self.last_data_time = None
|
||||
|
||||
self.logger.info(f"压力板管理器初始化完成 - 设备类型: {self.device_type}")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化压力板设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"正在初始化压力板设备 - 类型: {self.device_type}")
|
||||
|
||||
# 根据设备类型创建设备实例
|
||||
if self.device_type == 'real':
|
||||
self.device = RealPressureDevice()
|
||||
else:
|
||||
self.device = MockPressureDevice()
|
||||
|
||||
self.is_connected = True
|
||||
self._device_info.update({
|
||||
'device_type': self.device_type,
|
||||
'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown'
|
||||
})
|
||||
|
||||
self.logger.info(f"压力板初始化成功 - 设备类型: {self.device_type}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"压力板初始化失败: {e}")
|
||||
self.is_connected = False
|
||||
self.device = None
|
||||
return False
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
|
||||
"""
|
||||
开始压力数据流
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.device:
|
||||
self.logger.error("设备未连接,无法启动数据流")
|
||||
return False
|
||||
|
||||
if self.is_streaming:
|
||||
self.logger.warning("压力数据流已在运行")
|
||||
return True
|
||||
|
||||
self.is_streaming = True
|
||||
self.streaming_thread = threading.Thread(target=self._pressure_streaming_thread, daemon=True)
|
||||
self.streaming_thread.start()
|
||||
|
||||
self.logger.info("压力数据流启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动压力数据流失败: {e}")
|
||||
self.is_streaming = False
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止压力数据流
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_streaming:
|
||||
return True
|
||||
|
||||
self.is_streaming = False
|
||||
|
||||
if self.streaming_thread and self.streaming_thread.is_alive():
|
||||
self.streaming_thread.join(timeout=2.0)
|
||||
|
||||
self.logger.info("压力数据流已停止")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止压力数据流失败: {e}")
|
||||
return False
|
||||
|
||||
def _pressure_streaming_thread(self):
|
||||
"""
|
||||
压力数据流处理线程
|
||||
"""
|
||||
self.logger.info("压力数据流线程启动")
|
||||
|
||||
try:
|
||||
while self.is_streaming and self.is_connected:
|
||||
try:
|
||||
# 从设备读取数据
|
||||
if self.device:
|
||||
pressure_data = self.device.read_data()
|
||||
|
||||
if pressure_data:
|
||||
# 更新统计信息
|
||||
self.packet_count += 1
|
||||
self.last_data_time = time.time()
|
||||
|
||||
# 发送数据到前端
|
||||
if self._socketio:
|
||||
self._socketio.emit('pressure_data', pressure_data, namespace='/pressure')
|
||||
else:
|
||||
self.logger.warning("SocketIO实例为空,无法发送压力数据")
|
||||
|
||||
time.sleep(self.stream_interval)
|
||||
|
||||
except Exception as e:
|
||||
self.error_count += 1
|
||||
self.logger.error(f"压力数据流处理异常: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"压力数据流线程异常: {e}")
|
||||
finally:
|
||||
self.logger.info("压力数据流线程结束")
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
return {
|
||||
'device_type': self.device_type,
|
||||
'is_connected': self.is_connected,
|
||||
'is_streaming': self.is_streaming,
|
||||
'is_calibrated': self.is_calibrated,
|
||||
'packet_count': self.packet_count,
|
||||
'error_count': self.error_count,
|
||||
'last_data_time': self.last_data_time,
|
||||
'device_info': self.get_device_info()
|
||||
}
|
||||
|
||||
def calibrate(self) -> bool:
|
||||
"""
|
||||
校准压力传感器
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info("开始压力传感器校准...")
|
||||
|
||||
# 这里可以添加具体的校准逻辑
|
||||
# 目前简单设置为已校准状态
|
||||
self.is_calibrated = True
|
||||
self.calibration_data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'baseline': 'calibrated'
|
||||
}
|
||||
|
||||
self.logger.info("压力传感器校准完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"压力传感器校准失败: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""
|
||||
断开设备连接
|
||||
|
||||
Returns:
|
||||
bool: 断开是否成功
|
||||
"""
|
||||
try:
|
||||
# 停止数据流
|
||||
self.stop_streaming()
|
||||
|
||||
# 关闭设备连接
|
||||
if self.device and hasattr(self.device, 'close'):
|
||||
self.device.close()
|
||||
|
||||
self.device = None
|
||||
self.is_connected = False
|
||||
|
||||
self.logger.info("压力板设备连接已断开")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开压力板设备连接失败: {e}")
|
||||
return False
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""清理资源"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
self.disconnect()
|
||||
self.logger.info("压力板设备资源清理完成")
|
||||
except Exception as e:
|
||||
self.logger.error(f"压力板设备资源清理失败: {e}")
|
52150
backend/devices/test/Log/OrbbecSDK.log.txt
Normal file
52150
backend/devices/test/Log/OrbbecSDK.log.txt
Normal file
File diff suppressed because it is too large
Load Diff
595
backend/devices/test/devicetest.py
Normal file
595
backend/devices/test/devicetest.py
Normal file
@ -0,0 +1,595 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备测试类
|
||||
用于测试和模拟四个设备的推流功能:深度相机、普通相机、压力板、IMU
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import json
|
||||
import base64
|
||||
import numpy as np
|
||||
import cv2
|
||||
from flask import Flask, render_template, jsonify
|
||||
from flask_socketio import SocketIO, emit
|
||||
from typing import Dict, Any, Optional
|
||||
from collections import deque
|
||||
import random
|
||||
import math
|
||||
|
||||
# 添加父目录到路径
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.append(parent_dir)
|
||||
sys.path.append(os.path.dirname(parent_dir))
|
||||
|
||||
# 导入设备管理器(使用绝对路径)
|
||||
try:
|
||||
from devices.camera_manager import CameraManager
|
||||
from devices.imu_manager import IMUManager
|
||||
from devices.pressure_manager import PressureManager
|
||||
from devices.femtobolt_manager import FemtoBoltManager
|
||||
from devices.device_coordinator import DeviceCoordinator
|
||||
from devices.utils.config_manager import ConfigManager
|
||||
except ImportError:
|
||||
# 如果上面的导入失败,尝试直接导入
|
||||
from camera_manager import CameraManager
|
||||
import imu_manager
|
||||
import pressure_manager
|
||||
import femtobolt_manager
|
||||
import device_coordinator
|
||||
from utils import config_manager
|
||||
|
||||
IMUManager = imu_manager.IMUManager
|
||||
PressureManager = pressure_manager.PressureManager
|
||||
FemtoBoltManager = femtobolt_manager.FemtoBoltManager
|
||||
DeviceCoordinator = device_coordinator.DeviceCoordinator
|
||||
ConfigManager = config_manager.ConfigManager
|
||||
|
||||
|
||||
class DeviceTestServer:
|
||||
"""设备测试服务器"""
|
||||
|
||||
def __init__(self, host='localhost', port=5001):
|
||||
"""
|
||||
初始化测试服务器
|
||||
|
||||
Args:
|
||||
host: 服务器主机
|
||||
port: 服务器端口
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
# Flask应用
|
||||
self.app = Flask(__name__,
|
||||
template_folder=os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
static_folder=os.path.join(os.path.dirname(__file__), 'static'))
|
||||
self.app.config['SECRET_KEY'] = 'device_test_secret_key'
|
||||
|
||||
# SocketIO
|
||||
self.socketio = SocketIO(self.app, cors_allowed_origins="*",
|
||||
async_mode='threading', logger=False, engineio_logger=False)
|
||||
|
||||
# 日志配置
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
# 设备管理器
|
||||
self.config_manager = ConfigManager()
|
||||
self.device_coordinator = None
|
||||
|
||||
# 设备管理器和模拟数据生成器
|
||||
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)
|
||||
}
|
||||
self.mock_data_generators = {
|
||||
# 'imu': MockIMUGenerator(),
|
||||
# 'pressure': MockPressureGenerator()
|
||||
}
|
||||
|
||||
# 测试状态
|
||||
self.is_testing = False
|
||||
self.test_threads = {}
|
||||
|
||||
# 注册路由和事件
|
||||
self._register_routes()
|
||||
self._register_socketio_events()
|
||||
|
||||
def _register_routes(self):
|
||||
"""注册Flask路由"""
|
||||
|
||||
@self.app.route('/')
|
||||
def index():
|
||||
"""主页"""
|
||||
return render_template('deviceTest.html')
|
||||
|
||||
@self.app.route('/api/device/status')
|
||||
def get_device_status():
|
||||
"""获取设备状态"""
|
||||
if self.device_coordinator:
|
||||
status = self.device_coordinator.get_device_status()
|
||||
else:
|
||||
status = {
|
||||
'coordinator': {'is_initialized': False, 'is_running': False},
|
||||
'devices': {}
|
||||
}
|
||||
return jsonify(status)
|
||||
|
||||
@self.app.route('/api/test/start')
|
||||
def start_test():
|
||||
"""开始测试"""
|
||||
try:
|
||||
self.start_device_test()
|
||||
return jsonify({'success': True, 'message': '测试已开始'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
@self.app.route('/api/test/stop')
|
||||
def stop_test():
|
||||
"""停止测试"""
|
||||
try:
|
||||
self.stop_device_test()
|
||||
return jsonify({'success': True, 'message': '测试已停止'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
def _register_socketio_events(self):
|
||||
"""注册SocketIO事件"""
|
||||
|
||||
@self.socketio.on('connect')
|
||||
def handle_connect():
|
||||
self.logger.info(f'客户端连接: {id}')
|
||||
emit('status', {'message': '连接成功'})
|
||||
|
||||
@self.socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
self.logger.info(f'客户端断开连接')
|
||||
|
||||
@self.socketio.on('start_test')
|
||||
def handle_start_test():
|
||||
"""处理开始测试事件"""
|
||||
try:
|
||||
self.start_device_test()
|
||||
emit('test_status', {'status': 'started', 'message': '测试已开始'})
|
||||
except Exception as e:
|
||||
emit('test_status', {'status': 'error', 'message': str(e)})
|
||||
|
||||
@self.socketio.on('stop_test')
|
||||
def handle_stop_test():
|
||||
"""处理停止测试事件"""
|
||||
try:
|
||||
self.stop_device_test()
|
||||
emit('test_status', {'status': 'stopped', 'message': '测试已停止'})
|
||||
except Exception as e:
|
||||
emit('test_status', {'status': 'error', 'message': str(e)})
|
||||
|
||||
# 注册各设备命名空间的连接事件
|
||||
@self.socketio.on('connect', namespace='/camera')
|
||||
def handle_camera_connect():
|
||||
self.logger.info('相机命名空间客户端连接')
|
||||
emit('status', {'message': '相机命名空间连接成功'}, namespace='/camera')
|
||||
|
||||
@self.socketio.on('connect', namespace='/femtobolt')
|
||||
def handle_femtobolt_connect():
|
||||
self.logger.info('深度相机命名空间客户端连接')
|
||||
emit('status', {'message': '深度相机命名空间连接成功'}, namespace='/femtobolt')
|
||||
|
||||
@self.socketio.on('connect', namespace='/imu')
|
||||
def handle_imu_connect():
|
||||
self.logger.info('IMU命名空间客户端连接')
|
||||
emit('status', {'message': 'IMU命名空间连接成功'}, namespace='/imu')
|
||||
|
||||
@self.socketio.on('connect', namespace='/pressure')
|
||||
def handle_pressure_connect():
|
||||
self.logger.info('压力板命名空间客户端连接')
|
||||
emit('status', {'message': '压力板命名空间连接成功'}, namespace='/pressure')
|
||||
|
||||
@self.socketio.on('disconnect', namespace='/camera')
|
||||
def handle_camera_disconnect():
|
||||
self.logger.info('相机命名空间客户端断开连接')
|
||||
|
||||
@self.socketio.on('disconnect', namespace='/femtobolt')
|
||||
def handle_femtobolt_disconnect():
|
||||
self.logger.info('深度相机命名空间客户端断开连接')
|
||||
|
||||
@self.socketio.on('disconnect', namespace='/imu')
|
||||
def handle_imu_disconnect():
|
||||
self.logger.info('IMU命名空间客户端断开连接')
|
||||
|
||||
@self.socketio.on('disconnect', namespace='/pressure')
|
||||
def handle_pressure_disconnect():
|
||||
self.logger.info('压力板命名空间客户端断开连接')
|
||||
|
||||
def start_device_test(self):
|
||||
"""开始设备测试"""
|
||||
if self.is_testing:
|
||||
self.logger.warning('测试已在运行')
|
||||
return
|
||||
|
||||
try:
|
||||
self.logger.info('开始设备测试...')
|
||||
self.is_testing = True
|
||||
|
||||
# 并行启动真实设备管理器
|
||||
failed_devices = []
|
||||
device_threads = {}
|
||||
device_results = {}
|
||||
|
||||
def initialize_device(device_name, manager):
|
||||
"""设备初始化工作函数"""
|
||||
try:
|
||||
print(f"[DEBUG] 尝试初始化设备: {device_name}")
|
||||
if manager.initialize():
|
||||
print(f"[DEBUG] {device_name} 初始化成功,开始启动流")
|
||||
manager.start_streaming()
|
||||
device_results[device_name] = True
|
||||
self.logger.info(f'{device_name}真实设备启动成功')
|
||||
else:
|
||||
print(f"[DEBUG] {device_name} 初始化失败")
|
||||
device_results[device_name] = False
|
||||
self.logger.error(f'{device_name}真实设备启动失败,将使用模拟数据')
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] {device_name} 初始化异常: {e}")
|
||||
device_results[device_name] = False
|
||||
self.logger.error(f'{device_name}真实设备启动异常: {e},将使用模拟数据')
|
||||
|
||||
# 为每个设备创建初始化线程
|
||||
for device_name, manager in self.device_managers.items():
|
||||
thread = threading.Thread(
|
||||
target=initialize_device,
|
||||
args=(device_name, manager),
|
||||
name=f'Init-{device_name}',
|
||||
daemon=True
|
||||
)
|
||||
device_threads[device_name] = thread
|
||||
thread.start()
|
||||
|
||||
# 等待所有设备初始化完成(最多等待30秒)
|
||||
for device_name, thread in device_threads.items():
|
||||
thread.join(timeout=30.0)
|
||||
if thread.is_alive():
|
||||
self.logger.warning(f'{device_name}设备初始化超时,将使用模拟数据')
|
||||
device_results[device_name] = False
|
||||
|
||||
# 收集失败的设备
|
||||
for device_name, success in device_results.items():
|
||||
if not success:
|
||||
failed_devices.append(device_name)
|
||||
|
||||
# 启动模拟数据生成线程(包括失败的真实设备)
|
||||
for device_name, generator in self.mock_data_generators.items():
|
||||
# 如果真实设备启动成功且不在失败列表中,跳过模拟数据生成
|
||||
if device_name in self.device_managers and device_name not in failed_devices:
|
||||
continue
|
||||
|
||||
thread = threading.Thread(
|
||||
target=self._mock_device_worker,
|
||||
args=(device_name, generator),
|
||||
name=f'MockDevice-{device_name}',
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
self.test_threads[device_name] = thread
|
||||
self.logger.info(f'{device_name}模拟数据生成器已启动')
|
||||
|
||||
# 输出启动结果摘要
|
||||
successful_devices = [name for name, success in device_results.items() if success]
|
||||
if successful_devices:
|
||||
self.logger.info(f'成功启动的真实设备: {", ".join(successful_devices)}')
|
||||
if failed_devices:
|
||||
self.logger.info(f'使用模拟数据的设备: {", ".join(failed_devices)}')
|
||||
|
||||
self.logger.info('设备测试已启动')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'启动设备测试失败: {e}')
|
||||
self.is_testing = False
|
||||
raise
|
||||
|
||||
def stop_device_test(self):
|
||||
"""停止设备测试"""
|
||||
if not self.is_testing:
|
||||
self.logger.warning('测试未运行')
|
||||
return
|
||||
|
||||
try:
|
||||
self.logger.info('停止设备测试...')
|
||||
self.is_testing = False
|
||||
|
||||
# 停止真实设备管理器
|
||||
for device_name, manager in self.device_managers.items():
|
||||
manager.stop_streaming()
|
||||
manager.disconnect()
|
||||
self.logger.info(f'{device_name}真实设备已停止')
|
||||
|
||||
# 等待所有线程结束
|
||||
for device_name, thread in self.test_threads.items():
|
||||
if thread.is_alive():
|
||||
thread.join(timeout=2.0)
|
||||
|
||||
self.test_threads.clear()
|
||||
self.logger.info('设备测试已停止')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'停止设备测试失败: {e}')
|
||||
|
||||
def _mock_device_worker(self, device_name: str, generator):
|
||||
"""模拟设备工作线程"""
|
||||
self.logger.info(f'启动{device_name}模拟数据生成')
|
||||
|
||||
while self.is_testing:
|
||||
try:
|
||||
# 生成模拟数据
|
||||
data = generator.generate_data()
|
||||
|
||||
# 发送到对应的命名空间
|
||||
namespace = f'/{device_name}'
|
||||
event_name = self._get_event_name(device_name)
|
||||
|
||||
self.socketio.emit(event_name, data, namespace=namespace)
|
||||
|
||||
# 控制发送频率
|
||||
time.sleep(generator.get_interval())
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'{device_name}模拟数据生成异常: {e}')
|
||||
time.sleep(1.0)
|
||||
|
||||
self.logger.info(f'{device_name}模拟数据生成结束')
|
||||
|
||||
def _get_event_name(self, device_name: str) -> str:
|
||||
"""获取设备对应的事件名称"""
|
||||
event_map = {
|
||||
'camera': 'camera_frame',
|
||||
'femtobolt': 'femtobolt_frame',
|
||||
'imu': 'imu_data',
|
||||
'pressure': 'pressure_data'
|
||||
}
|
||||
return event_map.get(device_name, f'{device_name}_data')
|
||||
|
||||
def run(self, debug=False):
|
||||
"""运行测试服务器"""
|
||||
self.logger.info(f'启动设备测试服务器: http://{self.host}:{self.port}')
|
||||
self.socketio.run(self.app, host=self.host, port=self.port, debug=debug)
|
||||
|
||||
|
||||
# MockCameraGenerator已移除,使用真实的CameraManager
|
||||
|
||||
|
||||
class MockFemtoBoltGenerator:
|
||||
"""模拟FemtoBolt深度相机数据生成器"""
|
||||
|
||||
def __init__(self):
|
||||
self.frame_count = 0
|
||||
self.interval = 1.0 / 15 # 15 FPS
|
||||
|
||||
def generate_data(self) -> Dict[str, Any]:
|
||||
"""生成模拟深度相机数据"""
|
||||
# 生成深度图像
|
||||
height, width = 480, 640
|
||||
|
||||
# 创建深度图像(模拟人体轮廓)
|
||||
depth_image = np.full((height, width), 2000, dtype=np.uint16)
|
||||
|
||||
# 添加人体轮廓
|
||||
center_x = width // 2
|
||||
center_y = height // 2
|
||||
|
||||
# 头部
|
||||
cv2.circle(depth_image, (center_x, center_y - 100), 40, 1500, -1)
|
||||
# 身体
|
||||
cv2.rectangle(depth_image, (center_x - 50, center_y - 60),
|
||||
(center_x + 50, center_y + 100), 1600, -1)
|
||||
# 手臂
|
||||
cv2.rectangle(depth_image, (center_x - 80, center_y - 40),
|
||||
(center_x - 50, center_y + 20), 1700, -1)
|
||||
cv2.rectangle(depth_image, (center_x + 50, center_y - 40),
|
||||
(center_x + 80, center_y + 20), 1700, -1)
|
||||
|
||||
# 转换为伪彩色
|
||||
normalized = ((depth_image - 500) / (4500 - 500) * 255).astype(np.uint8)
|
||||
colored = cv2.applyColorMap(normalized, cv2.COLORMAP_JET)
|
||||
|
||||
# 添加文字
|
||||
cv2.putText(colored, f'Depth Frame {self.frame_count}',
|
||||
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||||
|
||||
# 编码为JPEG
|
||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
|
||||
_, buffer = cv2.imencode('.jpg', colored, encode_param)
|
||||
depth_data = base64.b64encode(buffer).decode('utf-8')
|
||||
|
||||
self.frame_count += 1
|
||||
|
||||
return {
|
||||
'timestamp': time.time(),
|
||||
'frame_count': self.frame_count,
|
||||
'depth_image': depth_data,
|
||||
'fps': 15,
|
||||
'device_id': 'mock_femtobolt',
|
||||
'depth_range': {'min': 500, 'max': 4500}
|
||||
}
|
||||
|
||||
def get_interval(self) -> float:
|
||||
return self.interval
|
||||
|
||||
|
||||
class MockIMUGenerator:
|
||||
"""模拟IMU传感器数据生成器"""
|
||||
|
||||
def __init__(self):
|
||||
self.frame_count = 0
|
||||
self.interval = 1.0 / 50 # 50 Hz
|
||||
self.base_time = time.time()
|
||||
|
||||
def generate_data(self) -> Dict[str, Any]:
|
||||
"""生成模拟IMU数据"""
|
||||
current_time = time.time() - self.base_time
|
||||
|
||||
# 生成模拟的头部姿态数据(正弦波模拟头部运动)
|
||||
rotation = 15 * math.sin(current_time * 0.5) # 旋转角
|
||||
tilt = 10 * math.cos(current_time * 0.3) # 倾斜角
|
||||
pitch = 8 * math.sin(current_time * 0.7) # 俯仰角
|
||||
|
||||
# 生成加速度和陀螺仪数据
|
||||
accel_x = 0.1 * math.sin(current_time * 2.0)
|
||||
accel_y = 0.1 * math.cos(current_time * 1.5)
|
||||
accel_z = 9.8 + 0.2 * math.sin(current_time * 0.8)
|
||||
|
||||
gyro_x = 2.0 * math.cos(current_time * 1.2)
|
||||
gyro_y = 1.5 * math.sin(current_time * 0.9)
|
||||
gyro_z = 1.0 * math.sin(current_time * 1.1)
|
||||
|
||||
self.frame_count += 1
|
||||
|
||||
return {
|
||||
'timestamp': time.time(),
|
||||
'frame_count': self.frame_count,
|
||||
'device_id': 'mock_imu',
|
||||
'head_pose': {
|
||||
'rotation': round(rotation, 2),
|
||||
'tilt': round(tilt, 2),
|
||||
'pitch': round(pitch, 2)
|
||||
},
|
||||
'accelerometer': {
|
||||
'x': round(accel_x, 3),
|
||||
'y': round(accel_y, 3),
|
||||
'z': round(accel_z, 3)
|
||||
},
|
||||
'gyroscope': {
|
||||
'x': round(gyro_x, 3),
|
||||
'y': round(gyro_y, 3),
|
||||
'z': round(gyro_z, 3)
|
||||
},
|
||||
'temperature': round(25.0 + 2.0 * math.sin(current_time * 0.1), 1)
|
||||
}
|
||||
|
||||
def get_interval(self) -> float:
|
||||
return self.interval
|
||||
|
||||
|
||||
class MockPressureGenerator:
|
||||
"""模拟压力传感器数据生成器"""
|
||||
|
||||
def __init__(self):
|
||||
self.frame_count = 0
|
||||
self.interval = 1.0 / 20 # 20 Hz
|
||||
self.base_time = time.time()
|
||||
|
||||
def generate_data(self) -> Dict[str, Any]:
|
||||
"""生成模拟压力数据"""
|
||||
current_time = time.time() - self.base_time
|
||||
|
||||
# 生成模拟的足部压力数据(模拟重心转移)
|
||||
base_pressure = 50
|
||||
shift = 20 * math.sin(current_time * 0.3) # 重心左右转移
|
||||
|
||||
left_total = max(0, base_pressure + shift + random.uniform(-5, 5))
|
||||
right_total = max(0, base_pressure - shift + random.uniform(-5, 5))
|
||||
|
||||
# 前后足压力分配
|
||||
left_front = left_total * (0.4 + 0.1 * math.sin(current_time * 0.5))
|
||||
left_rear = left_total - left_front
|
||||
right_front = right_total * (0.4 + 0.1 * math.cos(current_time * 0.5))
|
||||
right_rear = right_total - right_front
|
||||
|
||||
# 生成压力热力图
|
||||
pressure_image = self._generate_pressure_heatmap(
|
||||
left_front, left_rear, right_front, right_rear
|
||||
)
|
||||
|
||||
self.frame_count += 1
|
||||
|
||||
return {
|
||||
'timestamp': time.time(),
|
||||
'frame_count': self.frame_count,
|
||||
'device_id': 'mock_pressure',
|
||||
'pressure_data': {
|
||||
'left_total': round(left_total, 1),
|
||||
'right_total': round(right_total, 1),
|
||||
'left_front': round(left_front, 1),
|
||||
'left_rear': round(left_rear, 1),
|
||||
'right_front': round(right_front, 1),
|
||||
'right_rear': round(right_rear, 1),
|
||||
'total_pressure': round(left_total + right_total, 1),
|
||||
'balance_ratio': round(left_total / (left_total + right_total) * 100, 1) if (left_total + right_total) > 0 else 50.0
|
||||
},
|
||||
'pressure_image': pressure_image
|
||||
}
|
||||
|
||||
def _generate_pressure_heatmap(self, left_front, left_rear, right_front, right_rear) -> str:
|
||||
"""生成压力热力图"""
|
||||
# 创建足底压力图像
|
||||
height, width = 300, 300
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
# 左足区域
|
||||
left_foot_x = width // 4
|
||||
left_front_intensity = int(min(255, left_front * 5))
|
||||
left_rear_intensity = int(min(255, left_rear * 5))
|
||||
|
||||
# 左前足
|
||||
cv2.rectangle(image, (left_foot_x - 30, height // 4),
|
||||
(left_foot_x + 30, height // 2),
|
||||
(0, 0, left_front_intensity), -1)
|
||||
|
||||
# 左后足
|
||||
cv2.rectangle(image, (left_foot_x - 30, height // 2),
|
||||
(left_foot_x + 30, height * 3 // 4),
|
||||
(0, 0, left_rear_intensity), -1)
|
||||
|
||||
# 右足区域
|
||||
right_foot_x = width * 3 // 4
|
||||
right_front_intensity = int(min(255, right_front * 5))
|
||||
right_rear_intensity = int(min(255, right_rear * 5))
|
||||
|
||||
# 右前足
|
||||
cv2.rectangle(image, (right_foot_x - 30, height // 4),
|
||||
(right_foot_x + 30, height // 2),
|
||||
(0, 0, right_front_intensity), -1)
|
||||
|
||||
# 右后足
|
||||
cv2.rectangle(image, (right_foot_x - 30, height // 2),
|
||||
(right_foot_x + 30, height * 3 // 4),
|
||||
(0, 0, right_rear_intensity), -1)
|
||||
|
||||
# 添加分割线
|
||||
cv2.line(image, (width // 2, 0), (width // 2, height), (255, 255, 255), 2)
|
||||
cv2.line(image, (0, height // 2), (width, height // 2), (255, 255, 255), 1)
|
||||
|
||||
# 编码为Base64
|
||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
|
||||
_, buffer = cv2.imencode('.jpg', image, encode_param)
|
||||
return base64.b64encode(buffer).decode('utf-8')
|
||||
|
||||
def get_interval(self) -> float:
|
||||
return self.interval
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 创建templates目录
|
||||
templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
os.makedirs(templates_dir, exist_ok=True)
|
||||
|
||||
# 创建测试服务器
|
||||
server = DeviceTestServer(host='localhost', port=5001)
|
||||
|
||||
try:
|
||||
# 运行服务器
|
||||
server.run(debug=False)
|
||||
except KeyboardInterrupt:
|
||||
print('\n服务器已停止')
|
||||
except Exception as e:
|
||||
print(f'服务器运行异常: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
912
backend/devices/test/templates/deviceTest.html
Normal file
912
backend/devices/test/templates/deviceTest.html
Normal file
@ -0,0 +1,912 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设备测试页面</title>
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
background: linear-gradient(45deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-start:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn-stop {
|
||||
background: linear-gradient(45deg, #f44336, #d32f2f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-stop:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
padding: 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #ff4444;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: #44ff44;
|
||||
box-shadow: 0 0 10px rgba(68, 255, 68, 0.5);
|
||||
}
|
||||
|
||||
.devices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 20px;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.device-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.device-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
font-size: 0.9rem;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 68, 68, 0.2);
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.device-status.connected {
|
||||
background: rgba(68, 255, 68, 0.2);
|
||||
color: #44ff44;
|
||||
}
|
||||
|
||||
.device-content {
|
||||
height: calc(100% - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #000;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-container img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.no-signal {
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-display {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.imu-gauges {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.gauge-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gauge {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.gauge-label {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.pressure-visualization {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.foot-diagram {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.foot-diagram img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.frame-info {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.fps-counter {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.recording {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 300px;
|
||||
max-height: 200px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.8rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.log-success {
|
||||
color: #44ff44;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>设备测试控制台</h1>
|
||||
<p>实时监控四种设备的数据流:深度相机、普通相机、压力板、IMU传感器</p>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<button id="startBtn" class="btn btn-start">开始测试</button>
|
||||
<button id="stopBtn" class="btn btn-stop" disabled>停止测试</button>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<div id="serverStatus" class="status-indicator"></div>
|
||||
<span>服务器连接</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div id="cameraStatus" class="status-indicator"></div>
|
||||
<span>普通相机</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div id="femtoboltStatus" class="status-indicator"></div>
|
||||
<span>深度相机</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div id="imuStatus" class="status-indicator"></div>
|
||||
<span>IMU传感器</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div id="pressureStatus" class="status-indicator"></div>
|
||||
<span>压力板</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="devices-grid">
|
||||
<!-- 普通相机 -->
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<div class="device-title">📹 普通相机</div>
|
||||
<div id="cameraDeviceStatus" class="device-status">未连接</div>
|
||||
</div>
|
||||
<div class="device-content">
|
||||
<div class="video-container">
|
||||
<img id="cameraImage" src="" alt="相机画面" style="display: none;">
|
||||
<div id="cameraNoSignal" class="no-signal">等待相机信号...</div>
|
||||
<div id="cameraFrameInfo" class="frame-info" style="display: none;">帧数: 0</div>
|
||||
<div id="cameraFps" class="fps-counter" style="display: none;">FPS: 0</div>
|
||||
</div>
|
||||
<div class="data-display">
|
||||
<div class="data-row">
|
||||
<span class="data-label">分辨率:</span>
|
||||
<span id="cameraResolution" class="data-value">-</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">设备ID:</span>
|
||||
<span id="cameraDeviceId" class="data-value">-</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">最后更新:</span>
|
||||
<span id="cameraLastUpdate" class="data-value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 深度相机 -->
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<div class="device-title">🔍 深度相机</div>
|
||||
<div id="femtoboltDeviceStatus" class="device-status">未连接</div>
|
||||
</div>
|
||||
<div class="device-content">
|
||||
<div class="video-container">
|
||||
<img id="femtoboltImage" src="" alt="深度画面" style="display: none;">
|
||||
<div id="femtoboltNoSignal" class="no-signal">等待深度相机信号...</div>
|
||||
<div id="femtoboltFrameInfo" class="frame-info" style="display: none;">帧数: 0</div>
|
||||
<div id="femtoboltFps" class="fps-counter" style="display: none;">FPS: 0</div>
|
||||
</div>
|
||||
<div class="data-display">
|
||||
<div class="data-row">
|
||||
<span class="data-label">深度范围:</span>
|
||||
<span id="femtoboltDepthRange" class="data-value">-</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">设备ID:</span>
|
||||
<span id="femtoboltDeviceId" class="data-value">-</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">最后更新:</span>
|
||||
<span id="femtoboltLastUpdate" class="data-value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IMU传感器 -->
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<div class="device-title">🧭 IMU传感器</div>
|
||||
<div id="imuDeviceStatus" class="device-status">未连接</div>
|
||||
</div>
|
||||
<div class="device-content">
|
||||
<div class="imu-gauges">
|
||||
<div class="gauge-container">
|
||||
<div id="rotationGauge" class="gauge"></div>
|
||||
<div class="gauge-label">旋转角</div>
|
||||
<div id="rotationValue" class="data-value">0°</div>
|
||||
</div>
|
||||
<div class="gauge-container">
|
||||
<div id="tiltGauge" class="gauge"></div>
|
||||
<div class="gauge-label">倾斜角</div>
|
||||
<div id="tiltValue" class="data-value">0°</div>
|
||||
</div>
|
||||
<div class="gauge-container">
|
||||
<div id="pitchGauge" class="gauge"></div>
|
||||
<div class="gauge-label">俯仰角</div>
|
||||
<div id="pitchValue" class="data-value">0°</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-display">
|
||||
<div class="data-row">
|
||||
<span class="data-label">加速度 X:</span>
|
||||
<span id="accelX" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">加速度 Y:</span>
|
||||
<span id="accelY" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">加速度 Z:</span>
|
||||
<span id="accelZ" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">温度:</span>
|
||||
<span id="temperature" class="data-value">0°C</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 压力板 -->
|
||||
<div class="device-card">
|
||||
<div class="device-header">
|
||||
<div class="device-title">⚖️ 压力板</div>
|
||||
<div id="pressureDeviceStatus" class="device-status">未连接</div>
|
||||
</div>
|
||||
<div class="device-content">
|
||||
<div class="pressure-visualization">
|
||||
<div class="foot-diagram">
|
||||
<img id="pressureImage" src="" alt="压力分布" style="display: none;">
|
||||
<div id="pressureNoSignal" class="no-signal">等待压力数据...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-display">
|
||||
<div class="data-row">
|
||||
<span class="data-label">左足总压力:</span>
|
||||
<span id="leftTotal" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">右足总压力:</span>
|
||||
<span id="rightTotal" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">总压力:</span>
|
||||
<span id="totalPressure" class="data-value">0</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">平衡比例:</span>
|
||||
<span id="balanceRatio" class="data-value">50%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志面板 -->
|
||||
<div class="log-panel">
|
||||
<div style="font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 5px;">系统日志</div>
|
||||
<div id="logContainer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let socket = null;
|
||||
let isConnected = false;
|
||||
let isTesting = false;
|
||||
|
||||
// FPS计算
|
||||
const fpsCounters = {
|
||||
camera: { frames: 0, lastTime: Date.now() },
|
||||
femtobolt: { frames: 0, lastTime: Date.now() }
|
||||
};
|
||||
|
||||
// ECharts图表实例
|
||||
let rotationChart, tiltChart, pitchChart;
|
||||
|
||||
// DOM元素
|
||||
const elements = {
|
||||
startBtn: document.getElementById('startBtn'),
|
||||
stopBtn: document.getElementById('stopBtn'),
|
||||
serverStatus: document.getElementById('serverStatus'),
|
||||
cameraStatus: document.getElementById('cameraStatus'),
|
||||
femtoboltStatus: document.getElementById('femtoboltStatus'),
|
||||
imuStatus: document.getElementById('imuStatus'),
|
||||
pressureStatus: document.getElementById('pressureStatus'),
|
||||
logContainer: document.getElementById('logContainer')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeCharts();
|
||||
setupEventListeners();
|
||||
connectToServer();
|
||||
addLog('系统初始化完成', 'success');
|
||||
});
|
||||
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
elements.startBtn.addEventListener('click', startTest);
|
||||
elements.stopBtn.addEventListener('click', stopTest);
|
||||
}
|
||||
|
||||
// 初始化ECharts图表
|
||||
function initializeCharts() {
|
||||
const gaugeOption = {
|
||||
backgroundColor: 'transparent',
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
radius: '100%',
|
||||
min: -90,
|
||||
max: 90,
|
||||
splitNumber: 6,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 6,
|
||||
color: [[0.3, '#67e0e3'], [0.7, '#37a2da'], [1, '#fd666d']]
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
distance: -30,
|
||||
length: 8,
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
distance: -30,
|
||||
length: 30,
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
width: 4
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'auto',
|
||||
distance: 40,
|
||||
fontSize: 10
|
||||
},
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
formatter: '{value}°',
|
||||
color: 'auto',
|
||||
fontSize: 12
|
||||
},
|
||||
data: [{
|
||||
value: 0
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
rotationChart = echarts.init(document.getElementById('rotationGauge'));
|
||||
tiltChart = echarts.init(document.getElementById('tiltGauge'));
|
||||
pitchChart = echarts.init(document.getElementById('pitchGauge'));
|
||||
|
||||
rotationChart.setOption(gaugeOption);
|
||||
tiltChart.setOption(gaugeOption);
|
||||
pitchChart.setOption(gaugeOption);
|
||||
}
|
||||
|
||||
// 连接到服务器
|
||||
function connectToServer() {
|
||||
addLog('正在连接到测试服务器...');
|
||||
|
||||
// 创建主连接
|
||||
socket = io('http://localhost:5001', {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 10000,
|
||||
forceNew: true
|
||||
});
|
||||
|
||||
// 创建各设备命名空间连接
|
||||
const cameraSocket = io('http://localhost:5001/camera');
|
||||
const femtoboltSocket = io('http://localhost:5001/femtobolt');
|
||||
const imuSocket = io('http://localhost:5001/imu');
|
||||
const pressureSocket = io('http://localhost:5001/pressure');
|
||||
|
||||
// 主连接事件
|
||||
socket.on('connect', () => {
|
||||
isConnected = true;
|
||||
elements.serverStatus.classList.add('connected');
|
||||
addLog('服务器连接成功', 'success');
|
||||
elements.startBtn.disabled = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
isConnected = false;
|
||||
elements.serverStatus.classList.remove('connected');
|
||||
addLog('服务器连接断开', 'error');
|
||||
if (isTesting) {
|
||||
stopTest();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
addLog(`连接错误: ${error.message}`, 'error');
|
||||
});
|
||||
|
||||
// 测试状态事件
|
||||
socket.on('test_status', (data) => {
|
||||
addLog(`测试状态: ${data.message}`, data.status === 'error' ? 'error' : 'success');
|
||||
});
|
||||
|
||||
// 设备命名空间数据事件监听
|
||||
cameraSocket.on('camera_frame', handleCameraData);
|
||||
femtoboltSocket.on('femtobolt_frame', handleFemtoBoltData);
|
||||
imuSocket.on('imu_data', handleIMUData);
|
||||
pressureSocket.on('pressure_data', handlePressureData);
|
||||
|
||||
// 设备连接状态监听
|
||||
cameraSocket.on('connect', () => {
|
||||
elements.cameraStatus.classList.add('connected');
|
||||
addLog('相机命名空间已连接', 'success');
|
||||
});
|
||||
|
||||
femtoboltSocket.on('connect', () => {
|
||||
elements.femtoboltStatus.classList.add('connected');
|
||||
addLog('深度相机命名空间已连接', 'success');
|
||||
});
|
||||
|
||||
imuSocket.on('connect', () => {
|
||||
elements.imuStatus.classList.add('connected');
|
||||
addLog('IMU命名空间已连接', 'success');
|
||||
});
|
||||
|
||||
pressureSocket.on('connect', () => {
|
||||
elements.pressureStatus.classList.add('connected');
|
||||
addLog('压力板命名空间已连接', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// 设置设备事件监听器(已移至connectToServer函数中)
|
||||
function setupDeviceEventListeners() {
|
||||
// 此函数已废弃,事件监听器现在在connectToServer中设置
|
||||
}
|
||||
|
||||
// 开始测试
|
||||
function startTest() {
|
||||
if (!isConnected) {
|
||||
addLog('服务器未连接,无法开始测试', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
isTesting = true;
|
||||
elements.startBtn.disabled = true;
|
||||
elements.stopBtn.disabled = false;
|
||||
|
||||
socket.emit('start_test');
|
||||
addLog('开始设备测试', 'success');
|
||||
}
|
||||
|
||||
// 停止测试
|
||||
function stopTest() {
|
||||
isTesting = false;
|
||||
elements.startBtn.disabled = false;
|
||||
elements.stopBtn.disabled = true;
|
||||
|
||||
// 重置所有状态指示器
|
||||
resetDeviceStatus();
|
||||
|
||||
if (socket && isConnected) {
|
||||
socket.emit('stop_test');
|
||||
}
|
||||
addLog('停止设备测试', 'success');
|
||||
}
|
||||
|
||||
// 重置设备状态
|
||||
function resetDeviceStatus() {
|
||||
elements.cameraStatus.classList.remove('connected');
|
||||
elements.femtoboltStatus.classList.remove('connected');
|
||||
elements.imuStatus.classList.remove('connected');
|
||||
elements.pressureStatus.classList.remove('connected');
|
||||
|
||||
document.getElementById('cameraDeviceStatus').textContent = '未连接';
|
||||
document.getElementById('cameraDeviceStatus').classList.remove('connected');
|
||||
|
||||
document.getElementById('femtoboltDeviceStatus').textContent = '未连接';
|
||||
document.getElementById('femtoboltDeviceStatus').classList.remove('connected');
|
||||
|
||||
document.getElementById('imuDeviceStatus').textContent = '未连接';
|
||||
document.getElementById('imuDeviceStatus').classList.remove('connected');
|
||||
|
||||
document.getElementById('pressureDeviceStatus').textContent = '未连接';
|
||||
document.getElementById('pressureDeviceStatus').classList.remove('connected');
|
||||
|
||||
// 隐藏图像和数据
|
||||
document.getElementById('cameraImage').style.display = 'none';
|
||||
document.getElementById('cameraNoSignal').style.display = 'block';
|
||||
document.getElementById('femtoboltImage').style.display = 'none';
|
||||
document.getElementById('femtoboltNoSignal').style.display = 'block';
|
||||
document.getElementById('pressureImage').style.display = 'none';
|
||||
document.getElementById('pressureNoSignal').style.display = 'block';
|
||||
}
|
||||
|
||||
// 处理普通相机数据
|
||||
function handleCameraData(data) {
|
||||
if (!isTesting) return;
|
||||
|
||||
elements.cameraStatus.classList.add('connected');
|
||||
document.getElementById('cameraDeviceStatus').textContent = '已连接';
|
||||
document.getElementById('cameraDeviceStatus').classList.add('connected');
|
||||
|
||||
if (data.image) {
|
||||
const img = document.getElementById('cameraImage');
|
||||
img.src = 'data:image/jpeg;base64,' + data.image;
|
||||
img.style.display = 'block';
|
||||
document.getElementById('cameraNoSignal').style.display = 'none';
|
||||
|
||||
// 更新帧信息
|
||||
document.getElementById('cameraFrameInfo').textContent = `帧数: ${data.frame_count || 0}`;
|
||||
document.getElementById('cameraFrameInfo').style.display = 'block';
|
||||
|
||||
// 计算FPS
|
||||
updateFPS('camera', data.fps || 30);
|
||||
}
|
||||
|
||||
// 更新设备信息
|
||||
if (data.resolution) {
|
||||
document.getElementById('cameraResolution').textContent = `${data.resolution.width}x${data.resolution.height}`;
|
||||
}
|
||||
document.getElementById('cameraDeviceId').textContent = data.device_id || 'mock_camera';
|
||||
document.getElementById('cameraLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 处理深度相机数据
|
||||
function handleFemtoBoltData(data) {
|
||||
if (!isTesting) return;
|
||||
|
||||
elements.femtoboltStatus.classList.add('connected');
|
||||
document.getElementById('femtoboltDeviceStatus').textContent = '已连接';
|
||||
document.getElementById('femtoboltDeviceStatus').classList.add('connected');
|
||||
|
||||
if (data.depth_image) {
|
||||
const img = document.getElementById('femtoboltImage');
|
||||
img.src = 'data:image/jpeg;base64,' + data.depth_image;
|
||||
img.style.display = 'block';
|
||||
document.getElementById('femtoboltNoSignal').style.display = 'none';
|
||||
|
||||
// 更新帧信息
|
||||
document.getElementById('femtoboltFrameInfo').textContent = `帧数: ${data.frame_count || 0}`;
|
||||
document.getElementById('femtoboltFrameInfo').style.display = 'block';
|
||||
|
||||
// 计算FPS
|
||||
updateFPS('femtobolt', data.fps || 15);
|
||||
}
|
||||
|
||||
// 更新设备信息
|
||||
if (data.depth_range) {
|
||||
document.getElementById('femtoboltDepthRange').textContent = `${data.depth_range.min}-${data.depth_range.max}mm`;
|
||||
}
|
||||
document.getElementById('femtoboltDeviceId').textContent = data.device_id || 'mock_femtobolt';
|
||||
document.getElementById('femtoboltLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 处理IMU数据
|
||||
function handleIMUData(data) {
|
||||
if (!isTesting) return;
|
||||
|
||||
elements.imuStatus.classList.add('connected');
|
||||
document.getElementById('imuDeviceStatus').textContent = '已连接';
|
||||
document.getElementById('imuDeviceStatus').classList.add('connected');
|
||||
|
||||
if (data.head_pose) {
|
||||
const { rotation, tilt, pitch } = data.head_pose;
|
||||
|
||||
// 更新仪表盘
|
||||
rotationChart.setOption({
|
||||
series: [{ data: [{ value: rotation }] }]
|
||||
});
|
||||
tiltChart.setOption({
|
||||
series: [{ data: [{ value: tilt }] }]
|
||||
});
|
||||
pitchChart.setOption({
|
||||
series: [{ data: [{ value: pitch }] }]
|
||||
});
|
||||
|
||||
// 更新数值显示
|
||||
document.getElementById('rotationValue').textContent = `${rotation}°`;
|
||||
document.getElementById('tiltValue').textContent = `${tilt}°`;
|
||||
document.getElementById('pitchValue').textContent = `${pitch}°`;
|
||||
}
|
||||
|
||||
// 更新加速度和温度数据
|
||||
if (data.accelerometer) {
|
||||
document.getElementById('accelX').textContent = data.accelerometer.x;
|
||||
document.getElementById('accelY').textContent = data.accelerometer.y;
|
||||
document.getElementById('accelZ').textContent = data.accelerometer.z;
|
||||
}
|
||||
|
||||
if (data.temperature) {
|
||||
document.getElementById('temperature').textContent = `${data.temperature}°C`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理压力板数据
|
||||
function handlePressureData(data) {
|
||||
if (!isTesting) return;
|
||||
|
||||
elements.pressureStatus.classList.add('connected');
|
||||
document.getElementById('pressureDeviceStatus').textContent = '已连接';
|
||||
document.getElementById('pressureDeviceStatus').classList.add('connected');
|
||||
|
||||
if (data.pressure_image) {
|
||||
const img = document.getElementById('pressureImage');
|
||||
img.src = 'data:image/jpeg;base64,' + data.pressure_image;
|
||||
img.style.display = 'block';
|
||||
document.getElementById('pressureNoSignal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新压力数据
|
||||
if (data.pressure_data) {
|
||||
const pd = data.pressure_data;
|
||||
document.getElementById('leftTotal').textContent = pd.left_total;
|
||||
document.getElementById('rightTotal').textContent = pd.right_total;
|
||||
document.getElementById('totalPressure').textContent = pd.total_pressure;
|
||||
document.getElementById('balanceRatio').textContent = `${pd.balance_ratio}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新FPS显示
|
||||
function updateFPS(device, targetFps) {
|
||||
const counter = fpsCounters[device];
|
||||
counter.frames++;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - counter.lastTime >= 1000) {
|
||||
const fps = Math.round(counter.frames * 1000 / (now - counter.lastTime));
|
||||
document.getElementById(`${device}Fps`).textContent = `FPS: ${fps}`;
|
||||
document.getElementById(`${device}Fps`).style.display = 'block';
|
||||
|
||||
counter.frames = 0;
|
||||
counter.lastTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
function addLog(message, type = 'info') {
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = 'log-entry';
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logEntry.innerHTML = `
|
||||
<span class="log-timestamp">[${timestamp}]</span>
|
||||
<span class="log-message log-${type}">${message}</span>
|
||||
`;
|
||||
|
||||
elements.logContainer.appendChild(logEntry);
|
||||
elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
|
||||
|
||||
// 限制日志条数
|
||||
while (elements.logContainer.children.length > 50) {
|
||||
elements.logContainer.removeChild(elements.logContainer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面卸载时清理资源
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
137
backend/devices/test/test_parallel_startup.py
Normal file
137
backend/devices/test/test_parallel_startup.py
Normal file
@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
并行设备启动测试脚本
|
||||
测试设备启动修改为并行启动,确保深度相机启动失败不影响其他设备启动
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
|
||||
class ParallelStartupTester:
|
||||
def __init__(self, base_url="http://localhost:5001"):
|
||||
self.base_url = base_url
|
||||
|
||||
def test_parallel_startup(self):
|
||||
"""测试并行启动功能"""
|
||||
print("=== 测试并行设备启动功能 ===")
|
||||
|
||||
try:
|
||||
# 1. 首先停止任何正在运行的测试
|
||||
print("1. 停止当前测试...")
|
||||
response = requests.get(f"{self.base_url}/api/test/stop")
|
||||
if response.status_code == 200:
|
||||
print("✓ 成功停止当前测试")
|
||||
else:
|
||||
print(f"⚠ 停止测试响应: {response.status_code}")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# 2. 启动设备测试
|
||||
print("\n2. 启动设备测试...")
|
||||
response = requests.get(f"{self.base_url}/api/test/start")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✓ 设备测试启动成功")
|
||||
print(f"响应消息: {result.get('message', 'N/A')}")
|
||||
|
||||
# 等待设备初始化完成
|
||||
print("\n3. 等待设备初始化完成...")
|
||||
time.sleep(10) # 给设备足够时间进行并行初始化
|
||||
|
||||
# 3. 检查设备状态
|
||||
print("\n4. 检查设备状态...")
|
||||
status_response = requests.get(f"{self.base_url}/api/device/status")
|
||||
|
||||
if status_response.status_code == 200:
|
||||
status = status_response.json()
|
||||
print("✓ 获取设备状态成功")
|
||||
print(f"测试状态: {status.get('is_testing', 'N/A')}")
|
||||
|
||||
# 显示设备状态详情
|
||||
if 'devices' in status:
|
||||
print("\n设备状态详情:")
|
||||
for device_name, device_status in status['devices'].items():
|
||||
print(f" {device_name}: {device_status}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 获取设备状态失败: {status_response.status_code}")
|
||||
return False
|
||||
|
||||
else:
|
||||
print(f"✗ 设备测试启动失败: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("✗ 无法连接到设备测试服务器,请确保服务器正在运行")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ 测试过程中发生错误: {e}")
|
||||
return False
|
||||
|
||||
def test_device_resilience(self):
|
||||
"""测试设备故障恢复能力"""
|
||||
print("\n=== 测试设备故障恢复能力 ===")
|
||||
|
||||
try:
|
||||
# 多次启动和停止测试,验证系统稳定性
|
||||
for i in range(3):
|
||||
print(f"\n第 {i+1} 轮测试:")
|
||||
|
||||
# 启动测试
|
||||
start_response = requests.get(f"{self.base_url}/api/test/start")
|
||||
if start_response.status_code == 200:
|
||||
print(" ✓ 启动成功")
|
||||
else:
|
||||
print(f" ✗ 启动失败: {start_response.status_code}")
|
||||
|
||||
time.sleep(5) # 等待设备初始化
|
||||
|
||||
# 停止测试
|
||||
stop_response = requests.get(f"{self.base_url}/api/test/stop")
|
||||
if stop_response.status_code == 200:
|
||||
print(" ✓ 停止成功")
|
||||
else:
|
||||
print(f" ✗ 停止失败: {stop_response.status_code}")
|
||||
|
||||
time.sleep(2) # 等待清理
|
||||
|
||||
print("\n✓ 设备故障恢复能力测试完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 故障恢复测试失败: {e}")
|
||||
return False
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
print("开始并行设备启动测试...\n")
|
||||
|
||||
# 测试并行启动
|
||||
test1_result = self.test_parallel_startup()
|
||||
|
||||
# 测试设备故障恢复
|
||||
test2_result = self.test_device_resilience()
|
||||
|
||||
# 输出测试结果
|
||||
print("\n=== 测试结果汇总 ===")
|
||||
print(f"并行启动测试: {'✓ 通过' if test1_result else '✗ 失败'}")
|
||||
print(f"故障恢复测试: {'✓ 通过' if test2_result else '✗ 失败'}")
|
||||
|
||||
if test1_result and test2_result:
|
||||
print("\n🎉 所有测试通过!并行设备启动功能正常工作")
|
||||
else:
|
||||
print("\n⚠ 部分测试失败,请检查日志")
|
||||
|
||||
return test1_result and test2_result
|
||||
|
||||
def main():
|
||||
tester = ParallelStartupTester()
|
||||
tester.run_all_tests()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
227
backend/devices/test_config_api.py
Normal file
227
backend/devices/test_config_api.py
Normal file
@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
配置API测试脚本
|
||||
用于测试设备配置HTTP API的功能
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class ConfigAPITester:
|
||||
"""配置API测试器"""
|
||||
|
||||
def __init__(self, base_url="http://localhost:5002"):
|
||||
"""
|
||||
初始化测试器
|
||||
|
||||
Args:
|
||||
base_url: API基础URL
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.api_url = f"{base_url}/api/config"
|
||||
|
||||
def test_get_all_configs(self):
|
||||
"""测试获取所有设备配置"""
|
||||
print("\n=== 测试获取所有设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 获取所有设备配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_get_single_config(self, device_name):
|
||||
"""测试获取单个设备配置"""
|
||||
print(f"\n=== 测试获取{device_name}设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices/{device_name}")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print(f"✓ 获取{device_name}配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_imu_config(self):
|
||||
"""测试设置IMU配置"""
|
||||
print("\n=== 测试设置IMU配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/imu", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置IMU配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_pressure_config(self):
|
||||
"""测试设置压力板配置"""
|
||||
print("\n=== 测试设置压力板配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"use_mock": False,
|
||||
"port": "COM5",
|
||||
"baudrate": 115200
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/pressure", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置压力板配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_camera_config(self):
|
||||
"""测试设置相机配置"""
|
||||
print("\n=== 测试设置相机配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_index": 0,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 30
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/camera", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置相机配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_femtobolt_config(self):
|
||||
"""测试设置FemtoBolt配置"""
|
||||
print("\n=== 测试设置FemtoBolt配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"color_resolution": "1080P",
|
||||
"depth_mode": "NFOV_UNBINNED",
|
||||
"fps": 30,
|
||||
"depth_range_min": 1200,
|
||||
"depth_range_max": 1500
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/femtobolt", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置FemtoBolt配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_validate_config(self):
|
||||
"""测试验证配置"""
|
||||
print("\n=== 测试验证配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/validate")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 配置验证成功")
|
||||
validation_result = result['data']
|
||||
print(f"配置有效性: {validation_result['valid']}")
|
||||
if validation_result['errors']:
|
||||
print(f"错误: {validation_result['errors']}")
|
||||
if validation_result['warnings']:
|
||||
print(f"警告: {validation_result['warnings']}")
|
||||
else:
|
||||
print(f"✗ 验证失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_reload_config(self):
|
||||
"""测试重新加载配置"""
|
||||
print("\n=== 测试重新加载配置 ===")
|
||||
try:
|
||||
response = requests.post(f"{self.api_url}/reload")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 重新加载配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
else:
|
||||
print(f"✗ 重新加载失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
print("开始配置API功能测试...")
|
||||
print(f"API地址: {self.api_url}")
|
||||
|
||||
# 等待服务启动
|
||||
print("\n等待API服务启动...")
|
||||
time.sleep(2)
|
||||
|
||||
# 运行测试
|
||||
self.test_get_all_configs()
|
||||
|
||||
# 测试获取单个设备配置
|
||||
for device in ['imu', 'pressure', 'camera', 'femtobolt']:
|
||||
self.test_get_single_config(device)
|
||||
|
||||
# 测试设置配置
|
||||
self.test_set_imu_config()
|
||||
self.test_set_pressure_config()
|
||||
self.test_set_camera_config()
|
||||
self.test_set_femtobolt_config()
|
||||
|
||||
# 测试其他功能
|
||||
self.test_validate_config()
|
||||
self.test_reload_config()
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建测试器并运行测试
|
||||
tester = ConfigAPITester()
|
||||
tester.run_all_tests()
|
13
backend/devices/utils/__init__.py
Normal file
13
backend/devices/utils/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设备管理工具模块
|
||||
"""
|
||||
|
||||
from .socket_manager import SocketManager
|
||||
from .config_manager import ConfigManager
|
||||
|
||||
__all__ = [
|
||||
'SocketManager',
|
||||
'ConfigManager'
|
||||
]
|
60
backend/devices/utils/config.ini
Normal file
60
backend/devices/utils/config.ini
Normal file
@ -0,0 +1,60 @@
|
||||
[APP]
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
debug = True
|
||||
log_level = INFO
|
||||
|
||||
[SERVER]
|
||||
host = 0.0.0.0
|
||||
port = 5000
|
||||
cors_origins = *
|
||||
|
||||
[DATABASE]
|
||||
path = data/body_balance.db
|
||||
backup_interval = 24
|
||||
max_backups = 7
|
||||
|
||||
[CAMERA]
|
||||
device_index = 0
|
||||
width = 1280
|
||||
height = 720
|
||||
fps = 30
|
||||
|
||||
[FEMTOBOLT]
|
||||
color_resolution = 1080P
|
||||
depth_mode = NFOV_UNBINNED
|
||||
fps = 30
|
||||
depth_range_min = 1200
|
||||
depth_range_max = 1500
|
||||
|
||||
[DEVICES]
|
||||
imu_device_type = real
|
||||
imu_port = COM6
|
||||
imu_baudrate = 9600
|
||||
pressure_device_type = real
|
||||
pressure_use_mock = False
|
||||
pressure_port = COM5
|
||||
pressure_baudrate = 115200
|
||||
|
||||
[SYSTEM]
|
||||
log_level = INFO
|
||||
max_cache_size = 10
|
||||
cache_timeout = 5.0
|
||||
|
||||
[DETECTION]
|
||||
default_duration = 60
|
||||
sampling_rate = 30
|
||||
balance_threshold = 0.2
|
||||
posture_threshold = 5.0
|
||||
|
||||
[DATA_PROCESSING]
|
||||
filter_window = 5
|
||||
outlier_threshold = 2.0
|
||||
chart_dpi = 300
|
||||
export_format = csv
|
||||
|
||||
[SECURITY]
|
||||
secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
512
backend/devices/utils/config_manager.py
Normal file
512
backend/devices/utils/config_manager.py
Normal file
@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
配置管理工具
|
||||
提供设备配置的统一管理和读取
|
||||
"""
|
||||
|
||||
import os
|
||||
import configparser
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_path: Optional[str] = None):
|
||||
"""
|
||||
初始化配置管理器
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,默认为backend/config.ini
|
||||
"""
|
||||
self.logger = logging.getLogger(f"{__name__}.ConfigManager")
|
||||
|
||||
# 确定配置文件路径
|
||||
if config_path:
|
||||
self.config_path = config_path
|
||||
else:
|
||||
# 自动查找配置文件
|
||||
self.config_path = self._find_config_file()
|
||||
|
||||
self.config = configparser.ConfigParser()
|
||||
self._device_configs = {}
|
||||
self._load_config()
|
||||
|
||||
def _find_config_file(self) -> str:
|
||||
"""
|
||||
查找配置文件
|
||||
|
||||
Returns:
|
||||
str: 配置文件路径
|
||||
"""
|
||||
# 可能的配置文件路径
|
||||
possible_paths = [
|
||||
os.path.join(os.path.dirname(__file__), 'config.ini')
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
abs_path = os.path.abspath(path)
|
||||
if os.path.exists(abs_path):
|
||||
self.logger.info(f"找到配置文件: {abs_path}")
|
||||
return abs_path
|
||||
|
||||
|
||||
|
||||
def _load_config(self):
|
||||
"""
|
||||
加载配置文件
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
self.config.read(self.config_path, encoding='utf-8')
|
||||
self.logger.info(f"成功加载配置文件: {self.config_path}")
|
||||
else:
|
||||
self.logger.warning(f"配置文件不存在: {self.config_path}")
|
||||
self._create_default_config()
|
||||
except Exception as e:
|
||||
self.logger.error(f"加载配置文件失败: {e}")
|
||||
self._create_default_config()
|
||||
|
||||
def _create_default_config(self):
|
||||
"""
|
||||
创建默认配置
|
||||
"""
|
||||
self.config.clear()
|
||||
|
||||
# 默认设备配置
|
||||
self.config['DEVICES'] = {
|
||||
'imu_port': 'COM7',
|
||||
'imu_baudrate': '9600',
|
||||
'pressure_port': 'COM8',
|
||||
'pressure_baudrate': '115200'
|
||||
}
|
||||
|
||||
# 默认相机配置
|
||||
self.config['CAMERA'] = {
|
||||
'device_index': '0',
|
||||
'width': '1280',
|
||||
'height': '720',
|
||||
'fps': '30'
|
||||
}
|
||||
|
||||
# 默认FemtoBolt配置
|
||||
self.config['FEMTOBOLT'] = {
|
||||
'color_resolution': '1080P',
|
||||
'depth_mode': 'NFOV_UNBINNED',
|
||||
'fps': '15',
|
||||
'depth_range_min': '500',
|
||||
'depth_range_max': '4500'
|
||||
}
|
||||
|
||||
# 默认系统配置
|
||||
self.config['SYSTEM'] = {
|
||||
'log_level': 'INFO',
|
||||
'max_cache_size': '10',
|
||||
'cache_timeout': '5.0'
|
||||
}
|
||||
|
||||
self.logger.info("创建默认配置")
|
||||
|
||||
def get_device_config(self, device_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备配置
|
||||
|
||||
Args:
|
||||
device_name: 设备名称 (camera, femtobolt, imu, pressure)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备配置字典
|
||||
"""
|
||||
if device_name in self._device_configs:
|
||||
return self._device_configs[device_name].copy()
|
||||
|
||||
config = {}
|
||||
|
||||
if device_name == 'camera':
|
||||
config = self._get_camera_config()
|
||||
elif device_name == 'femtobolt':
|
||||
config = self._get_femtobolt_config()
|
||||
elif device_name == 'imu':
|
||||
config = self._get_imu_config()
|
||||
elif device_name == 'pressure':
|
||||
config = self._get_pressure_config()
|
||||
else:
|
||||
self.logger.warning(f"未知设备类型: {device_name}")
|
||||
|
||||
# 缓存配置
|
||||
self._device_configs[device_name] = config
|
||||
return config.copy()
|
||||
|
||||
def _get_camera_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取相机配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 相机配置
|
||||
"""
|
||||
return {
|
||||
'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')
|
||||
}
|
||||
|
||||
def _get_femtobolt_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取FemtoBolt配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: FemtoBolt配置
|
||||
"""
|
||||
return {
|
||||
'color_resolution': self.config.get('FEMTOBOLT', 'color_resolution', fallback='1080P'),
|
||||
'depth_mode': self.config.get('FEMTOBOLT', 'depth_mode', fallback='NFOV_UNBINNED'),
|
||||
'fps': self.config.getint('FEMTOBOLT', 'fps', fallback=15),
|
||||
'depth_range_min': self.config.getint('FEMTOBOLT', 'depth_range_min', fallback=500),
|
||||
'depth_range_max': self.config.getint('FEMTOBOLT', 'depth_range_max', fallback=4500),
|
||||
'synchronized_images_only': self.config.getboolean('FEMTOBOLT', 'synchronized_images_only', fallback=False)
|
||||
}
|
||||
|
||||
def _get_imu_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取IMU配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: IMU配置
|
||||
"""
|
||||
return {
|
||||
'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),
|
||||
'timeout': self.config.getfloat('DEVICES', 'imu_timeout', fallback=1.0),
|
||||
'calibration_samples': self.config.getint('DEVICES', 'imu_calibration_samples', fallback=100),
|
||||
}
|
||||
|
||||
def _get_pressure_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取压力传感器配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 压力传感器配置
|
||||
"""
|
||||
return {
|
||||
'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),
|
||||
'timeout': self.config.getfloat('DEVICES', 'pressure_timeout', fallback=1.0),
|
||||
'calibration_samples': self.config.getint('DEVICES', 'pressure_calibration_samples', fallback=50)
|
||||
}
|
||||
|
||||
def get_system_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取系统配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 系统配置
|
||||
"""
|
||||
return {
|
||||
'log_level': self.config.get('SYSTEM', 'log_level', fallback='INFO'),
|
||||
'max_cache_size': self.config.getint('SYSTEM', 'max_cache_size', fallback=10),
|
||||
'cache_timeout': self.config.getfloat('SYSTEM', 'cache_timeout', fallback=5.0),
|
||||
'heartbeat_interval': self.config.getfloat('SYSTEM', 'heartbeat_interval', fallback=30.0)
|
||||
}
|
||||
|
||||
def get_config_value(self, section: str, key: str, fallback: Any = None) -> Any:
|
||||
"""
|
||||
获取配置值
|
||||
|
||||
Args:
|
||||
section: 配置段
|
||||
key: 配置键
|
||||
fallback: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 配置值
|
||||
"""
|
||||
try:
|
||||
if isinstance(fallback, bool):
|
||||
return self.config.getboolean(section, key, fallback=fallback)
|
||||
elif isinstance(fallback, int):
|
||||
return self.config.getint(section, key, fallback=fallback)
|
||||
elif isinstance(fallback, float):
|
||||
return self.config.getfloat(section, key, fallback=fallback)
|
||||
else:
|
||||
return self.config.get(section, key, fallback=fallback)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"获取配置值失败 [{section}][{key}]: {e}")
|
||||
return fallback
|
||||
|
||||
def set_config_value(self, section: str, key: str, value: Any):
|
||||
"""
|
||||
设置配置值
|
||||
|
||||
Args:
|
||||
section: 配置段
|
||||
key: 配置键
|
||||
value: 配置值
|
||||
"""
|
||||
if not self.config.has_section(section):
|
||||
self.config.add_section(section)
|
||||
|
||||
self.config.set(section, key, str(value))
|
||||
|
||||
# 清除缓存
|
||||
self._device_configs.clear()
|
||||
|
||||
def save_config(self):
|
||||
"""
|
||||
保存配置到文件
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
||||
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
self.config.write(f)
|
||||
|
||||
self.logger.info(f"配置已保存到: {self.config_path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存配置失败: {e}")
|
||||
|
||||
def reload_config(self):
|
||||
"""
|
||||
重新加载配置
|
||||
"""
|
||||
self._device_configs.clear()
|
||||
self._load_config()
|
||||
self.logger.info("配置已重新加载")
|
||||
|
||||
def get_all_sections(self) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
获取所有配置段
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, str]]: 所有配置
|
||||
"""
|
||||
result = {}
|
||||
for section_name in self.config.sections():
|
||||
result[section_name] = dict(self.config[section_name])
|
||||
return result
|
||||
|
||||
def validate_config(self) -> Dict[str, list]:
|
||||
"""
|
||||
验证配置有效性
|
||||
|
||||
Returns:
|
||||
Dict[str, list]: 验证结果,包含错误和警告
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# 验证必需的配置段
|
||||
required_sections = ['DEVICES', 'CAMERA', 'FEMTOBOLT', 'SYSTEM']
|
||||
for section in required_sections:
|
||||
if not self.config.has_section(section):
|
||||
errors.append(f"缺少必需的配置段: {section}")
|
||||
|
||||
# 验证设备配置
|
||||
try:
|
||||
imu_config = self.get_device_config('imu')
|
||||
if not imu_config.get('port'):
|
||||
warnings.append("IMU串口未配置")
|
||||
except Exception as e:
|
||||
errors.append(f"IMU配置验证失败: {e}")
|
||||
|
||||
return {
|
||||
'errors': errors,
|
||||
'warnings': warnings,
|
||||
'valid': len(errors) == 0
|
||||
}
|
||||
|
||||
# HTTP接口设备参数设置方法
|
||||
def set_imu_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置IMU设备配置
|
||||
|
||||
Args:
|
||||
config_data: IMU配置数据
|
||||
{
|
||||
'device_type': 'real' | 'mock',
|
||||
'port': 'COM6',
|
||||
'baudrate': 9600
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_device_type', config_data['device_type'])
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_baudrate', str(config_data['baudrate']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"IMU配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'IMU配置更新成功',
|
||||
'config': self.get_device_config('imu')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置IMU配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置IMU配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_pressure_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置压力板设备配置
|
||||
|
||||
Args:
|
||||
config_data: 压力板配置数据
|
||||
{
|
||||
'device_type': 'real' | 'mock',
|
||||
'use_mock': False,
|
||||
'port': 'COM5',
|
||||
'baudrate': 115200
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_device_type', config_data['device_type'])
|
||||
if 'use_mock' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_use_mock', str(config_data['use_mock']))
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_baudrate', str(config_data['baudrate']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"压力板配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '压力板配置更新成功',
|
||||
'config': self.get_device_config('pressure')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置压力板配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置压力板配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_camera_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置相机设备配置
|
||||
|
||||
Args:
|
||||
config_data: 相机配置数据
|
||||
{
|
||||
'device_index': 1,
|
||||
'width': 1280,
|
||||
'height': 720,
|
||||
'fps': 30
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
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']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': self.get_device_config('camera')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置相机配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置相机配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置FemtoBolt设备配置
|
||||
|
||||
Args:
|
||||
config_data: FemtoBolt配置数据
|
||||
{
|
||||
'color_resolution': '1080P',
|
||||
'depth_mode': 'NFOV_UNBINNED',
|
||||
'fps': 30,
|
||||
'depth_range_min': 1200,
|
||||
'depth_range_max': 1500
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'color_resolution' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'color_resolution', config_data['color_resolution'])
|
||||
if 'depth_mode' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_mode', config_data['depth_mode'])
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'fps', str(config_data['fps']))
|
||||
if 'depth_range_min' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_range_min', str(config_data['depth_range_min']))
|
||||
if 'depth_range_max' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_range_max', str(config_data['depth_range_max']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"FemtoBolt配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'FemtoBolt配置更新成功',
|
||||
'config': self.get_device_config('femtobolt')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置FemtoBolt配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置FemtoBolt配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def get_all_device_configs(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取所有设备配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 所有设备配置
|
||||
"""
|
||||
return {
|
||||
'imu': self.get_device_config('imu'),
|
||||
'pressure': self.get_device_config('pressure'),
|
||||
'camera': self.get_device_config('camera'),
|
||||
'femtobolt': self.get_device_config('femtobolt')
|
||||
}
|
244
backend/devices/utils/socket_manager.py
Normal file
244
backend/devices/utils/socket_manager.py
Normal file
@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Socket连接管理工具
|
||||
提供设备间Socket.IO通信的统一管理
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SocketManager:
|
||||
"""Socket连接管理器"""
|
||||
|
||||
def __init__(self, socketio=None):
|
||||
"""
|
||||
初始化Socket管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
"""
|
||||
self.socketio = socketio
|
||||
self.logger = logging.getLogger(f"{__name__}.SocketManager")
|
||||
self._namespaces = {}
|
||||
self._event_handlers = {}
|
||||
self._connection_stats = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def set_socketio(self, socketio):
|
||||
"""
|
||||
设置SocketIO实例
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
"""
|
||||
self.socketio = socketio
|
||||
|
||||
def register_namespace(self, namespace: str, device_name: str):
|
||||
"""
|
||||
注册设备命名空间
|
||||
|
||||
Args:
|
||||
namespace: 命名空间路径(如 '/camera')
|
||||
device_name: 设备名称
|
||||
"""
|
||||
with self._lock:
|
||||
self._namespaces[namespace] = {
|
||||
'device_name': device_name,
|
||||
'registered_at': datetime.now().isoformat(),
|
||||
'active': True
|
||||
}
|
||||
self._connection_stats[namespace] = {
|
||||
'messages_sent': 0,
|
||||
'messages_failed': 0,
|
||||
'last_message_time': None,
|
||||
'connected_clients': 0
|
||||
}
|
||||
|
||||
self.logger.info(f"注册设备命名空间: {namespace} -> {device_name}")
|
||||
|
||||
def unregister_namespace(self, namespace: str):
|
||||
"""
|
||||
注销设备命名空间
|
||||
|
||||
Args:
|
||||
namespace: 命名空间路径
|
||||
"""
|
||||
with self._lock:
|
||||
if namespace in self._namespaces:
|
||||
self._namespaces[namespace]['active'] = False
|
||||
self.logger.info(f"注销设备命名空间: {namespace}")
|
||||
|
||||
def emit_to_namespace(self, namespace: str, event: str, data: Any,
|
||||
callback: Optional[Callable] = None) -> bool:
|
||||
"""
|
||||
向指定命名空间发送数据
|
||||
|
||||
Args:
|
||||
namespace: 命名空间路径
|
||||
event: 事件名称
|
||||
data: 数据
|
||||
callback: 回调函数
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
if not self.socketio:
|
||||
self.logger.warning("SocketIO未初始化")
|
||||
return False
|
||||
|
||||
try:
|
||||
with self._lock:
|
||||
if namespace not in self._namespaces:
|
||||
self.logger.warning(f"命名空间未注册: {namespace}")
|
||||
return False
|
||||
|
||||
if not self._namespaces[namespace]['active']:
|
||||
self.logger.warning(f"命名空间已停用: {namespace}")
|
||||
return False
|
||||
|
||||
# 发送数据
|
||||
if callback:
|
||||
self.socketio.emit(event, data, namespace=namespace, callback=callback)
|
||||
else:
|
||||
self.socketio.emit(event, data, namespace=namespace)
|
||||
|
||||
# 更新统计
|
||||
with self._lock:
|
||||
stats = self._connection_stats[namespace]
|
||||
stats['messages_sent'] += 1
|
||||
stats['last_message_time'] = time.time()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送数据到 {namespace} 失败: {e}")
|
||||
with self._lock:
|
||||
if namespace in self._connection_stats:
|
||||
self._connection_stats[namespace]['messages_failed'] += 1
|
||||
return False
|
||||
|
||||
def broadcast_to_all(self, event: str, data: Any,
|
||||
exclude_namespaces: Optional[list] = None) -> Dict[str, bool]:
|
||||
"""
|
||||
向所有活跃命名空间广播数据
|
||||
|
||||
Args:
|
||||
event: 事件名称
|
||||
data: 数据
|
||||
exclude_namespaces: 排除的命名空间列表
|
||||
|
||||
Returns:
|
||||
Dict[str, bool]: 各命名空间的发送结果
|
||||
"""
|
||||
exclude_namespaces = exclude_namespaces or []
|
||||
results = {}
|
||||
|
||||
with self._lock:
|
||||
active_namespaces = [
|
||||
ns for ns, info in self._namespaces.items()
|
||||
if info['active'] and ns not in exclude_namespaces
|
||||
]
|
||||
|
||||
for namespace in active_namespaces:
|
||||
results[namespace] = self.emit_to_namespace(namespace, event, data)
|
||||
|
||||
return results
|
||||
|
||||
def register_event_handler(self, namespace: str, event: str, handler: Callable):
|
||||
"""
|
||||
注册事件处理器
|
||||
|
||||
Args:
|
||||
namespace: 命名空间
|
||||
event: 事件名称
|
||||
handler: 处理函数
|
||||
"""
|
||||
if not self.socketio:
|
||||
self.logger.warning("SocketIO未初始化,无法注册事件处理器")
|
||||
return
|
||||
|
||||
handler_key = f"{namespace}:{event}"
|
||||
self._event_handlers[handler_key] = handler
|
||||
|
||||
# 注册到SocketIO
|
||||
@self.socketio.on(event, namespace=namespace)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as e:
|
||||
self.logger.error(f"事件处理器 {handler_key} 执行失败: {e}")
|
||||
|
||||
self.logger.info(f"注册事件处理器: {handler_key}")
|
||||
|
||||
def get_namespace_stats(self, namespace: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取命名空间统计信息
|
||||
|
||||
Args:
|
||||
namespace: 命名空间
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 统计信息
|
||||
"""
|
||||
with self._lock:
|
||||
if namespace in self._connection_stats:
|
||||
stats = self._connection_stats[namespace].copy()
|
||||
if stats['last_message_time']:
|
||||
stats['last_message_ago'] = time.time() - stats['last_message_time']
|
||||
return stats
|
||||
return None
|
||||
|
||||
def get_all_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取所有统计信息
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 所有统计信息
|
||||
"""
|
||||
with self._lock:
|
||||
return {
|
||||
'namespaces': self._namespaces.copy(),
|
||||
'stats': {ns: self.get_namespace_stats(ns) for ns in self._namespaces},
|
||||
'total_namespaces': len(self._namespaces),
|
||||
'active_namespaces': len([ns for ns, info in self._namespaces.items() if info['active']])
|
||||
}
|
||||
|
||||
def cleanup_namespace(self, namespace: str):
|
||||
"""
|
||||
清理命名空间资源
|
||||
|
||||
Args:
|
||||
namespace: 命名空间
|
||||
"""
|
||||
with self._lock:
|
||||
if namespace in self._namespaces:
|
||||
del self._namespaces[namespace]
|
||||
if namespace in self._connection_stats:
|
||||
del self._connection_stats[namespace]
|
||||
|
||||
# 清理事件处理器
|
||||
handlers_to_remove = [
|
||||
key for key in self._event_handlers.keys()
|
||||
if key.startswith(f"{namespace}:")
|
||||
]
|
||||
for key in handlers_to_remove:
|
||||
del self._event_handlers[key]
|
||||
|
||||
self.logger.info(f"清理命名空间: {namespace}")
|
||||
|
||||
def cleanup_all(self):
|
||||
"""
|
||||
清理所有资源
|
||||
"""
|
||||
with self._lock:
|
||||
namespaces = list(self._namespaces.keys())
|
||||
|
||||
for namespace in namespaces:
|
||||
self.cleanup_namespace(namespace)
|
||||
|
||||
self.logger.info("清理所有Socket资源")
|
@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
#define __DLL_EXPORTS__
|
||||
|
||||
#ifdef __DLL_EXPORTS__
|
||||
#define DLLAPI __declspec(dllexport)
|
||||
#else
|
||||
#define DLLAPI __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
|
||||
typedef void* SM_HANDLE;
|
||||
|
||||
typedef struct _FPMS_DEVICE
|
||||
{
|
||||
uint16_t mn;
|
||||
std::string sn;
|
||||
uint16_t fwVersion;
|
||||
uint8_t protoVer;
|
||||
uint16_t pid;
|
||||
uint16_t vid;
|
||||
uint16_t rows;
|
||||
uint16_t cols;
|
||||
|
||||
} FPMS_DEVICE_T;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_init(int debugFlag);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_get_device_list(std::vector<FPMS_DEVICE_T>& gDevList);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_open(FPMS_DEVICE_T dev, SM_HANDLE& gHandle);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_read_frame(SM_HANDLE gHandle, uint16_t* frame);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_config_sensitivity(SM_HANDLE gHandle, uint8_t bWriteFlash, const uint8_t level);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_get_sensitivity(SM_HANDLE gHandle, uint8_t& level);
|
||||
|
||||
DLLAPI
|
||||
int WINAPI fpms_usb_close(SM_HANDLE gHandle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
1481
backend/main.py
1481
backend/main.py
File diff suppressed because it is too large
Load Diff
@ -1,78 +0,0 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
身体平衡评估系统 - 后端服务入口
|
||||
独立运行的exe版本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# 设置工作目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 如果是打包后的exe
|
||||
application_path = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 如果是开发环境
|
||||
application_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
os.chdir(application_path)
|
||||
sys.path.insert(0, application_path)
|
||||
|
||||
# 创建必要的目录
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
os.makedirs('data', exist_ok=True)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('logs/backend.log', encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
logger.info("启动身体平衡评估系统后端服务...")
|
||||
logger.info(f"工作目录: {os.getcwd()}")
|
||||
|
||||
# 导入并启动Flask应用
|
||||
from app_minimal import app, socketio, init_app
|
||||
|
||||
# 初始化应用
|
||||
init_app()
|
||||
|
||||
# 启动服务器
|
||||
logger.info("后端服务器启动在 http://localhost:5000")
|
||||
if socketio is not None:
|
||||
socketio.run(
|
||||
app,
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
debug=False
|
||||
)
|
||||
else:
|
||||
logger.info("使用基本Flask模式启动(无SocketIO)")
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
debug=False
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断,正在关闭服务器...")
|
||||
except Exception as e:
|
||||
logger.error(f"启动失败: {e}")
|
||||
input("按回车键退出...")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -19,7 +19,7 @@ camera_index = 0
|
||||
camera_width = 640
|
||||
camera_height = 480
|
||||
camera_fps = 30
|
||||
imu_port = COM8
|
||||
imu_port = COM3
|
||||
pressure_port = COM4
|
||||
|
||||
[DETECTION]
|
||||
@ -35,7 +35,7 @@ chart_dpi = 300
|
||||
export_format = csv
|
||||
|
||||
[SECURITY]
|
||||
secret_key = 4bfd3d60ddecef23973b64d761371610130034f23ce90dd6f405756ad2f89495
|
||||
secret_key = 739bbbe1b291cd966ef91d7752701958bf6d3e48c7b41e3872a7281d45403685
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
|
@ -3,10 +3,7 @@ import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
@ -19,6 +16,17 @@ api.interceptors.request.use(
|
||||
config.baseURL = 'http://localhost:5000'
|
||||
}
|
||||
|
||||
// 只为需要发送数据的请求设置Content-Type
|
||||
if (['post', 'put', 'patch'].includes(config.method.toLowerCase())) {
|
||||
config.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
if (config.method === 'get') {
|
||||
config.params = {
|
||||
|
@ -532,6 +532,10 @@ const patientInfo = ref({
|
||||
|
||||
// WebSocket相关变量
|
||||
let socket = null
|
||||
let cameraSocket = null
|
||||
let femtoboltSocket = null
|
||||
let imuSocket = null
|
||||
let pressureSocket = null
|
||||
let frameCount = 0
|
||||
|
||||
// 后端服务器地址配置
|
||||
@ -800,21 +804,28 @@ function connectWebSocket() {
|
||||
try {
|
||||
console.log('正在连接到', BACKEND_URL)
|
||||
|
||||
// 如果已有连接,先断开
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
socket = null
|
||||
}
|
||||
|
||||
// 创建Socket.IO连接
|
||||
socket = io(BACKEND_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 10000,
|
||||
forceNew: true
|
||||
forceNew: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000
|
||||
})
|
||||
|
||||
// 连接成功事件
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ WebSocket连接成功!Socket ID:', socket.id)
|
||||
isConnected.value = true
|
||||
// 连接成功后自动启动视频流、IMU流和压力传感器流
|
||||
startVideoStream()
|
||||
startIMUStreaming();
|
||||
startPressureStreaming();
|
||||
// 连接成功后自动启动设备数据推送
|
||||
startDeviceDataPush()
|
||||
//绘制头部仪表盘
|
||||
initchart()
|
||||
})
|
||||
@ -834,9 +845,7 @@ function connectWebSocket() {
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('⚠️ 连接断开:', reason)
|
||||
isConnected.value = false
|
||||
stopVideoStream()
|
||||
stopIMUStreaming()
|
||||
stopPressureStreaming()
|
||||
stopDeviceDataPush()
|
||||
// 如果正在录像,停止录像
|
||||
if (isRecording.value) {
|
||||
stopRecording()
|
||||
@ -844,60 +853,88 @@ function connectWebSocket() {
|
||||
}
|
||||
})
|
||||
|
||||
// 重连事件
|
||||
socket.on('reconnect', (attemptNumber) => {
|
||||
console.log('🔄 WebSocket重连成功,尝试次数:', attemptNumber)
|
||||
isConnected.value = true
|
||||
})
|
||||
|
||||
// 监听视频状态事件
|
||||
socket.on('video_status', (data) => {
|
||||
// console.log('📺 视频状态:', data)
|
||||
if (data.status == 'started') {
|
||||
videoStatus.value = '已连接'
|
||||
} else {
|
||||
videoStatus.value = '未连接'
|
||||
// 重连尝试事件
|
||||
socket.on('reconnect_attempt', (attemptNumber) => {
|
||||
console.log('🔄 正在尝试重连...', attemptNumber)
|
||||
})
|
||||
|
||||
// 重连失败事件
|
||||
socket.on('reconnect_failed', () => {
|
||||
console.error('❌ WebSocket重连失败')
|
||||
isConnected.value = false
|
||||
})
|
||||
|
||||
|
||||
// 监听测试状态事件
|
||||
socket.on('test_status', (data) => {
|
||||
console.log('📊 测试状态:', data)
|
||||
if (data.status === 'started') {
|
||||
console.log('✅ 设备数据推送已开始')
|
||||
} else if (data.status === 'stopped') {
|
||||
console.log('⏹️ 设备数据推送已停止')
|
||||
} else if (data.status === 'error') {
|
||||
console.error('❌ 设备数据推送错误:', data.message)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听视频帧数据
|
||||
socket.on('video_frame', (data) => {
|
||||
// 监听相机命名空间状态
|
||||
cameraSocket = io(BACKEND_URL + '/camera')
|
||||
cameraSocket.on('connect', () => {
|
||||
console.log('📹 相机命名空间连接成功')
|
||||
videoStatus.value = '已连接'
|
||||
})
|
||||
cameraSocket.on('disconnect', () => {
|
||||
console.log('📹 相机命名空间断开连接')
|
||||
videoStatus.value = '未连接'
|
||||
})
|
||||
cameraSocket.on('video_frame', (data) => {
|
||||
frameCount++
|
||||
displayFrame(data.image)
|
||||
})
|
||||
|
||||
// 监听深度相机帧数据
|
||||
socket.on('depth_camera_frame', (data) => {
|
||||
// 监听深度相机命名空间状态
|
||||
femtoboltSocket = io(BACKEND_URL + '/femtobolt')
|
||||
femtoboltSocket.on('connect', () => {
|
||||
console.log('🔍 深度相机命名空间连接成功')
|
||||
})
|
||||
femtoboltSocket.on('disconnect', () => {
|
||||
console.log('🔍 深度相机命名空间断开连接')
|
||||
})
|
||||
femtoboltSocket.on('depth_camera_frame', (data) => {
|
||||
displayDepthCameraFrame(data.image)
|
||||
})
|
||||
|
||||
|
||||
// 监听IMU状态事件
|
||||
socket.on('imu_status', (data) => {
|
||||
// console.log('📡 IMU状态:', data)
|
||||
if (data.status === 'success') {
|
||||
imuStatus.value = '已连接'
|
||||
} else {
|
||||
imuStatus.value = '未连接'
|
||||
}
|
||||
// 监听IMU命名空间状态
|
||||
imuSocket = io(BACKEND_URL + '/imu')
|
||||
imuSocket.on('connect', () => {
|
||||
console.log('🧭 IMU命名空间连接成功')
|
||||
imuStatus.value = '已连接'
|
||||
})
|
||||
|
||||
// 监听IMU头部姿态数据
|
||||
socket.on('imu_data', (data) => {
|
||||
// console.log('🎯 IMU头部姿态数据:', data)
|
||||
imuSocket.on('disconnect', () => {
|
||||
console.log('🧭 IMU命名空间断开连接')
|
||||
imuStatus.value = '未连接'
|
||||
})
|
||||
imuSocket.on('imu_data', (data) => {
|
||||
handleIMUData(data)
|
||||
})
|
||||
|
||||
// 监听压力传感器状态事件
|
||||
socket.on('pressure_status', (data) => {
|
||||
// console.log('📡 压力传感器状态:', data)
|
||||
if (data.status === 'success') {
|
||||
pressureStatus.value = '已连接'
|
||||
// ElMessage.success(data.message)
|
||||
} else {
|
||||
pressureStatus.value = '未连接'
|
||||
// ElMessage.error(data.message)
|
||||
}
|
||||
// 监听压力板命名空间状态
|
||||
pressureSocket = io(BACKEND_URL + '/pressure')
|
||||
pressureSocket.on('connect', () => {
|
||||
console.log('⚖️ 压力板命名空间连接成功')
|
||||
pressureStatus.value = '已连接'
|
||||
})
|
||||
|
||||
// 监听压力传感器足部压力数据
|
||||
socket.on('pressure_data', (data) => {
|
||||
// console.log('👣 压力传感器足部压力数据:', data)
|
||||
pressureSocket.on('disconnect', () => {
|
||||
console.log('⚖️ 压力板命名空间断开连接')
|
||||
pressureStatus.value = '未连接'
|
||||
})
|
||||
pressureSocket.on('pressure_data', (data) => {
|
||||
handlePressureData(data)
|
||||
})
|
||||
|
||||
@ -913,50 +950,76 @@ function connectWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 启动视频推流
|
||||
function startVideoStream() {
|
||||
// 启动设备数据推送
|
||||
function startDeviceDataPush() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🚀 发送start_video事件')
|
||||
|
||||
socket.emit('start_video_stream', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ start_video事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ start_video事件无确认响应')
|
||||
}
|
||||
})
|
||||
|
||||
frameCount = 0
|
||||
|
||||
// 设置超时检查
|
||||
setTimeout(() => {
|
||||
if (frameCount === 0) {
|
||||
console.log('⏰ 5秒后仍未收到视频帧,可能存在问题')
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
console.log('🚀 发送启动设备数据推送请求...')
|
||||
socket.emit('start_push_data')
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法启动视频流')
|
||||
console.warn('⚠️ Socket未连接,无法启动设备数据推送')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止视频流
|
||||
function stopVideoStream() {
|
||||
// 停止设备数据推送
|
||||
function stopDeviceDataPush() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🛑 发送stop_video事件')
|
||||
socket.emit('stop_video_stream', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ stop_video事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ stop_video事件无确认响应')
|
||||
}
|
||||
})
|
||||
console.log('🛑 发送停止设备数据推送请求...')
|
||||
socket.emit('stop_push_data')
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法停止视频流')
|
||||
console.warn('⚠️ Socket未连接,无法停止设备数据推送')
|
||||
}
|
||||
}
|
||||
|
||||
// 断开WebSocket连接函数
|
||||
function disconnectWebSocket() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('正在主动断开WebSocket连接...')
|
||||
|
||||
// 停止设备数据推送
|
||||
stopDeviceDataPush()
|
||||
|
||||
// 断开主连接
|
||||
socket.disconnect()
|
||||
socket = null
|
||||
isConnected.value = false
|
||||
|
||||
console.log('✅ 主WebSocket连接已断开')
|
||||
}
|
||||
|
||||
// 断开所有命名空间连接
|
||||
if (cameraSocket && cameraSocket.connected) {
|
||||
cameraSocket.disconnect()
|
||||
cameraSocket = null
|
||||
console.log('📹 相机命名空间连接已断开')
|
||||
}
|
||||
|
||||
if (femtoboltSocket && femtoboltSocket.connected) {
|
||||
femtoboltSocket.disconnect()
|
||||
femtoboltSocket = null
|
||||
console.log('🔍 深度相机命名空间连接已断开')
|
||||
}
|
||||
|
||||
if (imuSocket && imuSocket.connected) {
|
||||
imuSocket.disconnect()
|
||||
imuSocket = null
|
||||
console.log('🧭 IMU命名空间连接已断开')
|
||||
}
|
||||
|
||||
if (pressureSocket && pressureSocket.connected) {
|
||||
pressureSocket.disconnect()
|
||||
pressureSocket = null
|
||||
console.log('⚖️ 压力板命名空间连接已断开')
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
videoStatus.value = '未连接'
|
||||
imuStatus.value = '未连接'
|
||||
pressureStatus.value = '未连接'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 简单的帧显示函数
|
||||
function displayFrame(base64Image) {
|
||||
if (base64Image && base64Image.length > 0) {
|
||||
@ -1254,73 +1317,7 @@ function handlePressureData(data) {
|
||||
console.error('❌ 处理压力传感器数据失败:', error)
|
||||
}
|
||||
}
|
||||
// 启动IMU头部姿态数据推流
|
||||
function startIMUStreaming() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🚀 发送start_imu_streaming事件')
|
||||
socket.emit('start_imu_streaming', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ start_imu_streaming事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ start_imu_streaming事件无确认响应')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法启动IMU数据推流')
|
||||
ElMessage.error('WebSocket未连接,无法启动IMU数据推流')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止IMU头部姿态数据推流
|
||||
function stopIMUStreaming() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🛑 发送stop_imu_streaming事件')
|
||||
socket.emit('stop_imu_streaming', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ stop_imu_streaming事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ stop_imu_streaming事件无确认响应')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法停止IMU数据推流')
|
||||
ElMessage.error('WebSocket未连接,无法停止IMU数据推流')
|
||||
}
|
||||
}
|
||||
|
||||
// 启动压力传感器足部压力数据推流
|
||||
function startPressureStreaming() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🚀 发送start_pressure_streaming事件')
|
||||
socket.emit('start_pressure_streaming', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ start_pressure_streaming事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ start_pressure_streaming事件无确认响应')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法启动压力传感器数据推流')
|
||||
ElMessage.error('WebSocket未连接,无法启动压力传感器数据推流')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止压力传感器足部压力数据推流
|
||||
function stopPressureStreaming() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🛑 发送stop_pressure_streaming事件')
|
||||
socket.emit('stop_pressure_streaming', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ stop_pressure_streaming事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ stop_pressure_streaming事件无确认响应')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法停止压力传感器数据推流')
|
||||
ElMessage.error('WebSocket未连接,无法停止压力传感器数据推流')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDiagnosticInfo(status) {
|
||||
try {
|
||||
@ -1981,22 +1978,23 @@ const loadPatientInfo = async () => {
|
||||
}
|
||||
|
||||
// 处理页面关闭或刷新事件
|
||||
const handleBeforeUnload = () => {
|
||||
const handleBeforeUnload = (event) => {
|
||||
console.log('页面即将关闭,正在清理资源...')
|
||||
|
||||
// 停止检测(如果正在检测)
|
||||
if (isStart.value) {
|
||||
stopDetection()
|
||||
}
|
||||
// 停止视频播放
|
||||
stopVideoStream()
|
||||
// 停止IMU流
|
||||
stopIMUStreaming()
|
||||
// 停止压力传感器流
|
||||
stopPressureStreaming()
|
||||
|
||||
// 断开WebSocket连接
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
|
||||
// 停止录像(如果正在录像)
|
||||
if (isRecording.value) {
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
// 断开WebSocket连接
|
||||
disconnectWebSocket()
|
||||
|
||||
console.log('✅ 资源清理完成')
|
||||
}
|
||||
const creatorId = ref('')
|
||||
let rotationCharts = null;
|
||||
@ -2074,7 +2072,7 @@ onMounted(() => {
|
||||
// 加载患者信息
|
||||
loadPatientInfo()
|
||||
|
||||
// 组件挂载时连接WebSocket并自动开始推流
|
||||
// 页面加载时自动连接WebSocket
|
||||
connectWebSocket()
|
||||
|
||||
// 监听页面关闭或刷新事件
|
||||
@ -2098,15 +2096,10 @@ onUnmounted(() => {
|
||||
stopDetection()
|
||||
}
|
||||
|
||||
// 停止视频播放并断开连接
|
||||
stopVideoStream()
|
||||
|
||||
// 断开WebSocket连接
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
socket = null
|
||||
console.log('组件卸载时主动断开连接')
|
||||
}
|
||||
// 页面关闭时断开WebSocket连接
|
||||
disconnectWebSocket()
|
||||
|
||||
// 清理图表资源
|
||||
if (tiltCharts) {
|
||||
tiltCharts.dispose();
|
||||
}
|
||||
@ -2116,7 +2109,8 @@ onUnmounted(() => {
|
||||
if (pitchCharts) {
|
||||
pitchCharts.dispose();
|
||||
}
|
||||
// 移除页面关闭事件监听
|
||||
|
||||
// 移除页面关闭事件监听器
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
</script>
|
||||
|
@ -23,11 +23,11 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
port: 3002,
|
||||
host: '0.0.0.0',
|
||||
// 开发服务器配置
|
||||
cors: true,
|
||||
strictPort: true
|
||||
strictPort: false
|
||||
},
|
||||
// Electron环境变量
|
||||
define: {
|
||||
|
Loading…
Reference in New Issue
Block a user