增加了两个相机的支持。

This commit is contained in:
root 2025-11-16 11:43:41 +08:00
parent c974350345
commit 96ba7c098a
14 changed files with 1323 additions and 1068 deletions

View File

@ -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"
}

View File

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

View File

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

View File

@ -113,10 +113,10 @@ class DeviceCoordinator:
# 注册Socket.IO命名空间
self._register_namespaces()
# 初始化设备
# 初始化设备(失败则降级继续)
if not self._initialize_devices():
self.logger.warning("设备初始化失败,将以降级模式继续运行")
# 启动监控线程
self._start_monitor()
@ -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
if camera.initialize():
# 构造实例覆盖配置:优先读取目标配置段,否则回退到 [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"]:
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}设备不支持推流停止")
# 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控停止
if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
self.logger.info(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:

View File

@ -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(
@ -386,23 +413,24 @@ class RecordingManager:
femtobolt_video_path, fourcc, self.femtobolt_current_fps, (self.femtobolt_region[2], self.femtobolt_region[3])
)
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')
]

View File

@ -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'

View File

@ -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()

View File

@ -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}
}

View File

@ -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,15 +902,16 @@ 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}')
return jsonify({'success': False, 'error': str(e)}), 500
@ -952,48 +960,52 @@ 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}')
return jsonify({'success': False, 'error': str(e)}), 500
@ -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')
}
@ -1076,21 +1089,8 @@ class AppServer:
except Exception as e:
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
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 设备配置API ====================
@ -1222,6 +1222,19 @@ class AppServer:
except Exception as e:
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('停止检测失败,更新会话状态失败')
@ -1284,32 +1295,35 @@ 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]
screen_location = data.get('screen_location') # [0,0,1920,1080]
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"]}')
# 更新视频文件路径
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'])
# 保存检测视频记录(映射到 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'),
}
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
}), 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')
self.logger.info(f'停止录制结果: {restrt}')
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
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
raise
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
@ -1542,16 +1521,7 @@ 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': {
@ -1583,57 +1553,74 @@ class AppServer:
self.logger.error(f'获取会话数据失败: {e}')
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)
return jsonify({
'success': True,
'message': '检测数据删除成功'
})
if not data_id:
return jsonify({'success': False, 'error': '未提供检测数据ID'}), 400
# 支持批量:逗号分隔
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
except Exception as e:
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):

View File

@ -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'):

View 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>`
> 注:具体字段与数据结构以采集模块与数据库模型为准,本文档以主干流程为纲要,建议结合实际返回进行前端适配与校验。

View File

@ -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)],
})

View File

@ -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 {