diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index 672b6da7..ae99d1b7 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -70,9 +70,11 @@ class CameraManager(BaseDevice): self.fps_start_time = time.time() self.actual_fps = 0 - # 重连机制 - self.max_reconnect_attempts = 3 - self.reconnect_delay = 2.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._last_connected_state = None # 设备标识和性能统计 self.device_id = f"camera_{self.device_index}" @@ -131,6 +133,7 @@ class CameraManager(BaseDevice): return False self.is_connected = True + self._last_connected_state = True self._device_info.update({ 'device_index': self.device_index, 'resolution': f"{self.width}x{self.height}", @@ -235,7 +238,7 @@ class CameraManager(BaseDevice): for i in range(5): ret, _ = self.cap.read() if not ret: - self.logger.warning(f"校准时读取第{i+1}帧失败") + self.logger.warning(f"校时时读取第{i+1}帧失败") self.logger.info("相机校准完成") return True @@ -305,6 +308,7 @@ class CameraManager(BaseDevice): self.logger.info("相机流工作线程启动") reconnect_attempts = 0 + consecutive_read_failures = 0 # 基于目标FPS的简单节拍器,防止无上限地读取/编码/发送导致对象堆积 frame_interval = 1.0 / max(self.fps, 1) @@ -313,24 +317,73 @@ class CameraManager(BaseDevice): while self.is_streaming: loop_start = time.time() try: + # 如果设备未打开,进入重连流程 if not self.cap or not self.cap.isOpened(): - if reconnect_attempts < self.max_reconnect_attempts: - self.logger.warning(f"相机连接丢失,尝试重连 ({reconnect_attempts + 1}/{self.max_reconnect_attempts})") + # 仅在状态变化时广播一次断连状态 + if self._last_connected_state is not False: + try: + self._socketio.emit('camera_status', { + 'status': 'disconnected', + 'device_id': self.device_id, + 'timestamp': time.time() + }, namespace='/devices') + except Exception: + pass + self._last_connected_state = False + + # 无限重连:max_reconnect_attempts == -1;否则按次数重试 + if self.max_reconnect_attempts == -1 or reconnect_attempts < self.max_reconnect_attempts: + self.logger.warning(f"相机连接丢失,尝试重连 ({'∞' if self.max_reconnect_attempts == -1 else reconnect_attempts + 1}/{self.max_reconnect_attempts if self.max_reconnect_attempts != -1 else '∞'})") + if not self.is_streaming: + break if self._reconnect(): reconnect_attempts = 0 + consecutive_read_failures = 0 + # 广播恢复 + try: + self._socketio.emit('camera_status', { + 'status': 'connected', + 'device_id': self.device_id, + 'timestamp': time.time() + }, namespace='/devices') + except Exception: + pass + self._last_connected_state = True continue else: reconnect_attempts += 1 time.sleep(self.reconnect_delay) continue else: - self.logger.error("相机重连失败次数过多,停止流") - break + # 超过次数也不退出线程,降频重试,防止永久停机 + self.logger.error("相机重连失败次数过多,进入降频重试模式") + time.sleep(max(self.reconnect_delay, 5.0)) + # 重置计数以便继续尝试 + reconnect_attempts = 0 + continue ret, frame = self.cap.read() if not ret or frame is None: + consecutive_read_failures += 1 self.dropped_frames += 1 + if consecutive_read_failures >= getattr(self, 'read_fail_threshold', 30): + self.logger.warning(f"连续读帧失败 {consecutive_read_failures} 次,执行相机软复位并进入重连") + try: + if self.cap: + try: + self.cap.release() + except Exception: + pass + self.cap = None + self.is_connected = False + except Exception: + pass + # 进入下一轮循环会走到未打开分支 + consecutive_read_failures = 0 + time.sleep(self.reconnect_delay) + continue + if self.dropped_frames > 10: self.logger.warning(f"连续丢帧过多: {self.dropped_frames}") # 仅在异常情况下触发一次GC,避免高频强制GC @@ -340,10 +393,11 @@ class CameraManager(BaseDevice): pass self.dropped_frames = 0 # 防止空转占满CPU - time.sleep(0.005) + time.sleep(0.02) continue - # 重置丢帧计数 + # 读帧成功,重置失败计数 + consecutive_read_failures = 0 self.dropped_frames = 0 # 保存原始帧到队列(用于录制) @@ -366,10 +420,6 @@ class CameraManager(BaseDevice): # 处理帧(降采样以优化传输负载) processed_frame = self._process_frame(frame) - # # 缓存帧(不复制,减少内存占用) - # self.last_frame = processed_frame - # self.frame_cache.append(processed_frame) - # 发送帧数据 self._send_frame_data(processed_frame) @@ -378,7 +428,6 @@ class CameraManager(BaseDevice): # 主动释放局部引用,帮助GC更快识别可回收对象 del frame - # 注意:processed_frame 被 last_frame 和 frame_cache 引用,不可删除其对象本身 # 限速:保证不超过目标FPS,减小发送端积压 now = time.time() @@ -394,7 +443,7 @@ class CameraManager(BaseDevice): except Exception as e: self.logger.error(f"相机流处理异常: {e}") # 小退避,避免异常情况下空转 - time.sleep(0.02) + time.sleep(0.05) self.logger.info("相机流工作线程结束") @@ -601,6 +650,10 @@ class CameraManager(BaseDevice): self.buffer_size = config.get('buffer_size', 1) self.fourcc = config.get('fourcc', 'MJPG') self._tx_max_width = int(config.get('tx_max_width', 640)) + # 新增:动态更新重连/阈值配置 + self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', self.max_reconnect_attempts)) + self.reconnect_delay = float(config.get('reconnect_delay', self.reconnect_delay)) + self.read_fail_threshold = int(config.get('read_fail_threshold', self.read_fail_threshold)) # 更新帧缓存队列大小 frame_cache_len = int(config.get('frame_cache_len', 2)) diff --git a/backend/devices/pressure_manager.py b/backend/devices/pressure_manager.py index ecb744e9..c86df087 100644 --- a/backend/devices/pressure_manager.py +++ b/backend/devices/pressure_manager.py @@ -186,6 +186,18 @@ class RealPressureDevice: r = self.dll.fpms_usb_read_frame_wrap(self.device_handle.value, self.buf, self.frame_size) if r != 0: logger.warning(f"读取帧失败, code= {r}") + # 如果返回负数,多半表示物理断开或严重错误,标记断连并关闭句柄,触发上层重连 + if r < 0: + try: + if self.device_handle: + try: + self.dll.fpms_usb_close_wrap(self.device_handle.value) + except Exception: + pass + self.device_handle = None + except Exception: + pass + self.is_connected = False return self._get_empty_data() # 转换为numpy数组 @@ -219,7 +231,7 @@ class RealPressureDevice: except Exception as e: logger.error(f"读取压力数据异常: {e}") return self._get_empty_data() - + def _calculate_foot_pressure_zones(self, raw_data): """计算足部区域压力,返回百分比: - 左足、右足:相对于双足总压的百分比 @@ -728,6 +740,12 @@ class PressureManager(BaseDevice): self.error_count = 0 self.last_data_time = None + # 重连相关配置(与camera_manager保持一致的键名和默认值) + self.max_reconnect_attempts = int(self.config.get('max_reconnect_attempts', -1)) # -1 表示无限重连 + self.reconnect_delay = float(self.config.get('reconnect_delay', 2.0)) + self.read_fail_threshold = int(self.config.get('read_fail_threshold', 30)) + self._last_connected_state = None # 去抖动状态广播 + self.logger.info(f"压力板管理器初始化完成 - 设备类型: {self.device_type}") def initialize(self) -> bool: @@ -825,74 +843,141 @@ class PressureManager(BaseDevice): """ self.logger.info("压力数据流线程启动") + reconnect_attempts = 0 + consecutive_read_failures = 0 + try: - while self.is_streaming and self.is_connected: + while self.is_streaming: try: + # 若设备未连接或不存在,进入重连流程 + if not self.device or not self.is_connected or (hasattr(self.device, 'is_connected') and not self.device.is_connected): + # 广播断开状态(仅状态变化时) + if self._last_connected_state is not False: + try: + if self._socketio: + self._socketio.emit('pressure_status', { + 'status': 'disconnected', + 'timestamp': time.time() + }, namespace='/devices') + except Exception: + pass + self._last_connected_state = False + + if self.max_reconnect_attempts == -1 or reconnect_attempts < self.max_reconnect_attempts: + self.logger.warning(f"压力设备连接丢失,尝试重连 ({'∞' if self.max_reconnect_attempts == -1 else reconnect_attempts + 1}/{self.max_reconnect_attempts if self.max_reconnect_attempts != -1 else '∞'})") + if not self.is_streaming: + break + if self._reconnect(): + reconnect_attempts = 0 + consecutive_read_failures = 0 + # 广播恢复 + try: + if self._socketio: + self._socketio.emit('pressure_status', { + 'status': 'connected', + 'timestamp': time.time() + }, namespace='/devices') + except Exception: + pass + self._last_connected_state = True + continue + else: + reconnect_attempts += 1 + time.sleep(self.reconnect_delay) + continue + else: + self.logger.error("压力设备重连失败次数过多,进入降频重试模式") + time.sleep(max(self.reconnect_delay, 5.0)) + reconnect_attempts = 0 + continue + # 从设备读取数据 + pressure_data = None if self.device: pressure_data = self.device.read_data() - - if pressure_data and 'foot_pressure' in pressure_data: - foot_pressure = pressure_data['foot_pressure'] - # 获取各区域压力值 - left_front = foot_pressure['left_front'] - left_rear = foot_pressure['left_rear'] - right_front = foot_pressure['right_front'] - right_rear = foot_pressure['right_rear'] - left_total = foot_pressure['left_total'] - right_total = foot_pressure['right_total'] - - # 计算总压力 - total_pressure = left_total + right_total - - # 计算平衡比例(左脚压力占总压力的比例) - balance_ratio = left_total / total_pressure if total_pressure > 0 else 0.5 - - # 计算压力中心偏移 - pressure_center_offset = (balance_ratio - 0.5) * 100 # 转换为百分比 - - # 计算前后足压力分布 - left_front_ratio = left_front / left_total if left_total > 0 else 0.5 - right_front_ratio = right_front / right_total if right_total > 0 else 0.5 - - # 构建完整的足部压力数据 - complete_pressure_data = { - # 分区压力值 - 'pressure_zones': { - 'left_front': left_front, - 'left_rear': left_rear, - 'right_front': right_front, - 'right_rear': right_rear, - 'left_total': left_total, - 'right_total': right_total, - 'total_pressure': total_pressure - }, - # 平衡分析 - 'balance_analysis': { - 'balance_ratio': round(balance_ratio, 3), - 'pressure_center_offset': round(pressure_center_offset, 2), - 'balance_status': 'balanced' if abs(pressure_center_offset) < 10 else 'unbalanced', - 'left_front_ratio': round(left_front_ratio, 3), - 'right_front_ratio': round(right_front_ratio, 3) - }, - # 压力图片 - 'pressure_image': pressure_data.get('pressure_image', ''), - 'timestamp': pressure_data['timestamp'] - } - - # 更新统计信息 - self.packet_count += 1 - self.last_data_time = time.time() - - # 发送数据到前端 - if self._socketio: - self._socketio.emit('pressure_data', { - 'foot_pressure': complete_pressure_data, - 'timestamp': datetime.now().isoformat() - }, namespace='/devices') - else: - self.logger.warning("SocketIO实例为空,无法发送压力数据") - + # 如果底层设备在读取时标记了断开,则在此处进入下一轮以触发重连 + if hasattr(self.device, 'is_connected') and not self.device.is_connected: + self.is_connected = False + time.sleep(self.reconnect_delay) + continue + + if not pressure_data or 'foot_pressure' not in pressure_data: + consecutive_read_failures += 1 + if consecutive_read_failures >= self.read_fail_threshold: + self.logger.warning(f"连续读取压力数据失败 {consecutive_read_failures} 次,执行设备软复位并进入重连") + try: + if self.device and hasattr(self.device, 'close'): + self.device.close() + except Exception: + pass + self.is_connected = False + consecutive_read_failures = 0 + time.sleep(self.reconnect_delay) + continue + time.sleep(0.05) + continue + + # 读数成功,重置失败计数 + consecutive_read_failures = 0 + self.is_connected = True + + foot_pressure = pressure_data['foot_pressure'] + # 获取各区域压力值 + left_front = foot_pressure['left_front'] + left_rear = foot_pressure['left_rear'] + right_front = foot_pressure['right_front'] + right_rear = foot_pressure['right_rear'] + left_total = foot_pressure['left_total'] + right_total = foot_pressure['right_total'] + + # 计算总压力 + total_pressure = left_total + right_total + + # 计算平衡比例(左脚压力占总压力的比例) + balance_ratio = left_total / total_pressure if total_pressure > 0 else 0.5 + + # 计算压力中心偏移 + pressure_center_offset = (balance_ratio - 0.5) * 100 # 转换为百分比 + + # 计算前后足压力分布 + left_front_ratio = left_front / left_total if left_total > 0 else 0.5 + right_front_ratio = right_front / right_total if right_total > 0 else 0.5 + + # 构建完整的足部压力数据 + complete_pressure_data = { + 'pressure_zones': { + 'left_front': left_front, + 'left_rear': left_rear, + 'right_front': right_front, + 'right_rear': right_rear, + 'left_total': left_total, + 'right_total': right_total, + 'total_pressure': total_pressure + }, + 'balance_analysis': { + 'balance_ratio': round(balance_ratio, 3), + 'pressure_center_offset': round(pressure_center_offset, 2), + 'balance_status': 'balanced' if abs(pressure_center_offset) < 10 else 'unbalanced', + 'left_front_ratio': round(left_front_ratio, 3), + 'right_front_ratio': round(right_front_ratio, 3) + }, + 'pressure_image': pressure_data.get('pressure_image', ''), + 'timestamp': pressure_data['timestamp'] + } + + # 更新统计信息 + self.packet_count += 1 + self.last_data_time = time.time() + + # 发送数据到前端 + if self._socketio: + self._socketio.emit('pressure_data', { + 'foot_pressure': complete_pressure_data, + 'timestamp': datetime.now().isoformat() + }, namespace='/devices') + else: + self.logger.warning("SocketIO实例为空,无法发送压力数据") + time.sleep(self.stream_interval) except Exception as e: @@ -905,6 +990,42 @@ class PressureManager(BaseDevice): finally: self.logger.info("压力数据流线程结束") + def _reconnect(self) -> bool: + """重新连接压力设备""" + try: + # 先清理旧设备 + try: + if self.device and hasattr(self.device, 'close'): + self.device.close() + except Exception: + pass + self.device = None + self.is_connected = False + + time.sleep(1.0) # 等待设备释放 + + # 重新创建设备 + if self.device_type == 'real': + self.device = RealPressureDevice() + else: + self.device = MockPressureDevice() + + self.is_connected = True + # 广播一次连接状态 + try: + if self._socketio: + self._socketio.emit('pressure_status', { + 'status': 'connected', + 'timestamp': time.time() + }, namespace='/devices') + except Exception: + pass + return True + except Exception as e: + self.logger.error(f"压力设备重连失败: {e}") + self.is_connected = False + return False + def get_status(self) -> Dict[str, Any]: """ 获取设备状态 @@ -990,6 +1111,10 @@ class PressureManager(BaseDevice): self.config = new_config self.device_type = new_config.get('device_type', 'mock') self.stream_interval = new_config.get('stream_interval', 0.1) + # 动态更新重连参数 + self.max_reconnect_attempts = int(new_config.get('max_reconnect_attempts', self.max_reconnect_attempts)) + self.reconnect_delay = float(new_config.get('reconnect_delay', self.reconnect_delay)) + self.read_fail_threshold = int(new_config.get('read_fail_threshold', self.read_fail_threshold)) self.logger.info(f"压力板配置重新加载成功 - 设备类型: {self.device_type}, 流间隔: {self.stream_interval}") return True diff --git a/backend/devices/utils/config.ini b/backend/devices/utils/config.ini index 89b6a562..35a5c897 100644 --- a/backend/devices/utils/config.ini +++ b/backend/devices/utils/config.ini @@ -15,7 +15,7 @@ backup_interval = 24 max_backups = 7 [CAMERA] -device_index = 1 +device_index = 3 width = 1280 height = 720 fps = 30 @@ -33,8 +33,9 @@ fps = 15 synchronized_images_only = False [DEVICES] -imu_device_type = real +imu_device_type = ble imu_port = COM9 +imu_mac_address = ef:3c:1a:0a:fe:02 imu_baudrate = 9600 pressure_device_type = real pressure_use_mock = False diff --git a/backend/testcamera.py b/backend/testcamera.py index a1cf6658..263025dc 100644 --- a/backend/testcamera.py +++ b/backend/testcamera.py @@ -30,7 +30,7 @@ class CameraViewer: if __name__ == "__main__": # 修改这里的数字可以切换不同摄像头设备 - viewer = CameraViewer(device_index=1) + viewer = CameraViewer(device_index=3) viewer.start_stream() # import ctypes