BodyBalanceEvaluation/backend/devices/test/templates/deviceTest.html

912 lines
33 KiB
HTML
Raw Normal View History

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