增加了单个设备的重启功能,并修复了刷新的错误。

This commit is contained in:
root 2025-09-29 15:20:40 +08:00
parent 7b7394bbac
commit 76c61d75df
3 changed files with 209 additions and 151 deletions

View File

@ -65,6 +65,9 @@ class DeviceCoordinator:
# 事件回调 # 事件回调
self.event_callbacks: Dict[str, List[Callable]] = defaultdict(list) self.event_callbacks: Dict[str, List[Callable]] = defaultdict(list)
# 状态变化回调存储
self._status_change_callback = None
# 性能统计 # 性能统计
self.stats = { self.stats = {
'start_time': None, 'start_time': None,
@ -78,6 +81,20 @@ class DeviceCoordinator:
self.logger.info("设备协调器初始化完成") 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: def initialize(self) -> bool:
""" """
初始化所有设备 初始化所有设备
@ -530,7 +547,7 @@ class DeviceCoordinator:
cleanup_start = time.time() cleanup_start = time.time()
self.logger.info(f"正在彻底清理 {device_name} 设备...") self.logger.info(f"正在彻底清理 {device_name} 设备...")
# 断开连接 # 断开连接但暂时不广播状态变化,避免重启过程中的状态冲突
if hasattr(device, 'disconnect'): if hasattr(device, 'disconnect'):
try: try:
device.disconnect() device.disconnect()
@ -538,6 +555,12 @@ class DeviceCoordinator:
except Exception as e: except Exception as e:
self.logger.warning(f"断开 {device_name} 连接异常: {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'): if hasattr(device, 'cleanup'):
try: try:
@ -605,6 +628,12 @@ class DeviceCoordinator:
create_time = (time.time() - create_start) * 1000 create_time = (time.time() - create_start) * 1000
self.logger.info(f"{device_name} 设备实例重新创建成功 (耗时: {create_time:.1f}ms)") 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: except Exception as e:
create_time = (time.time() - create_start) * 1000 create_time = (time.time() - create_start) * 1000
self.logger.error(f"重新创建 {device_name} 设备实例失败: {e} (耗时: {create_time:.1f}ms)") self.logger.error(f"重新创建 {device_name} 设备实例失败: {e} (耗时: {create_time:.1f}ms)")
@ -624,6 +653,14 @@ class DeviceCoordinator:
init_time = (time.time() - init_start) * 1000 init_time = (time.time() - init_start) * 1000
self.logger.info(f"{device_name} 设备初始化成功 (耗时: {init_time:.1f}ms)") 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 stream_time = 0
if was_streaming and hasattr(new_device, 'start_streaming'): if was_streaming and hasattr(new_device, 'start_streaming'):

View File

@ -187,13 +187,15 @@ class AppServer:
# 初始化设备协调器(统一管理所有设备) # 初始化设备协调器(统一管理所有设备)
self.logger.info('正在初始化设备协调器...') self.logger.info('正在初始化设备协调器...')
self.device_coordinator = DeviceCoordinator(self.socketio) self.device_coordinator = DeviceCoordinator(self.socketio)
# 设置状态变化回调
self.device_coordinator.set_status_change_callback(self._on_device_status_change)
# 调用初始化方法来初始化设备 # 调用初始化方法来初始化设备
if self.device_coordinator.initialize(): if self.device_coordinator.initialize():
self.logger.info('设备协调器初始化完成') self.logger.info('设备协调器初始化完成')
# 获取设备管理器实例 # 获取设备管理器实例
self.device_managers = self.device_coordinator.get_device_managers() self.device_managers = self.device_coordinator.get_device_managers()
# 为每个设备添加状态变化回调 # 为每个设备添加状态变化回调(双重保险)
for device_name, manager in self.device_managers.items(): for device_name, manager in self.device_managers.items():
if manager and hasattr(manager, 'add_status_change_callback'): if manager and hasattr(manager, 'add_status_change_callback'):
manager.add_status_change_callback(self._on_device_status_change) manager.add_status_change_callback(self._on_device_status_change)
@ -1488,7 +1490,7 @@ class AppServer:
'device_type': device_type, 'device_type': device_type,
'message': f'{device_type} 设备重启成功!', 'message': f'{device_type} 设备重启成功!',
'timestamp': time.time() 'timestamp': time.time()
}) }, namespace='/devices')
return True return True
else: else:
self.logger.error(f'{device_type} 设备重启失败!') self.logger.error(f'{device_type} 设备重启失败!')
@ -1497,7 +1499,7 @@ class AppServer:
'device_type': device_type, 'device_type': device_type,
'message': f'{device_type} 设备重启失败!', 'message': f'{device_type} 设备重启失败!',
'timestamp': time.time() 'timestamp': time.time()
}) }, namespace='/devices')
return False return False
except Exception as e: except Exception as e:
@ -1509,7 +1511,7 @@ class AppServer:
'error': str(e), 'error': str(e),
'message': f'重启 {device_type} 设备时发生异常', 'message': f'重启 {device_type} 设备时发生异常',
'timestamp': time.time() 'timestamp': time.time()
}) }, namespace='/devices')
return False return False
@ -1649,7 +1651,8 @@ class AppServer:
def broadcast_all_device_status(self): 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: if manager is not None:
try: try:
# 检查设备是否连接使用is_connected属性 # 检查设备是否连接使用is_connected属性

View File

@ -8,7 +8,6 @@
<div style="display: flex;align-items: center;"> <div style="display: flex;align-items: center;">
<div v-if="!isRecording" class="top-bar-left" @click="routeTo('/')" style="cursor: pointer;"> <div v-if="!isRecording" class="top-bar-left" @click="routeTo('/')" style="cursor: pointer;">
<img src="@/assets/svg/u14.svg" alt=""> <img src="@/assets/svg/u14.svg" alt="">
<!-- <el-icon class="back-icon" @click="handleBack"><ArrowLeft /></el-icon> -->
<span class="page-title">实时检测</span> <span class="page-title">实时检测</span>
</div> </div>
<div style="padding-left: 10px;"> <div style="padding-left: 10px;">
@ -18,7 +17,6 @@
style="margin-left: 20px;cursor: pointer; width: 24px;height: 24px;" style="margin-left: 20px;cursor: pointer; width: 24px;height: 24px;"
@click="cameraUpdate"> @click="cameraUpdate">
<!-- 录制时间 --> <!-- 录制时间 -->
<div v-if="isRecording" class="icon-container"> <div v-if="isRecording" class="icon-container">
<img src="@/assets/record.png" class="blink-icon" :class="{ blinking: isRunning }" alt=""> <img src="@/assets/record.png" class="blink-icon" :class="{ blinking: isRunning }" alt="">
@ -97,12 +95,12 @@
头部姿态 头部姿态
</div> </div>
<div> <div>
<el-button type="primary" class="start-btn" @click="calibrationClick" :disabled="isRecording" <el-button type="primary" class="start-btn" @click="calibrationClick"
style="background-color: #0099ff;font-size: 14px; style="background-color: #0099ff;font-size: 14px;
--el-button-border-color: transparent !important;border-radius: 20px;height:26px;border:none;width: 100px;"> --el-button-border-color: transparent !important;border-radius: 20px;height:26px;border:none;width: 100px;">
校准 校准
</el-button> </el-button>
<el-button type="primary" class="start-btn" @click="clearAndStartTracking" :disabled="isRecording" <el-button type="primary" class="start-btn" @click="clearAndStartTracking"
style="background-color: #0099ff;font-size: 14px;margin-left: 15px; style="background-color: #0099ff;font-size: 14px;margin-left: 15px;
--el-button-border-color: transparent !important;border-radius: 20px;height:26px;border:none;width: 100px;"> --el-button-border-color: transparent !important;border-radius: 20px;height:26px;border:none;width: 100px;">
清零 清零
@ -581,13 +579,11 @@ import HistoryDashboard from '@/views/PatientProfile.vue'
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const isStart = ref(false) const isRecording = ref(false)
const isConnected = ref(false) const isConnected = ref(false)
const rtspImgSrc = ref('') const rtspImgSrc = ref('')
const depthCameraImgSrc = ref('') // const depthCameraImgSrc = ref('') //
const screenshotLoading = ref(false) const screenshotLoading = ref(false)
const dataCollectionLoading = ref(false)
const isRecording = ref(false)
const cameraDialogVisible =ref(false) // const cameraDialogVisible =ref(false) //
const contenGridRef =ref(null) // box const contenGridRef =ref(null) // box
const wholeBodyRef = ref(null) // 姿ref const wholeBodyRef = ref(null) // 姿ref
@ -893,16 +889,17 @@ const savePatient = async () => {
throw error throw error
} }
} }
function routeTo(path) { const isPreventCombo = ref(false)
if( isPreventCombo.value == true){ function routeTo(path = '/') {
setTimeout(() => { if (isPreventCombo.value === true) {
isPreventCombo.value = false ElMessage.warning(`请勿连续点击回退按钮!`)
}, 2000);
ElMessage.warning(`请勿连击点击刷新按钮`)
return return
} }
isPreventCombo.value = true isPreventCombo.value = true
router.push(`/`) setTimeout(() => {
isPreventCombo.value = false
}, 2000)
router.push(path)
} }
function cameraUpdate() { // function cameraUpdate() { //
cameraForm.value = { // cameraForm.value = { //
@ -1013,21 +1010,11 @@ function connectWebSocket() {
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
console.error('❌ 主连接失败:', error.message) console.error('❌ 主连接失败:', error.message)
isConnected.value = false isConnected.value = false
//
if (isRecording.value) {
stopRecording()
isStart.value = false
}
}) })
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.log('⚠️ 主连接断开:', reason) console.log('⚠️ 主连接断开:', reason)
isConnected.value = false isConnected.value = false
if (isRecording.value) {
stopRecording()
isStart.value = false
}
}) })
socket.on('reconnect', (attemptNumber) => { socket.on('reconnect', (attemptNumber) => {
@ -1077,25 +1064,26 @@ function connectWebSocket() {
console.error('❌ 设备命名空间连接失败:', error.message) console.error('❌ 设备命名空间连接失败:', error.message)
}) })
//
devicesSocket.on('device_restart_message', (data) => {
ElMessage.success({
message: data.message,
duration: 5000
})
})
// //
devicesSocket.on('camera_frame', (data) => { devicesSocket.on('camera_frame', (data) => {
frameCount++ frameCount++
tempInfo.value.camera_frame = data tempInfo.value.camera_frame = data
displayFrame(data.image) displayFrame(data.image)
}) })
devicesSocket.on('device_restart_message', (data) => {
debugger
})
devicesSocket.on('femtobolt_frame', (data) => { devicesSocket.on('femtobolt_frame', (data) => {
tempInfo.value.femtobolt_frame = data tempInfo.value.femtobolt_frame = data
displayDepthCameraFrame(data.depth_image || data.image) displayDepthCameraFrame(data.depth_image || data.image)
}) })
devicesSocket.on('imu_data', (data) => { devicesSocket.on('imu_data', (data) => {
tempInfo.value.imu_data = data tempInfo.value.imu_data = data
handleIMUData(data) handleIMUData(data)
@ -1164,34 +1152,51 @@ function startDeviceDataPush() {
// WebSocket // WebSocket
function disconnectWebSocket() { function disconnectWebSocket() {
if (socket && socket.connected) { try {
if (socket) {
if (socket.connected) {
console.log('正在主动断开WebSocket连接...') console.log('正在主动断开WebSocket连接...')
//
socket.removeAllListeners()
// //
socket.disconnect() socket.disconnect()
socket = null
isConnected.value = false
console.log('✅ 主WebSocket连接已断开') console.log('✅ 主WebSocket连接已断开')
} }
socket = null
isConnected.value = false
}
// //
if (devicesSocket && devicesSocket.connected) { if (devicesSocket) {
if (devicesSocket.connected) {
// //
try {
devicesSocket.emit('unsubscribe_device', { device_type: 'camera' }) devicesSocket.emit('unsubscribe_device', { device_type: 'camera' })
devicesSocket.emit('unsubscribe_device', { device_type: 'femtobolt' }) devicesSocket.emit('unsubscribe_device', { device_type: 'femtobolt' })
devicesSocket.emit('unsubscribe_device', { device_type: 'imu' }) devicesSocket.emit('unsubscribe_device', { device_type: 'imu' })
devicesSocket.emit('unsubscribe_device', { device_type: 'pressure' }) devicesSocket.emit('unsubscribe_device', { device_type: 'pressure' })
} catch (e) {
console.warn('取消设备订阅时出错:', e)
}
//
devicesSocket.removeAllListeners()
//
devicesSocket.disconnect() devicesSocket.disconnect()
console.log('🔗 统一设备命名空间连接已断开')
}
devicesSocket = null devicesSocket = null
cameraSocket = null cameraSocket = null
femtoboltSocket = null femtoboltSocket = null
imuSocket = null imuSocket = null
pressureSocket = null pressureSocket = null
restartSocket = null restartSocket = null
console.log('🔗 统一设备命名空间连接已断开')
} }
// //
@ -1199,6 +1204,24 @@ function disconnectWebSocket() {
femtoboltStatus.value = '未连接' femtoboltStatus.value = '未连接'
imuStatus.value = '未连接' imuStatus.value = '未连接'
pressureStatus.value = '未连接' pressureStatus.value = '未连接'
} catch (error) {
console.warn('断开WebSocket连接时出错:', error)
}
}
// WebSocket
function reconnectWebSocket() {
console.log('开始重新连接WebSocket...')
//
disconnectWebSocket()
//
setTimeout(() => {
connectWebSocket()
console.log('WebSocket重连完成')
}, 1000)
} }
@ -1666,19 +1689,7 @@ async function sendDetectionData(data) {
// / // /
async function handleStartStop() { async function handleStartStop() {
if (!isConnected.value) { if (isRecording.value) {
ElMessage.warning('WebSocket未连接无法操作')
return
}
if( isPreventCombo.value == true){
setTimeout(() => {
isPreventCombo.value = false
}, 2000);
ElMessage.warning(`请勿连击点击刷新按钮`)
return
}
isPreventCombo.value = true
if (isStart.value) {
// //
await stopRecord() await stopRecord()
} else { } else {
@ -1740,7 +1751,6 @@ async function stopDetection() {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
} }
isRecording.value = false
} catch (error) { } catch (error) {
console.error('❌ 停止检测失败:', error) console.error('❌ 停止检测失败:', error)
ElMessage.error(`停止检测失败: ${error.message}`) ElMessage.error(`停止检测失败: ${error.message}`)
@ -1787,10 +1797,6 @@ const loadPatientInfo = async () => {
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
console.log('页面即将关闭,正在清理资源...') console.log('页面即将关闭,正在清理资源...')
//
if (isRecording.value) {
stopRecording()
}
// //
stopDetection() stopDetection()
@ -1963,53 +1969,70 @@ onMounted(() => {
}) })
onUnmounted(() => { onUnmounted(() => {
console.log('🔄 Detection组件正在卸载开始清理资源...')
try {
//
if (timerId.value) { if (timerId.value) {
clearInterval(timerId.value); clearInterval(timerId.value)
timerId.value = null
console.log('✅ 定时器已清理')
} }
//
if (isRecording.value) { //
stopRecording() if (isRecording.value === true) {
}
if(isStart.value == true){
stopRecord() stopRecord()
console.log('✅ 录制已停止')
} }
//
stopDetection() stopDetection()
// WebSocket console.log('✅ 检测已停止')
// WebSocket
disconnectWebSocket() disconnectWebSocket()
console.log('✅ WebSocket连接已断开')
// //
if (tiltCharts) { if (tiltCharts) {
try { try {
tiltCharts.dispose(); tiltCharts.dispose()
console.log('✅ tiltCharts已清理')
} catch (e) { } catch (e) {
console.warn('tiltCharts dispose error in onUnmounted:', e); console.warn('tiltCharts dispose error in onUnmounted:', e)
} }
tiltCharts = null; tiltCharts = null
} }
if (rotationCharts) { if (rotationCharts) {
try { try {
rotationCharts.dispose(); rotationCharts.dispose()
console.log('✅ rotationCharts已清理')
} catch (e) { } catch (e) {
console.warn('rotationCharts dispose error in onUnmounted:', e); console.warn('rotationCharts dispose error in onUnmounted:', e)
} }
rotationCharts = null; rotationCharts = null
} }
if (pitchCharts) { if (pitchCharts) {
try { try {
pitchCharts.dispose(); pitchCharts.dispose()
console.log('✅ pitchCharts已清理')
} catch (e) { } catch (e) {
console.warn('pitchCharts dispose error in onUnmounted:', e); console.warn('pitchCharts dispose error in onUnmounted:', e)
} }
pitchCharts = null; pitchCharts = null
} }
// //
window.removeEventListener('beforeunload', handleBeforeUnload) window.removeEventListener('beforeunload', handleBeforeUnload)
console.log('✅ beforeunload事件监听器已移除')
console.log('🎉 Detection组件资源清理完成')
} catch (error) {
console.error('❌ Detection组件卸载时出错:', error)
}
}) })
const startRecord = async () => { // const startRecord = async () => { //
@ -2020,7 +2043,6 @@ const startRecord = async () => { // 开始录屏
if (!patientInfo.value || !patientInfo.value.sessionId) { if (!patientInfo.value || !patientInfo.value.sessionId) {
throw new Error('缺少患者信息,无法开始录屏') throw new Error('缺少患者信息,无法开始录屏')
} }
isRecording.value = true
let screen_location = contenGridRef.value.getBoundingClientRect() let screen_location = contenGridRef.value.getBoundingClientRect()
let femtobolt_location = wholeBodyRef.value.getBoundingClientRect() let femtobolt_location = wholeBodyRef.value.getBoundingClientRect()
let camera_location = videoImgRef.value.getBoundingClientRect() let camera_location = videoImgRef.value.getBoundingClientRect()
@ -2051,7 +2073,7 @@ const startRecord = async () => { // 开始录屏
// ID // ID
patientInfo.value.detectionStartTime = Date.now() patientInfo.value.detectionStartTime = Date.now()
console.log('✅ 录屏会话创建成功会话ID:', patientInfo.value.sessionId) console.log('✅ 录屏会话创建成功会话ID:', patientInfo.value.sessionId)
isStart.value = true isRecording.value = true
ElMessage.success('录屏已开始') ElMessage.success('录屏已开始')
} else { } else {
throw new Error(result.message || '开始录屏失败') throw new Error(result.message || '开始录屏失败')
@ -2085,7 +2107,6 @@ const stopRecord = async () => { // 停止录屏
throw new Error(`HTTP ${response.status}: ${response.statusText}`) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
} }
isRecording.value = false isRecording.value = false
isStart.value = false
} catch (error) { } catch (error) {
console.error('❌ 停止检测失败:', error) console.error('❌ 停止检测失败:', error)
@ -2096,21 +2117,23 @@ function routerClick(){
historyDialogVisible.value = true historyDialogVisible.value = true
} }
const isRestart = ref(false) //
const isPreventCombo =ref(false) //
// //
function refreshClick(type) { function refreshClick(type) {
if( isPreventCombo.value == true){ //
setTimeout(() => { if (isRestart.value === true) {
isPreventCombo.value = false ElMessage.warning(`请勿连续点击设备重启按钮请等待5秒后再试`)
}, 5000);
ElMessage.warning(`请勿连击点击刷新按钮`)
return return
} }
isPreventCombo.value = true //
isRestart.value = true
// 5
setTimeout(() => {
isRestart.value = false
}, 5000)
ElMessage.warning(`🚀 发送重启设备请求...`)
if (devicesSocket && devicesSocket.connected) { if (devicesSocket && devicesSocket.connected) {
console.log('🚀 发送重启设备请求...')
if(type == 'camera'){ if(type == 'camera'){
devicesSocket.emit('restart_device', { device_type: 'camera' }) devicesSocket.emit('restart_device', { device_type: 'camera' })
}else if(type == 'femtobolt'){ }else if(type == 'femtobolt'){
@ -2120,13 +2143,8 @@ function refreshClick(type) {
}else if(type == 'pressure'){ }else if(type == 'pressure'){
devicesSocket.emit('restart_device', { device_type: 'pressure' }) devicesSocket.emit('restart_device', { device_type: 'pressure' })
} }
} else { } else {
console.warn('⚠️ 设备Socket未连接无法启动设备数据推送') console.warn('⚠️ Socket服务未连接无法重启设备')
} }
} }
</script> </script>