From 76c61d75df290ea75a024e8a1049018acc094b22 Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Mon, 29 Sep 2025 15:20:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8D=95=E4=B8=AA?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=9A=84=E9=87=8D=E5=90=AF=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/devices/device_coordinator.py | 39 ++- backend/main.py | 13 +- frontend/src/renderer/src/views/Detection.vue | 308 +++++++++--------- 3 files changed, 209 insertions(+), 151 deletions(-) diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py index 6c9a2a02..df87bbf2 100644 --- a/backend/devices/device_coordinator.py +++ b/backend/devices/device_coordinator.py @@ -65,6 +65,9 @@ class DeviceCoordinator: # 事件回调 self.event_callbacks: Dict[str, List[Callable]] = defaultdict(list) + # 状态变化回调存储 + self._status_change_callback = None + # 性能统计 self.stats = { 'start_time': None, @@ -78,6 +81,20 @@ class DeviceCoordinator: self.logger.info("设备协调器初始化完成") + def set_status_change_callback(self, callback: Callable): + """ + 设置状态变化回调函数 + + Args: + callback: 状态变化回调函数 + """ + self._status_change_callback = callback + # 为已存在的设备注册回调 + for device_name, device in self.devices.items(): + if device and hasattr(device, 'add_status_change_callback'): + device.add_status_change_callback(callback) + self.logger.info(f"{device_name} 设备状态变化回调已注册") + def initialize(self) -> bool: """ 初始化所有设备 @@ -530,7 +547,7 @@ class DeviceCoordinator: cleanup_start = time.time() self.logger.info(f"正在彻底清理 {device_name} 设备...") - # 断开连接 + # 断开连接但暂时不广播状态变化,避免重启过程中的状态冲突 if hasattr(device, 'disconnect'): try: device.disconnect() @@ -538,6 +555,12 @@ class DeviceCoordinator: except Exception as e: self.logger.warning(f"断开 {device_name} 连接异常: {e}") + # 静默设置设备状态为未连接,不触发状态变化通知 + # 这样可以避免在重启过程中广播中间状态 + if hasattr(device, 'is_connected'): + device.is_connected = False + self.logger.info(f"{device_name} 设备状态已静默更新为未连接(重启过程中)") + # 彻底清理资源 if hasattr(device, 'cleanup'): try: @@ -605,6 +628,12 @@ class DeviceCoordinator: create_time = (time.time() - create_start) * 1000 self.logger.info(f"{device_name} 设备实例重新创建成功 (耗时: {create_time:.1f}ms)") + # 重新注册状态变化回调(如果有的话) + if hasattr(self, '_status_change_callback') and self._status_change_callback: + if hasattr(new_device, 'add_status_change_callback'): + new_device.add_status_change_callback(self._status_change_callback) + self.logger.info(f"{device_name} 设备状态变化回调已重新注册") + except Exception as e: create_time = (time.time() - create_start) * 1000 self.logger.error(f"重新创建 {device_name} 设备实例失败: {e} (耗时: {create_time:.1f}ms)") @@ -624,6 +653,14 @@ class DeviceCoordinator: init_time = (time.time() - init_start) * 1000 self.logger.info(f"{device_name} 设备初始化成功 (耗时: {init_time:.1f}ms)") + # 设备初始化成功后,确保状态广播正确 + # 此时设备应该已经通过initialize()方法中的set_connected(True)触发了状态变化通知 + # 但为了确保状态一致性,我们再次确认状态 + if hasattr(new_device, 'is_connected') and new_device.is_connected: + self.logger.info(f"{device_name} 设备重启后状态确认:已连接") + else: + self.logger.warning(f"{device_name} 设备重启后状态异常:未连接") + # 第六步:如果之前在推流,则启动推流 stream_time = 0 if was_streaming and hasattr(new_device, 'start_streaming'): diff --git a/backend/main.py b/backend/main.py index 82525250..e8507dea 100644 --- a/backend/main.py +++ b/backend/main.py @@ -187,13 +187,15 @@ class AppServer: # 初始化设备协调器(统一管理所有设备) self.logger.info('正在初始化设备协调器...') self.device_coordinator = DeviceCoordinator(self.socketio) + # 设置状态变化回调 + self.device_coordinator.set_status_change_callback(self._on_device_status_change) # 调用初始化方法来初始化设备 if self.device_coordinator.initialize(): self.logger.info('设备协调器初始化完成') # 获取设备管理器实例 self.device_managers = self.device_coordinator.get_device_managers() - # 为每个设备添加状态变化回调 + # 为每个设备添加状态变化回调(双重保险) for device_name, manager in self.device_managers.items(): if manager and hasattr(manager, 'add_status_change_callback'): manager.add_status_change_callback(self._on_device_status_change) @@ -1488,7 +1490,7 @@ class AppServer: 'device_type': device_type, 'message': f'{device_type} 设备重启成功!', 'timestamp': time.time() - }) + }, namespace='/devices') return True else: self.logger.error(f'{device_type} 设备重启失败!') @@ -1497,7 +1499,7 @@ class AppServer: 'device_type': device_type, 'message': f'{device_type} 设备重启失败!', 'timestamp': time.time() - }) + }, namespace='/devices') return False except Exception as e: @@ -1509,7 +1511,7 @@ class AppServer: 'error': str(e), 'message': f'重启 {device_type} 设备时发生异常', 'timestamp': time.time() - }) + }, namespace='/devices') return False @@ -1649,7 +1651,8 @@ class AppServer: def broadcast_all_device_status(self): """广播所有设备状态""" - for device_name, manager in self.device_managers.items(): + current_devices = self.device_coordinator.get_device_managers() + for device_name, manager in current_devices.items(): if manager is not None: try: # 检查设备是否连接(使用is_connected属性) diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index ddf30795..80903a48 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -8,7 +8,6 @@
- 实时检测
@@ -17,7 +16,6 @@ -
@@ -97,12 +95,12 @@ 头部姿态
- 校准 - 清零 @@ -581,13 +579,11 @@ import HistoryDashboard from '@/views/PatientProfile.vue' const authStore = useAuthStore() const router = useRouter() const route = useRoute() -const isStart = ref(false) +const isRecording = ref(false) const isConnected = ref(false) const rtspImgSrc = ref('') const depthCameraImgSrc = ref('') // 深度相机视频流 const screenshotLoading = ref(false) -const dataCollectionLoading = ref(false) -const isRecording = ref(false) const cameraDialogVisible =ref(false) // 设置相机参数弹框 const contenGridRef =ref(null) // 实时检查整体box const wholeBodyRef = ref(null) // 身体姿态ref @@ -893,16 +889,17 @@ const savePatient = async () => { throw error } } -function routeTo(path) { - if( isPreventCombo.value == true){ - setTimeout(() => { - isPreventCombo.value = false - }, 2000); - ElMessage.warning(`请勿连击点击刷新按钮`) +const isPreventCombo = ref(false) +function routeTo(path = '/') { + if (isPreventCombo.value === true) { + ElMessage.warning(`请勿连续点击回退按钮!`) return } isPreventCombo.value = true - router.push(`/`) + setTimeout(() => { + isPreventCombo.value = false + }, 2000) + router.push(path) } function cameraUpdate() { // 相机设置数据更新弹框 cameraForm.value = { // 相机参数 @@ -1012,22 +1009,12 @@ function connectWebSocket() { socket.on('connect_error', (error) => { console.error('❌ 主连接失败:', error.message) - isConnected.value = false - // 如果正在录像,停止录像 - if (isRecording.value) { - stopRecording() - isStart.value = false - } + isConnected.value = false }) socket.on('disconnect', (reason) => { console.log('⚠️ 主连接断开:', reason) - isConnected.value = false - - if (isRecording.value) { - stopRecording() - isStart.value = false - } + isConnected.value = false }) socket.on('reconnect', (attemptNumber) => { @@ -1077,24 +1064,25 @@ function connectWebSocket() { console.error('❌ 设备命名空间连接失败:', error.message) }) + // 监听设备重启消息事件 + devicesSocket.on('device_restart_message', (data) => { + ElMessage.success({ + message: data.message, + duration: 5000 + }) + }) + // 监听各设备数据事件 devicesSocket.on('camera_frame', (data) => { frameCount++ tempInfo.value.camera_frame = data displayFrame(data.image) - }) - devicesSocket.on('device_restart_message', (data) => { - debugger - }) - + }) - - devicesSocket.on('femtobolt_frame', (data) => { tempInfo.value.femtobolt_frame = data displayDepthCameraFrame(data.depth_image || data.image) - }) - + }) devicesSocket.on('imu_data', (data) => { tempInfo.value.imu_data = data @@ -1164,41 +1152,76 @@ function startDeviceDataPush() { // 断开WebSocket连接函数 function disconnectWebSocket() { - if (socket && socket.connected) { - console.log('正在主动断开WebSocket连接...') + try { + if (socket) { + if (socket.connected) { + console.log('正在主动断开WebSocket连接...') - // 断开主连接 - socket.disconnect() - socket = null - isConnected.value = false + // 移除所有事件监听器 + socket.removeAllListeners() + + // 断开主连接 + socket.disconnect() + + console.log('✅ 主WebSocket连接已断开') + } + socket = null + isConnected.value = false + } - console.log('✅ 主WebSocket连接已断开') + // 断开统一设备命名空间连接 + if (devicesSocket) { + if (devicesSocket.connected) { + // 取消订阅所有设备 + try { + devicesSocket.emit('unsubscribe_device', { device_type: 'camera' }) + devicesSocket.emit('unsubscribe_device', { device_type: 'femtobolt' }) + devicesSocket.emit('unsubscribe_device', { device_type: 'imu' }) + devicesSocket.emit('unsubscribe_device', { device_type: 'pressure' }) + } catch (e) { + console.warn('取消设备订阅时出错:', e) + } + + // 移除所有事件监听器 + devicesSocket.removeAllListeners() + + // 断开连接 + devicesSocket.disconnect() + + console.log('🔗 统一设备命名空间连接已断开') + } + + devicesSocket = null + cameraSocket = null + femtoboltSocket = null + imuSocket = null + pressureSocket = null + restartSocket = null + } + + // 重置所有设备状态 + cameraStatus.value = '未连接' + femtoboltStatus.value = '未连接' + imuStatus.value = '未连接' + pressureStatus.value = '未连接' + + } catch (error) { + console.warn('断开WebSocket连接时出错:', error) } +} + +// WebSocket重连函数 +function reconnectWebSocket() { + console.log('开始重新连接WebSocket...') - // 断开统一设备命名空间连接 - if (devicesSocket && devicesSocket.connected) { - // 取消订阅所有设备 - devicesSocket.emit('unsubscribe_device', { device_type: 'camera' }) - devicesSocket.emit('unsubscribe_device', { device_type: 'femtobolt' }) - devicesSocket.emit('unsubscribe_device', { device_type: 'imu' }) - devicesSocket.emit('unsubscribe_device', { device_type: 'pressure' }) - - devicesSocket.disconnect() - devicesSocket = null - cameraSocket = null - femtoboltSocket = null - imuSocket = null - pressureSocket = null - restartSocket = null - - console.log('🔗 统一设备命名空间连接已断开') - } + // 先断开现有连接 + disconnectWebSocket() - // 重置所有设备状态 - cameraStatus.value = '未连接' - femtoboltStatus.value = '未连接' - imuStatus.value = '未连接' - pressureStatus.value = '未连接' + // 延迟一段时间后重新连接 + setTimeout(() => { + connectWebSocket() + console.log('WebSocket重连完成') + }, 1000) } @@ -1666,19 +1689,7 @@ async function sendDetectionData(data) { // 处理开始/停止按钮点击 async function handleStartStop() { - if (!isConnected.value) { - ElMessage.warning('WebSocket未连接,无法操作') - return - } - if( isPreventCombo.value == true){ - setTimeout(() => { - isPreventCombo.value = false - }, 2000); - ElMessage.warning(`请勿连击点击刷新按钮`) - return - } - isPreventCombo.value = true - if (isStart.value) { + if (isRecording.value) { // 停止录制视频 await stopRecord() } else { @@ -1739,8 +1750,7 @@ async function stopDetection() { }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - isRecording.value = false + } } catch (error) { console.error('❌ 停止检测失败:', error) ElMessage.error(`停止检测失败: ${error.message}`) @@ -1787,11 +1797,7 @@ const loadPatientInfo = async () => { const handleBeforeUnload = (event) => { console.log('页面即将关闭,正在清理资源...') - // 停止录像(如果正在录像) - if (isRecording.value) { - stopRecording() - } - + // 停止检测 stopDetection() // 断开WebSocket连接 @@ -1963,53 +1969,70 @@ onMounted(() => { }) onUnmounted(() => { + console.log('🔄 Detection组件正在卸载,开始清理资源...') - if (timerId.value) { - clearInterval(timerId.value); - } - // 停止录像(如果正在录像) - if (isRecording.value) { - stopRecording() - } - if(isStart.value == true){ - stopRecord() - } + try { + // 清理定时器 + if (timerId.value) { + clearInterval(timerId.value) + timerId.value = null + console.log('✅ 定时器已清理') + } + + // 停止录制 + if (isRecording.value === true) { + stopRecord() + console.log('✅ 录制已停止') + } - stopDetection() - // 页面关闭时断开WebSocket连接 - disconnectWebSocket() - - - - - // 清理图表资源 - if (tiltCharts) { - try { - tiltCharts.dispose(); - } catch (e) { - console.warn('tiltCharts dispose error in onUnmounted:', e); + // 停止检测 + stopDetection() + console.log('✅ 检测已停止') + + // 断开WebSocket连接 + disconnectWebSocket() + console.log('✅ WebSocket连接已断开') + + // 清理图表资源 + if (tiltCharts) { + try { + tiltCharts.dispose() + console.log('✅ tiltCharts已清理') + } catch (e) { + console.warn('tiltCharts dispose error in onUnmounted:', e) + } + tiltCharts = null } - tiltCharts = null; - } - if (rotationCharts) { - try { - rotationCharts.dispose(); - } catch (e) { - console.warn('rotationCharts dispose error in onUnmounted:', e); + + if (rotationCharts) { + try { + rotationCharts.dispose() + console.log('✅ rotationCharts已清理') + } catch (e) { + console.warn('rotationCharts dispose error in onUnmounted:', e) + } + rotationCharts = null } - rotationCharts = null; - } - if (pitchCharts) { - try { - pitchCharts.dispose(); - } catch (e) { - console.warn('pitchCharts dispose error in onUnmounted:', e); + + if (pitchCharts) { + try { + pitchCharts.dispose() + console.log('✅ pitchCharts已清理') + } catch (e) { + console.warn('pitchCharts dispose error in onUnmounted:', e) + } + pitchCharts = null } - pitchCharts = null; + + // 移除页面关闭事件监听器 + window.removeEventListener('beforeunload', handleBeforeUnload) + console.log('✅ beforeunload事件监听器已移除') + + console.log('🎉 Detection组件资源清理完成') + + } catch (error) { + console.error('❌ Detection组件卸载时出错:', error) } - - // 移除页面关闭事件监听器 - window.removeEventListener('beforeunload', handleBeforeUnload) }) const startRecord = async () => { // 开始录屏 @@ -2020,7 +2043,6 @@ const startRecord = async () => { // 开始录屏 if (!patientInfo.value || !patientInfo.value.sessionId) { throw new Error('缺少患者信息,无法开始录屏') } - isRecording.value = true let screen_location = contenGridRef.value.getBoundingClientRect() let femtobolt_location = wholeBodyRef.value.getBoundingClientRect() let camera_location = videoImgRef.value.getBoundingClientRect() @@ -2051,7 +2073,7 @@ const startRecord = async () => { // 开始录屏 // 保存会话ID和检测开始时间 patientInfo.value.detectionStartTime = Date.now() console.log('✅ 录屏会话创建成功,会话ID:', patientInfo.value.sessionId) - isStart.value = true + isRecording.value = true ElMessage.success('录屏已开始') } else { throw new Error(result.message || '开始录屏失败') @@ -2085,7 +2107,6 @@ const stopRecord = async () => { // 停止录屏 throw new Error(`HTTP ${response.status}: ${response.statusText}`) } isRecording.value = false - isStart.value = false } catch (error) { console.error('❌ 停止检测失败:', error) @@ -2096,21 +2117,23 @@ function routerClick(){ historyDialogVisible.value = true } - -const isPreventCombo =ref(false) // 防止连击 +const isRestart = ref(false) // 防止连击 // 单个刷新数据 function refreshClick(type) { - if( isPreventCombo.value == true){ - setTimeout(() => { - isPreventCombo.value = false - }, 5000); - ElMessage.warning(`请勿连击点击刷新按钮`) + // 检查是否在冷却期内 + if (isRestart.value === true) { + ElMessage.warning(`请勿连续点击设备重启按钮!请等待5秒后再试`) return - } - isPreventCombo.value = true + } + // 设置冷却状态 + isRestart.value = true + // 5秒后重置状态 + setTimeout(() => { + isRestart.value = false + }, 5000) + ElMessage.warning(`🚀 发送重启设备请求...`) if (devicesSocket && devicesSocket.connected) { - console.log('🚀 发送重启设备请求...') if(type == 'camera'){ devicesSocket.emit('restart_device', { device_type: 'camera' }) }else if(type == 'femtobolt'){ @@ -2120,13 +2143,8 @@ function refreshClick(type) { }else if(type == 'pressure'){ devicesSocket.emit('restart_device', { device_type: 'pressure' }) } - - - - - } else { - console.warn('⚠️ 设备Socket未连接,无法启动设备数据推送') + console.warn('⚠️ Socket服务未连接,无法重启设备!') } }