增加了两个相机的支持。
This commit is contained in:
parent
c974350345
commit
96ba7c098a
@ -1,15 +0,0 @@
|
||||
{
|
||||
"product": "BodyBalanceEvaluation",
|
||||
"version": "1.0.0",
|
||||
"machine_id": "W10-D13710C7BD317C29",
|
||||
"platform": "Windows",
|
||||
"request_time": "2025-11-04T05:35:19.472181+00:00",
|
||||
"hardware_info": {
|
||||
"system": "Windows",
|
||||
"machine": "AMD64",
|
||||
"processor": "Intel64 Family 6 Model 165 Stepping 2, GenuineIntel",
|
||||
"node": "MSI"
|
||||
},
|
||||
"company_name": "北京天宏博科技有限公司",
|
||||
"contact_info": "thb@163.com"
|
||||
}
|
||||
@ -17,7 +17,7 @@ max_backups = 7
|
||||
[FILEPATH]
|
||||
path = D:/BodyCheck/file/
|
||||
|
||||
[CAMERA]
|
||||
[CAMERA1]
|
||||
enabled = True
|
||||
device_index = 0
|
||||
width = 1280
|
||||
@ -27,6 +27,16 @@ buffer_size = 1
|
||||
fourcc = MJPG
|
||||
backend = directshow
|
||||
|
||||
[CAMERA2]
|
||||
enabled = True
|
||||
device_index = 1
|
||||
width = 1280
|
||||
height = 720
|
||||
fps = 30
|
||||
buffer_size = 1
|
||||
fourcc = MJPG
|
||||
backend = directshow
|
||||
|
||||
[FEMTOBOLT]
|
||||
enabled = True
|
||||
algorithm_type = plt
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -26,34 +26,49 @@ except ImportError:
|
||||
class CameraManager(BaseDevice):
|
||||
"""普通相机管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None,
|
||||
device_name: str = "camera1",
|
||||
instance_config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
初始化相机管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
device_name: 设备名称(仅支持 'camera1' | 'camera2')
|
||||
instance_config: 覆盖默认配置的实例级配置(如 device_index、分辨率、fps 等)
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
config = self.config_manager.get_device_config('camera')
|
||||
# 校验设备名,仅允许 camera1/camera2
|
||||
if device_name not in ('camera1', 'camera2'):
|
||||
raise ValueError(f"不支持的设备名: {device_name},仅支持 'camera1'/'camera2'")
|
||||
# 根据设备名选择配置源:'camera1' 使用 [CAMERA1];'camera2' 使用 [CAMERA2]
|
||||
base_key = 'camera1' if device_name == 'camera1' else 'camera2'
|
||||
base_config = self.config_manager.get_device_config(base_key)
|
||||
# 合并实例覆盖配置
|
||||
if instance_config:
|
||||
try:
|
||||
base_config = {**base_config, **instance_config}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
super().__init__("camera", config)
|
||||
super().__init__(device_name, base_config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 相机相关属性
|
||||
self.cap = None
|
||||
self.device_index = config.get('device_index', 0)
|
||||
self.width = config.get('width', 1280)
|
||||
self.height = config.get('height', 720)
|
||||
self.fps = config.get('fps', 30)
|
||||
self.buffer_size = config.get('buffer_size', 1)
|
||||
self.fourcc = config.get('fourcc', 'MJPG')
|
||||
self.device_index = base_config.get('device_index', 0)
|
||||
self.width = base_config.get('width', 1280)
|
||||
self.height = base_config.get('height', 720)
|
||||
self.fps = base_config.get('fps', 30)
|
||||
self.buffer_size = base_config.get('buffer_size', 1)
|
||||
self.fourcc = base_config.get('fourcc', 'MJPG')
|
||||
|
||||
# OpenCV后端配置 (DirectShow性能最佳)
|
||||
backend_name = config.get('backend', 'directshow').lower()
|
||||
backend_name = base_config.get('backend', 'directshow').lower()
|
||||
self.backend_map = {
|
||||
'directshow': cv2.CAP_DSHOW,
|
||||
'dshow': cv2.CAP_DSHOW,
|
||||
@ -64,12 +79,12 @@ class CameraManager(BaseDevice):
|
||||
self.backend_name = backend_name
|
||||
|
||||
# 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输)
|
||||
self._tx_max_width = int(config.get('tx_max_width', 1920))
|
||||
self._tx_max_width = int(base_config.get('tx_max_width', 1920))
|
||||
|
||||
# 流控制
|
||||
self.streaming_thread = None
|
||||
# 减小缓存长度,保留最近2帧即可,避免累计占用
|
||||
self.frame_cache = queue.Queue(maxsize=int(config.get('frame_cache_len', 2)))
|
||||
self.frame_cache = queue.Queue(maxsize=int(base_config.get('frame_cache_len', 2)))
|
||||
self.last_frame = None
|
||||
self.frame_count = 0
|
||||
self.dropped_frames = 0
|
||||
@ -80,13 +95,14 @@ class CameraManager(BaseDevice):
|
||||
self.actual_fps = 0
|
||||
|
||||
# 重连与断连检测机制(-1 表示无限重连)
|
||||
self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', -1))
|
||||
self.reconnect_delay = float(config.get('reconnect_delay', 2.0))
|
||||
self.read_fail_threshold = int(config.get('read_fail_threshold', 30))
|
||||
self.max_reconnect_attempts = int(base_config.get('max_reconnect_attempts', -1))
|
||||
self.reconnect_delay = float(base_config.get('reconnect_delay', 2.0))
|
||||
self.read_fail_threshold = int(base_config.get('read_fail_threshold', 30))
|
||||
self._last_connected_state = None
|
||||
|
||||
# 设备标识和性能统计
|
||||
self.device_id = f"camera_{self.device_index}"
|
||||
# 使用设备名作为ID,便于前端区分
|
||||
self.device_id = device_name
|
||||
self.performance_stats = {
|
||||
'frames_processed': 0,
|
||||
'actual_fps': 0,
|
||||
@ -373,6 +389,9 @@ class CameraManager(BaseDevice):
|
||||
|
||||
total_config_time = (time.time() - config_start) * 1000
|
||||
|
||||
# 若未进行性能优化,确保变量存在
|
||||
optimization_time = locals().get('optimization_time', 0.0)
|
||||
|
||||
self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
|
||||
self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
|
||||
self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
|
||||
@ -852,8 +871,9 @@ class CameraManager(BaseDevice):
|
||||
|
||||
|
||||
|
||||
# 获取最新配置
|
||||
config = self.config_manager.get_device_config('camera')
|
||||
# 获取最新配置(按设备名映射,已限制为 camera1/camera2)
|
||||
key = self.device_name
|
||||
config = self.config_manager.get_device_config(key)
|
||||
|
||||
# 更新配置属性
|
||||
self.device_index = config.get('device_index', 0)
|
||||
@ -880,8 +900,8 @@ class CameraManager(BaseDevice):
|
||||
# 创建新队列
|
||||
self.frame_cache = queue.Queue(maxsize=frame_cache_len)
|
||||
|
||||
# 更新设备信息
|
||||
self.device_id = f"camera_{self.device_index}"
|
||||
# 更新设备信息(设备ID直接使用设备名)
|
||||
self.device_id = self.device_name
|
||||
|
||||
self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}")
|
||||
return True
|
||||
|
||||
@ -113,7 +113,7 @@ class DeviceCoordinator:
|
||||
# 注册Socket.IO命名空间
|
||||
self._register_namespaces()
|
||||
|
||||
# 初始化设备
|
||||
# 初始化设备(失败则降级继续)
|
||||
if not self._initialize_devices():
|
||||
self.logger.warning("设备初始化失败,将以降级模式继续运行")
|
||||
|
||||
@ -163,10 +163,12 @@ class DeviceCoordinator:
|
||||
future = self.executor.submit(self._init_femtobolt)
|
||||
futures.append(('femtobolt', future))
|
||||
|
||||
# 普通相机
|
||||
if self.device_configs.get('camera', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_camera)
|
||||
futures.append(('camera', future))
|
||||
# 普通相机:初始化两个实例(camera1 与 camera2)
|
||||
# camera1 使用 [CAMERA1] 配置;camera2 使用 [CAMERA2](若不存在则回退为 device_index+1)
|
||||
if self.device_configs.get('camera1', {}).get('enabled', True):
|
||||
futures.append(('camera1', self.executor.submit(self._init_camera_by_name, 'camera1', 'CAMERA1')))
|
||||
if self.device_configs.get('camera2', {}).get('enabled', True):
|
||||
futures.append(('camera2', self.executor.submit(self._init_camera_by_name, 'camera2', 'CAMERA2')))
|
||||
|
||||
# IMU传感器
|
||||
if self.device_configs.get('imu', {}).get('enabled', False):
|
||||
@ -205,21 +207,72 @@ class DeviceCoordinator:
|
||||
self.logger.error(f"设备初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_camera(self) -> bool:
|
||||
def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool:
|
||||
"""
|
||||
初始化普通相机
|
||||
按名称初始化相机,支持 camera1/camera2 并覆盖配置段
|
||||
|
||||
Args:
|
||||
device_name: 设备名称(如 'camera1'、'camera2')
|
||||
section: 配置段名称('CAMERA1' 或 'CAMERA2')
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
camera = CameraManager(self.socketio, self.config_manager)
|
||||
self.devices['camera'] = camera
|
||||
# 构造实例覆盖配置:优先读取目标配置段,否则回退到 [CAMERA1]
|
||||
cfg = {}
|
||||
parser = getattr(self.config_manager, 'config', None)
|
||||
base_cam = self.config_manager.get_device_config('camera1')
|
||||
if parser and parser.has_section(section):
|
||||
# 读取所有相关键
|
||||
def get_opt(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.get(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_int(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getint(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_bool(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getboolean(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
enabled = get_bool(section, 'enabled', True)
|
||||
if not enabled:
|
||||
self.logger.info(f"{device_name} 未启用,跳过初始化")
|
||||
return False
|
||||
# 填充覆盖项
|
||||
idx2 = get_int(section, 'device_index', None)
|
||||
if idx2 is not None:
|
||||
cfg['device_index'] = idx2
|
||||
w = get_int(section, 'width', None)
|
||||
h = get_int(section, 'height', None)
|
||||
f = get_int(section, 'fps', None)
|
||||
buf = get_int(section, 'buffer_size', None)
|
||||
fourcc = get_opt(section, 'fourcc', None)
|
||||
backend = get_opt(section, 'backend', None)
|
||||
if w is not None: cfg['width'] = w
|
||||
if h is not None: cfg['height'] = h
|
||||
if f is not None: cfg['fps'] = f
|
||||
if buf is not None: cfg['buffer_size'] = buf
|
||||
if fourcc is not None: cfg['fourcc'] = fourcc
|
||||
if backend is not None: cfg['backend'] = backend
|
||||
else:
|
||||
# section 不存在时:camera2 默认使用 device_index+1
|
||||
if device_name.lower() == 'camera2':
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
|
||||
else:
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0))
|
||||
|
||||
camera = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
|
||||
self.devices[device_name] = camera
|
||||
if camera.initialize():
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化相机失败: {e}")
|
||||
self.logger.error(f"初始化{device_name}失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_imu(self) -> bool:
|
||||
@ -441,8 +494,8 @@ class DeviceCoordinator:
|
||||
success_count = 0
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
# 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流
|
||||
if device_name in ['femtobolt', 'camera',"imu"]:
|
||||
# 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控
|
||||
if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
|
||||
continue
|
||||
|
||||
if hasattr(device, '_start_connection_monitor'):
|
||||
@ -475,19 +528,9 @@ class DeviceCoordinator:
|
||||
success_count = 0
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
# 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流
|
||||
if device_name in ['femtobolt', 'camera',"imu"]:
|
||||
# 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控停止
|
||||
if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
|
||||
self.logger.info(f"停止{device_name}设备推流")
|
||||
|
||||
# # 调用设备的cleanup方法清理资源,停止推流
|
||||
# if hasattr(device, 'cleanup'):
|
||||
# if device.cleanup():
|
||||
# success_count += 1
|
||||
# self.logger.info(f"{device_name}设备推流已停止")
|
||||
# else:
|
||||
# self.logger.warning(f"{device_name}设备推流停止失败")
|
||||
# else:
|
||||
# self.logger.warning(f"{device_name}设备不支持推流停止")
|
||||
continue
|
||||
|
||||
if hasattr(device, '_stop_connection_monitor'):
|
||||
@ -592,13 +635,58 @@ class DeviceCoordinator:
|
||||
|
||||
new_device = None
|
||||
try:
|
||||
# 根据设备类型重新创建实例
|
||||
if device_name == 'camera':
|
||||
# 根据设备类型重新创建实例(仅支持 camera1/camera2)
|
||||
if device_name in ('camera1', 'camera2'):
|
||||
try:
|
||||
from .camera_manager import CameraManager
|
||||
except ImportError:
|
||||
from camera_manager import CameraManager
|
||||
new_device = CameraManager(self.socketio, self.config_manager)
|
||||
# 为 camera1/camera2 构造实例配置
|
||||
section = 'CAMERA1' if device_name == 'camera1' else 'CAMERA2'
|
||||
cfg = {}
|
||||
parser = getattr(self.config_manager, 'config', None)
|
||||
base_cam = self.config_manager.get_device_config('camera1')
|
||||
if parser and parser.has_section(section):
|
||||
def get_opt(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.get(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_int(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getint(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_bool(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getboolean(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
enabled = get_bool(section, 'enabled', True)
|
||||
if not enabled:
|
||||
raise Exception(f"{device_name} 未启用")
|
||||
idx2 = get_int(section, 'device_index', None)
|
||||
if idx2 is not None:
|
||||
cfg['device_index'] = idx2
|
||||
w = get_int(section, 'width', None)
|
||||
h = get_int(section, 'height', None)
|
||||
f = get_int(section, 'fps', None)
|
||||
buf = get_int(section, 'buffer_size', None)
|
||||
fourcc = get_opt(section, 'fourcc', None)
|
||||
backend = get_opt(section, 'backend', None)
|
||||
if w is not None: cfg['width'] = w
|
||||
if h is not None: cfg['height'] = h
|
||||
if f is not None: cfg['fps'] = f
|
||||
if buf is not None: cfg['buffer_size'] = buf
|
||||
if fourcc is not None: cfg['fourcc'] = fourcc
|
||||
if backend is not None: cfg['backend'] = backend
|
||||
else:
|
||||
# section 不存在时:camera2 默认使用 [CAMERA1] 的 device_index + 1
|
||||
if device_name == 'camera2':
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
|
||||
else:
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0))
|
||||
new_device = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
|
||||
elif device_name == 'imu':
|
||||
try:
|
||||
from .imu_manager import IMUManager
|
||||
@ -812,7 +900,12 @@ class DeviceCoordinator:
|
||||
self.executor.shutdown(wait=True)
|
||||
|
||||
# 清理Socket管理器
|
||||
self.socket_manager.cleanup()
|
||||
try:
|
||||
self.socket_manager.cleanup_all()
|
||||
except Exception:
|
||||
# 兼容旧接口
|
||||
if hasattr(self.socket_manager, 'cleanup'):
|
||||
self.socket_manager.cleanup()
|
||||
|
||||
self.logger.info("设备协调器已关闭")
|
||||
|
||||
@ -837,7 +930,7 @@ def test_restart_device(device_name=None):
|
||||
|
||||
Args:
|
||||
device_name (str, optional): 指定要测试的设备名称。如果为None,则自动选择第一个可用设备。
|
||||
可选值: 'camera', 'imu', 'pressure', 'femtobolt'
|
||||
可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
@ -847,22 +940,13 @@ def test_restart_device(device_name=None):
|
||||
print("设备协调器重启功能测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建模拟的SocketIO和配置管理器
|
||||
# 创建模拟的SocketIO(使用真实配置文件)
|
||||
mock_socketio = Mock()
|
||||
mock_config_manager = Mock()
|
||||
|
||||
# 模拟配置数据
|
||||
mock_config_manager.get_device_config.return_value = {
|
||||
'camera': {'enabled': True, 'device_id': 0, 'fps': 30},
|
||||
'imu': {'enabled': True, 'device_type': 'mock'},
|
||||
'pressure': {'enabled': True, 'device_type': 'mock'},
|
||||
'femtobolt': {'enabled': False}
|
||||
}
|
||||
|
||||
try:
|
||||
# 创建设备协调器实例
|
||||
print("1. 创建设备协调器...")
|
||||
coordinator = DeviceCoordinator(mock_socketio, mock_config_manager)
|
||||
coordinator = DeviceCoordinator(mock_socketio)
|
||||
|
||||
# 初始化设备协调器
|
||||
print("2. 初始化设备协调器...")
|
||||
@ -882,13 +966,14 @@ def test_restart_device(device_name=None):
|
||||
print("❌ 没有可用的设备进行测试")
|
||||
return False
|
||||
|
||||
# 根据参数选择测试设备
|
||||
# 根据参数选择测试设备(仅支持 camera1/camera2/imu/pressure/femtobolt)
|
||||
if device_name:
|
||||
if device_name in available_devices:
|
||||
allowed = {'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'}
|
||||
if device_name in available_devices and device_name in allowed:
|
||||
test_device = device_name
|
||||
print(f"3. 使用指定的测试设备: {test_device}")
|
||||
else:
|
||||
print(f"❌ 指定的设备 '{device_name}' 不存在")
|
||||
print(f"❌ 指定的设备 '{device_name}' 不存在或不受支持")
|
||||
print(f" 可用设备: {available_devices}")
|
||||
return False
|
||||
else:
|
||||
@ -992,7 +1077,7 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
# 执行测试
|
||||
# 可选值: 'camera', 'imu', 'pressure', 'femtobolt'
|
||||
# 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
|
||||
success = test_restart_device('pressure')
|
||||
|
||||
if success:
|
||||
|
||||
@ -242,7 +242,7 @@ class RecordingManager:
|
||||
|
||||
return frame
|
||||
|
||||
def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]:
|
||||
def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera1_location: List[int], camera2_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
启动同步录制
|
||||
|
||||
@ -250,9 +250,10 @@ class RecordingManager:
|
||||
session_id: 检测会话ID
|
||||
patient_id: 患者ID
|
||||
screen_location: 屏幕录制区域 [x, y, w, h]
|
||||
camera_location: 相机录制区域 [x, y, w, h]
|
||||
camera1_location: 相机1录制区域 [x, y, w, h]
|
||||
camera2_location: 相机2录制区域 [x, y, w, h]
|
||||
femtobolt_location: FemtoBolt录制区域 [x, y, w, h]
|
||||
recording_types: 录制类型列表 ['screen', 'feet', 'femtobolt'],默认全部录制
|
||||
recording_types: 录制类型列表 ['screen', 'camera1', 'camera2', 'feet', 'femtobolt'],默认全部录制
|
||||
|
||||
Returns:
|
||||
Dict: 录制启动状态和信息
|
||||
@ -263,6 +264,8 @@ class RecordingManager:
|
||||
'patient_id': patient_id,
|
||||
'recording_start_time': None,
|
||||
'video_paths': {
|
||||
'camera1_video': None,
|
||||
'camera2_video': None,
|
||||
'feet_video': None,
|
||||
'screen_video': None,
|
||||
'femtobolt_video': None
|
||||
@ -277,7 +280,7 @@ class RecordingManager:
|
||||
return result
|
||||
|
||||
# 设置默认录制类型
|
||||
recording_types = ['screen', 'feet']
|
||||
recording_types = ['screen', 'camera1' ,'camera2']
|
||||
|
||||
# 验证录制区域参数(仅对启用的录制类型进行验证)
|
||||
if 'screen' in recording_types:
|
||||
@ -286,10 +289,16 @@ class RecordingManager:
|
||||
result['message'] = '屏幕录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||||
return result
|
||||
|
||||
if 'feet' in recording_types:
|
||||
if not camera_location or not isinstance(camera_location, list) or len(camera_location) != 4:
|
||||
if 'camera1' in recording_types:
|
||||
if not camera1_location or not isinstance(camera1_location, list) or len(camera1_location) != 4:
|
||||
result['success'] = False
|
||||
result['message'] = '相机录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||||
result['message'] = '相机1录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||||
return result
|
||||
|
||||
if 'camera2' in recording_types:
|
||||
if not camera2_location or not isinstance(camera2_location, list) or len(camera2_location) != 4:
|
||||
result['success'] = False
|
||||
result['message'] = '相机2录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]'
|
||||
return result
|
||||
|
||||
if 'femtobolt' in recording_types:
|
||||
@ -303,19 +312,22 @@ class RecordingManager:
|
||||
# self.logger.info(f'检测sessionID................: {self.current_session_id}')
|
||||
self.current_patient_id = patient_id
|
||||
self.screen_region = tuple(screen_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
self.camera_region = tuple(camera_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
self.camera1_region = tuple(camera1_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
self.camera2_region = tuple(camera2_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
|
||||
# 根据录制区域大小设置自适应帧率
|
||||
if 'screen' in recording_types:
|
||||
self._set_adaptive_fps_by_region('screen', self.screen_region)
|
||||
if 'feet' in recording_types:
|
||||
self._set_adaptive_fps_by_region('camera', self.camera_region)
|
||||
if 'camera1' in recording_types:
|
||||
self._set_adaptive_fps_by_region('camera1', self.camera1_region)
|
||||
if 'camera2' in recording_types:
|
||||
self._set_adaptive_fps_by_region('camera2', self.camera2_region)
|
||||
if 'femtobolt' in recording_types:
|
||||
self._set_adaptive_fps_by_region('femtobolt', self.femtobolt_region)
|
||||
|
||||
# 设置录制同步
|
||||
active_recording_count = len([t for t in recording_types if t in ['screen', 'feet', 'femtobolt']])
|
||||
active_recording_count = len([t for t in recording_types if t in ['screen', 'camera1', 'camera2', 'femtobolt']])
|
||||
self.recording_sync_barrier = threading.Barrier(active_recording_count)
|
||||
self.recording_start_sync.clear()
|
||||
self.global_recording_start_time = None
|
||||
@ -340,7 +352,8 @@ class RecordingManager:
|
||||
return result
|
||||
|
||||
|
||||
feet_video_path = os.path.join(base_path, 'feet.mp4')
|
||||
camera1_video_path = os.path.join(base_path, 'camera1.mp4')
|
||||
camera2_video_path = os.path.join(base_path, 'camera2.mp4')
|
||||
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
||||
femtobolt_video_path = os.path.join(base_path, 'femtobolt.mp4')
|
||||
|
||||
@ -350,7 +363,8 @@ class RecordingManager:
|
||||
'session_id': session_id,
|
||||
'status': 'recording',
|
||||
'video_paths': {
|
||||
'normal_video_path': os.path.join(db_base_path, 'feet.mp4'),
|
||||
'camera1_video_path': os.path.join(db_base_path, 'camera1.mp4'),
|
||||
'camera2_video_path': os.path.join(db_base_path, 'camera2.mp4'),
|
||||
'screen_video_path': os.path.join(db_base_path, 'screen.mp4'),
|
||||
'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4')
|
||||
}
|
||||
@ -373,7 +387,20 @@ class RecordingManager:
|
||||
# 根据录制类型选择性地初始化视频写入器,使用各自的自适应帧率
|
||||
self.screen_video_writer = None
|
||||
self.femtobolt_video_writer = None
|
||||
self.feet_video_writer = None
|
||||
self.camera1_video_writer = None
|
||||
self.camera2_video_writer = None
|
||||
|
||||
if 'camera1' in recording_types:
|
||||
self.camera1_video_writer = cv2.VideoWriter(
|
||||
camera1_video_path, fourcc, self.camera1_current_fps, (self.camera1_region[2], self.camera1_region[3])
|
||||
)
|
||||
self.logger.info(f'相机1视频写入器使用帧率: {self.camera1_current_fps}fps')
|
||||
|
||||
if 'camera2' in recording_types:
|
||||
self.camera2_video_writer = cv2.VideoWriter(
|
||||
camera2_video_path, fourcc, self.camera2_current_fps, (self.camera2_region[2], self.camera2_region[3])
|
||||
)
|
||||
self.logger.info(f'相机2视频写入器使用帧率: {self.camera2_current_fps}fps')
|
||||
|
||||
if 'screen' in recording_types:
|
||||
self.screen_video_writer = cv2.VideoWriter(
|
||||
@ -387,22 +414,23 @@ class RecordingManager:
|
||||
)
|
||||
self.logger.info(f'FemtoBolt视频写入器使用帧率: {self.femtobolt_current_fps}fps')
|
||||
|
||||
if 'feet' in recording_types:
|
||||
self.feet_video_writer = cv2.VideoWriter(
|
||||
feet_video_path, fourcc, self.camera_current_fps, (self.camera_region[2], self.camera_region[3])
|
||||
)
|
||||
self.logger.info(f'足部视频写入器使用帧率: {self.camera_current_fps}fps')
|
||||
|
||||
# 检查视频写入器状态(仅检查启用的录制类型)
|
||||
# 检查足部视频写入器
|
||||
if 'feet' in recording_types:
|
||||
if self.feet_video_writer and self.feet_video_writer.isOpened():
|
||||
self.logger.info(f'足部视频写入器初始化成功: {feet_video_path}')
|
||||
# 检查相机1视频写入器
|
||||
if 'camera1' in recording_types:
|
||||
if self.camera1_video_writer and self.camera1_video_writer.isOpened():
|
||||
self.logger.info(f'相机1视频写入器初始化成功: {camera1_video_path}')
|
||||
else:
|
||||
self.logger.error(f'足部视频写入器初始化失败: {feet_video_path}')
|
||||
self.logger.error(f'相机1视频写入器初始化失败: {camera1_video_path}')
|
||||
else:
|
||||
self.logger.info('相机1录制功能已禁用')
|
||||
# 检查相机2视频写入器
|
||||
if 'camera2' in recording_types:
|
||||
if self.camera2_video_writer and self.camera2_video_writer.isOpened():
|
||||
self.logger.info(f'相机2视频写入器初始化成功: {camera2_video_path}')
|
||||
else:
|
||||
self.logger.error(f'相机2视频写入器初始化失败: {camera2_video_path}')
|
||||
else:
|
||||
self.logger.info('足部录制功能已禁用')
|
||||
|
||||
# 检查屏幕视频写入器
|
||||
if 'screen' in recording_types:
|
||||
if self.screen_video_writer and self.screen_video_writer.isOpened():
|
||||
@ -426,15 +454,25 @@ class RecordingManager:
|
||||
self.sync_recording = True
|
||||
|
||||
# 根据录制类型启动对应的录制线程
|
||||
if 'feet' in recording_types and self.feet_video_writer and self.feet_video_writer.isOpened():
|
||||
self.feet_recording_thread = threading.Thread(
|
||||
if 'camera1' in recording_types and self.camera1_video_writer and self.camera1_video_writer.isOpened():
|
||||
self.camera1_recording_thread = threading.Thread(
|
||||
target=self._generic_recording_thread,
|
||||
args=('camera', self.camera_region, feet_video_path, self.feet_video_writer),
|
||||
args=('camera1', self.camera1_region, camera1_video_path, self.camera1_video_writer),
|
||||
daemon=True,
|
||||
name='FeetRecordingThread'
|
||||
name='Camera1RecordingThread'
|
||||
)
|
||||
self.feet_recording_thread.start()
|
||||
# self.logger.info(f'足部录制线程已启动 - 区域: {self.camera_region}, 输出文件: {feet_video_path}')
|
||||
self.camera1_recording_thread.start()
|
||||
# self.logger.info(f'相机1录制线程已启动 - 区域: {self.camera1_region}, 输出文件: {camera1_video_path}')
|
||||
|
||||
if 'camera2' in recording_types and self.camera2_video_writer and self.camera2_video_writer.isOpened():
|
||||
self.camera2_recording_thread = threading.Thread(
|
||||
target=self._generic_recording_thread,
|
||||
args=('camera2', self.camera2_region, camera2_video_path, self.camera2_video_writer),
|
||||
daemon=True,
|
||||
name='Camera2RecordingThread'
|
||||
)
|
||||
self.camera2_recording_thread.start()
|
||||
# self.logger.info(f'相机2录制线程已启动 - 区域: {self.camera2_region}, 输出文件: {camera2_video_path}')
|
||||
|
||||
if 'screen' in recording_types and self.screen_video_writer and self.screen_video_writer.isOpened():
|
||||
self.screen_recording_thread = threading.Thread(
|
||||
@ -506,8 +544,10 @@ class RecordingManager:
|
||||
|
||||
# 收集活跃的录制线程
|
||||
active_threads = []
|
||||
if hasattr(self, 'feet_recording_thread') and self.feet_recording_thread and self.feet_recording_thread.is_alive():
|
||||
active_threads.append(('feet', self.feet_recording_thread))
|
||||
if hasattr(self, 'camera1_recording_thread') and self.camera1_recording_thread and self.camera1_recording_thread.is_alive():
|
||||
active_threads.append(('camera1', self.camera1_recording_thread))
|
||||
if hasattr(self, 'camera2_recording_thread') and self.camera2_recording_thread and self.camera2_recording_thread.is_alive():
|
||||
active_threads.append(('camera2', self.camera2_recording_thread))
|
||||
if hasattr(self, 'screen_recording_thread') and self.screen_recording_thread and self.screen_recording_thread.is_alive():
|
||||
active_threads.append(('screen', self.screen_recording_thread))
|
||||
if hasattr(self, 'femtobolt_recording_thread') and self.femtobolt_recording_thread and self.femtobolt_recording_thread.is_alive():
|
||||
@ -535,9 +575,12 @@ class RecordingManager:
|
||||
if thread_name == 'screen':
|
||||
expected_frames = int(actual_recording_duration * self.screen_current_fps)
|
||||
self.logger.info(f' 屏幕录制预期帧数: {expected_frames}帧 (帧率{self.screen_current_fps}fps)')
|
||||
elif thread_name == 'feet':
|
||||
elif thread_name == 'camera1':
|
||||
expected_frames = int(actual_recording_duration * self.camera_current_fps)
|
||||
self.logger.info(f' 足部录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
|
||||
self.logger.info(f' 相机1录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
|
||||
elif thread_name == 'camera2':
|
||||
expected_frames = int(actual_recording_duration * self.camera_current_fps)
|
||||
self.logger.info(f' 相机2录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
|
||||
elif thread_name == 'femtobolt':
|
||||
expected_frames = int(actual_recording_duration * self.femtobolt_current_fps)
|
||||
self.logger.info(f' FemtoBolt录制预期帧数: {expected_frames}帧 (帧率{self.femtobolt_current_fps}fps)')
|
||||
@ -575,7 +618,7 @@ class RecordingManager:
|
||||
通用录制线程,支持屏幕、相机和FemtoBolt录制
|
||||
|
||||
Args:
|
||||
recording_type: 录制类型 ('screen', 'camera', 'femtobolt')
|
||||
recording_type: 录制类型 ('screen', 'camera1', 'camera2', 'femtobolt')
|
||||
region: 录制区域 (x, y, width, height)
|
||||
output_file_name: 输出文件名
|
||||
video_writer: 视频写入器对象
|
||||
@ -587,8 +630,10 @@ class RecordingManager:
|
||||
# 根据录制类型获取对应的自适应帧率
|
||||
if recording_type == 'screen':
|
||||
target_fps = self.screen_current_fps
|
||||
elif recording_type == 'camera':
|
||||
target_fps = self.camera_current_fps
|
||||
elif recording_type == 'camera1':
|
||||
target_fps = self.camera1_current_fps
|
||||
elif recording_type == 'camera2':
|
||||
target_fps = self.camera2_current_fps
|
||||
elif recording_type == 'femtobolt':
|
||||
target_fps = self.femtobolt_current_fps
|
||||
else:
|
||||
@ -789,9 +834,9 @@ class RecordingManager:
|
||||
|
||||
|
||||
|
||||
def collect_detection_data(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def save_detection_images(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
保存前端传入的检测数据和图片
|
||||
保存前端传入的检测图片到指定目录
|
||||
|
||||
Args:
|
||||
session_id: 检测会话ID
|
||||
@ -824,7 +869,8 @@ class RecordingManager:
|
||||
'body_image': None,
|
||||
'foot_data': detection_data.get('foot_data'),
|
||||
'foot_data_image': None,
|
||||
'foot_image': None,
|
||||
'foot1_image': None,
|
||||
'foot2_image': None,
|
||||
'screen_image': None,
|
||||
'timestamp': timestamp
|
||||
}
|
||||
@ -833,7 +879,8 @@ class RecordingManager:
|
||||
# 保存图片数据
|
||||
image_fields = [
|
||||
('body_image', 'body'),
|
||||
('foot_image', 'foot'),
|
||||
('foot1_image', 'foot1'),
|
||||
('foot2_image', 'foot2'),
|
||||
('foot_data_image', 'foot_data')
|
||||
]
|
||||
|
||||
|
||||
@ -84,7 +84,8 @@ class DeviceTestServer:
|
||||
|
||||
# 设备管理器和模拟数据生成器
|
||||
self.device_managers = {
|
||||
'camera': CameraManager(self.socketio, self.config_manager),
|
||||
'camera1': CameraManager(self.socketio, self.config_manager, device_name='camera1'),
|
||||
'camera2': CameraManager(self.socketio, self.config_manager, device_name='camera2'),
|
||||
'femtobolt': FemtoBoltManager(self.socketio, self.config_manager),
|
||||
'imu': IMUManager(self.socketio, self.config_manager),
|
||||
'pressure': PressureManager(self.socketio, self.config_manager)
|
||||
@ -340,7 +341,8 @@ class DeviceTestServer:
|
||||
def _get_event_name(self, device_name: str) -> str:
|
||||
"""获取设备对应的事件名称"""
|
||||
event_map = {
|
||||
'camera': 'camera_frame',
|
||||
'camera1': 'camera_frame',
|
||||
'camera2': 'camera_frame',
|
||||
'femtobolt': 'femtobolt_frame',
|
||||
'imu': 'imu_data',
|
||||
'pressure': 'pressure_data'
|
||||
|
||||
@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
配置API测试脚本
|
||||
用于测试设备配置HTTP API的功能
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class ConfigAPITester:
|
||||
"""配置API测试器"""
|
||||
|
||||
def __init__(self, base_url="http://localhost:5002"):
|
||||
"""
|
||||
初始化测试器
|
||||
|
||||
Args:
|
||||
base_url: API基础URL
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.api_url = f"{base_url}/api/config"
|
||||
|
||||
def test_get_all_configs(self):
|
||||
"""测试获取所有设备配置"""
|
||||
print("\n=== 测试获取所有设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 获取所有设备配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_get_single_config(self, device_name):
|
||||
"""测试获取单个设备配置"""
|
||||
print(f"\n=== 测试获取{device_name}设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices/{device_name}")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print(f"✓ 获取{device_name}配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_imu_config(self):
|
||||
"""测试设置IMU配置"""
|
||||
print("\n=== 测试设置IMU配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/imu", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置IMU配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_pressure_config(self):
|
||||
"""测试设置压力板配置"""
|
||||
print("\n=== 测试设置压力板配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"use_mock": False,
|
||||
"port": "COM5",
|
||||
"baudrate": 115200
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/pressure", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置压力板配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_camera_config(self):
|
||||
"""测试设置相机配置"""
|
||||
print("\n=== 测试设置相机配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_index": 0,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 30
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/camera", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置相机配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_femtobolt_config(self):
|
||||
"""测试设置FemtoBolt配置"""
|
||||
print("\n=== 测试设置FemtoBolt配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"color_resolution": "1080P",
|
||||
"depth_mode": "NFOV_UNBINNED",
|
||||
"fps": 30,
|
||||
"depth_range_min": 1200,
|
||||
"depth_range_max": 1500
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/femtobolt", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置FemtoBolt配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_validate_config(self):
|
||||
"""测试验证配置"""
|
||||
print("\n=== 测试验证配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/validate")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 配置验证成功")
|
||||
validation_result = result['data']
|
||||
print(f"配置有效性: {validation_result['valid']}")
|
||||
if validation_result['errors']:
|
||||
print(f"错误: {validation_result['errors']}")
|
||||
if validation_result['warnings']:
|
||||
print(f"警告: {validation_result['warnings']}")
|
||||
else:
|
||||
print(f"✗ 验证失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_reload_config(self):
|
||||
"""测试重新加载配置"""
|
||||
print("\n=== 测试重新加载配置 ===")
|
||||
try:
|
||||
response = requests.post(f"{self.api_url}/reload")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 重新加载配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
else:
|
||||
print(f"✗ 重新加载失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
print("开始配置API功能测试...")
|
||||
print(f"API地址: {self.api_url}")
|
||||
|
||||
# 等待服务启动
|
||||
print("\n等待API服务启动...")
|
||||
time.sleep(2)
|
||||
|
||||
# 运行测试
|
||||
self.test_get_all_configs()
|
||||
|
||||
# 测试获取单个设备配置
|
||||
for device in ['imu', 'pressure', 'camera', 'femtobolt']:
|
||||
self.test_get_single_config(device)
|
||||
|
||||
# 测试设置配置
|
||||
self.test_set_imu_config()
|
||||
self.test_set_pressure_config()
|
||||
self.test_set_camera_config()
|
||||
self.test_set_femtobolt_config()
|
||||
|
||||
# 测试其他功能
|
||||
self.test_validate_config()
|
||||
self.test_reload_config()
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建测试器并运行测试
|
||||
tester = ConfigAPITester()
|
||||
tester.run_all_tests()
|
||||
@ -102,8 +102,8 @@ class ConfigManager:
|
||||
'pressure_baudrate': '115200'
|
||||
}
|
||||
|
||||
# 默认相机配置
|
||||
self.config['CAMERA'] = {
|
||||
# 默认相机1配置
|
||||
self.config['CAMERA1'] = {
|
||||
'device_index': '0',
|
||||
'width': '1280',
|
||||
'height': '720',
|
||||
@ -111,6 +111,15 @@ class ConfigManager:
|
||||
'backend': 'directshow'
|
||||
}
|
||||
|
||||
# 默认相机2配置
|
||||
self.config['CAMERA2'] = {
|
||||
'device_index': '1',
|
||||
'width': '1280',
|
||||
'height': '720',
|
||||
'fps': '30',
|
||||
'backend': 'directshow'
|
||||
}
|
||||
|
||||
# 默认FemtoBolt配置
|
||||
self.config['FEMTOBOLT'] = {
|
||||
'color_resolution': '1080P',
|
||||
@ -134,7 +143,7 @@ class ConfigManager:
|
||||
获取设备配置
|
||||
|
||||
Args:
|
||||
device_name: 设备名称 (camera, femtobolt, imu, pressure)
|
||||
device_name: 设备名称 (camera1, camera2, femtobolt, imu, pressure)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备配置字典
|
||||
@ -144,8 +153,10 @@ class ConfigManager:
|
||||
|
||||
config = {}
|
||||
|
||||
if device_name == 'camera':
|
||||
config = self._get_camera_config()
|
||||
if device_name == 'camera1':
|
||||
config = self._get_camera1_config()
|
||||
elif device_name == 'camera2':
|
||||
config = self._get_camera2_config()
|
||||
elif device_name == 'femtobolt':
|
||||
config = self._get_femtobolt_config()
|
||||
elif device_name == 'imu':
|
||||
@ -159,7 +170,7 @@ class ConfigManager:
|
||||
self._device_configs[device_name] = config
|
||||
return config.copy()
|
||||
|
||||
def _get_camera_config(self) -> Dict[str, Any]:
|
||||
def _get_camera1_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取相机配置
|
||||
|
||||
@ -167,16 +178,32 @@ class ConfigManager:
|
||||
Dict[str, Any]: 相机配置
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('CAMERA', 'enabled', fallback=True),
|
||||
'device_index': self.config.getint('CAMERA', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA', 'backend', fallback='directshow')
|
||||
'enabled': self.config.getboolean('CAMERA1', 'enabled', fallback=True),
|
||||
'device_index': self.config.getint('CAMERA1', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA1', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA1', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA1', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA1', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA1', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA1', 'backend', fallback='directshow')
|
||||
}
|
||||
def _get_camera2_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取相机配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('CAMERA2', 'enabled', fallback=True),
|
||||
'device_index': self.config.getint('CAMERA2', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA2', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA2', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA2', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA2', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA2', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA2', 'backend', fallback='directshow')
|
||||
}
|
||||
def _get_femtobolt_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取FemtoBolt配置
|
||||
@ -332,7 +359,7 @@ class ConfigManager:
|
||||
warnings = []
|
||||
|
||||
# 验证必需的配置段
|
||||
required_sections = ['DEVICES', 'CAMERA', 'FEMTOBOLT', 'SYSTEM']
|
||||
required_sections = ['DEVICES', 'CAMERA1', 'CAMERA2', 'FEMTOBOLT', 'SYSTEM']
|
||||
for section in required_sections:
|
||||
if not self.config.has_section(section):
|
||||
errors.append(f"缺少必需的配置段: {section}")
|
||||
@ -438,7 +465,7 @@ class ConfigManager:
|
||||
'message': f'设置压力板配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_camera_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def set_camera1_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置相机设备配置
|
||||
|
||||
@ -457,15 +484,15 @@ class ConfigManager:
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index']))
|
||||
self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA', 'width', str(config_data['width']))
|
||||
self.set_config_value('CAMERA1', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA', 'height', str(config_data['height']))
|
||||
self.set_config_value('CAMERA1', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA', 'fps', str(config_data['fps']))
|
||||
self.set_config_value('CAMERA1', 'fps', str(config_data['fps']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA', 'backend', str(config_data['backend']))
|
||||
self.set_config_value('CAMERA1', 'backend', str(config_data['backend']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
@ -474,7 +501,7 @@ class ConfigManager:
|
||||
return {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': self.get_device_config('camera')
|
||||
'config': self.get_device_config('camera1')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置相机配置失败: {e}")
|
||||
@ -482,7 +509,50 @@ class ConfigManager:
|
||||
'success': False,
|
||||
'message': f'设置相机配置失败: {str(e)}'
|
||||
}
|
||||
def set_camera2_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置相机设备配置
|
||||
|
||||
Args:
|
||||
config_data: 相机配置数据
|
||||
{
|
||||
'device_index': 1,
|
||||
'width': 1280,
|
||||
'height': 720,
|
||||
'fps': 30
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA2', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA2', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA2', 'fps', str(config_data['fps']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA2', 'backend', str(config_data['backend']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': self.get_device_config('camera2')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置相机配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置相机配置失败: {str(e)}'
|
||||
}
|
||||
def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置FemtoBolt设备配置
|
||||
@ -541,7 +611,8 @@ class ConfigManager:
|
||||
return {
|
||||
'imu': self.get_device_config('imu'),
|
||||
'pressure': self.get_device_config('pressure'),
|
||||
'camera': self.get_device_config('camera'),
|
||||
'camera1': self.get_device_config('camera1'),
|
||||
'camera2': self.get_device_config('camera2'),
|
||||
'femtobolt': self.get_device_config('femtobolt')
|
||||
}
|
||||
|
||||
@ -610,27 +681,27 @@ class ConfigManager:
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# 相机配置
|
||||
if 'camera' in configs:
|
||||
if 'camera1' in configs:
|
||||
try:
|
||||
config_data = configs['camera']
|
||||
config_data = configs['camera1']
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index']))
|
||||
self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA', 'width', str(config_data['width']))
|
||||
self.set_config_value('CAMERA1', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA', 'height', str(config_data['height']))
|
||||
self.set_config_value('CAMERA1', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA', 'fps', str(config_data['fps']))
|
||||
self.set_config_value('CAMERA1', 'fps', str(config_data['fps']))
|
||||
if 'buffer_size' in config_data:
|
||||
self.set_config_value('CAMERA', 'buffer_size', str(config_data['buffer_size']))
|
||||
self.set_config_value('CAMERA1', 'buffer_size', str(config_data['buffer_size']))
|
||||
if 'fourcc' in config_data:
|
||||
self.set_config_value('CAMERA', 'fourcc', config_data['fourcc'])
|
||||
self.set_config_value('CAMERA1', 'fourcc', config_data['fourcc'])
|
||||
if 'tx_max_width' in config_data:
|
||||
self.set_config_value('CAMERA', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
self.set_config_value('CAMERA1', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA', 'backend', str(config_data['backend']))
|
||||
self.set_config_value('CAMERA1', 'backend', str(config_data['backend']))
|
||||
|
||||
results['camera'] = {
|
||||
results['camera1'] = {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': config_data
|
||||
@ -638,10 +709,42 @@ class ConfigManager:
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
except Exception as e:
|
||||
error_msg = f'设置相机配置失败: {str(e)}'
|
||||
results['camera'] = {'success': False, 'message': error_msg}
|
||||
results['camera1'] = {'success': False, 'message': error_msg}
|
||||
errors.append(f"相机: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
if 'camera2' in configs:
|
||||
try:
|
||||
config_data = configs['camera2']
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA2', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA2', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA2', 'fps', str(config_data['fps']))
|
||||
if 'buffer_size' in config_data:
|
||||
self.set_config_value('CAMERA2', 'buffer_size', str(config_data['buffer_size']))
|
||||
if 'fourcc' in config_data:
|
||||
self.set_config_value('CAMERA2', 'fourcc', config_data['fourcc'])
|
||||
if 'tx_max_width' in config_data:
|
||||
self.set_config_value('CAMERA2', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA2', 'backend', str(config_data['backend']))
|
||||
|
||||
results['camera2'] = {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': config_data
|
||||
}
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
except Exception as e:
|
||||
error_msg = f'设置相机配置失败: {str(e)}'
|
||||
results['camera2'] = {'success': False, 'message': error_msg}
|
||||
errors.append(f"相机2: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# FemtoBolt配置
|
||||
if 'femtobolt' in configs:
|
||||
try:
|
||||
@ -703,7 +806,8 @@ class ConfigManager:
|
||||
{
|
||||
'imu': {'device_type': 'real', 'port': 'COM7', 'baudrate': 9600},
|
||||
'pressure': {'device_type': 'real', 'port': 'COM8', 'baudrate': 115200},
|
||||
'camera': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'camera1': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'camera2': {'device_index': 1, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15}
|
||||
}
|
||||
|
||||
|
||||
333
backend/main.py
333
backend/main.py
@ -25,6 +25,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
# 导入模块
|
||||
from database import DatabaseManager
|
||||
from utils import config as app_config
|
||||
from utils import DataValidator # 添加数据验证器导入
|
||||
from devices.camera_manager import CameraManager
|
||||
from devices.imu_manager import IMUManager
|
||||
from devices.pressure_manager import PressureManager
|
||||
@ -163,8 +164,9 @@ class AppServer:
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'config.ini')
|
||||
|
||||
self.config.read(config_path, encoding='utf-8')
|
||||
device_index = self.config.get('CAMERA', 'device_index', fallback=None)
|
||||
print(f"设备号: {device_index}")
|
||||
camera1_index = self.config.get('CAMERA1', 'device_index', fallback=None)
|
||||
camera2_index = self.config.get('CAMERA2', 'device_index', fallback=None)
|
||||
print(f"相机1设备号: {camera1_index}, 相机2设备号: {camera2_index}")
|
||||
|
||||
def init_app(self):
|
||||
"""初始化应用组件"""
|
||||
@ -243,16 +245,21 @@ class AppServer:
|
||||
|
||||
# 初始化录制管理器
|
||||
self.logger.info('正在初始化录制管理器...')
|
||||
camera_manager = self.device_managers.get('camera')
|
||||
if camera_manager:
|
||||
self.recording_manager = RecordingManager(
|
||||
camera_manager=camera_manager,
|
||||
db_manager=self.db_manager
|
||||
)
|
||||
self.logger.info('录制管理器初始化完成')
|
||||
else:
|
||||
self.recording_manager = None
|
||||
self.logger.warning('相机设备未初始化,录制管理器将不可用')
|
||||
camera1_manager = self.device_managers.get('camera1')
|
||||
camera2_manager = self.device_managers.get('camera2')
|
||||
femtobolt_manager = self.device_managers.get('femtobolt')
|
||||
pressure_manager = self.device_managers.get('pressure')
|
||||
|
||||
# 录制管理器当前采用屏幕区域截取方式进行相机录制,不依赖 CameraManager
|
||||
# 但保留其他设备管理器以便后续扩展(如FemtoBolt、压力传感器)
|
||||
self.recording_manager = RecordingManager(
|
||||
camera_manager=None,
|
||||
db_manager=self.db_manager,
|
||||
femtobolt_manager=femtobolt_manager,
|
||||
pressure_manager=pressure_manager,
|
||||
config_manager=self.config_manager
|
||||
)
|
||||
self.logger.info('录制管理器初始化完成')
|
||||
|
||||
# 启动Flask应用
|
||||
host = self.host
|
||||
@ -895,14 +902,15 @@ class AppServer:
|
||||
if status not in ['approved', 'rejected']:
|
||||
return jsonify({'success': False, 'error': '无效的审核状态'}), 400
|
||||
|
||||
result = self.db_manager.update_user_status(user_id, status)
|
||||
if result:
|
||||
# 使用数据库层已有的审核方法
|
||||
try:
|
||||
self.db_manager.approve_user(user_id, approved=(status == 'approved'))
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'用户已{"通过" if status == "approved" else "拒绝"}审核'
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '用户不存在'}), 404
|
||||
except Exception:
|
||||
return jsonify({'success': False, 'error': '用户不存在或审核失败'}), 404
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'审核用户失败: {e}')
|
||||
@ -952,47 +960,51 @@ class AppServer:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
elif flask_request.method == 'POST':
|
||||
# 创建患者
|
||||
# 创建新患者
|
||||
try:
|
||||
# 检查Content-Type
|
||||
if not flask_request.is_json:
|
||||
return jsonify({'success': False, 'message': '请求Content-Type必须为application/json'}), 415
|
||||
data = flask_request.get_json()
|
||||
|
||||
data = flask_request.get_json(force=True)
|
||||
|
||||
required_fields = ['name', 'gender', 'age']
|
||||
for field in required_fields:
|
||||
if not data.get(field):
|
||||
return jsonify({'success': False, 'error': f'{field}不能为空'}), 400
|
||||
# 验证患者数据
|
||||
validation_result = DataValidator.validate_patient_data(data)
|
||||
if not validation_result['valid']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '; '.join(validation_result['errors'])
|
||||
}), 400
|
||||
|
||||
# 准备患者数据
|
||||
patient_data = {
|
||||
'name': data['name'],
|
||||
'gender': data['gender'],
|
||||
'age': data['age'],
|
||||
'birth_date': data.get('birth_date'),
|
||||
'nationality': data.get('nationality'),
|
||||
'residence': data.get('residence'),
|
||||
'height': data.get('height'),
|
||||
'weight': data.get('weight'),
|
||||
'shoe_size': data.get('shoe_size'),
|
||||
'phone': data.get('phone'),
|
||||
'email': data.get('email'),
|
||||
'occupation': data.get('occupation'),
|
||||
'workplace': data.get('workplace'),
|
||||
'medical_history': data.get('medical_history', ''),
|
||||
'notes': data.get('notes', '')
|
||||
'name': validation_result['data'].get('name'),
|
||||
'gender': validation_result['data'].get('gender'),
|
||||
'birth_date': validation_result['data'].get('birth_date'),
|
||||
'nationality': validation_result['data'].get('nationality'),
|
||||
'residence': validation_result['data'].get('residence'),
|
||||
'height': validation_result['data'].get('height'),
|
||||
'weight': validation_result['data'].get('weight'),
|
||||
'shoe_size': validation_result['data'].get('shoe_size'),
|
||||
'phone': validation_result['data'].get('phone'),
|
||||
'email': validation_result['data'].get('email'),
|
||||
'occupation': validation_result['data'].get('occupation'),
|
||||
'workplace': validation_result['data'].get('workplace'),
|
||||
'idcode': validation_result['data'].get('idcode'),
|
||||
'medical_history': validation_result['data'].get('medical_history'),
|
||||
'notes': validation_result['data'].get('notes')
|
||||
}
|
||||
|
||||
# 创建患者
|
||||
patient_id = self.db_manager.create_patient(patient_data)
|
||||
|
||||
if patient_id:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '患者创建成功',
|
||||
'data': {'patient_id': patient_id}
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '患者创建失败'}), 500
|
||||
# 获取创建的患者信息
|
||||
patient = self.db_manager.get_patient(patient_id)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '患者创建成功',
|
||||
'data': {
|
||||
'patient_id': patient_id,
|
||||
'patient': patient
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'创建患者失败: {e}')
|
||||
@ -1033,6 +1045,7 @@ class AppServer:
|
||||
'email': data.get('email'),
|
||||
'occupation': data.get('occupation'),
|
||||
'workplace': data.get('workplace'),
|
||||
'idcode': data.get('idcode'),
|
||||
'medical_history': data.get('medical_history'),
|
||||
'notes': data.get('notes')
|
||||
}
|
||||
@ -1078,19 +1091,6 @@ class AppServer:
|
||||
self.logger.error(f'获取设备状态失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/devices/refresh', methods=['POST'])
|
||||
def refresh_devices():
|
||||
"""刷新设备"""
|
||||
try:
|
||||
if self.device_coordinator:
|
||||
result = self.device_coordinator.refresh_all_devices()
|
||||
return jsonify({'success': True, 'data': result})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '设备协调器未初始化'}), 500
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'刷新设备失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# ==================== 设备配置API ====================
|
||||
|
||||
@ -1223,6 +1223,19 @@ class AppServer:
|
||||
self.logger.error(f'开始检测失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/<session_id>/has_data', methods=['GET'])
|
||||
def has_session_detection_data(session_id: str):
|
||||
"""检查指定会话是否存在检测数据,用于判断单次检测是否有效"""
|
||||
try:
|
||||
if not self.db_manager:
|
||||
return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500
|
||||
|
||||
exists = self.db_manager.has_session_detection_data(session_id)
|
||||
return jsonify({'success': True, 'session_id': session_id, 'has_data': bool(exists)})
|
||||
except Exception as e:
|
||||
self.logger.error(f'检查会话检测数据存在失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/<session_id>/stop', methods=['POST'])
|
||||
def stop_detection(session_id):
|
||||
"""停止检测"""
|
||||
@ -1246,24 +1259,22 @@ class AppServer:
|
||||
'message': '空白会话已删除'
|
||||
})
|
||||
else:
|
||||
# 正常会话的停止流程
|
||||
# 如果提供了duration,更新到数据库
|
||||
# 正常会话的停止流程:调用数据库层结束检测,自动计算时长并写入结束信息
|
||||
data = flask_request.get_json() or {}
|
||||
duration = data.get('duration')
|
||||
if duration is not None and isinstance(duration, (int, float)):
|
||||
try:
|
||||
self.db_manager.update_session_duration(session_id, int(duration))
|
||||
self.logger.info(f'更新会话持续时间: {session_id} -> {duration}秒')
|
||||
except Exception as duration_error:
|
||||
self.logger.error(f'更新会话持续时间失败: {duration_error}')
|
||||
|
||||
# 更新会话状态为已完成
|
||||
success = self.db_manager.update_session_status(session_id, 'completed')
|
||||
diagnosis_info = data.get('diagnosis_info')
|
||||
treatment_info = data.get('treatment_info')
|
||||
suggestion_info = data.get('suggestion_info')
|
||||
success = self.db_manager.update_session_endcheck(
|
||||
session_id,
|
||||
diagnosis_info=diagnosis_info,
|
||||
treatment_info=treatment_info,
|
||||
suggestion_info=suggestion_info
|
||||
)
|
||||
if success:
|
||||
self.logger.info(f'检测会话已停止 - 会话ID: {session_id}')
|
||||
self.logger.info(f'检测会话已结束检查 - 会话ID: {session_id}')
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '检测已停止'
|
||||
'message': '检测已结束并已写入总结信息'
|
||||
})
|
||||
else:
|
||||
self.logger.error('停止检测失败,更新会话状态失败')
|
||||
@ -1285,31 +1296,34 @@ class AppServer:
|
||||
data = flask_request.get_json()
|
||||
patient_id = data.get('patient_id')
|
||||
screen_location = data.get('screen_location') # [0,0,1920,1080]
|
||||
camera_location = data.get('camera_location') # [0,0,640,480]
|
||||
femtobolt_location = data.get('femtobolt_location') # [0,0,640,480]
|
||||
|
||||
|
||||
camera1_location = data.get('camera1_location') # [0,0,640,480]
|
||||
camera2_location = data.get('camera2_location') # [0,0,640,480]
|
||||
if not patient_id:
|
||||
return jsonify({'success': False, 'error': '缺少患者ID'}), 400
|
||||
|
||||
# 开始视频录制
|
||||
recording_response = None
|
||||
try:
|
||||
recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_location,femtobolt_location)
|
||||
recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera1_location,camera2_location,femtobolt_location)
|
||||
|
||||
# 处理录制管理器返回的数据库更新信息
|
||||
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
||||
db_updates = recording_response['database_updates']
|
||||
try:
|
||||
# 更新会话状态
|
||||
if not self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']):
|
||||
self.logger.error(f'更新会话状态失败 - 会话ID: {db_updates["session_id"]}, 状态: {db_updates["status"]}')
|
||||
# 保存检测视频记录(映射到 detection_video 表字段)
|
||||
video_paths = db_updates.get('video_paths', {})
|
||||
video_record = {
|
||||
'screen_video_path': video_paths.get('screen_video_path'),
|
||||
'femtobolt_video_path': video_paths.get('femtobolt_video_path'),
|
||||
'camera1_video_path': video_paths.get('camera1_video_path'),
|
||||
'camera2_video_path': video_paths.get('camera2_video_path'),
|
||||
}
|
||||
|
||||
# 更新视频文件路径
|
||||
video_paths = db_updates['video_paths']
|
||||
self.db_manager.update_session_normal_video_path(db_updates['session_id'], video_paths['normal_video_path'])
|
||||
self.db_manager.update_session_screen_video_path(db_updates['session_id'], video_paths['screen_video_path'])
|
||||
self.db_manager.update_session_femtobolt_video_path(db_updates['session_id'], video_paths['femtobolt_video_path'])
|
||||
try:
|
||||
self.db_manager.save_detection_video(db_updates['session_id'], video_record)
|
||||
except Exception as video_err:
|
||||
self.logger.error(f'保存检测视频记录失败: {video_err}')
|
||||
|
||||
self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}')
|
||||
except Exception as db_error:
|
||||
@ -1329,55 +1343,20 @@ class AppServer:
|
||||
def stop_record(session_id):
|
||||
"""停止视频录制"""
|
||||
try:
|
||||
if not self.db_manager or not self.device_coordinator:
|
||||
self.logger.error('数据库管理器或设备管理器未初始化')
|
||||
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
||||
|
||||
if not session_id:
|
||||
self.logger.error('缺少会话ID')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '缺少会话ID'
|
||||
}), 400
|
||||
|
||||
# 停止同步录制,传递视频数据
|
||||
try:
|
||||
restrt = self.recording_manager.stop_recording(session_id)
|
||||
self.logger.info(f'停止录制结果: {restrt}')
|
||||
|
||||
# 处理录制管理器返回的数据库更新信息
|
||||
if restrt and restrt.get('success') and 'database_updates' in restrt:
|
||||
db_updates = restrt['database_updates']
|
||||
try:
|
||||
# 更新会话状态
|
||||
success = self.db_manager.update_session_status(db_updates['session_id'], db_updates['status'])
|
||||
self.logger.info(f'会话状态已更新为: {db_updates["status"]} - 会话ID: {db_updates["session_id"]}')
|
||||
except Exception as db_error:
|
||||
self.logger.error(f'处理停止录制的数据库更新失败: {db_error}')
|
||||
success = False
|
||||
else:
|
||||
# 如果录制管理器没有返回数据库更新信息,则手动更新
|
||||
success = self.db_manager.update_session_status(session_id, 'recorded')
|
||||
|
||||
except Exception as rec_e:
|
||||
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
||||
# 即使录制停止失败,也尝试更新数据库状态
|
||||
success = self.db_manager.update_session_status(session_id, 'recorded')
|
||||
raise
|
||||
|
||||
if success:
|
||||
self.logger.info(f'检测会话已停止 - 会话ID: {session_id}')
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '检测已停止'
|
||||
})
|
||||
else:
|
||||
self.logger.error('停止检测失败,更新会话状态失败')
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '停止检测失败'
|
||||
}), 500
|
||||
|
||||
return jsonify({'success': True,'msg': '停止录制成功'})
|
||||
except Exception as e:
|
||||
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
@ -1475,8 +1454,8 @@ class AppServer:
|
||||
self.logger.error(f'保存会话信息失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/<session_id>/collect', methods=['POST'])
|
||||
def collect_detection_data(session_id):
|
||||
@self.app.route('/api/detection/<session_id>/save-data', methods=['POST'])
|
||||
def save_detection_data(session_id):
|
||||
"""采集检测数据"""
|
||||
try:
|
||||
if not self.db_manager:
|
||||
@ -1506,8 +1485,8 @@ class AppServer:
|
||||
'error': '无法获取患者ID'
|
||||
}), 400
|
||||
|
||||
# 调用录制管理器采集数据
|
||||
collected_data = self.recording_manager.collect_detection_data(
|
||||
# 调用录制管理器保存检测截图到文件
|
||||
collected_data = self.recording_manager.save_detection_images(
|
||||
session_id=session_id,
|
||||
patient_id=patient_id,
|
||||
detection_data=data
|
||||
@ -1543,15 +1522,6 @@ class AppServer:
|
||||
sessions = self.db_manager.get_detection_sessions(page, size, patient_id)
|
||||
total = self.db_manager.get_sessions_count(patient_id)
|
||||
|
||||
# 为每个会话补充最新的检测数据
|
||||
for session in sessions:
|
||||
session_id = session.get('id')
|
||||
if session_id:
|
||||
latest_data = self.db_manager.get_latest_detection_data(session_id, 5)
|
||||
session['latest_detection_data'] = latest_data
|
||||
else:
|
||||
session['latest_detection_data'] = []
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
@ -1584,50 +1554,40 @@ class AppServer:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
|
||||
@self.app.route('/api/detection/data/<session_id>/latest', methods=['GET'])
|
||||
def get_latest_detection_data(session_id):
|
||||
"""获取最新的检测数据"""
|
||||
@self.app.route('/api/detection/data/details', methods=['GET'])
|
||||
def get_detection_data_by_ids():
|
||||
"""根据多个主键ID查询检测数据详情,ids为逗号分隔"""
|
||||
try:
|
||||
limit = int(flask_request.args.get('limit', 5))
|
||||
data = self.db_manager.get_latest_detection_data(session_id, limit)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': data
|
||||
})
|
||||
|
||||
ids_param = flask_request.args.get('ids')
|
||||
if not ids_param:
|
||||
return jsonify({'success': False, 'error': '缺少ids参数'}), 400
|
||||
ids = [i.strip() for i in ids_param.split(',') if i.strip()]
|
||||
data_list = self.db_manager.get_detection_data_by_ids(ids)
|
||||
return jsonify({'success': True, 'data': data_list})
|
||||
except Exception as e:
|
||||
self.logger.error(f'获取最新检测数据失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/data/detail/<data_id>', methods=['GET'])
|
||||
def get_detection_data_by_id(data_id):
|
||||
"""根据主键ID查询检测数据详情"""
|
||||
try:
|
||||
data = self.db_manager.get_detection_data_by_id(data_id)
|
||||
if data is None:
|
||||
return jsonify({'success': False, 'error': '检测数据不存在'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'获取检测数据详情失败: {e}')
|
||||
self.logger.error(f'批量获取检测数据失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/data/<data_id>', methods=['DELETE'])
|
||||
def delete_detection_data(data_id):
|
||||
"""删除检测数据记录"""
|
||||
"""删除检测数据记录(支持单个或多个ID,多个用逗号分隔)"""
|
||||
try:
|
||||
self.db_manager.delete_detection_data(data_id)
|
||||
if not data_id:
|
||||
return jsonify({'success': False, 'error': '未提供检测数据ID'}), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '检测数据删除成功'
|
||||
})
|
||||
# 支持批量:逗号分隔
|
||||
ids = [i.strip() for i in str(data_id).split(',') if i.strip()]
|
||||
payload = ids if len(ids) > 1 else (ids[0] if ids else data_id)
|
||||
|
||||
success = self.db_manager.delete_detection_data(payload)
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '检测数据删除成功',
|
||||
'deleted_ids': ids
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '检测数据删除失败'}), 500
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 404
|
||||
@ -1635,6 +1595,33 @@ class AppServer:
|
||||
self.logger.error(f'删除检测数据失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/video/<video_id>', methods=['DELETE'])
|
||||
def delete_detection_video(video_id):
|
||||
"""删除检测视频记录(支持单个或多个ID,多个用逗号分隔)"""
|
||||
try:
|
||||
if not video_id:
|
||||
return jsonify({'success': False, 'error': '未提供检测视频ID'}), 400
|
||||
|
||||
# 支持批量:逗号分隔
|
||||
ids = [i.strip() for i in str(video_id).split(',') if i.strip()]
|
||||
payload = ids if len(ids) > 1 else (ids[0] if ids else video_id)
|
||||
|
||||
success = self.db_manager.delete_detection_video(payload)
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '检测视频删除成功',
|
||||
'deleted_ids': ids
|
||||
})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': '检测视频删除失败'}), 500
|
||||
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 404
|
||||
except Exception as e:
|
||||
self.logger.error(f'删除检测视频失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@self.app.route('/api/detection/sessions/<session_id>', methods=['DELETE'])
|
||||
def delete_detection_session(session_id):
|
||||
"""删除检测会话及其相关的检测数据"""
|
||||
|
||||
@ -222,8 +222,13 @@ class DataValidator:
|
||||
|
||||
# 性别验证
|
||||
if data.get('gender'):
|
||||
if data['gender'] not in ['male', 'female', 'other']:
|
||||
errors.append('性别值无效')
|
||||
# 支持中文和英文性别值
|
||||
gender_map = {'男': 'male', '女': 'female', 'male': 'male', 'female': 'female'}
|
||||
gender_value = data['gender'].strip()
|
||||
if gender_value in gender_map:
|
||||
data['gender'] = gender_map[gender_value]
|
||||
else:
|
||||
errors.append('性别值无效,应为:男、女、male、female')
|
||||
|
||||
# 出生日期验证
|
||||
if data.get('birth_date'):
|
||||
|
||||
259
document/Web接口调用说明.md
Normal file
259
document/Web接口调用说明.md
Normal file
@ -0,0 +1,259 @@
|
||||
# Web 接口调用说明
|
||||
|
||||
本文档基于 `backend/main.py` 中注册的路由,整理对外提供的 Web API 调用方式、参数与返回示例,便于前端或第三方系统集成。
|
||||
|
||||
## 总览
|
||||
|
||||
- 基础与健康检查
|
||||
- `GET /health`
|
||||
- `GET /api/health`
|
||||
- 授权相关
|
||||
- `GET /api/license/info`
|
||||
- `POST /api/license/activation-request`
|
||||
- `POST /api/license/verify`
|
||||
- `POST /api/license/activate-package`
|
||||
- 认证与用户
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/register`
|
||||
- `POST /api/auth/logout`
|
||||
- `GET /api/auth/verify`
|
||||
- `POST /api/auth/forgot-password`
|
||||
- `GET /api/users`
|
||||
- `POST /api/users/<user_id>/approve`
|
||||
- `DELETE /api/users/<user_id>`
|
||||
- 设备与配置
|
||||
- `GET /api/devices/status`
|
||||
- `POST /api/devices/refresh`
|
||||
- `GET /api/config/devices`
|
||||
- `POST /api/config/devices/all`
|
||||
- `POST /api/devices/calibrate`
|
||||
- `POST /api/devices/calibrate/imu`
|
||||
- 患者管理
|
||||
- `GET /api/patients`
|
||||
- `GET|PUT|DELETE /api/patients/<patient_id>`
|
||||
- 检测流程
|
||||
- `POST /api/detection/start`
|
||||
- `GET /api/detection/<session_id>/has_data`
|
||||
- `POST /api/detection/<session_id>/start_record`
|
||||
- `POST /api/detection/<session_id>/stop_record`
|
||||
- `POST /api/detection/<session_id>/save-data`
|
||||
- `POST /api/detection/<session_id>/save-info`
|
||||
- `GET /api/detection/<session_id>/status`
|
||||
- `POST /api/detection/<session_id>/stop`
|
||||
- 历史与数据查询
|
||||
- `GET /api/history/sessions`
|
||||
- `GET /api/history/sessions/<session_id>`
|
||||
- `GET /api/detection/data/details?ids=<id1,id2,...>`
|
||||
- 删除操作
|
||||
- `DELETE /api/detection/data/<data_id[,data_id2,...]>`
|
||||
- `DELETE /api/detection/video/<video_id[,video_id2,...]>`
|
||||
- `DELETE /api/detection/sessions/<session_id>`
|
||||
|
||||
## 统一响应约定
|
||||
|
||||
- 成功:`{ "success": true, ... }`
|
||||
- 失败:`{ "success": false, "error": "错误信息" }`
|
||||
- 时间字段统一使用 ISO 文本或 `YYYY-MM-DD HH:mm:ss` 字符串。
|
||||
|
||||
## 基础与授权
|
||||
|
||||
### GET /health | GET /api/health
|
||||
- 功能:健康检查与服务存活状态。
|
||||
- 示例响应:
|
||||
```json
|
||||
{ "status": "healthy", "timestamp": "2024-01-01T12:00:00", "version": "1.0.0" }
|
||||
```
|
||||
|
||||
### GET /api/license/info
|
||||
- 功能:获取授权状态与基础信息。
|
||||
- 响应字段:`valid`, `message`, `license_type`, `license_id`, `expires_at`, `features`, `machine_id`。
|
||||
|
||||
### POST /api/license/activation-request
|
||||
- 功能:生成离线激活请求文件。
|
||||
- Body:`{ "company_name": "公司名", "contact_info": "联系方式" }`
|
||||
- 返回:`request_file` 路径与 `content` 文本。
|
||||
|
||||
### POST /api/license/verify
|
||||
- 功能:上传并验证授权文件。
|
||||
- Form-Data:`license_file`(文件)。
|
||||
- 返回:授权验证结果与解析出的授权信息。
|
||||
|
||||
### POST /api/license/activate-package
|
||||
- 功能:上传激活包进行离线激活。
|
||||
- 细节见服务端实现,返回激活状态与信息。
|
||||
|
||||
## 认证与用户
|
||||
|
||||
### POST /api/auth/login
|
||||
- 功能:用户登录。
|
||||
- Body:`{ "username": "string", "password": "string" }`
|
||||
|
||||
### POST /api/auth/register
|
||||
- 功能:用户注册。
|
||||
- Body:包含用户名、密码、手机号等注册信息。
|
||||
|
||||
### POST /api/auth/logout
|
||||
- 功能:登出当前会话。
|
||||
|
||||
### GET /api/auth/verify
|
||||
- 功能:登录状态校验。
|
||||
|
||||
### POST /api/auth/forgot-password
|
||||
- 功能:忘记密码,根据用户名和手机号找回。
|
||||
- Body:`{ "username": "string", "phone": "string" }`
|
||||
|
||||
### GET /api/users
|
||||
- 功能:获取用户列表。
|
||||
- Query 可选:分页参数。
|
||||
|
||||
### POST /api/users/<user_id>/approve
|
||||
- 功能:批准用户,使其 `is_active = 1`。
|
||||
- Body 可选:`{ "approved_by": 123 }`
|
||||
|
||||
### DELETE /api/users/<user_id>
|
||||
- 功能:删除用户。
|
||||
|
||||
## 患者管理
|
||||
|
||||
### GET /api/patients
|
||||
- 功能:获取患者列表。
|
||||
- Query 可选:分页筛选。
|
||||
|
||||
### POST /api/patients
|
||||
- 功能:创建新患者。
|
||||
- Body:患者基本信息字段(姓名、性别、出生日期等)。
|
||||
- 必填字段:`name`(姓名)、`gender`(性别)、`birth_date`(出生日期)
|
||||
- 可选字段:`phone`(电话)、`email`(邮箱)、`height`(身高)、`weight`(体重)、`nationality`(民族)、`residence`(居住地)、`occupation`(职业)、`workplace`(工作单位)、`idcode`(身份证号)、`medical_history`(病史)、`notes`(备注)
|
||||
|
||||
### GET|PUT|DELETE /api/patients/<patient_id>
|
||||
- 功能:获取/更新/删除单个患者。
|
||||
- PUT Body:患者基本信息字段(姓名、性别、出生日期、联系方式等)。
|
||||
|
||||
## 设备与配置
|
||||
|
||||
### GET /api/devices/status
|
||||
- 功能:获取各设备连接与工作状态。
|
||||
|
||||
### POST /api/devices/refresh
|
||||
- 功能:刷新设备状态(重扫/重连)。
|
||||
|
||||
### GET /api/config/devices
|
||||
- 功能:获取当前设备配置。
|
||||
|
||||
### POST /api/config/devices/all
|
||||
- 功能:批量设置设备配置。
|
||||
- Body:设备配置对象集合。
|
||||
|
||||
### POST /api/devices/calibrate
|
||||
- 功能:触发设备标定(通用)。
|
||||
|
||||
### POST /api/devices/calibrate/imu
|
||||
- 功能:仅触发 IMU 设备标定。
|
||||
|
||||
## 检测流程
|
||||
|
||||
### POST /api/detection/start
|
||||
- 功能:创建检测会话并启动设备连接监控。
|
||||
- Body:`{ "patient_id": "string", "creator_id": "string" }`
|
||||
- 返回:`session_id`。
|
||||
|
||||
### GET /api/detection/<session_id>/has_data
|
||||
- 功能:会话是否存在检测数据,用于判断单次检测是否有效。
|
||||
- 返回:`{ "has_data": true|false }`
|
||||
### GET /api/detection/<session_id>/status
|
||||
- 功能:获取会话最新状态与聚合数据(源自 `get_session_data`)。
|
||||
|
||||
### POST /api/detection/<session_id>/stop
|
||||
- 功能:结束检测并写入总结信息(自动计算时长与结束时间)。
|
||||
- Body 可选:
|
||||
```json
|
||||
{
|
||||
"diagnosis_info": "string",
|
||||
"treatment_info": "string",
|
||||
"remark_info": "string"
|
||||
}
|
||||
```
|
||||
- 说明:服务端已统一走 `update_session_endcheck` 数据库流程。
|
||||
### POST /api/detection/<session_id>/start_record
|
||||
- 功能:开始同步录制(屏幕/相机/设备流)。
|
||||
- Body 示例:
|
||||
```json
|
||||
{
|
||||
"patient_id": "p001",
|
||||
"screen_location": [0,0,1920,1080],
|
||||
"femtobolt_location": [0,0,640,480],
|
||||
"camera1_location": [0,0,640,480],
|
||||
"camera2_location": [0,0,640,480]
|
||||
}
|
||||
```
|
||||
- 返回:`recording` 含 `database_updates.video_paths`;服务端会自动调用 `save_detection_video` 记录视频路径(`screen_video_path`, `femtobolt_video_path`, `camera1_video_path`, `camera2_video_path`)。
|
||||
|
||||
### POST /api/detection/<session_id>/stop_record
|
||||
- 功能:停止同步录制。
|
||||
|
||||
### POST /api/detection/<session_id>/save-data
|
||||
- 功能:采集检测数据与截图并持久化。
|
||||
- Body:检测数据载荷(结构由实际采集模块决定)。
|
||||
- 返回:`timestamp` 与是否采集成功。
|
||||
|
||||
### POST /api/detection/<session_id>/save-info
|
||||
- 功能:保存会话信息(诊断、处理、建议、状态)。
|
||||
- Body:
|
||||
```json
|
||||
{
|
||||
"diagnosis_info": "string",
|
||||
"treatment_info": "string",
|
||||
"remark_info": "string",
|
||||
"status": "completed|running|..."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 历史与数据查询
|
||||
|
||||
### GET /api/history/sessions
|
||||
- 功能:分页获取检测会话历史。
|
||||
- Query:`page`(默认 1)、`size`(默认 10)、`patient_id`(可选)。
|
||||
- 返回:`sessions` 与 `total`。
|
||||
|
||||
### GET /api/history/sessions/<session_id>
|
||||
- 功能:获取会话详细数据(聚合会话、检测数据、视频)。
|
||||
|
||||
### GET /api/detection/data/details?ids=<id1,id2,...>
|
||||
- 功能:根据多个逗号分隔的 ID 批量获取检测数据详情。
|
||||
- Query:`ids` 逗号分隔字符串。
|
||||
|
||||
## 删除操作
|
||||
|
||||
### DELETE /api/detection/data/<data_id[,data_id2,...]>
|
||||
- 功能:删除检测数据记录,支持多个 ID 逗号分隔。
|
||||
- 返回:`deleted_ids` 列表。
|
||||
|
||||
### DELETE /api/detection/video/<video_id[,video_id2,...]>
|
||||
- 功能:删除检测视频记录,支持多个 ID 逗号分隔。
|
||||
- 返回:`deleted_ids` 列表。
|
||||
|
||||
### DELETE /api/detection/sessions/<session_id>
|
||||
- 功能:删除检测会话,同时清理关联的检测数据与视频记录。
|
||||
|
||||
## 错误码与常见失败
|
||||
|
||||
- 400:缺少必要参数或请求体格式错误。
|
||||
- 403:授权校验失败或未授权功能。
|
||||
- 404:会话或资源不存在。
|
||||
- 500:服务内部错误(设备不可用、数据库失败等)。
|
||||
|
||||
## 典型调用序列(示例)
|
||||
|
||||
1. 创建会话:`POST /api/detection/start`
|
||||
2. 开始录制:`POST /api/detection/<session_id>/start_record`
|
||||
3. 采集数据:多次 `POST /api/detection/<session_id>/save-data`
|
||||
4. 停止录制:`POST /api/detection/<session_id>/stop_record`
|
||||
5. 保存诊断建议:`POST /api/detection/<session_id>/save-info`
|
||||
6. 结束检测:`POST /api/detection/<session_id>/stop`
|
||||
7. 校验有效性:`GET /api/detection/<session_id>/has_data`
|
||||
8. 查询历史:`GET /api/history/sessions`
|
||||
9. 查看详情:`GET /api/history/sessions/<session_id>`
|
||||
|
||||
> 注:具体字段与数据结构以采集模块与数据库模型为准,本文档以主干流程为纲要,建议结合实际返回进行前端适配与校验。
|
||||
@ -357,9 +357,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="videoImgRef" style="width: 100%;height: calc(100% - 47px)">
|
||||
<img :src="(cameraStatus === '已连接' && rtspImgSrc) ? rtspImgSrc : noImageSvg" alt=""
|
||||
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
|
||||
<div ref="videoImgRef" style="width: 100%;height: calc(100% - 47px); display: flex; gap: 4px;">
|
||||
<div ref="camera1Ref" style="flex: 1; height: 100%; position: relative;">
|
||||
<img :src="(cameraStatus === '已连接' && camera1ImgSrc) ? camera1ImgSrc : noImageSvg" alt="camera1"
|
||||
style="width: 100%; height: 100%; object-fit: contain; background:#323232;" />
|
||||
<div style="position:absolute; left:6px; top:6px; padding:2px 6px; font-size:12px; color:#fff; background:rgba(0,0,0,0.4); border-radius:4px;">
|
||||
相机1
|
||||
</div>
|
||||
</div>
|
||||
<div ref="camera2Ref" style="flex: 1; height: 100%; position: relative;">
|
||||
<img :src="(cameraStatus === '已连接' && camera2ImgSrc) ? camera2ImgSrc : noImageSvg" alt="camera2"
|
||||
style="width: 100%; height: 100%; object-fit: contain; background:#323232;" />
|
||||
<div style="position:absolute; left:6px; top:6px; padding:2px 6px; font-size:12px; color:#fff; background:rgba(0,0,0,0.4); border-radius:4px;">
|
||||
相机2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 使用img元素显示视频流(优化的Data URL方案) -->
|
||||
|
||||
@ -567,7 +579,7 @@
|
||||
<div v-if="isBig" style="position: fixed;top: 0;right: 0;
|
||||
width: 100%;height: 100vh;z-index: 9999;background: red;border: 2px solid #b0b0b0">
|
||||
<svg @click="isBig=false" style="position: absolute;right: 10px;top:10px;cursor: pointer;" t="1760175800150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5743" width="24" height="24"><path d="M796 163.1L511.1 448l-285-285-63.9 64 285 285-285 285 63.8 63.8 285-285 285 285 63.8-63.8-285-285 285-285-63.8-63.9z" fill="#ffffff" p-id="5744"></path></svg>
|
||||
<img v-if="isBig" :src="(cameraStatus === '已连接' && rtspImgSrc) ? rtspImgSrc : noImageSvg" alt=""
|
||||
<img v-if="isBig" :src="(cameraStatus === '已连接' && camera1ImgSrc) ? camera1ImgSrc : noImageSvg" alt=""
|
||||
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
|
||||
</div>
|
||||
</div>
|
||||
@ -591,12 +603,16 @@ const route = useRoute()
|
||||
const isRecording = ref(false)
|
||||
const isConnected = ref(false)
|
||||
const rtspImgSrc = ref('')
|
||||
const camera1ImgSrc = ref('')
|
||||
const camera2ImgSrc = ref('')
|
||||
const depthCameraImgSrc = ref('') // 深度相机视频流
|
||||
const screenshotLoading = ref(false)
|
||||
const cameraDialogVisible =ref(false) // 设置相机参数弹框
|
||||
const contenGridRef =ref(null) // 实时检查整体box
|
||||
const wholeBodyRef = ref(null) // 身体姿态ref
|
||||
const videoImgRef =ref(null) // 视频流图片ref
|
||||
const camera1Ref = ref(null)
|
||||
const camera2Ref = ref(null)
|
||||
const historyDialogVisible = ref(false)
|
||||
// 录像相关变量
|
||||
let mediaRecorder = null
|
||||
@ -1084,8 +1100,19 @@ function connectWebSocket() {
|
||||
// 监听各设备数据事件
|
||||
devicesSocket.on('camera_frame', (data) => {
|
||||
frameCount++
|
||||
tempInfo.value.camera_frame = data
|
||||
displayFrame(data.image)
|
||||
// 区分 camera1 / camera2 帧
|
||||
const devId = (data && data.device_id) ? String(data.device_id).toLowerCase() : ''
|
||||
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 为空)
|
||||
tempInfo.value.camera_frames['camera1'] = data
|
||||
displayCameraFrameById('camera1', data.image)
|
||||
}
|
||||
})
|
||||
|
||||
devicesSocket.on('femtobolt_frame', (data) => {
|
||||
@ -1238,8 +1265,20 @@ function reconnectWebSocket() {
|
||||
|
||||
// 简单的帧显示函数
|
||||
function displayFrame(base64Image) {
|
||||
// 兼容旧调用:默认作为 camera1 更新
|
||||
displayCameraFrameById('camera1', base64Image)
|
||||
}
|
||||
|
||||
function displayCameraFrameById(deviceId, base64Image) {
|
||||
if (base64Image && base64Image.length > 0) {
|
||||
rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image
|
||||
const url = 'data:image/jpeg;base64,' + base64Image
|
||||
if (String(deviceId).toLowerCase() === 'camera2') {
|
||||
camera2ImgSrc.value = url
|
||||
} else {
|
||||
camera1ImgSrc.value = url
|
||||
// 旧变量保留(避免其它位置引用出错)
|
||||
rtspImgSrc.value = url
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 收到空的视频帧数据')
|
||||
}
|
||||
@ -2054,7 +2093,8 @@ const startRecord = async () => { // 开始录屏
|
||||
}
|
||||
let screen_location = contenGridRef.value.getBoundingClientRect()
|
||||
let femtobolt_location = wholeBodyRef.value.getBoundingClientRect()
|
||||
let camera_location = videoImgRef.value.getBoundingClientRect()
|
||||
let camera1_location = camera1Ref.value?.getBoundingClientRect()
|
||||
let camera2_location = camera2Ref.value?.getBoundingClientRect()
|
||||
let titile_height = 24
|
||||
// 调用后端API开始录屏
|
||||
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/start_record`, {
|
||||
@ -2067,7 +2107,14 @@ const startRecord = async () => { // 开始录屏
|
||||
// 可以添加其他录屏参数
|
||||
creator_id: creatorId.value,
|
||||
screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height)],
|
||||
camera_location:[Math.round(camera_location.x), Math.round(camera_location.y)+ titile_height, Math.round(camera_location.width), Math.round(camera_location.height)],
|
||||
camera1_location:[
|
||||
Math.round(camera1_location.x), Math.round(camera1_location.y)+ titile_height,
|
||||
Math.round(camera1_location.width), Math.round(camera1_location.height)
|
||||
],
|
||||
camera2_location:[
|
||||
Math.round(camera2_location.x), Math.round(camera2_location.y)+ titile_height,
|
||||
Math.round(camera2_location.width), Math.round(camera2_location.height)
|
||||
],
|
||||
femtobolt_location:[Math.round(femtobolt_location.x), Math.round(femtobolt_location.y) + titile_height, Math.round(femtobolt_location.width), Math.round(femtobolt_location.height)],
|
||||
|
||||
})
|
||||
|
||||
@ -258,21 +258,25 @@ const validateForm = async () => {
|
||||
}
|
||||
|
||||
const savePatient = async () => {
|
||||
// 性别值映射:中文转英文
|
||||
const genderMap = { '男': 'male', '女': 'female' }
|
||||
const genderValue = genderMap[patientForm.gender] || patientForm.gender
|
||||
|
||||
const patientData = {
|
||||
id: patientForm.id,
|
||||
name: patientForm.name,
|
||||
gender: patientForm.gender,
|
||||
age: calculatedAge.value,
|
||||
gender: genderValue,
|
||||
birth_date: patientForm.birth_date,
|
||||
height: patientForm.height,
|
||||
weight: patientForm.weight,
|
||||
shoe_size: patientForm.shoe_size,
|
||||
height: parseFloat(patientForm.height) || null,
|
||||
weight: parseFloat(patientForm.weight) || null,
|
||||
shoe_size: patientForm.shoe_size ? parseFloat(patientForm.shoe_size) : null,
|
||||
phone: patientForm.phone,
|
||||
occupation: patientForm.occupation,
|
||||
email: patientForm.email,
|
||||
nationality: patientForm.nationality,
|
||||
residence: patientForm.residence,
|
||||
workplace: patientForm.workplace
|
||||
workplace: patientForm.workplace,
|
||||
medical_history: '', // 添加病史字段
|
||||
notes: '' // 添加备注字段
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user