diff --git a/backend/app.py b/backend/app.py index 157a24d5..e721813b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -618,47 +618,28 @@ def stop_video_streaming(): def start_detection(): """开始检测""" try: - if not db_manager: - return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 + if not db_manager or not device_manager: + return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 data = flask_request.get_json() - patient_id = data.get('patientId') or data.get('patient_id') - settings = data.get('settings', '{}') + patient_id = data.get('patient_id') creator_id = data.get('creator_id') + if not patient_id or not creator_id: + return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400 - if not patient_id: - return jsonify({ - 'success': False, - 'error': '缺少必要参数: patient_id' - }), 400 + # 调用create_detection_session方法,settings传空字典 + session_id = db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) - # 解析设置参数 - if isinstance(settings, str): - try: - settings = json.loads(settings) - except json.JSONDecodeError: - settings = {} + # 开始同步录制 + recording_response = None + try: + recording_response = device_manager.start_recording(session_id, patient_id) + except Exception as rec_e: + logger.error(f'开始同步录制失败: {rec_e}') - # 创建检测会话 - session_id = db_manager.create_detection_session( - patient_id=str(patient_id), - settings=settings, - creator_id=creator_id - ) + start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - if session_id: - logger.info(f'检测会话已创建 - 会话ID: {session_id}, 患者ID: {patient_id}') - return jsonify({ - 'success': True, - 'session_id': session_id, - 'message': '检测会话创建成功' - }) - else: - return jsonify({ - 'success': False, - 'error': '创建检测会话失败' - }), 500 - + return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response}) except Exception as e: logger.error(f'开始检测失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @@ -667,15 +648,50 @@ def start_detection(): def stop_detection(session_id): """停止检测""" try: - if not db_manager: - return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 + if not db_manager or not device_manager: + logger.error('数据库管理器或设备管理器未初始化') + return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 if not session_id: + logger.error('缺少会话ID') return jsonify({ 'success': False, 'error': '缺少会话ID' }), 400 + data = flask_request.get_json() + logger.debug(f'接收到停止检测请求,session_id: {session_id}, 请求数据: {data}') + # video_data = data.get('videoData') if data else None + video_data = data['videoData'] + mime_type = data.get('mimeType', 'video/webm;codecs=vp9') # 默认webm格式 + + # 验证base64视频数据格式 + if not video_data.startswith('data:video/'): + return jsonify({ + 'success': False, + 'message': '无效的视频数据格式' + }), 400 + # try: + # header, encoded = video_data.split(',', 1) + # video_bytes = base64.b64decode(encoded) + # except Exception as e: + # return jsonify({ + # 'success': False, + # 'message': f'视频数据解码失败: {str(e)}' + # }), 400 + # 停止同步录制,传递视频数据 + try: + logger.debug(f'调用device_manager.stop_recording,session_id: {session_id}, video_data长度: {len(video_data) if video_data else 0}') + if video_data is None: + logger.warning(f'视频数据为空,session_id: {session_id}') + else: + logger.debug(f'视频数据长度: {len(video_data)} 字符,约 {len(video_data)*3/4/1024:.2f} KB, session_id: {session_id}') + restrt=device_manager.stop_recording(session_id, video_data_base64=video_data) + logger.error(restrt) + except Exception as rec_e: + logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) + raise + # 更新会话状态为已完成 success = db_manager.update_session_status(session_id, 'completed') @@ -686,13 +702,14 @@ def stop_detection(session_id): 'message': '检测已停止' }) else: + logger.error('停止检测失败,更新会话状态失败') return jsonify({ 'success': False, 'error': '停止检测失败' }), 500 except Exception as e: - logger.error(f'停止检测失败: {e}') + logger.error(f'停止检测失败: {e}', exc_info=True) return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/detection//status', methods=['GET']) @@ -823,6 +840,7 @@ def stop_sync_recording(): data = flask_request.get_json() session_id = data.get('session_id') + video_data = data.get('videoData') # 新增接收前端传递的视频数据 if not session_id: return jsonify({ @@ -830,20 +848,19 @@ def stop_sync_recording(): 'error': '缺少必要参数: session_id' }), 400 - result = device_manager.stop_recording(session_id) + result = device_manager.stop_recording(session_id, video_data_base64=video_data) if result['success']: logger.info(f'同步录制已停止 - 会话ID: {session_id}') return jsonify(result) else: return jsonify(result), 500 - + except Exception as e: logger.error(f'停止同步录制失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 - @app.route('/api/history/sessions', methods=['GET']) def get_detection_sessions(): """获取检测会话历史""" diff --git a/backend/data/body_balance.db b/backend/data/body_balance.db index 957f42e9..278245b9 100644 Binary files a/backend/data/body_balance.db and b/backend/data/body_balance.db differ diff --git a/backend/database.py b/backend/database.py index 8a1eae6b..89170f6e 100644 --- a/backend/database.py +++ b/backend/database.py @@ -632,21 +632,19 @@ class DatabaseManager: cursor.execute(''' INSERT INTO detection_sessions ( - id, patient_id, creator_id, duration, frequency, settings, status, - diagnosis_info, treatment_info, suggestion_info, notes, start_time, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + id, patient_id, creator_id, duration, settings, status, + diagnosis_info, treatment_info, suggestion_info, start_time, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( session_id, patient_id, creator_id, settings.get('duration', 60), - settings.get('frequency', 60), json.dumps(settings), - 'created', + 'running', settings.get('diagnosis_info', ''), settings.get('treatment_info', ''), settings.get('suggestion_info', ''), - settings.get('notes', ''), china_time, china_time )) @@ -660,7 +658,7 @@ class DatabaseManager: logger.error(f'创建检测会话失败: {e}') raise - def update_session_status(self, session_id: str, status: str, data_points: int = 0): + def update_session_status(self, session_id: str, status: str): """更新会话状态""" conn = self.get_connection() cursor = conn.cursor() @@ -671,15 +669,15 @@ class DatabaseManager: china_time = self.get_china_time() cursor.execute(''' UPDATE detection_sessions SET - status = ?, data_points = ?, end_time = ? + status = ?, end_time = ? WHERE id = ? - ''', (status, data_points, china_time, session_id)) + ''', (status, china_time, session_id)) else: cursor.execute(''' UPDATE detection_sessions SET - status = ?, data_points = ? + status = ? WHERE id = ? - ''', (status, data_points, session_id)) + ''', (status, session_id)) conn.commit() logger.info(f'更新会话状态: {session_id} -> {status}') diff --git a/backend/device_manager.py b/backend/device_manager.py index 8023f7aa..7f10b1c6 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -24,6 +24,7 @@ from concurrent.futures import ThreadPoolExecutor import logging # 数据库管理 +# from backend.app import get_detection_sessions from database import DatabaseManager # FemtoBolt深度相机支持 @@ -265,10 +266,14 @@ class DeviceManager: if platform.system() == "Windows": # 优先使用Orbbec SDK K4A Wrapper(与azure_kinect_image_example.py一致) - orbbec_paths = [ - r"D:\OrbbecSDK_K4A_Wrapper_v1.10.3_windows_202408091749\bin\k4a.dll", - ] - + base_dir = os.path.dirname(os.path.abspath(__file__)) + dll_path = os.path.join(base_dir, "dll", "bin", "k4a.dll") + orbbec_paths = [] + if os.path.exists(dll_path): + orbbec_paths.append(dll_path) + # orbbec_paths = [ + # r"D:\BodyBalanceEvaluation\backend\dll\bin\k4a.dll", + # ] # Azure Kinect SDK标准安装路径(备用) standard_paths = [ r"C:\Program Files\Azure Kinect SDK v1.4.1\sdk\windows-desktop\amd64\release\bin\k4a.dll", @@ -498,54 +503,80 @@ class DeviceManager: } try: - # 1. 采集头部姿态数据(从IMU设备获取) - if self.device_status['imu']: - head_pose_data = self._collect_head_pose_data() - if head_pose_data: - data['head_pose'] = json.dumps(head_pose_data) - logger.debug(f'头部姿态数据采集成功: {session_id}') + # # 1. 采集头部姿态数据(从IMU设备获取) + # if self.device_status['imu']: + # head_pose_data = self._collect_head_pose_data() + # if head_pose_data: + # data['head_pose'] = json.dumps(head_pose_data) + # logger.debug(f'头部姿态数据采集成功: {session_id}') - # 2. 采集身体姿态数据(从FemtoBolt深度相机获取) - if self.device_status['femtobolt']: - body_pose_data = self._collect_body_pose_data() - if body_pose_data: - data['body_pose'] = json.dumps(body_pose_data) - logger.debug(f'身体姿态数据采集成功: {session_id}') + # # 2. 采集身体姿态数据(从FemtoBolt深度相机获取) + # if self.device_status['femtobolt']: + # body_pose_data = self._collect_body_pose_data() + # if body_pose_data: + # data['body_pose'] = json.dumps(body_pose_data) + # logger.debug(f'身体姿态数据采集成功: {session_id}') - # 3. 采集身体视频截图(从FemtoBolt深度相机获取) - if self.device_status['femtobolt']: - body_image_path = self._capture_body_image(data_dir) - if body_image_path: - data['body_image'] = str(body_image_path) - logger.debug(f'身体截图保存成功: {body_image_path}') + # # 3. 采集身体视频截图(从FemtoBolt深度相机获取) + # if self.device_status['femtobolt']: + # body_image_path = self._capture_body_image(data_dir) + # if body_image_path: + # data['body_image'] = str(body_image_path) + # logger.debug(f'身体截图保存成功: {body_image_path}') - # 4. 采集足部压力数据(从压力传感器获取) - if self.device_status['pressure']: - foot_data = self._collect_foot_pressure_data() - if foot_data: - data['foot_data'] = json.dumps(foot_data) - logger.debug(f'足部压力数据采集成功: {session_id}') + # # 4. 采集足部压力数据(从压力传感器获取) + # if self.device_status['pressure']: + # foot_data = self._collect_foot_pressure_data() + # if foot_data: + # data['foot_data'] = json.dumps(foot_data) + # logger.debug(f'足部压力数据采集成功: {session_id}') - # 5. 采集足部监测视频截图(从摄像头获取) - if self.device_status['camera']: - foot_image_path = self._capture_foot_image(data_dir) - if foot_image_path: - data['foot_image'] = str(foot_image_path) - logger.debug(f'足部截图保存成功: {foot_image_path}') + # # 5. 采集足部监测视频截图(从摄像头获取) + # if self.device_status['camera']: + # foot_image_path = self._capture_foot_image(data_dir) + # if foot_image_path: + # data['foot_image'] = str(foot_image_path) + # logger.debug(f'足部截图保存成功: {foot_image_path}') - # 6. 生成足底压力数据图(从压力传感器数据生成) - if self.device_status['pressure']: - foot_data_image_path = self._generate_foot_pressure_image(data_dir) - if foot_data_image_path: - data['foot_data_image'] = str(foot_data_image_path) - logger.debug(f'足底压力数据图生成成功: {foot_data_image_path}') + # # 6. 生成足底压力数据图(从压力传感器数据生成) + # if self.device_status['pressure']: + # foot_data_image_path = self._generate_foot_pressure_image(data_dir) + # if foot_data_image_path: + # data['foot_data_image'] = str(foot_data_image_path) + # logger.debug(f'足底压力数据图生成成功: {foot_data_image_path}') # 7. 保存屏幕录制截图(从前端传入的base64数据) if screen_image_base64: - screen_image_path = self._save_screen_image(data_dir, screen_image_base64) - if screen_image_path: - data['screen_image'] = str(screen_image_path) - logger.debug(f'屏幕截图保存成功: {screen_image_path}') + try: + logger.debug(f'屏幕截图保存.................{screen_image_base64}') + # 保存屏幕截图的base64数据为图片文件 + screen_image_path = None + if screen_image_base64: + try: + if screen_image_base64.startswith('data:image/'): + base64_data = screen_image_base64.split(',')[1] + else: + base64_data = screen_image_base64 + image_data = base64.b64decode(base64_data) + image_path = data_dir / 'screen_image.png' + with open(image_path, 'wb') as f: + f.write(image_data) + abs_image_path = image_path.resolve() + abs_cwd = Path.cwd().resolve() + screen_image_path = str(abs_image_path.relative_to(abs_cwd)) + logger.debug(f'屏幕截图保存成功: {screen_image_path}') + except Exception as e: + logger.error(f'屏幕截图保存失败: {e}') + import traceback + logger.error(traceback.format_exc()) + + if screen_image_path: + data['screen_image'] = str(screen_image_path) + logger.debug(f'屏幕截图保存成功: {screen_image_path}') + except Exception as e: + logger.error(f'屏幕截图保存失败: {e}') + import traceback + logger.error(traceback.format_exc()) # 更新最新数据 with self.data_lock: @@ -1073,10 +1104,10 @@ class DeviceManager: body_video_path, fourcc, fps, (1280, 720) ) - # 屏幕录制写入器(默认分辨率,后续根据实际帧调整) - self.screen_video_writer = cv2.VideoWriter( - screen_video_path, fourcc, fps, (1920, 1080) - ) + # # 屏幕录制写入器(默认分辨率,后续根据实际帧调整) + # self.screen_video_writer = cv2.VideoWriter( + # screen_video_path, fourcc, fps, (1920, 1080) + # ) # 重置停止事件 self.recording_stop_event.clear() @@ -1097,14 +1128,14 @@ class DeviceManager: name='BodyRecordingThread' ) self.body_recording_thread.start() - #屏幕录制 - if self.screen_video_writer: - self.screen_recording_thread = threading.Thread( - target=self._screen_recording_thread, - daemon=True, - name='ScreenRecordingThread' - ) - self.screen_recording_thread.start() + # #屏幕录制 + # if self.screen_video_writer: + # self.screen_recording_thread = threading.Thread( + # target=self._screen_recording_thread, + # daemon=True, + # name='ScreenRecordingThread' + # ) + # self.screen_recording_thread.start() # 设置录制状态 self.sync_recording = True @@ -1122,21 +1153,15 @@ class DeviceManager: return result - def stop_recording(self, session_id: str) -> Dict[str, Any]: + def stop_recording(self, session_id: str, video_data_base64: str = None) -> Dict[str, Any]: """停止同步录制 Args: session_id: 检测会话ID - + video_data_base64: 屏幕录制视频的base64编码数据,可选 + Returns: Dict: 录制停止状态和信息 - { - 'success': bool, - 'session_id': str, - 'recording_duration': float, - 'video_files': List[str], - 'message': str - } """ result = { 'success': False, @@ -1158,12 +1183,18 @@ class DeviceManager: # 设置停止事件 self.recording_stop_event.set() + session_data = self.db_manager.get_session_data(session_id) + base_path = os.path.join('data', 'patients', session_data['patient_id'], session_id) + + # 定义视频文件路径 + feet_video_path = os.path.join(base_path, 'feet.mp4') + body_video_path = os.path.join(base_path, 'body.mp4') + screen_video_path = os.path.join(base_path, 'screen.mp4') # 等待录制线程结束 threads_to_join = [ (self.feet_recording_thread, 'feet'), - (self.body_recording_thread, 'body'), - (self.screen_recording_thread, 'screen') + (self.body_recording_thread, 'body') ] for thread, name in threads_to_join: @@ -1179,16 +1210,30 @@ class DeviceManager: # 清理视频写入器并收集文件信息 video_files = self._cleanup_video_writers() + + # 保存传入的屏幕录制视频数据,替代原有屏幕录制视频保存逻辑 + if video_data_base64: + try: + video_bytes = base64.b64decode(video_data_base64) + with open(screen_video_path, 'wb') as f: + f.write(video_bytes) + video_files.append(screen_video_path) + logger.info(f'屏幕录制视频保存成功,路径: {screen_video_path}, 文件大小: {os.path.getsize(screen_video_path)} 字节') + except Exception as e: + logger.error(f'保存屏幕录制视频失败: {e}', exc_info=True) + logger.debug(f'视频数据长度: {len(video_data_base64)}') + raise + result['video_files'] = video_files # 更新数据库中的会话信息 if self.db_manager and result['recording_duration'] > 0: try: - # 更新会话持续时间 duration_seconds = int(result['recording_duration']) self.db_manager.update_session_duration(session_id, duration_seconds) - - # 更新会话状态为已完成 + self.db_manager.update_session_normal_video_path(session_id, feet_video_path) + self.db_manager.update_session_femtobolt_video_path(session_id, body_video_path) + self.db_manager.update_session_screen_video_path(session_id, screen_video_path) self.db_manager.update_session_status(session_id, 'completed') logger.debug(f'数据库会话信息更新成功 - 会话ID: {session_id}, 持续时间: {duration_seconds}秒') @@ -1207,7 +1252,7 @@ class DeviceManager: logger.debug(f'同步录制已停止 - 会话ID: {session_id}, 录制时长: {result["recording_duration"]:.2f}秒') except Exception as e: - logger.error(f'停止同步录制失败: {e}') + logger.error(f'停止同步录制失败: {e}', exc_info=True) result['message'] = f'停止录制失败: {str(e)}' return result