优化系统框架结构,设备独立管理,实现并行数据推送

This commit is contained in:
root 2025-08-17 12:48:10 +08:00
parent 1db5a5cb33
commit 7053427249
26 changed files with 60457 additions and 692 deletions

View File

@ -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

View 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'

View 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})>"

View 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}")

View 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)

View 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()
```

View 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()

View 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}")

View 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}")

View 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}")

File diff suppressed because it is too large Load Diff

View 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()

View 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"></div>
</div>
<div class="gauge-container">
<div id="tiltGauge" class="gauge"></div>
<div class="gauge-label">倾斜角</div>
<div id="tiltValue" class="data-value"></div>
</div>
<div class="gauge-container">
<div id="pitchGauge" class="gauge"></div>
<div class="gauge-label">俯仰角</div>
<div id="pitchValue" class="data-value"></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>

View 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()

View 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()

View 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'
]

View 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

View 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')
}

View 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资源")

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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

View File

@ -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 = {

View File

@ -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>

View File

@ -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: {