BodyBalanceEvaluation/backend/devices/test/devicetest.py

595 lines
22 KiB
Python
Raw Normal View History

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