diff --git a/backend/config.ini b/backend/config.ini index f84a9c3f..83ba47db 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -30,7 +30,7 @@ backend = directshow [CAMERA2] enable = True -device_index = 1 +device_index = 2 width = 1280 height = 720 fps = 30 @@ -39,7 +39,7 @@ fourcc = MJPG backend = directshow [FEMTOBOLT] -enable = False +enable = True algorithm_type = plt color_resolution = 1080P depth_mode = NFOV_2X2BINNED @@ -54,13 +54,13 @@ imu_enable = True imu_use_mock = False imu_ble_name = WT901BLE67 imu_mac_address = FA:E8:88:06:FE:F3 -pressure_enable = False -pressure_use_mock = False -pressure_port = COM5 +pressure_enable = True +pressure_use_mock = True +pressure_port = COM3 pressure_baudrate = 115200 [REMOTE] -enable = True +enable = False port = COM6 baudrate = 115200 timeout = 0.1 diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py index 7c569fb4..cb720318 100644 --- a/backend/devices/device_coordinator.py +++ b/backend/devices/device_coordinator.py @@ -57,6 +57,10 @@ class DeviceCoordinator: self.is_initialized = False self.is_running = False self.coordinator_lock = threading.RLock() + self._init_summary = { + 'initialized_at': None, + 'device_results': {}, + } # 监控线程 self.monitor_thread = None @@ -116,18 +120,22 @@ class DeviceCoordinator: self._register_namespaces() # 初始化设备(失败则降级继续) - if not self._initialize_devices(): - self.logger.warning("设备初始化失败,将以降级模式继续运行") - + init_ok = bool(self._initialize_devices()) + self._init_summary['initialized_at'] = time.time() + if not init_ok: + self.logger.warning("设备初始化失败(没有任何设备初始化成功)") + self.is_initialized = False + return False + self.is_initialized = True self.stats['start_time'] = time.time() - + # 启动监控线程 self._start_monitor() - + self.logger.info("设备协调器初始化成功") self._emit_event('coordinator_initialized', {'devices': list(self.devices.keys())}) - + return True except Exception as e: @@ -195,12 +203,14 @@ class DeviceCoordinator: try: timeout_s = 45 if device_name == 'imu' else 30 result = future.result(timeout=timeout_s) + self._init_summary['device_results'][device_name] = bool(result) if result: success_count += 1 self.logger.info(f"{device_name}设备初始化成功") else: self.logger.error(f"{device_name}设备初始化失败") except Exception as e: + self._init_summary['device_results'][device_name] = False self.logger.error(f"{device_name}设备初始化异常: {e}") # 至少需要一个设备初始化成功 @@ -214,6 +224,97 @@ class DeviceCoordinator: except Exception as e: self.logger.error(f"设备初始化失败: {e}") return False + + def get_enabled_devices(self) -> List[str]: + enabled = [] + try: + for name, cfg in (self.device_configs or {}).items(): + if isinstance(cfg, dict) and cfg.get('enable', False): + enabled.append(name) + except Exception: + pass + return enabled + + def get_required_devices_for_detection(self) -> List[str]: + enabled = self.get_enabled_devices() + required = [d for d in enabled if d not in ('remote',)] + return required + + def get_device_readiness(self, device_name: str) -> Dict[str, Any]: + enabled = bool(self.device_configs.get(device_name, {}).get('enable', False)) + device = self.devices.get(device_name) + readiness = { + 'device_name': device_name, + 'enabled': enabled, + 'exists': device is not None, + 'initializing': False, + 'is_connected': False, + 'is_streaming': False, + 'ready': False, + } + + if not device: + return readiness + status = None + try: + if hasattr(device, 'get_status'): + status = device.get_status() + except Exception: + status = None + + try: + readiness['initializing'] = bool(getattr(device, '_initializing', False)) + except Exception: + readiness['initializing'] = False + try: + if isinstance(status, dict) and 'is_connected' in status: + readiness['is_connected'] = bool(status.get('is_connected')) + else: + readiness['is_connected'] = bool(getattr(device, 'is_connected', False)) + except Exception: + readiness['is_connected'] = False + try: + if isinstance(status, dict) and 'is_streaming' in status: + readiness['is_streaming'] = bool(status.get('is_streaming')) + else: + readiness['is_streaming'] = bool(getattr(device, 'is_streaming', False)) + except Exception: + readiness['is_streaming'] = False + + ok = readiness['is_connected'] and (not readiness['initializing']) + readiness['ready'] = bool(ok) + return readiness + + def get_readiness_snapshot(self, required_devices: Optional[List[str]] = None) -> Dict[str, Any]: + required = required_devices if required_devices is not None else self.get_required_devices_for_detection() + devices = {} + for name in required: + devices[name] = self.get_device_readiness(name) + + all_ready = bool(self.is_initialized) and all(devices[name].get('ready', False) for name in devices) + return { + 'coordinator': { + 'is_initialized': bool(self.is_initialized), + 'is_running': bool(self.is_running), + 'enabled_devices': self.get_enabled_devices(), + 'required_devices': required, + 'init_summary': self._init_summary, + }, + 'devices': devices, + 'all_ready': all_ready, + } + + def wait_until_ready_for_detection(self, timeout_s: float = 10.0, poll_interval_s: float = 0.2) -> Dict[str, Any]: + deadline = time.time() + max(0.0, float(timeout_s)) + last_snapshot = self.get_readiness_snapshot() + while time.time() < deadline: + last_snapshot = self.get_readiness_snapshot() + if last_snapshot.get('all_ready', False): + return last_snapshot + time.sleep(max(0.05, float(poll_interval_s))) + last_snapshot['timeout'] = True + last_snapshot['timeout_s'] = float(timeout_s) + return last_snapshot def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool: """ diff --git a/backend/devices/pressure_manager.py b/backend/devices/pressure_manager.py index 78c8b3a8..1d81f4d8 100644 --- a/backend/devices/pressure_manager.py +++ b/backend/devices/pressure_manager.py @@ -704,6 +704,7 @@ class PressureManager(BaseDevice): Returns: bool: 初始化是否成功 """ + self._initializing = True try: self.logger.info(f"正在初始化压力板设备...") @@ -716,13 +717,28 @@ class PressureManager(BaseDevice): else: self.device = MockPressureDevice() + connected = False + try: + if self.use_mock: + connected = True + elif hasattr(self.device, 'is_connected'): + connected = bool(self.device.is_connected) + else: + connected = bool(self.check_hardware_connection()) + except Exception: + connected = False + # 使用set_connected方法启动连接监控线程 - self.set_connected(True) + self.set_connected(bool(connected)) self._device_info.update({ 'device_type': 'mock' if self.use_mock else 'real', 'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown' }) + if not connected: + self.logger.warning("压力板初始化完成但硬件未连接") + return False + self.logger.info(f"压力板初始化成功 - use_mock: {self.use_mock}") return True @@ -732,6 +748,8 @@ class PressureManager(BaseDevice): self.set_connected(False) self.device = None return False + finally: + self._initializing = False def start_streaming(self) -> bool: diff --git a/backend/main.py b/backend/main.py index 6d981aac..e5da5118 100644 --- a/backend/main.py +++ b/backend/main.py @@ -265,7 +265,7 @@ class AppServer: def _initialize_devices(self): """ - 初始化设备(在用户登录成功后调用) + """ try: self.logger.info('开始初始化设备...') @@ -1293,22 +1293,38 @@ class AppServer: try: if not self.db_manager or not self.device_coordinator: return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 - - # 检查设备是否已初始化 - if self.device_coordinator and not self.device_coordinator.is_initialized: - self.logger.info('设备尚未初始化,等待初始化完成...') - # 最多等待10秒 - start_wait = time.time() - while not self.device_coordinator.is_initialized: - if time.time() - start_wait > 10: - return jsonify({'success': False, 'error': '设备初始化超时,请稍后重试'}), 503 - time.sleep(0.5) - self.logger.info('设备初始化完成,继续开始检测') - data = flask_request.get_json() + data = flask_request.get_json() or {} patient_id = data.get('patient_id') - creator_id = data.get('creator_id') + creator_id = data.get('creator_id') + force_start = bool(data.get('force_start', False)) + if self.device_coordinator: + if not getattr(self.device_coordinator, 'is_initialized', False): + try: + threading.Thread(target=self._initialize_devices, daemon=True).start() + except Exception: + pass + + readiness = {} + if not force_start: + try: + readiness = self.device_coordinator.wait_until_ready_for_detection(timeout_s=10.0, poll_interval_s=0.2) + except Exception as e: + self.logger.error(f'等待设备就绪失败: {e}') + readiness = {} + + if not readiness or not readiness.get('all_ready', False): + return jsonify({ + 'success': False, + 'error': '设备未就绪,请稍后重试', + 'readiness': readiness + }), 503 + else: + try: + readiness = self.device_coordinator.get_readiness_snapshot() + except Exception: + readiness = {} if not patient_id or not creator_id: return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400 @@ -1324,7 +1340,7 @@ class AppServer: except Exception as monitor_error: self.logger.error(f'启动设备连接监控失败: {monitor_error}') - return jsonify({'success': True, 'session_id': session_id}) + return jsonify({'success': True, 'session_id': session_id, 'forced': bool(force_start), 'readiness': readiness}) except Exception as e: self.logger.error(f'开始检测失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @@ -1366,10 +1382,8 @@ class AppServer: for item in session_data['videos']: item_copy = item.copy() item_copy['type'] = 'video' - result_data.append(item_copy) + result_data.append(item_copy) - # 按时间戳排序 - result_data.sort(key=lambda x: x.get('timestamp', ''), reverse=False) return jsonify({ 'success': True, diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 1cf48ced..6bfcb605 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -39,7 +39,7 @@
-
+
@@ -239,16 +239,17 @@
- 右前足 - + {{ footPressure.right_front }}% + 右前足 +
- 右后足 - + {{ footPressure.right_rear }}% + 右后足
+
+
+
+
+
{{ initLoadingText }}
+
+
+
设备未就绪
+
{{ initErrorText || '请检查设备连接与配置后重试' }}
+
+
+
+
{{ getDeviceDisplayName(item.device_name) }}
+
{{ getNotReadyReason(item) }}
+
+
+
是否启用: {{ item.enabled === false ? 'N' : 'Y' }}
+
是否创建实例: {{ item.exists ? 'Y' : 'N' }}
+
是否初始化中: {{ item.initializing ? 'Y' : 'N' }}
+
是否已连接: {{ item.is_connected ? 'Y' : 'N' }}
+ +
+
+
+
+ 返回 + 进入检测 +
+
+
+
@@ -808,7 +840,7 @@
-
相机参数设置
+
设备参数设置
@@ -826,7 +858,7 @@
-
+
足部相机
@@ -856,20 +888,9 @@
-
-
-
遥控器
-
-
-
串口号
- - - - + -
- -
+
IMU设备
@@ -883,7 +904,30 @@
- + +
+
+
足底压力板
+
+
+
串口号
+ + + + +
+
+
+
遥控器
+
+
+
串口号
+ + + + + +
退出 { + const r = initReadiness.value + const devices = r && r.devices ? r.devices : null + if (!devices) return [] + const list = [] + Object.keys(devices).forEach((k) => { + const v = devices[k] + if (v && v.enabled !== false && v.ready === false) { + list.push(v) + } + }) + return list +}) +function getNotReadyReason(item) { + if (!item) return '未知' + if (item.enabled === false) return '未启用' + if (!item.exists) return '设备实例未创建' + if (item.initializing) return '初始化中' + if (!item.is_connected) return '未连接' + return '未就绪' +} +function getDeviceDisplayName(deviceName) { + const name = String(deviceName || '').trim() + const map = { + camera1: '足部相机上', + camera2: '足部相机下', + femtobolt: '深度相机', + imu: '头部姿态(IMU)', + pressure: '足底压力板', + remote: '遥控器', + } + return map[name] || name +} +function handleInitBack() { + try { + isSystemInitializing.value = false + initFailed.value = false + initErrorText.value = '' + initReadiness.value = null + } catch (e) { + } + try { + emit('endChange', false) + } catch (e2) { + } + try { + routeTo('/') + } catch (e3) { + } +} +async function handleInitProceed() { + try { + initLoadingText.value = '正在进入检测...' + initFailed.value = false + initErrorText.value = '' + initReadiness.value = null + isSystemInitializing.value = true + await startDetection({ forceStart: true, silent: true }) + connectWebSocket() + isSystemInitializing.value = false + } catch (e) { + const msg = String((e && e.message) ? e.message : (e || '')) + const readiness = (e && e.readiness) ? e.readiness : null + if (readiness) { + initReadiness.value = readiness + } + initFailed.value = true + initErrorText.value = msg || '进入检测失败' + initLoadingText.value = '系统设备正在初始化,请等待...' + isSystemInitializing.value = true + ElMessage.error(`进入检测失败: ${initErrorText.value}`) + } +} const isStartVideo = ref(false) function startVideoClick() { startRecord() @@ -1117,6 +1239,10 @@ const cameraForm = ref({ // 相机参数 port: '', // 遥控器串口号 enable: false }, + pressure:{ + port: '', + enable: false + }, }) const calculatedAge = ref(null) @@ -1359,8 +1485,13 @@ function cameraUpdate() { // 相机设置数据更新弹框 enable: false }, remote:{ - port: '', // IMU串口号 - } + port: '', + enable: false + }, + pressure:{ + port: '', + enable: false + } } // 加载相机参数信息 getDevicesInit() @@ -2198,10 +2329,13 @@ async function sendDetectionData(data) { // } // } // 开始检测 -async function startDetection() { +async function startDetection(options = {}) { try { console.log('🚀 正在开始检测...') + const forceStart = !!(options && options.forceStart) + const silent = !!(options && options.silent) + // 调用后端API开始检测 const response = await fetch(`${BACKEND_URL}/api/detection/start`, { method: 'POST', @@ -2211,11 +2345,22 @@ async function startDetection() { body: JSON.stringify({ patient_id: patientId.value, // 可以添加其他检测参数 - creator_id: creatorId.value, + creator_id: creatorId.value, + force_start: forceStart, }) }) if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) + let errorMsg = `HTTP ${response.status}: ${response.statusText}` + let readiness = null + try { + const err = await response.json() + readiness = (err && err.readiness) ? err.readiness : null + errorMsg = (err && (err.error || err.message)) ? (err.error || err.message) : errorMsg + } catch (e) { + } + const ex = new Error(errorMsg) + ex.readiness = readiness + throw ex } const result = await response.json() if (result.success) { @@ -2228,7 +2373,9 @@ async function startDetection() { } catch (error) { console.error('💥 开始检测失败:', error) - ElMessage.error(`开始检测失败: ${error.message}`) + if (!(options && options.silent)) { + ElMessage.error(`开始检测失败: ${error.message}`) + } throw error } } @@ -2432,7 +2579,7 @@ const getDevicesInit = async () => { if (response.ok) { const result = await response.json() if (result.success) { - console.log('相机参数加载成功:', result.data) + console.log('设备参数加载成功:', result.data) cameraForm.value = result.data cameraDialogVisible.value = true // console.log('相机参数加载成功:', patientInfo.value) @@ -2443,12 +2590,12 @@ const getDevicesInit = async () => { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } } catch (error) { - console.error('加载相机参数失败:', error) - ElMessage.warning('加载相机参数失败,请检查网络连接') + console.error('加载设备参数失败:', error) + ElMessage.warning('加载设备参数失败,请检查网络连接') } } -onMounted(() => { +onMounted(async () => { for(let i = 0; i < 20; i++){ let port = "COM" + (i + 1) remotePortData.value.push(port) @@ -2459,12 +2606,35 @@ onMounted(() => { } patientId.value = props.selectedPatient.id //patientId.value = '202511150005' - // 加载患者信息 - loadPatientInfo() - // 启动检测 - startDetection() - // 页面加载时自动连接WebSocket - connectWebSocket() + try { + isSystemInitializing.value = true + initFailed.value = false + initErrorText.value = '' + initReadiness.value = null + await loadPatientInfo() + await startDetection() + connectWebSocket() + isSystemInitializing.value = false + } catch (e) { + const msg = String((e && e.message) ? e.message : (e || '')) + const readiness = (e && e.readiness) ? e.readiness : null + if (readiness) { + initReadiness.value = readiness + initFailed.value = true + initErrorText.value = msg || '设备未就绪,请稍后重试' + initLoadingText.value = '系统设备正在初始化,请等待...' + isSystemInitializing.value = true + ElMessage.error('设备初始化失败,请检查未就绪设备') + return + } + isSystemInitializing.value = false + if (msg) { + ElMessage.error(msg) + } else { + ElMessage.error('进入检测失败') + } + handleInitBack() + } // 监听页面关闭或刷新事件 window.addEventListener('beforeunload', handleBeforeUnload) @@ -3349,6 +3519,129 @@ function viewClick(e){ height: 100vh; background: rgba(0, 0, 0, 0.7); } +.init-mask{ + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} +.init-loading-container{ + width: 520px; + max-width: calc(100vw - 40px); + max-height: calc(100vh - 140px); + border-radius: 12px; + background: linear-gradient(135deg, rgba(53, 67, 90, 1) 0%, rgba(53, 67, 90, 1) 0%, rgba(62, 79, 105, 1) 99%, rgba(62, 79, 105, 1) 100%); + display: flex; + align-items: center; + justify-content: center; + box-shadow: rgb(17, 24, 33) 0px 0px 10px; + padding: 18px 20px; + box-sizing: border-box; +} +.init-loading-content{ + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.init-spinner{ + width: 34px; + height: 34px; + border: 3px solid rgba(255, 255, 255, 0.25); + border-top-color: rgba(59, 242, 198, 1); + border-radius: 50%; + animation: initSpin 0.9s linear infinite; +} +.init-loading-text{ + margin-top: 16px; + font-family:'Noto Sans SC Bold', 'Noto Sans SC Regular', 'Noto Sans SC'; + font-weight:700; + font-style:normal; + font-size:16px; + color:#FFFFFF; + text-align: center; +} +@keyframes initSpin { + to { transform: rotate(360deg); } +} +.init-failed-content{ + width: 100%; +} +.init-failed-title{ + font-family:'Noto Sans SC Bold', 'Noto Sans SC Regular', 'Noto Sans SC'; + font-weight:700; + font-style:normal; + font-size:18px; + color:#FFFFFF; + text-align: left; +} +.init-failed-subtitle{ + margin-top: 8px; + font-family:'Noto Sans SC', 'Noto Sans SC Regular', 'Noto Sans SC'; + font-weight:400; + font-style:normal; + font-size:14px; + color: rgba(255, 255, 255, 0.75); + text-align: left; +} +.init-failed-list{ + margin-top: 14px; + max-height: 320px; + overflow: auto; + border-radius: 8px; + background: rgba(0, 0, 0, 0.18); + padding: 10px; + box-sizing: border-box; +} +.init-failed-item{ + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} +.init-failed-item:last-child{ + border-bottom: none; +} +.init-failed-item-left{ + display: flex; + flex-direction: column; +} +.init-failed-item-name{ + font-family:'Noto Sans SC Bold', 'Noto Sans SC Regular', 'Noto Sans SC'; + font-weight:700; + font-style:normal; + font-size:14px; + color:#FFFFFF; +} +.init-failed-item-reason{ + margin-top: 4px; + font-family:'Noto Sans SC', 'Noto Sans SC Regular', 'Noto Sans SC'; + font-weight:400; + font-style:normal; + font-size:12px; + color: rgba(59, 242, 198, 0.95); +} +.init-failed-item-right{ + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + font-family: 'Noto Sans SC'; + font-weight:400; + font-style:normal; + font-size:12px; + color: rgba(255, 255, 255, 0.75); +} +.init-failed-item-kv{ + white-space: nowrap; +} +.init-failed-actions{ + margin-top: 14px; + display: flex; + justify-content: flex-end; +} .pop-up-tip-container { width: 400px; @@ -3364,7 +3657,7 @@ function viewClick(e){ } .pop-up-camera-container{ width: 668px; - height:630px; + height:700px; position: absolute; top: 0; right: 0; diff --git a/frontend/src/renderer/src/views/Login.vue b/frontend/src/renderer/src/views/Login.vue index a27ca536..ab290c05 100644 --- a/frontend/src/renderer/src/views/Login.vue +++ b/frontend/src/renderer/src/views/Login.vue @@ -426,7 +426,6 @@ const handleRegisterSubmit = async () => { // 登录处理 const handleLogin = async () => { - isLoading.value = true // 验证用户名 if (!form.value.account) { showError('请输入登录账号!') @@ -439,7 +438,9 @@ const handleLogin = async () => { return } - // 网络访问测试(最多重试3次,每次延迟2秒) + isLoading.value = true + + // 网络访问测试(最多重试3次,短退避:200/400/800ms,总上限约 1.4s) let healthOk = false for (let attempt = 1; attempt <= 3; attempt++) { try { @@ -451,9 +452,11 @@ const handleLogin = async () => { // 后台返回非 healthy 状态 if (attempt === 3) { showError('后台服务异常,请稍后重试!') + isLoading.value = false return } - await new Promise(resolve => setTimeout(resolve, 2000)) + const delay = 200 * Math.pow(2, attempt - 1) + await new Promise(resolve => setTimeout(resolve, delay)) } } catch (error) { // 捕获异常,等待后重试 @@ -462,7 +465,8 @@ const handleLogin = async () => { isLoading.value = false return } - await new Promise(resolve => setTimeout(resolve, 2000)) + const delay = 200 * Math.pow(2, attempt - 1) + await new Promise(resolve => setTimeout(resolve, delay)) } } @@ -1210,4 +1214,4 @@ const copyPassword = async () => { background: transparent; } - \ No newline at end of file +