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 @@ - @@ -89,8 +87,8 @@
性别:
- - {{ selectedPatient.gender =='male'?'男':'女' }} + + {{ selectedPatient.gender }}
diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 14c8c890..105cce2e 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -10,7 +10,7 @@ color: #00CC00; margin-left:20px">检测中...
{{ patientInfo.name }}
-
{{ patientInfo.gender =='male'||patientInfo.gender =='男' ?'男':'女' }}
+
{{ patientInfo.gender }}
{{ calculateAge(patientInfo.birth_date) }}
结束监测 @@ -128,9 +128,7 @@ {{ headlist.rotation }}°
- - - +
@@ -261,7 +259,7 @@
{{ patientInfo.name }}
-
{{ patientInfo.gender =='male'||patientInfo.gender =='男' ?'男':'女' }}
+
{{ patientInfo.gender }}
{{ calculateAge(patientInfo.birth_date) }}
@@ -707,9 +705,9 @@ const formattedTime = computed(() => { return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }); const headlist = ref({ - rotation: '0', - tilt: '0', - pitch: '0' + rotation: 0, + tilt: 0, + pitch: 0 }) // 开始计时器 const startTimer = () => { @@ -987,13 +985,14 @@ function connectWebSocket() { if (!tempInfo.value.camera_frames) { tempInfo.value.camera_frames = {} } - if (devId === 'camera2') { - tempInfo.value.camera_frames['camera2'] = data - displayCameraFrameById('camera2', data.image) - } else { - // 默认 camera1(兼容旧逻辑 device_id 为空) + if (devId === 'camera1') { tempInfo.value.camera_frames['camera1'] = data + tempInfo.value.camera1_frame = data displayCameraFrameById('camera1', data.image) + } else if (devId === 'camera2') { + tempInfo.value.camera_frames['camera2'] = data + tempInfo.value.camera2_frame = data + displayCameraFrameById('camera2', data.image) } }) diff --git a/frontend/src/renderer/src/views/PatientCreate.vue b/frontend/src/renderer/src/views/PatientCreate.vue index 697ec8a9..b9b5bb3f 100644 --- a/frontend/src/renderer/src/views/PatientCreate.vue +++ b/frontend/src/renderer/src/views/PatientCreate.vue @@ -241,13 +241,9 @@ const validateForm = async () => { } const savePatient = async () => { - // 性别值映射:中文转英文 - const genderMap = { '男': 'male', '女': 'female' } - const genderValue = genderMap[patientForm.gender] || patientForm.gender - const patientData = { name: patientForm.name, - gender: genderValue, + gender: patientForm.gender, birth_date: patientForm.birth_date, height: parseFloat(patientForm.height) || null, weight: parseFloat(patientForm.weight) || null, diff --git a/frontend/src/renderer/src/views/PatientProfile.vue b/frontend/src/renderer/src/views/PatientProfile.vue index d1799280..4fcf19cb 100644 --- a/frontend/src/renderer/src/views/PatientProfile.vue +++ b/frontend/src/renderer/src/views/PatientProfile.vue @@ -1060,4 +1060,4 @@ async function handleDiagnosticInfo(status) { // 保存诊断信息 color: #00CC00; margin-left: 20px; } - \ No newline at end of file + diff --git a/frontend/src/renderer/src/views/model.vue b/frontend/src/renderer/src/views/model.vue index 727a6590..0ecc37af 100644 --- a/frontend/src/renderer/src/views/model.vue +++ b/frontend/src/renderer/src/views/model.vue @@ -1,14 +1,14 @@