合并代码
This commit is contained in:
commit
6e4ec82e17
@ -30,7 +30,7 @@ backend = directshow
|
|||||||
|
|
||||||
[CAMERA2]
|
[CAMERA2]
|
||||||
enable = True
|
enable = True
|
||||||
device_index = 1
|
device_index = 2
|
||||||
width = 1280
|
width = 1280
|
||||||
height = 720
|
height = 720
|
||||||
fps = 30
|
fps = 30
|
||||||
@ -39,7 +39,7 @@ fourcc = MJPG
|
|||||||
backend = directshow
|
backend = directshow
|
||||||
|
|
||||||
[FEMTOBOLT]
|
[FEMTOBOLT]
|
||||||
enable = False
|
enable = True
|
||||||
algorithm_type = plt
|
algorithm_type = plt
|
||||||
color_resolution = 1080P
|
color_resolution = 1080P
|
||||||
depth_mode = NFOV_2X2BINNED
|
depth_mode = NFOV_2X2BINNED
|
||||||
@ -54,13 +54,13 @@ imu_enable = True
|
|||||||
imu_use_mock = False
|
imu_use_mock = False
|
||||||
imu_ble_name = WT901BLE67
|
imu_ble_name = WT901BLE67
|
||||||
imu_mac_address = FA:E8:88:06:FE:F3
|
imu_mac_address = FA:E8:88:06:FE:F3
|
||||||
pressure_enable = False
|
pressure_enable = True
|
||||||
pressure_use_mock = False
|
pressure_use_mock = True
|
||||||
pressure_port = COM5
|
pressure_port = COM3
|
||||||
pressure_baudrate = 115200
|
pressure_baudrate = 115200
|
||||||
|
|
||||||
[REMOTE]
|
[REMOTE]
|
||||||
enable = True
|
enable = False
|
||||||
port = COM6
|
port = COM6
|
||||||
baudrate = 115200
|
baudrate = 115200
|
||||||
timeout = 0.1
|
timeout = 0.1
|
||||||
|
|||||||
@ -57,6 +57,10 @@ class DeviceCoordinator:
|
|||||||
self.is_initialized = False
|
self.is_initialized = False
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.coordinator_lock = threading.RLock()
|
self.coordinator_lock = threading.RLock()
|
||||||
|
self._init_summary = {
|
||||||
|
'initialized_at': None,
|
||||||
|
'device_results': {},
|
||||||
|
}
|
||||||
|
|
||||||
# 监控线程
|
# 监控线程
|
||||||
self.monitor_thread = None
|
self.monitor_thread = None
|
||||||
@ -116,8 +120,12 @@ class DeviceCoordinator:
|
|||||||
self._register_namespaces()
|
self._register_namespaces()
|
||||||
|
|
||||||
# 初始化设备(失败则降级继续)
|
# 初始化设备(失败则降级继续)
|
||||||
if not self._initialize_devices():
|
init_ok = bool(self._initialize_devices())
|
||||||
self.logger.warning("设备初始化失败,将以降级模式继续运行")
|
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.is_initialized = True
|
||||||
self.stats['start_time'] = time.time()
|
self.stats['start_time'] = time.time()
|
||||||
@ -195,12 +203,14 @@ class DeviceCoordinator:
|
|||||||
try:
|
try:
|
||||||
timeout_s = 45 if device_name == 'imu' else 30
|
timeout_s = 45 if device_name == 'imu' else 30
|
||||||
result = future.result(timeout=timeout_s)
|
result = future.result(timeout=timeout_s)
|
||||||
|
self._init_summary['device_results'][device_name] = bool(result)
|
||||||
if result:
|
if result:
|
||||||
success_count += 1
|
success_count += 1
|
||||||
self.logger.info(f"{device_name}设备初始化成功")
|
self.logger.info(f"{device_name}设备初始化成功")
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"{device_name}设备初始化失败")
|
self.logger.error(f"{device_name}设备初始化失败")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._init_summary['device_results'][device_name] = False
|
||||||
self.logger.error(f"{device_name}设备初始化异常: {e}")
|
self.logger.error(f"{device_name}设备初始化异常: {e}")
|
||||||
|
|
||||||
# 至少需要一个设备初始化成功
|
# 至少需要一个设备初始化成功
|
||||||
@ -215,6 +225,97 @@ class DeviceCoordinator:
|
|||||||
self.logger.error(f"设备初始化失败: {e}")
|
self.logger.error(f"设备初始化失败: {e}")
|
||||||
return False
|
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:
|
def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool:
|
||||||
"""
|
"""
|
||||||
按名称初始化相机,支持 camera1/camera2 并覆盖配置段
|
按名称初始化相机,支持 camera1/camera2 并覆盖配置段
|
||||||
|
|||||||
@ -704,6 +704,7 @@ class PressureManager(BaseDevice):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: 初始化是否成功
|
bool: 初始化是否成功
|
||||||
"""
|
"""
|
||||||
|
self._initializing = True
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"正在初始化压力板设备...")
|
self.logger.info(f"正在初始化压力板设备...")
|
||||||
|
|
||||||
@ -716,13 +717,28 @@ class PressureManager(BaseDevice):
|
|||||||
else:
|
else:
|
||||||
self.device = MockPressureDevice()
|
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方法启动连接监控线程
|
# 使用set_connected方法启动连接监控线程
|
||||||
self.set_connected(True)
|
self.set_connected(bool(connected))
|
||||||
self._device_info.update({
|
self._device_info.update({
|
||||||
'device_type': 'mock' if self.use_mock else 'real',
|
'device_type': 'mock' if self.use_mock else 'real',
|
||||||
'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown'
|
'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}")
|
self.logger.info(f"压力板初始化成功 - use_mock: {self.use_mock}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -732,6 +748,8 @@ class PressureManager(BaseDevice):
|
|||||||
self.set_connected(False)
|
self.set_connected(False)
|
||||||
self.device = None
|
self.device = None
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
self._initializing = False
|
||||||
|
|
||||||
def start_streaming(self) -> bool:
|
def start_streaming(self) -> bool:
|
||||||
|
|
||||||
|
|||||||
@ -265,7 +265,7 @@ class AppServer:
|
|||||||
|
|
||||||
def _initialize_devices(self):
|
def _initialize_devices(self):
|
||||||
"""
|
"""
|
||||||
初始化设备(在用户登录成功后调用)
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.logger.info('开始初始化设备...')
|
self.logger.info('开始初始化设备...')
|
||||||
@ -1294,21 +1294,37 @@ class AppServer:
|
|||||||
if not self.db_manager or not self.device_coordinator:
|
if not self.db_manager or not self.device_coordinator:
|
||||||
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
||||||
|
|
||||||
# 检查设备是否已初始化
|
data = flask_request.get_json() or {}
|
||||||
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()
|
|
||||||
patient_id = data.get('patient_id')
|
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:
|
if not patient_id or not creator_id:
|
||||||
return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400
|
return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400
|
||||||
@ -1324,7 +1340,7 @@ class AppServer:
|
|||||||
except Exception as monitor_error:
|
except Exception as monitor_error:
|
||||||
self.logger.error(f'启动设备连接监控失败: {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:
|
except Exception as e:
|
||||||
self.logger.error(f'开始检测失败: {e}')
|
self.logger.error(f'开始检测失败: {e}')
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
@ -1368,8 +1384,6 @@ class AppServer:
|
|||||||
item_copy['type'] = 'video'
|
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="displayleft" style="width: 550px;
|
<div class="displayleft" style="width: 550px;
|
||||||
justify-content: flex-end;padding-right: 5px;">
|
justify-content: flex-end;padding-right: 5px;">
|
||||||
<div class="icon-box" title="相机参数设置" @click="cameraUpdate">
|
<div class="icon-box" title="设备参数设置" @click="cameraUpdate">
|
||||||
<img src="@/assets/detection/settings.png" alt="" >
|
<img src="@/assets/detection/settings.png" alt="" >
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-box" title="查看档案" @click="routerClick">
|
<div class="icon-box" title="查看档案" @click="routerClick">
|
||||||
@ -239,16 +239,17 @@
|
|||||||
<div style="width:100%;height: 50px;"></div>
|
<div style="width:100%;height: 50px;"></div>
|
||||||
<div class="body-footbottom-leftbottom">
|
<div class="body-footbottom-leftbottom">
|
||||||
<div class="body-footbottom-leftbox">
|
<div class="body-footbottom-leftbox">
|
||||||
<span class="currencytext1">右前足</span>
|
<span class="currencytext2">
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_front }}%
|
{{ footPressure.right_front }}%
|
||||||
</span>
|
</span>
|
||||||
|
<span class="currencytext1">右前足</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="body-footbottom-leftbox">
|
<div class="body-footbottom-leftbox">
|
||||||
<span class="currencytext1">右后足</span>
|
<span class="currencytext2">
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_rear }}%
|
{{ footPressure.right_rear }}%
|
||||||
</span>
|
</span>
|
||||||
|
<span class="currencytext1">右后足</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="body-footbottom-leftbox">
|
<!-- <div class="body-footbottom-leftbox">
|
||||||
<span class="currencytext1">右足总压力</span>
|
<span class="currencytext1">右足总压力</span>
|
||||||
@ -780,6 +781,37 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
<!-- 无操作退出提示 -->
|
<!-- 无操作退出提示 -->
|
||||||
|
<div class="pop-up-mask init-mask" v-if="isSystemInitializing">
|
||||||
|
<div class="init-loading-container">
|
||||||
|
<div v-if="!initFailed" class="init-loading-content">
|
||||||
|
<div class="init-spinner"></div>
|
||||||
|
<div class="init-loading-text">{{ initLoadingText }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="init-failed-content">
|
||||||
|
<div class="init-failed-title">设备未就绪</div>
|
||||||
|
<div class="init-failed-subtitle">{{ initErrorText || '请检查设备连接与配置后重试' }}</div>
|
||||||
|
<div class="init-failed-list" v-if="notReadyDevices.length">
|
||||||
|
<div class="init-failed-item" v-for="item in notReadyDevices" :key="item.device_name">
|
||||||
|
<div class="init-failed-item-left">
|
||||||
|
<div class="init-failed-item-name">{{ getDeviceDisplayName(item.device_name) }}</div>
|
||||||
|
<div class="init-failed-item-reason">{{ getNotReadyReason(item) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="init-failed-item-right">
|
||||||
|
<div class="init-failed-item-kv">是否启用: {{ item.enabled === false ? 'N' : 'Y' }}</div>
|
||||||
|
<div class="init-failed-item-kv">是否创建实例: {{ item.exists ? 'Y' : 'N' }}</div>
|
||||||
|
<div class="init-failed-item-kv">是否初始化中: {{ item.initializing ? 'Y' : 'N' }}</div>
|
||||||
|
<div class="init-failed-item-kv">是否已连接: {{ item.is_connected ? 'Y' : 'N' }}</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="init-failed-actions">
|
||||||
|
<el-button @click="handleInitBack">返回</el-button>
|
||||||
|
<el-button type="primary" @click="handleInitProceed">进入检测</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pop-up-mask" v-if="isTip">
|
<div class="pop-up-mask" v-if="isTip">
|
||||||
<div class="pop-up-tip-container">
|
<div class="pop-up-tip-container">
|
||||||
<div class="pop-up-tip-header">
|
<div class="pop-up-tip-header">
|
||||||
@ -808,7 +840,7 @@
|
|||||||
<div class="pop-up-mask" v-if="cameraDialogVisible">
|
<div class="pop-up-mask" v-if="cameraDialogVisible">
|
||||||
<div class="pop-up-camera-container">
|
<div class="pop-up-camera-container">
|
||||||
<div class="pop-up-camera-header">
|
<div class="pop-up-camera-header">
|
||||||
<div>相机参数设置</div>
|
<div>设备参数设置</div>
|
||||||
<img src="@/assets/close.png" alt="" style="cursor: pointer;" @click="handleCameraCancel">
|
<img src="@/assets/close.png" alt="" style="cursor: pointer;" @click="handleCameraCancel">
|
||||||
</div>
|
</div>
|
||||||
<div class="pop-up-camera-body">
|
<div class="pop-up-camera-body">
|
||||||
@ -826,7 +858,7 @@
|
|||||||
<el-checkbox v-model="cameraForm.femtobolt.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
<el-checkbox v-model="cameraForm.femtobolt.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 10px;">
|
<div class="pop-up-camera-display" style="padding-top: 20px;padding-bottom: 10px;">
|
||||||
<div class="pop-up-camera-line"></div>
|
<div class="pop-up-camera-line"></div>
|
||||||
<div class="pop-up-camera-title">足部相机</div>
|
<div class="pop-up-camera-title">足部相机</div>
|
||||||
</div>
|
</div>
|
||||||
@ -856,20 +888,9 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<el-checkbox v-model="cameraForm.camera2.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
<el-checkbox v-model="cameraForm.camera2.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 00px;">
|
|
||||||
<div class="pop-up-camera-line"></div>
|
|
||||||
<div class="pop-up-camera-title">遥控器</div>
|
|
||||||
</div>
|
|
||||||
<div class="pop-up-camera-display" style="padding-top: 10px;">
|
|
||||||
<div class="pop-up-camera-name">串口号</div>
|
|
||||||
<el-select v-model="cameraForm.remote.port" placeholder="请选择" style="width: 434px;">
|
|
||||||
<el-option v-for="item in remotePortData" :label="item" :value="item" />
|
|
||||||
</el-select>
|
|
||||||
<el-checkbox v-model="cameraForm.remote.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 00px;">
|
<div class="pop-up-camera-display" style="padding-top: 20px;padding-bottom: 00px;">
|
||||||
<div class="pop-up-camera-line"></div>
|
<div class="pop-up-camera-line"></div>
|
||||||
<div class="pop-up-camera-title">IMU设备</div>
|
<div class="pop-up-camera-title">IMU设备</div>
|
||||||
</div>
|
</div>
|
||||||
@ -884,6 +905,29 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pop-up-camera-display" style="padding-top: 20px;padding-bottom: 0px;">
|
||||||
|
<div class="pop-up-camera-line"></div>
|
||||||
|
<div class="pop-up-camera-title">足底压力板</div>
|
||||||
|
</div>
|
||||||
|
<div class="pop-up-camera-display" style="padding-top: 10px;">
|
||||||
|
<div class="pop-up-camera-name">串口号</div>
|
||||||
|
<el-select v-model="cameraForm.pressure.port" placeholder="请选择" style="width: 434px;">
|
||||||
|
<el-option v-for="item in remotePortData" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
<el-checkbox v-model="cameraForm.pressure.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
||||||
|
</div>
|
||||||
|
<div class="pop-up-camera-display" style="padding-top: 20px;padding-bottom: 00px;">
|
||||||
|
<div class="pop-up-camera-line"></div>
|
||||||
|
<div class="pop-up-camera-title">遥控器</div>
|
||||||
|
</div>
|
||||||
|
<div class="pop-up-camera-display" style="padding-top: 10px;">
|
||||||
|
<div class="pop-up-camera-name">串口号</div>
|
||||||
|
<el-select v-model="cameraForm.remote.port" placeholder="请选择" style="width: 434px;">
|
||||||
|
<el-option v-for="item in remotePortData" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
<el-checkbox v-model="cameraForm.remote.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="form-actions-display">
|
<div class="form-actions-display">
|
||||||
<el-button @click="handleCameraCancel" class="formreturnCancel">退出</el-button>
|
<el-button @click="handleCameraCancel" class="formreturnCancel">退出</el-button>
|
||||||
<el-button type="primary" class="formsaveCancel"
|
<el-button type="primary" class="formsaveCancel"
|
||||||
@ -960,6 +1004,84 @@ const isCloseCreat =ref(false) // 是否打开患者信息编辑框
|
|||||||
const isoperation = ref(false) // 是否保存数据
|
const isoperation = ref(false) // 是否保存数据
|
||||||
const isVideoOperation = ref(false) // 是否录制视频
|
const isVideoOperation = ref(false) // 是否录制视频
|
||||||
const isTip =ref(false)
|
const isTip =ref(false)
|
||||||
|
const isSystemInitializing = ref(false)
|
||||||
|
const initLoadingText = ref('系统设备正在初始化,请等待...')
|
||||||
|
const initFailed = ref(false)
|
||||||
|
const initErrorText = ref('')
|
||||||
|
const initReadiness = ref(null)
|
||||||
|
const notReadyDevices = computed(() => {
|
||||||
|
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)
|
const isStartVideo = ref(false)
|
||||||
function startVideoClick() {
|
function startVideoClick() {
|
||||||
startRecord()
|
startRecord()
|
||||||
@ -1117,6 +1239,10 @@ const cameraForm = ref({ // 相机参数
|
|||||||
port: '', // 遥控器串口号
|
port: '', // 遥控器串口号
|
||||||
enable: false
|
enable: false
|
||||||
},
|
},
|
||||||
|
pressure:{
|
||||||
|
port: '',
|
||||||
|
enable: false
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
const calculatedAge = ref(null)
|
const calculatedAge = ref(null)
|
||||||
@ -1359,8 +1485,13 @@ function cameraUpdate() { // 相机设置数据更新弹框
|
|||||||
enable: false
|
enable: false
|
||||||
},
|
},
|
||||||
remote:{
|
remote:{
|
||||||
port: '', // IMU串口号
|
port: '',
|
||||||
}
|
enable: false
|
||||||
|
},
|
||||||
|
pressure:{
|
||||||
|
port: '',
|
||||||
|
enable: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 加载相机参数信息
|
// 加载相机参数信息
|
||||||
getDevicesInit()
|
getDevicesInit()
|
||||||
@ -2198,10 +2329,13 @@ async function sendDetectionData(data) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// 开始检测
|
// 开始检测
|
||||||
async function startDetection() {
|
async function startDetection(options = {}) {
|
||||||
try {
|
try {
|
||||||
console.log('🚀 正在开始检测...')
|
console.log('🚀 正在开始检测...')
|
||||||
|
|
||||||
|
const forceStart = !!(options && options.forceStart)
|
||||||
|
const silent = !!(options && options.silent)
|
||||||
|
|
||||||
// 调用后端API开始检测
|
// 调用后端API开始检测
|
||||||
const response = await fetch(`${BACKEND_URL}/api/detection/start`, {
|
const response = await fetch(`${BACKEND_URL}/api/detection/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -2212,10 +2346,21 @@ async function startDetection() {
|
|||||||
patient_id: patientId.value,
|
patient_id: patientId.value,
|
||||||
// 可以添加其他检测参数
|
// 可以添加其他检测参数
|
||||||
creator_id: creatorId.value,
|
creator_id: creatorId.value,
|
||||||
|
force_start: forceStart,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
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()
|
const result = await response.json()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -2228,7 +2373,9 @@ async function startDetection() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('💥 开始检测失败:', error)
|
console.error('💥 开始检测失败:', error)
|
||||||
ElMessage.error(`开始检测失败: ${error.message}`)
|
if (!(options && options.silent)) {
|
||||||
|
ElMessage.error(`开始检测失败: ${error.message}`)
|
||||||
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2432,7 +2579,7 @@ const getDevicesInit = async () => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('相机参数加载成功:', result.data)
|
console.log('设备参数加载成功:', result.data)
|
||||||
cameraForm.value = result.data
|
cameraForm.value = result.data
|
||||||
cameraDialogVisible.value = true
|
cameraDialogVisible.value = true
|
||||||
// console.log('相机参数加载成功:', patientInfo.value)
|
// console.log('相机参数加载成功:', patientInfo.value)
|
||||||
@ -2443,12 +2590,12 @@ const getDevicesInit = async () => {
|
|||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载相机参数失败:', error)
|
console.error('加载设备参数失败:', error)
|
||||||
ElMessage.warning('加载相机参数失败,请检查网络连接')
|
ElMessage.warning('加载设备参数失败,请检查网络连接')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
for(let i = 0; i < 20; i++){
|
for(let i = 0; i < 20; i++){
|
||||||
let port = "COM" + (i + 1)
|
let port = "COM" + (i + 1)
|
||||||
remotePortData.value.push(port)
|
remotePortData.value.push(port)
|
||||||
@ -2459,12 +2606,35 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
patientId.value = props.selectedPatient.id
|
patientId.value = props.selectedPatient.id
|
||||||
//patientId.value = '202511150005'
|
//patientId.value = '202511150005'
|
||||||
// 加载患者信息
|
try {
|
||||||
loadPatientInfo()
|
isSystemInitializing.value = true
|
||||||
// 启动检测
|
initFailed.value = false
|
||||||
startDetection()
|
initErrorText.value = ''
|
||||||
// 页面加载时自动连接WebSocket
|
initReadiness.value = null
|
||||||
connectWebSocket()
|
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)
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
@ -3349,6 +3519,129 @@ function viewClick(e){
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
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 {
|
.pop-up-tip-container {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@ -3364,7 +3657,7 @@ function viewClick(e){
|
|||||||
}
|
}
|
||||||
.pop-up-camera-container{
|
.pop-up-camera-container{
|
||||||
width: 668px;
|
width: 668px;
|
||||||
height:630px;
|
height:700px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@ -426,7 +426,6 @@ const handleRegisterSubmit = async () => {
|
|||||||
|
|
||||||
// 登录处理
|
// 登录处理
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
isLoading.value = true
|
|
||||||
// 验证用户名
|
// 验证用户名
|
||||||
if (!form.value.account) {
|
if (!form.value.account) {
|
||||||
showError('请输入登录账号!')
|
showError('请输入登录账号!')
|
||||||
@ -439,7 +438,9 @@ const handleLogin = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络访问测试(最多重试3次,每次延迟2秒)
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 网络访问测试(最多重试3次,短退避:200/400/800ms,总上限约 1.4s)
|
||||||
let healthOk = false
|
let healthOk = false
|
||||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||||
try {
|
try {
|
||||||
@ -451,9 +452,11 @@ const handleLogin = async () => {
|
|||||||
// 后台返回非 healthy 状态
|
// 后台返回非 healthy 状态
|
||||||
if (attempt === 3) {
|
if (attempt === 3) {
|
||||||
showError('后台服务异常,请稍后重试!')
|
showError('后台服务异常,请稍后重试!')
|
||||||
|
isLoading.value = false
|
||||||
return
|
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) {
|
} catch (error) {
|
||||||
// 捕获异常,等待后重试
|
// 捕获异常,等待后重试
|
||||||
@ -462,7 +465,8 @@ const handleLogin = async () => {
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
const delay = 200 * Math.pow(2, attempt - 1)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user