From 03c3f0a6c9201eab6e3a9457581ad7d4c44ebc4c Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Wed, 10 Sep 2025 09:13:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 8 +- backend/devices/base_device.py | 88 ++++++++++++++++++- backend/devices/camera_manager.py | 43 ++++++--- backend/devices/femtobolt_manager.py | 29 ++++++ backend/devices/imu_manager.py | 40 +++++++++ backend/devices/pressure_manager.py | 37 ++++++++ backend/devices/screen_recorder.py | 22 +---- backend/main.py | 127 +++++++++++++++++++-------- 8 files changed, 322 insertions(+), 72 deletions(-) diff --git a/backend/database.py b/backend/database.py index e1ac4191..bd43c49b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -641,10 +641,10 @@ class DatabaseManager: creator_id, settings.get('duration', 60), json.dumps(settings), - 'running', - settings.get('diagnosis_info', ''), - settings.get('treatment_info', ''), - settings.get('suggestion_info', ''), + 'checking', + '', + '', + '', china_time, china_time )) diff --git a/backend/devices/base_device.py b/backend/devices/base_device.py index 9d96beb6..ee85f8a6 100644 --- a/backend/devices/base_device.py +++ b/backend/devices/base_device.py @@ -44,6 +44,12 @@ class BaseDevice(ABC): # 状态变化回调 self._status_change_callbacks = [] + # 设备连接监控 + self._connection_monitor_thread = None + self._monitor_stop_event = threading.Event() + self._connection_check_interval = config.get('connection_check_interval', 5.0) # 默认5秒检查一次 + self._connection_timeout = config.get('connection_timeout', 30.0) # 默认30秒超时 + # 设备状态信息 self._device_info = { 'name': device_name, @@ -55,7 +61,7 @@ class BaseDevice(ABC): # 性能统计 self._stats = { - 'frames_processed': 0, + 'frames_processed': 0, 'errors_count': 0, 'start_time': None, 'last_frame_time': None @@ -137,6 +143,16 @@ class BaseDevice(ABC): bool: 重新加载是否成功 """ pass + + @abstractmethod + def check_hardware_connection(self) -> bool: + """ + 检查设备硬件连接状态 + + Returns: + bool: 设备是否物理连接 + """ + pass def set_socketio(self, socketio): """ @@ -193,6 +209,12 @@ class BaseDevice(ABC): # 只有状态真正改变时才触发回调 if old_status != is_connected: self._notify_status_change(is_connected) + + # 启动或停止连接监控 + if is_connected and not self._connection_monitor_thread: + self._start_connection_monitor() + elif not is_connected and self._connection_monitor_thread: + self._stop_connection_monitor() def emit_data(self, event: str, data: Any, namespace: Optional[str] = None): """ @@ -305,6 +327,64 @@ class BaseDevice(ABC): """ with self._lock: self._stats['start_time'] = None + + def _start_connection_monitor(self): + """ + 启动连接监控线程 + """ + if self._connection_monitor_thread and self._connection_monitor_thread.is_alive(): + return + + self._monitor_stop_event.clear() + self._connection_monitor_thread = threading.Thread( + target=self._connection_monitor_worker, + name=f"{self.device_name}_connection_monitor", + daemon=True + ) + self._connection_monitor_thread.start() + self.logger.info(f"设备 {self.device_name} 连接监控线程已启动") + + def _stop_connection_monitor(self): + """ + 停止连接监控线程 + """ + if self._connection_monitor_thread: + self._monitor_stop_event.set() + if self._connection_monitor_thread.is_alive(): + self._connection_monitor_thread.join(timeout=2.0) + self._connection_monitor_thread = None + self.logger.info(f"设备 {self.device_name} 连接监控线程已停止") + + def _connection_monitor_worker(self): + """ + 连接监控工作线程 + """ + self.logger.info(f"设备 {self.device_name} 连接监控开始") + + while not self._monitor_stop_event.is_set(): + try: + # 检查硬件连接状态 + hardware_connected = self.check_hardware_connection() + + # 如果硬件断开但软件状态仍为连接,则更新状态 + if not hardware_connected and self.is_connected: + self.logger.warning(f"检测到设备 {self.device_name} 硬件连接断开") + self.set_connected(False) + break # 硬件断开后停止监控 + + # 检查心跳超时 + if self.is_connected and time.time() - self._last_heartbeat > self._connection_timeout: + self.logger.warning(f"设备 {self.device_name} 心跳超时,判定为断开连接") + self.set_connected(False) + break # 超时后停止监控 + + except Exception as e: + self.logger.error(f"设备 {self.device_name} 连接监控异常: {e}") + + # 等待下次检查 + self._monitor_stop_event.wait(self._connection_check_interval) + + self.logger.info(f"设备 {self.device_name} 连接监控结束") def __enter__(self): """ @@ -318,5 +398,11 @@ class BaseDevice(ABC): """ self.cleanup() + def _cleanup_monitoring(self): + """ + 清理监控线程 + """ + self._stop_connection_monitor() + def __repr__(self): return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>" \ No newline at end of file diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index b1d9997d..4dc9711c 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -612,40 +612,61 @@ class CameraManager(BaseDevice): self.logger.error(f"重新加载相机配置失败: {e}") return False + def check_hardware_connection(self) -> bool: + """ + 检查相机硬件连接状态 + + Returns: + bool: 相机是否物理连接 + """ + try: + if self.cap and self.cap.isOpened(): + # 尝试读取一帧来验证连接 + ret, _ = self.cap.read() + return ret + return False + except Exception as e: + self.logger.debug(f"检查相机硬件连接时发生异常: {e}") + return False + def cleanup(self): """ 清理资源 """ try: - self.stop_streaming() + self.logger.info("开始清理相机资源") + + # 清理监控线程 + self._cleanup_monitoring() + + # 停止流 + if self.is_streaming: + self.stop_streaming() + + # 断开连接 + self.disconnect() - if self.cap: - try: - self.cap.release() - except Exception: - pass - self.cap = None - # 清理帧缓存 while not self.frame_cache.empty(): try: self.frame_cache.get_nowait() except queue.Empty: break - self.last_frame = None - # 清理帧队列 + # 清理全局帧队列 while not self.frame_queue.empty(): try: self.frame_queue.get_nowait() except queue.Empty: break + self.last_frame = None + super().cleanup() self.logger.info("相机资源清理完成") except Exception as e: - self.logger.error(f"清理相机资源失败: {e}") + self.logger.error(f"清理相机资源时发生错误: {e}") def _save_frame_to_cache(self, frame, frame_type='camera'): """保存帧到全局缓存""" diff --git a/backend/devices/femtobolt_manager.py b/backend/devices/femtobolt_manager.py index e1e815a3..67fbe04c 100644 --- a/backend/devices/femtobolt_manager.py +++ b/backend/devices/femtobolt_manager.py @@ -851,11 +851,40 @@ class FemtoBoltManager(BaseDevice): self.logger.error(f"重新加载FemtoBolt配置失败: {e}") return False + def check_hardware_connection(self) -> bool: + """ + 检查FemtoBolt设备硬件连接状态 + + Returns: + bool: 设备是否物理连接 + """ + try: + if not self.sdk_initialized or not self.device_handle: + return False + + # 尝试获取设备状态来验证连接 + if hasattr(self.femtobolt, 'device_get_capture'): + try: + # 尝试获取一帧数据来验证设备连接 + capture = self.femtobolt.device_get_capture(self.device_handle, timeout_in_ms=1000) + if capture: + self.femtobolt.capture_release(capture) + return True + except Exception: + pass + return False + except Exception as e: + self.logger.debug(f"检查FemtoBolt硬件连接时发生异常: {e}") + return False + def cleanup(self): """ 清理资源 """ try: + # 清理监控线程 + self._cleanup_monitoring() + self.stop_streaming() self._cleanup_device() diff --git a/backend/devices/imu_manager.py b/backend/devices/imu_manager.py index b597bb57..d8c5b26c 100644 --- a/backend/devices/imu_manager.py +++ b/backend/devices/imu_manager.py @@ -628,11 +628,51 @@ class IMUManager(BaseDevice): self.logger.error(f"重新加载IMU配置失败: {e}") return False + def check_hardware_connection(self) -> bool: + """ + 检查IMU硬件连接状态 + """ + try: + if not self.imu_device: + return False + + # 对于真实设备,检查串口连接状态 + if hasattr(self.imu_device, 'ser') and self.imu_device.ser: + # 检查串口是否仍然打开 + if not self.imu_device.ser.is_open: + return False + + # 尝试读取数据来验证连接 + try: + # 保存当前超时设置 + original_timeout = self.imu_device.ser.timeout + self.imu_device.ser.timeout = 0.1 # 设置短超时 + + # 尝试读取少量数据 + test_data = self.imu_device.ser.read(1) + + # 恢复原始超时设置 + self.imu_device.ser.timeout = original_timeout + + return True # 如果没有异常,认为连接正常 + except Exception: + return False + + # 对于模拟设备,总是返回True + return True + + except Exception as e: + self.logger.debug(f"检查IMU硬件连接时出错: {e}") + return False + def cleanup(self): """ 清理资源 """ try: + # 停止连接监控 + self._cleanup_monitoring() + self.disconnect() # 清理缓冲区 diff --git a/backend/devices/pressure_manager.py b/backend/devices/pressure_manager.py index bc9e0fc7..ecb744e9 100644 --- a/backend/devices/pressure_manager.py +++ b/backend/devices/pressure_manager.py @@ -998,9 +998,46 @@ class PressureManager(BaseDevice): self.logger.error(f"重新加载压力板配置失败: {e}") return False + def check_hardware_connection(self) -> bool: + """ + 检查压力板硬件连接状态 + + Returns: + bool: 硬件连接是否正常 + """ + try: + if not self.device: + return False + + # 对于真实设备,检查DLL和设备句柄状态 + if hasattr(self.device, 'dll') and hasattr(self.device, 'device_handle'): + if not self.device.dll or not self.device.device_handle: + return False + + # 检查设备连接状态 + if not self.device.is_connected: + return False + + # 尝试读取一次数据来验证连接 + try: + test_data = self.device.read_data() + return test_data is not None and 'foot_pressure' in test_data + except Exception: + return False + + # 对于模拟设备,总是返回True + return True + + except Exception as e: + self.logger.debug(f"检查压力板硬件连接时出错: {e}") + return False + def cleanup(self) -> None: """清理资源""" try: + # 停止连接监控 + self._cleanup_monitoring() + self.stop_streaming() self.disconnect() self.logger.info("压力板设备资源清理完成") diff --git a/backend/devices/screen_recorder.py b/backend/devices/screen_recorder.py index e1eaeb8e..d67cf5bc 100644 --- a/backend/devices/screen_recorder.py +++ b/backend/devices/screen_recorder.py @@ -547,7 +547,7 @@ class RecordingManager: if self.current_session_id: result['database_updates'] = { 'session_id': self.current_session_id, - 'status': 'checked' + 'status': 'recorded' } self.logger.info(f'数据库更新信息已准备 - 会话ID: {self.current_session_id}') @@ -647,12 +647,7 @@ class RecordingManager: # 写入视频帧 if frame is not None: video_writer.write(frame) - frame_count += 1 - - # # 每100帧记录一次进度 - # if frame_count % 100 == 0: - # elapsed_recording_time = current_time - recording_start_time - # self.logger.debug(f'{recording_type}录制进度: {frame_count}帧, 已录制{elapsed_recording_time:.1f}秒, 目标帧率{target_fps}fps') + frame_count += 1 else: self.logger.warning(f'{recording_type}获取帧失败,跳过此帧') @@ -664,17 +659,8 @@ class RecordingManager: # 计算录制统计信息 if self.global_recording_start_time: - total_recording_time = time.time() - self.global_recording_start_time - actual_fps = frame_count / total_recording_time if total_recording_time > 0 else 0 - expected_frames = int(total_recording_time * target_fps) - - self.logger.info(f'{recording_type}录制线程结束统计:') - self.logger.info(f' 实际录制帧数: {frame_count}帧') - self.logger.info(f' 预期录制帧数: {expected_frames}帧') - self.logger.info(f' 目标帧率: {target_fps}fps') - self.logger.info(f' 实际平均帧率: {actual_fps:.2f}fps') - self.logger.info(f' 录制时长: {total_recording_time:.3f}秒') - + total_recording_time = time.time() - self.global_recording_start_time + expected_frames = int(total_recording_time * target_fps) if abs(frame_count - expected_frames) > target_fps * 0.1: # 如果帧数差异超过0.1秒的帧数 self.logger.warning(f'{recording_type}帧数异常: 实际{frame_count}帧 vs 预期{expected_frames}帧,差异{frame_count - expected_frames}帧') else: diff --git a/backend/main.py b/backend/main.py index d8f7a669..cf65c454 100644 --- a/backend/main.py +++ b/backend/main.py @@ -977,47 +977,15 @@ class AppServer: data = flask_request.get_json() patient_id = data.get('patient_id') - creator_id = data.get('creator_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] + creator_id = data.get('creator_id') if not patient_id or not creator_id: return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400 # 调用create_detection_session方法,settings传空字典 - session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) - - # 开始同步录制 - recording_response = None - try: - recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_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']) - - self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}') - except Exception as db_error: - self.logger.error(f'处理数据库更新失败: {db_error}') - - except Exception as rec_e: - self.logger.error(f'开始同步录制失败: {rec_e}') - - start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response}) + session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id) + return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time}) except Exception as e: self.logger.error(f'开始检测失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @@ -1049,6 +1017,89 @@ class AppServer: except Exception as duration_error: self.logger.error(f'更新会话持续时间失败: {duration_error}') + success = self.db_manager.update_session_status(session_id, 'completed') + if success: + self.logger.info(f'检测会话已停止 - 会话ID: {session_id}') + return jsonify({ + 'success': True, + 'message': '检测已停止' + }) + else: + self.logger.error('停止检测失败,更新会话状态失败') + return jsonify({ + 'success': False, + 'error': '停止检测失败' + }), 500 + + except Exception as e: + self.logger.error(f'停止检测失败: {e}', exc_info=True) + return jsonify({'success': False, 'error': str(e)}), 500 + + @self.app.route('/api/detection//start_record', methods=['POST']) + def start_record(session_id): + """开始视频录制""" + try: + if not self.db_manager or not self.device_coordinator: + return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 + + 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] + + + 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) + + # 处理录制管理器返回的数据库更新信息 + 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']) + + self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}') + except Exception as db_error: + self.logger.error(f'处理数据库更新失败: {db_error}') + + except Exception as rec_e: + self.logger.error(f'开始同步录制失败: {rec_e}') + + start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response}) + except Exception as e: + self.logger.error(f'开始录制失败: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 + + @self.app.route('/api/detection//stop_record', methods=['POST']) + 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) @@ -1066,12 +1117,12 @@ class AppServer: success = False else: # 如果录制管理器没有返回数据库更新信息,则手动更新 - success = self.db_manager.update_session_status(session_id, 'completed') + 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, 'completed') + success = self.db_manager.update_session_status(session_id, 'recorded') raise if success: @@ -1090,7 +1141,7 @@ class AppServer: except Exception as e: self.logger.error(f'停止检测失败: {e}', exc_info=True) return jsonify({'success': False, 'error': str(e)}), 500 - + @self.app.route('/api/detection//status', methods=['GET']) def get_detection_status(session_id): """获取检测状态"""