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

912 lines
33 KiB
HTML
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.

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