diff --git a/backend/config.ini b/backend/config.ini
index e0b3b986..b5a47c44 100644
--- a/backend/config.ini
+++ b/backend/config.ini
@@ -19,7 +19,7 @@ path = D:/BodyCheck/file/
[CAMERA1]
enabled = True
-device_index = 0
+device_index = 2
width = 1280
height = 720
fps = 30
@@ -29,7 +29,7 @@ backend = directshow
[CAMERA2]
enabled = True
-device_index = 1
+device_index = 0
width = 1280
height = 720
fps = 30
@@ -50,12 +50,12 @@ synchronized_images_only = False
[DEVICES]
imu_enabled = True
-imu_device_type = ble
+imu_device_type = mock
imu_port = COM9
imu_mac_address = ef:3c:1a:0a:fe:02
imu_baudrate = 9600
pressure_enabled = True
-pressure_device_type = real
+pressure_device_type = mock
pressure_use_mock = False
pressure_port = COM5
pressure_baudrate = 115200
diff --git a/backend/devices/base_device.py b/backend/devices/base_device.py
index a5dadcba..b10763b2 100644
--- a/backend/devices/base_device.py
+++ b/backend/devices/base_device.py
@@ -311,7 +311,7 @@ class BaseDevice(ABC):
try:
# 检查硬件连接状态
hardware_connected = self.check_hardware_connection()
- self.logger.info(f"检测到设备 {self.device_name} 硬件连接状态: {hardware_connected} is_connected:{self.is_connected}")
+ # self.logger.info(f"检测到设备 {self.device_name} 硬件连接状态: {hardware_connected} is_connected:{self.is_connected}")
# 如果硬件断开但软件状态仍为连接,则更新状态
if not hardware_connected and self.is_connected:
self.logger.warning(f"检测到设备 {self.device_name} 硬件连接断开")
diff --git a/backend/devices/imu_manager.py b/backend/devices/imu_manager.py
index 037a2a32..d9073d17 100644
--- a/backend/devices/imu_manager.py
+++ b/backend/devices/imu_manager.py
@@ -4,201 +4,25 @@
IMU传感器管理器
负责IMU传感器的连接、校准和头部姿态数据采集
"""
-
import serial
import threading
import time
-import json
import numpy as np
-from typing import Optional, Dict, Any, List, Tuple
+from typing import Optional, Dict, Any
import logging
from collections import deque
-import struct
from datetime import datetime
import asyncio
-
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 # 串口端口号(如COM3、/dev/ttyUSB0等)
- self.baudrate = baudrate # 波特率,通常为9600或115200
- self.ser = None # 串口连接对象,初始为空
-
- # 数据缓冲区和校准相关
- self.buffer = bytearray() # 接收数据的缓冲区
- self.calibration_data = None # 校准数据,用于修正传感器偏差
-
- # 头部姿态偏移量,用于校准初始姿态
- self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
-
- # 最后一次读取的IMU数据
- self.last_data = {
- 'roll': 0.0, # 横滚角(绕X轴旋转)
- 'pitch': 0.0, # 俯仰角(绕Y轴旋转)
- 'yaw': 0.0, # 偏航角(绕Z轴旋转)
- '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']
- 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()
- angle=head_pose['rotation'] - self.head_pose_offset['rotation']
- # 减去基准值(零点偏移)
- head_pose['rotation'] = round(((angle + 180) % 360) - 180, 1)
- 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,1)
- pitch = -round(pitchl / 32768.0 * k_angle,1)
- yaw = -round(yawl / 32768.0 * k_angle,1)
- temp = temp / 100.0
- self.last_data = {
- 'roll': roll,
- 'pitch': pitch,
- 'yaw': yaw,
- 'temperature': temp
- }
- # print(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 BleIMUDevice:
"""蓝牙IMU设备,基于bleak实现,解析逻辑参考tests/testblueimu.py"""
def __init__(self, mac_address: str):
@@ -444,7 +268,102 @@ class BleIMUDevice:
@property
def connected(self) -> bool:
return self._connected
-
+
+class MockIMUDevice:
+ def __init__(self):
+ self.running = False
+ self.thread = None
+ self._lock = threading.Lock()
+ self._connected = False
+ 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
+ }
+ self._phase = 0.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()
+ angle = head_pose['rotation'] - self.head_pose_offset['rotation']
+ head_pose['rotation'] = round(((angle + 180) % 360) - 180, 1)
+ head_pose['tilt'] = round(head_pose['tilt'] - self.head_pose_offset['tilt'], 1)
+ head_pose['pitch'] = round(head_pose['pitch'] - self.head_pose_offset['pitch'], 1)
+ calibrated_data['head_pose'] = head_pose
+ return calibrated_data
+
+ def start(self):
+ if self.running:
+ return
+ self.running = True
+ self._connected = True
+ self.thread = threading.Thread(target=self._run_loop, daemon=True)
+ self.thread.start()
+
+ def stop(self):
+ self.running = False
+ try:
+ if self.thread and self.thread.is_alive():
+ self.thread.join(timeout=2.0)
+ except Exception:
+ pass
+ self._connected = False
+
+ def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]:
+ with self._lock:
+ raw = {
+ 'head_pose': {
+ 'rotation': self.last_data['yaw'],
+ 'tilt': self.last_data['pitch'],
+ 'pitch': self.last_data['roll']
+ },
+ 'temperature': self.last_data.get('temperature', 25.0),
+ 'timestamp': datetime.now().isoformat()
+ }
+ return self.apply_calibration(raw) if apply_calibration else raw
+
+ def _run_loop(self):
+ # 模拟IMU设备的后台数据生成线程(约60Hz),输出平滑变化的姿态与温度
+ import math
+ try:
+ while self.running:
+ # 相位累加,用于驱动正弦/余弦波形
+ self._phase += 0.05
+ # 航向角(yaw,左右旋转),幅度约±30°
+ yaw = math.sin(self._phase * 0.6) * 30.0
+ # yaw = 0
+ # 俯仰角(pitch,上下点头),幅度约±10°
+ pitch = math.sin(self._phase) * 5.0
+ # pitch = 0
+ # 横滚角(roll,左右侧倾),幅度约±8°
+ roll = math.cos(self._phase * 0.8) * 5.0
+ # roll = 0
+ # 写入最新数据,使用1位或2位小数以模拟设备精度
+ with self._lock:
+ self.last_data['yaw'] = round(yaw, 1)
+ self.last_data['pitch'] = round(pitch, 1)
+ self.last_data['roll'] = round(roll, 1)
+ # 温度模拟:以25℃为基准,叠加±0.5℃的轻微波动
+ self.last_data['temperature'] = round(25.0 + math.sin(self._phase * 0.2) * 0.5, 2)
+ # 控制输出频率为约60Hz
+ time.sleep(1.0 / 30.0)
+ except Exception:
+ # 忽略模拟线程异常,避免影响主流程
+ pass
+
+ @property
+ def connected(self) -> bool:
+ return self._connected
class IMUManager(BaseDevice):
"""IMU传感器管理器"""
@@ -470,7 +389,6 @@ class IMUManager(BaseDevice):
self.port = config.get('port', 'COM7')
self.baudrate = config.get('baudrate', 9600)
self.device_type = config.get('device_type', 'ble') # 'real' | 'mock' | 'ble'
- self.use_mock = config.get('use_mock', False) # 保持向后兼容
self.mac_address = config.get('mac_address', '')
# IMU设备实例
self.imu_device = None
@@ -516,29 +434,17 @@ class IMUManager(BaseDevice):
self.imu_device = BleIMUDevice(self.mac_address)
self.imu_device.start()
# 使用set_connected方法来正确启动连接监控线程
- self.set_connected(True)
- elif self.device_type == 'real' or (self.device_type != 'mock' and not self.use_mock):
- self.logger.info(f"使用真实IMU设备 - 端口: {self.port}, 波特率: {self.baudrate}")
- self.imu_device = RealIMUDevice(self.port, self.baudrate)
-
- # 检查真实设备是否连接成功
- if self.imu_device.ser is None:
- self.logger.error(f"IMU设备连接失败: 无法打开串口 {self.port}")
- self.is_connected = False
- self.imu_device = None
- return False
- # 使用set_connected方法来正确启动连接监控线程
- self.set_connected(True)
+ self.set_connected(True)
else:
self.logger.info("使用模拟IMU设备")
self.imu_device = MockIMUDevice()
+ self.imu_device.start()
# 使用set_connected方法来正确启动连接监控线程
self.set_connected(True)
self._device_info.update({
'port': self.port,
'baudrate': self.baudrate,
- 'use_mock': self.use_mock,
'mac_address': self.mac_address,
})
@@ -696,13 +602,6 @@ class IMUManager(BaseDevice):
data = self.imu_device.read_data(apply_calibration=True)
if data:
- # 缓存数据
- # self.data_buffer.append(data)
- # self.last_valid_data = data
-
- # 更新心跳时间,防止连接监控线程判定为超时
- # self.update_heartbeat()
-
# 发送数据到前端
if self._socketio:
self._socketio.emit('imu_data', data, namespace='/devices')
@@ -792,7 +691,6 @@ class IMUManager(BaseDevice):
self.port = config.get('port', 'COM7')
self.baudrate = config.get('baudrate', 9600)
self.device_type = config.get('device_type', 'mock')
- self.use_mock = config.get('use_mock', False)
self.mac_address = config.get('mac_address', '')
# 更新数据缓存队列大小
@@ -880,4 +778,4 @@ class IMUManager(BaseDevice):
self.logger.info("IMU资源清理完成")
except Exception as e:
- self.logger.error(f"清理IMU资源失败: {e}")
\ No newline at end of file
+ self.logger.error(f"清理IMU资源失败: {e}")
diff --git a/backend/devices/imu_manager_usb_bak.py b/backend/devices/imu_manager_usb_bak.py
deleted file mode 100644
index 01c5ae50..00000000
--- a/backend/devices/imu_manager_usb_bak.py
+++ /dev/null
@@ -1,649 +0,0 @@
-#!/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()
- angle=head_pose['rotation'] - self.head_pose_offset['rotation']
- # 减去基准值(零点偏移)
- head_pose['rotation'] = ((angle + 180) % 360) - 180
- 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
- }
- # print(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设备...")
-
- # 使用构造函数中已加载的配置,避免并发读取配置文件
- self.logger.info(f"使用已加载配置: port={self.port}, baudrate={self.baudrate}, device_type={self.device_type}")
-
- # 根据配置选择真实设备或模拟设备
- # 优先使用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)
-
- # 检查真实设备是否连接成功
- if self.imu_device.ser is None:
- self.logger.error(f"IMU设备连接失败: 无法打开串口 {self.port}")
- self.is_connected = False
- self.imu_device = None
- return False
- 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快速零点校准...')
-
- # 直接读取一次原始数据作为校准偏移量
- raw_data = self.imu_device.read_data(apply_calibration=False)
- if not raw_data or 'head_pose' not in raw_data:
- return {'status': 'error', 'error': '无法读取IMU原始数据'}
-
- # 使用当前姿态作为零点偏移
- self.head_pose_offset = {
- 'rotation': raw_data['head_pose']['rotation'],
- 'tilt': raw_data['head_pose']['tilt'],
- 'pitch': raw_data['head_pose']['pitch']
- }
-
- # 应用校准到设备
- 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
- }
-
- 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='/devices')
-
- # 更新统计
- 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 reload_config(self) -> bool:
- """
- 重新加载设备配置
-
- Returns:
- bool: 重新加载是否成功
- """
- try:
- self.logger.info("正在重新加载IMU配置...")
-
-
-
- # 获取最新配置
- config = self.config_manager.get_device_config('imu')
-
- # 更新配置属性
- self.port = config.get('port', 'COM7')
- self.baudrate = config.get('baudrate', 9600)
- self.device_type = config.get('device_type', 'mock')
- self.use_mock = config.get('use_mock', False)
-
- # 更新数据缓存队列大小
- buffer_size = config.get('buffer_size', 100)
- if buffer_size != self.data_buffer.maxlen:
- # 保存当前数据
- current_data = list(self.data_buffer)
- # 创建新缓冲区
- self.data_buffer = deque(maxlen=buffer_size)
- # 恢复数据(保留最新的数据)
- for data in current_data[-buffer_size:]:
- self.data_buffer.append(data)
-
- self.logger.info(f"IMU配置重新加载成功 - 端口: {self.port}, 波特率: {self.baudrate}, 设备类型: {self.device_type}")
- return True
-
- except Exception as e:
- self.logger.error(f"重新加载IMU配置失败: {e}")
- return False
-
- 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}")
\ No newline at end of file
diff --git a/backend/devices/pressure_manager.py b/backend/devices/pressure_manager.py
index 68755cb4..11a9465d 100644
--- a/backend/devices/pressure_manager.py
+++ b/backend/devices/pressure_manager.py
@@ -463,7 +463,186 @@ class RealPressureDevice:
"""析构函数,确保资源清理"""
self.close()
+class MockPressureDevice:
+ def __init__(self, rows: int = 32, cols: int = 32, seed: Optional[int] = None):
+ self.rows = rows
+ self.cols = cols
+ self.is_connected = True
+ self._rng = np.random.RandomState(seed if seed is not None else (int(time.time()) & 0xFFFF))
+ self._phase = 0.0
+ def read_data(self) -> Dict[str, Any]:
+ try:
+ if not self.is_connected:
+ return self._get_empty_data()
+ raw_data = self._generate_raw_frame()
+ zones = self._calculate_foot_pressure_zones(raw_data)
+ image_base64 = self._generate_pressure_image(
+ zones['left_front'], zones['left_rear'], zones['right_front'], zones['right_rear'], raw_data
+ )
+ return {
+ 'foot_pressure': {
+ 'left_front': round(zones['left_front'], 2),
+ 'left_rear': round(zones['left_rear'], 2),
+ 'right_front': round(zones['right_front'], 2),
+ 'right_rear': round(zones['right_rear'], 2),
+ 'left_total': round(zones['left_total'], 2),
+ 'right_total': round(zones['right_total'], 2)
+ },
+ 'pressure_image': image_base64,
+ 'timestamp': datetime.now().isoformat()
+ }
+ except Exception:
+ return self._get_empty_data()
+
+ def _generate_raw_frame(self) -> np.ndarray:
+ rows, cols = self.rows, self.cols
+ gy, gx = np.meshgrid(np.arange(rows), np.arange(cols), indexing='ij')
+ gy = gy.astype(np.float64)
+ gx = gx.astype(np.float64)
+ self._phase += 0.15
+ lf_cy = rows * 0.30 + 0.6 * np.sin(self._phase)
+ lf_cx = cols * 0.25 + 0.3 * np.cos(self._phase * 0.7)
+ lr_cy = rows * 0.75 + 0.5 * np.sin(self._phase * 0.8)
+ lr_cx = cols * 0.25 + 0.2 * np.sin(self._phase * 0.6)
+ rf_cy = rows * 0.30 + 0.6 * np.cos(self._phase * 0.9)
+ rf_cx = cols * 0.75 + 0.3 * np.sin(self._phase)
+ rr_cy = rows * 0.75 + 0.5 * np.cos(self._phase * 0.5)
+ rr_cx = cols * 0.75 + 0.2 * np.cos(self._phase * 0.4)
+ sy = rows * 0.10
+ sx = cols * 0.10
+ def gauss(cy: float, cx: float, amp: float) -> np.ndarray:
+ return amp * np.exp(-(((gy - cy) ** 2) / (2 * sy * sy) + ((gx - cx) ** 2) / (2 * sx * sx)))
+ lf = gauss(lf_cy, lf_cx, 300.0 + 120.0 * self._rng.rand())
+ lr = gauss(lr_cy, lr_cx, 280.0 + 120.0 * self._rng.rand())
+ rf = gauss(rf_cy, rf_cx, 300.0 + 120.0 * self._rng.rand())
+ rr = gauss(rr_cy, rr_cx, 280.0 + 120.0 * self._rng.rand())
+ base = lf + lr + rf + rr
+ noise = self._rng.normal(0.0, 5.0, size=(rows, cols))
+ frame = base + noise
+ frame = np.clip(frame, 0, 65535).astype(np.uint16)
+ return frame
+
+ def _calculate_foot_pressure_zones(self, raw_data: np.ndarray) -> Dict[str, Any]:
+ try:
+ 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
+ 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 / total_abs * 100) if total_abs > 0 else 0)
+ left_rear_pct = float((left_rear / total_abs * 100) if total_abs > 0 else 0)
+ right_front_pct = float((right_front / total_abs * 100) if total_abs > 0 else 0)
+ right_rear_pct = float((right_rear / total_abs * 100) if 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:
+ 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: float, left_rear: float, right_front: float, right_rear: float, raw_data: Optional[np.ndarray] = None) -> str:
+ 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:
+ return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
+
+ def _generate_heatmap_image(self, raw_data: np.ndarray) -> str:
+ try:
+ vmin = 10
+ dmin, dmax = np.min(raw_data), np.max(raw_data)
+ norm = np.clip((raw_data - dmin) / max(dmax - dmin, 1) * 255, 0, 255).astype(np.uint8)
+ heatmap = cv2.applyColorMap(norm, cv2.COLORMAP_JET)
+ heatmap[raw_data <= vmin] = (0, 0, 0)
+ rows, cols = raw_data.shape
+ heatmap = cv2.resize(heatmap, (cols * 4, rows * 4), interpolation=cv2.INTER_NEAREST)
+ heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
+ from PIL import Image
+ buffer = BytesIO()
+ Image.fromarray(heatmap_rgb).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:
+ return self._generate_simple_pressure_image(0, 0, 0, 0)
+
+ def _generate_simple_pressure_image(self, left_front: float, left_rear: float, right_front: float, right_rear: float) -> str:
+ try:
+ import matplotlib
+ matplotlib.use('Agg')
+ import matplotlib.pyplot as plt
+ import matplotlib.patches as patches
+ 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')
+ m = max(left_front, left_rear, right_front, right_rear)
+ if m > 0:
+ lf_c = plt.cm.Reds(left_front / m)
+ lr_c = plt.cm.Reds(left_rear / m)
+ rf_c = plt.cm.Reds(right_front / m)
+ rr_c = plt.cm.Reds(right_rear / m)
+ else:
+ lf_c = lr_c = rf_c = rr_c = 'lightgray'
+ ax.add_patch(patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=lf_c))
+ ax.add_patch(patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=lr_c))
+ ax.add_patch(patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=rf_c))
+ ax.add_patch(patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=rr_c))
+ 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')
+ fig.patch.set_facecolor('black')
+ ax.set_facecolor('black')
+ 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:
+ return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
+
+ def _get_empty_data(self) -> Dict[str, Any]:
+ 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):
+ self.is_connected = False
class PressureManager(BaseDevice):
"""压力板管理器"""
@@ -899,4 +1078,4 @@ class PressureManager(BaseDevice):
self.disconnect()
self.logger.info("压力板设备资源清理完成")
except Exception as e:
- self.logger.error(f"压力板设备资源清理失败: {e}")
\ No newline at end of file
+ self.logger.error(f"压力板设备资源清理失败: {e}")
diff --git a/frontend/src/renderer/package-lock.json b/frontend/src/renderer/package-lock.json
index f520bf5c..173ed90c 100644
--- a/frontend/src/renderer/package-lock.json
+++ b/frontend/src/renderer/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "body-balance-renderer",
- "version": "1.0.0",
+ "name": "body-balance-system",
+ "version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "body-balance-renderer",
- "version": "1.0.0",
+ "name": "body-balance-system",
+ "version": "1.5.0",
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.0",
@@ -15,6 +15,7 @@
"html2canvas": "^1.4.1",
"pinia": "^2.1.6",
"socket.io-client": "^4.7.2",
+ "three": "^0.160.0",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.4"
@@ -5790,6 +5791,12 @@
"utrie": "^1.0.2"
}
},
+ "node_modules/three": {
+ "version": "0.160.1",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
+ "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
+ "license": "MIT"
+ },
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
diff --git a/frontend/src/renderer/package.json b/frontend/src/renderer/package.json
index a03346ec..b4ea5d44 100644
--- a/frontend/src/renderer/package.json
+++ b/frontend/src/renderer/package.json
@@ -20,6 +20,7 @@
"echarts": "^5.4.3",
"element-plus": "^2.3.9",
"html2canvas": "^1.4.1",
+ "three": "^0.160.0",
"pinia": "^2.1.6",
"socket.io-client": "^4.7.2",
"vue": "^3.3.4",
diff --git a/frontend/src/renderer/src/assets/glb/女.glb b/frontend/src/renderer/src/assets/glb/女.glb
new file mode 100644
index 00000000..cf9d819a
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/女.glb differ
diff --git a/frontend/src/renderer/src/assets/glb/女人光头.glb b/frontend/src/renderer/src/assets/glb/女人光头.glb
new file mode 100644
index 00000000..1c0d110b
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/女人光头.glb differ
diff --git a/frontend/src/renderer/src/assets/glb/男.glb b/frontend/src/renderer/src/assets/glb/男.glb
new file mode 100644
index 00000000..d953c5c4
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/男.glb differ
diff --git a/frontend/src/renderer/src/assets/glb/男人光头.glb b/frontend/src/renderer/src/assets/glb/男人光头.glb
new file mode 100644
index 00000000..a2739a5e
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/男人光头.glb differ
diff --git a/frontend/src/renderer/src/views/Dashboard.vue b/frontend/src/renderer/src/views/Dashboard.vue
index c253e007..7ed94a00 100644
--- a/frontend/src/renderer/src/views/Dashboard.vue
+++ b/frontend/src/renderer/src/views/Dashboard.vue
@@ -30,10 +30,8 @@