BodyBalanceEvaluation/backend/devices/test/devicetest.py

595 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()