Merge branch 'dev-v15' of http://121.37.111.42:3000/ThbTech/BodyBalanceEvaluation into dev-v15
This commit is contained in:
commit
c71f07f931
@ -60,7 +60,7 @@ pressure_port = COM5
|
|||||||
pressure_baudrate = 115200
|
pressure_baudrate = 115200
|
||||||
|
|
||||||
[REMOTE]
|
[REMOTE]
|
||||||
enable = False
|
enable = True
|
||||||
port = COM6
|
port = COM6
|
||||||
baudrate = 115200
|
baudrate = 115200
|
||||||
timeout = 0.1
|
timeout = 0.1
|
||||||
|
|||||||
@ -830,7 +830,7 @@ class DatabaseManager:
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT * FROM detection_data
|
SELECT * FROM detection_data
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp desc
|
||||||
''', (session_id,))
|
''', (session_id,))
|
||||||
|
|
||||||
data_rows = cursor.fetchall()
|
data_rows = cursor.fetchall()
|
||||||
@ -849,7 +849,7 @@ class DatabaseManager:
|
|||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT * FROM detection_video
|
SELECT * FROM detection_video
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp desc
|
||||||
''', (session_id,))
|
''', (session_id,))
|
||||||
|
|
||||||
video_rows = cursor.fetchall()
|
video_rows = cursor.fetchall()
|
||||||
@ -1103,12 +1103,15 @@ class DatabaseManager:
|
|||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO detection_video (
|
INSERT INTO detection_video (
|
||||||
id, session_id, screen_video, timestamp
|
id, session_id, screen_video, body_video, foot_video1, foot_video2, timestamp
|
||||||
) VALUES (?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
video_id,
|
video_id,
|
||||||
session_id,
|
session_id,
|
||||||
video.get('screen_video_path'),
|
video.get('screen_video_path'),
|
||||||
|
video.get('body_video_path'),
|
||||||
|
video.get('foot_video1_path'),
|
||||||
|
video.get('foot_video2_path'),
|
||||||
china_time
|
china_time
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -1120,6 +1123,50 @@ class DatabaseManager:
|
|||||||
logger.error(f'保存检测视频失败: {e}')
|
logger.error(f'保存检测视频失败: {e}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_detection_video_latest(self, session_id: str, video: Dict[str, Any]) -> bool:
|
||||||
|
conn = self.get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT id, screen_video, body_video, foot_video1, foot_video2
|
||||||
|
FROM detection_video
|
||||||
|
WHERE session_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 1
|
||||||
|
''', (session_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return self.save_detection_video(session_id, video)
|
||||||
|
|
||||||
|
row_dict = dict(row)
|
||||||
|
video_id = row_dict.get('id')
|
||||||
|
|
||||||
|
screen_video = video.get('screen_video_path')
|
||||||
|
body_video = video.get('body_video_path')
|
||||||
|
foot_video1 = video.get('foot_video1_path')
|
||||||
|
foot_video2 = video.get('foot_video2_path')
|
||||||
|
|
||||||
|
if not screen_video:
|
||||||
|
screen_video = row_dict.get('screen_video')
|
||||||
|
if not body_video:
|
||||||
|
body_video = row_dict.get('body_video')
|
||||||
|
if not foot_video1:
|
||||||
|
foot_video1 = row_dict.get('foot_video1')
|
||||||
|
if not foot_video2:
|
||||||
|
foot_video2 = row_dict.get('foot_video2')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE detection_video
|
||||||
|
SET screen_video = ?, body_video = ?, foot_video1 = ?, foot_video2 = ?
|
||||||
|
WHERE id = ?
|
||||||
|
''', (screen_video, body_video, foot_video1, foot_video2, video_id))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error(f'更新检测视频失败: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
|
def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
|
||||||
"""删除检测视频记录(支持单个或多个ID)"""
|
"""删除检测视频记录(支持单个或多个ID)"""
|
||||||
conn = self.get_connection()
|
conn = self.get_connection()
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from typing import Optional, Dict, Any
|
|||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .base_device import BaseDevice
|
from .base_device import BaseDevice
|
||||||
@ -110,7 +111,19 @@ class CameraManager(BaseDevice):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 全局帧队列(用于录制)
|
# 全局帧队列(用于录制)
|
||||||
self.frame_queue = queue.Queue(maxsize=10) # 最大长度10,自动丢弃旧帧
|
self.frame_queue = queue.Queue(maxsize=10)
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_index = 0
|
||||||
|
self._recording_written = 0
|
||||||
|
self._recording_drop = 0
|
||||||
|
self._recording_queue = queue.Queue(maxsize=300)
|
||||||
|
self._recording_thread = None
|
||||||
|
self._recording_stop_event = threading.Event()
|
||||||
|
|
||||||
# 属性缓存机制 - 避免重复设置相同属性值
|
# 属性缓存机制 - 避免重复设置相同属性值
|
||||||
self._property_cache = {}
|
self._property_cache = {}
|
||||||
@ -126,6 +139,130 @@ class CameraManager(BaseDevice):
|
|||||||
|
|
||||||
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
|
||||||
|
|
||||||
|
def start_jpeg_recording(self, session_id: str, frames_dir: str, record_fps: Optional[int] = None) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if not session_id:
|
||||||
|
return {'success': False, 'message': '缺少session_id'}
|
||||||
|
if not frames_dir:
|
||||||
|
return {'success': False, 'message': '缺少frames_dir'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(frames_dir, exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': f'创建录制目录失败: {e}'}
|
||||||
|
|
||||||
|
if record_fps is None:
|
||||||
|
record_fps = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'fps', fallback='10'))
|
||||||
|
record_fps = max(1, int(record_fps))
|
||||||
|
|
||||||
|
self._recording_session_id = session_id
|
||||||
|
self._recording_frames_dir = frames_dir
|
||||||
|
self._recording_target_fps = record_fps
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_index = 0
|
||||||
|
self._recording_written = 0
|
||||||
|
self._recording_drop = 0
|
||||||
|
self._recording_stop_event.clear()
|
||||||
|
self._recording_enabled = True
|
||||||
|
|
||||||
|
if not self._recording_thread or not self._recording_thread.is_alive():
|
||||||
|
self._recording_thread = threading.Thread(
|
||||||
|
target=self._recording_writer_loop,
|
||||||
|
name=f"{self.device_id}-JpegWriter",
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self._recording_thread.start()
|
||||||
|
|
||||||
|
return {'success': True, 'message': '相机JPEG录制已启动', 'device_id': self.device_id, 'fps': record_fps, 'frames_dir': frames_dir}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
|
def stop_jpeg_recording(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if session_id and self._recording_session_id and session_id != self._recording_session_id:
|
||||||
|
return {'success': False, 'message': 'session_id不匹配'}
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
frames_dir = self._recording_frames_dir
|
||||||
|
fps = self._recording_target_fps
|
||||||
|
written = int(self._recording_written)
|
||||||
|
dropped = int(self._recording_drop)
|
||||||
|
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
|
||||||
|
self._recording_stop_event.set()
|
||||||
|
if self._recording_thread and self._recording_thread.is_alive():
|
||||||
|
self._recording_thread.join(timeout=2.0)
|
||||||
|
self._recording_thread = None
|
||||||
|
|
||||||
|
while not self._recording_queue.empty():
|
||||||
|
try:
|
||||||
|
self._recording_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
self._recording_stop_event.clear()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': '相机JPEG录制已停止',
|
||||||
|
'device_id': self.device_id,
|
||||||
|
'frames_dir': frames_dir,
|
||||||
|
'fps': fps,
|
||||||
|
'frames_written': written,
|
||||||
|
'frames_dropped': dropped
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
|
def _recording_writer_loop(self):
|
||||||
|
while not self._recording_stop_event.is_set():
|
||||||
|
try:
|
||||||
|
item = self._recording_queue.get(timeout=0.2)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
frames_dir, idx, jpeg_bytes = item
|
||||||
|
if not frames_dir or not jpeg_bytes:
|
||||||
|
continue
|
||||||
|
filename = f"frame_{idx:06d}.jpg"
|
||||||
|
fpath = os.path.join(frames_dir, filename)
|
||||||
|
with open(fpath, 'wb') as f:
|
||||||
|
f.write(jpeg_bytes)
|
||||||
|
self._recording_written += 1
|
||||||
|
except Exception:
|
||||||
|
self._recording_drop += 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._recording_queue.task_done()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _maybe_enqueue_recording(self, timestamp: float, frame_bytes: bytes):
|
||||||
|
if not self._recording_enabled:
|
||||||
|
return
|
||||||
|
frames_dir = self._recording_frames_dir
|
||||||
|
target_fps = self._recording_target_fps
|
||||||
|
if not frames_dir or not target_fps or target_fps <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._recording_last_ts > 0:
|
||||||
|
min_interval = 1.0 / float(target_fps)
|
||||||
|
if (timestamp - self._recording_last_ts) < min_interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = self._recording_index
|
||||||
|
self._recording_index += 1
|
||||||
|
self._recording_last_ts = timestamp
|
||||||
|
try:
|
||||||
|
self._recording_queue.put_nowait((frames_dir, idx, frame_bytes))
|
||||||
|
except queue.Full:
|
||||||
|
self._recording_drop += 1
|
||||||
|
|
||||||
def _set_property_optimized(self, prop, value):
|
def _set_property_optimized(self, prop, value):
|
||||||
"""
|
"""
|
||||||
优化的属性设置方法,使用缓存避免重复设置
|
优化的属性设置方法,使用缓存避免重复设置
|
||||||
@ -649,23 +786,6 @@ class CameraManager(BaseDevice):
|
|||||||
# 更新心跳时间,防止连接监控线程判定为超时
|
# 更新心跳时间,防止连接监控线程判定为超时
|
||||||
self.update_heartbeat()
|
self.update_heartbeat()
|
||||||
|
|
||||||
# 保存原始帧到队列(用于录制)
|
|
||||||
try:
|
|
||||||
self.frame_queue.put_nowait({
|
|
||||||
'frame': frame.copy(),
|
|
||||||
'timestamp': time.time()
|
|
||||||
})
|
|
||||||
except queue.Full:
|
|
||||||
# 队列满时丢弃最旧的帧,添加新帧
|
|
||||||
try:
|
|
||||||
self.frame_queue.get_nowait() # 移除最旧的帧
|
|
||||||
self.frame_queue.put_nowait({
|
|
||||||
'frame': frame.copy(),
|
|
||||||
'timestamp': time.time()
|
|
||||||
})
|
|
||||||
except queue.Empty:
|
|
||||||
pass # 队列为空,忽略
|
|
||||||
|
|
||||||
# 处理帧(降采样以优化传输负载)
|
# 处理帧(降采样以优化传输负载)
|
||||||
processed_frame = self._process_frame(frame)
|
processed_frame = self._process_frame(frame)
|
||||||
|
|
||||||
@ -740,6 +860,7 @@ class CameraManager(BaseDevice):
|
|||||||
|
|
||||||
# 转换为bytes再做base64,减少中间numpy对象的长生命周期
|
# 转换为bytes再做base64,减少中间numpy对象的长生命周期
|
||||||
frame_bytes = buffer.tobytes()
|
frame_bytes = buffer.tobytes()
|
||||||
|
self._maybe_enqueue_recording(time.time(), frame_bytes)
|
||||||
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
|
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
|
||||||
|
|
||||||
# 发送数据
|
# 发送数据
|
||||||
@ -986,6 +1107,21 @@ class CameraManager(BaseDevice):
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self._recording_enabled = False
|
||||||
|
self._recording_session_id = None
|
||||||
|
self._recording_frames_dir = None
|
||||||
|
self._recording_target_fps = None
|
||||||
|
self._recording_last_ts = 0.0
|
||||||
|
self._recording_stop_event.set()
|
||||||
|
if self._recording_thread and self._recording_thread.is_alive():
|
||||||
|
self._recording_thread.join(timeout=2.0)
|
||||||
|
self._recording_thread = None
|
||||||
|
while not self._recording_queue.empty():
|
||||||
|
try:
|
||||||
|
self._recording_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
self.last_frame = None
|
self.last_frame = None
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|||||||
@ -291,9 +291,11 @@ class DeviceCoordinator:
|
|||||||
from .remote_control_manager import RemoteControlManager
|
from .remote_control_manager import RemoteControlManager
|
||||||
remote = RemoteControlManager(self.socketio, self.config_manager)
|
remote = RemoteControlManager(self.socketio, self.config_manager)
|
||||||
self.devices['remote'] = remote
|
self.devices['remote'] = remote
|
||||||
if remote.initialize():
|
if not remote.initialize():
|
||||||
return True
|
return False
|
||||||
return False
|
if hasattr(remote, 'start_streaming'):
|
||||||
|
return bool(remote.start_streaming())
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"初始化遥控器失败: {e}")
|
self.logger.error(f"初始化遥控器失败: {e}")
|
||||||
return False
|
return False
|
||||||
@ -743,6 +745,12 @@ class DeviceCoordinator:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from femtobolt_manager import FemtoBoltManager
|
from femtobolt_manager import FemtoBoltManager
|
||||||
new_device = FemtoBoltManager(self.socketio, self.config_manager)
|
new_device = FemtoBoltManager(self.socketio, self.config_manager)
|
||||||
|
elif device_name == 'remote':
|
||||||
|
try:
|
||||||
|
from .remote_control_manager import RemoteControlManager
|
||||||
|
except ImportError:
|
||||||
|
from remote_control_manager import RemoteControlManager
|
||||||
|
new_device = RemoteControlManager(self.socketio, self.config_manager)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"未知的设备类型: {device_name}")
|
raise ValueError(f"未知的设备类型: {device_name}")
|
||||||
|
|
||||||
@ -777,6 +785,17 @@ 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)")
|
||||||
|
|
||||||
|
if device_name == 'remote' and hasattr(new_device, 'start_streaming'):
|
||||||
|
self.logger.info(f"正在启动 {device_name} 设备推流...")
|
||||||
|
try:
|
||||||
|
if not new_device.start_streaming():
|
||||||
|
self.logger.error(f"启动 {device_name} 设备推流失败")
|
||||||
|
return False
|
||||||
|
was_streaming = True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"启动 {device_name} 推流异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# 设备初始化成功后,确保状态广播正确
|
# 设备初始化成功后,确保状态广播正确
|
||||||
# 此时设备应该已经通过initialize()方法中的set_connected(True)触发了状态变化通知
|
# 此时设备应该已经通过initialize()方法中的set_connected(True)触发了状态变化通知
|
||||||
# 但为了确保状态一致性,我们再次确认状态
|
# 但为了确保状态一致性,我们再次确认状态
|
||||||
|
|||||||
@ -66,7 +66,7 @@ class RemoteControlManager(BaseDevice):
|
|||||||
def initialize(self) -> bool:
|
def initialize(self) -> bool:
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"初始化遥控器串口: {self.port}, {self.baudrate}bps, 8N1")
|
self.logger.info(f"初始化遥控器串口: {self.port}, {self.baudrate}bps, 8N1")
|
||||||
self.set_connected(True)
|
self.set_connected(False)
|
||||||
self._device_info['initialized_at'] = time.time()
|
self._device_info['initialized_at'] = time.time()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -88,6 +88,9 @@ class RemoteControlManager(BaseDevice):
|
|||||||
stopbits=self.stopbits,
|
stopbits=self.stopbits,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
|
self.set_connected(True)
|
||||||
|
self.update_heartbeat()
|
||||||
|
self.is_streaming = True
|
||||||
self._running = True
|
self._running = True
|
||||||
self._thread = threading.Thread(target=self._worker_loop, daemon=True)
|
self._thread = threading.Thread(target=self._worker_loop, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
@ -110,6 +113,9 @@ class RemoteControlManager(BaseDevice):
|
|||||||
self._thread.join(timeout=2.0)
|
self._thread.join(timeout=2.0)
|
||||||
if self._ser and self._ser.is_open:
|
if self._ser and self._ser.is_open:
|
||||||
self._ser.close()
|
self._ser.close()
|
||||||
|
self._ser = None
|
||||||
|
self.set_connected(False)
|
||||||
|
self.is_streaming = False
|
||||||
self.logger.info("遥控器串口监听已停止")
|
self.logger.info("遥控器串口监听已停止")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -248,6 +254,7 @@ class RemoteControlManager(BaseDevice):
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
continue
|
continue
|
||||||
chunk = self._ser.read(64)
|
chunk = self._ser.read(64)
|
||||||
|
self.update_heartbeat()
|
||||||
if chunk:
|
if chunk:
|
||||||
try:
|
try:
|
||||||
hexstr = ' '.join(f'{b:02X}' for b in chunk)
|
hexstr = ' '.join(f'{b:02X}' for b in chunk)
|
||||||
@ -262,5 +269,14 @@ class RemoteControlManager(BaseDevice):
|
|||||||
self.logger.debug("遥控器串口暂无数据")
|
self.logger.debug("遥控器串口暂无数据")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"遥控器串口读取异常: {e}")
|
self.logger.error(f"遥控器串口读取异常: {e}")
|
||||||
|
try:
|
||||||
|
if self._ser and self._ser.is_open:
|
||||||
|
self._ser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ser = None
|
||||||
|
self.set_connected(False)
|
||||||
|
self.is_streaming = False
|
||||||
|
self._running = False
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
self.logger.info("遥控器串口线程结束")
|
self.logger.info("遥控器串口线程结束")
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import signal
|
|||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional, Callable
|
||||||
|
import threading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pyautogui
|
import pyautogui
|
||||||
@ -39,11 +40,38 @@ class RecordingManager:
|
|||||||
# FFmpeg进程管理
|
# FFmpeg进程管理
|
||||||
self._ffmpeg_processes = {}
|
self._ffmpeg_processes = {}
|
||||||
self._ffmpeg_meta = {}
|
self._ffmpeg_meta = {}
|
||||||
|
self._transcode_threads = {}
|
||||||
|
|
||||||
# 默认参数
|
# 默认参数
|
||||||
self.screen_fps = 25
|
self.screen_fps = 25
|
||||||
self.screen_size = self._get_screen_size()
|
self.screen_size = self._get_screen_size()
|
||||||
|
|
||||||
|
def _resolve_ffmpeg_path(self) -> Optional[str]:
|
||||||
|
ffmpeg_path = None
|
||||||
|
if self.config_manager:
|
||||||
|
ffmpeg_path = (
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
|
||||||
|
)
|
||||||
|
if ffmpeg_path and os.path.isfile(str(ffmpeg_path)):
|
||||||
|
return str(ffmpeg_path)
|
||||||
|
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
||||||
|
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
|
||||||
|
if os.path.isfile(alt_path):
|
||||||
|
return alt_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_active_video_base(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
meta = self._ffmpeg_meta.get('screen') or {}
|
||||||
|
if meta.get('session_id') != session_id:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'base_path': meta.get('base_path'),
|
||||||
|
'file_dir': meta.get('file_dir'),
|
||||||
|
'patient_id': meta.get('patient_id'),
|
||||||
|
'session_id': meta.get('session_id')
|
||||||
|
}
|
||||||
|
|
||||||
def _get_screen_size(self):
|
def _get_screen_size(self):
|
||||||
try:
|
try:
|
||||||
import pyautogui
|
import pyautogui
|
||||||
@ -82,20 +110,10 @@ class RecordingManager:
|
|||||||
os.makedirs(base_path, exist_ok=True)
|
os.makedirs(base_path, exist_ok=True)
|
||||||
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
||||||
target_fps = fps or self.screen_fps
|
target_fps = fps or self.screen_fps
|
||||||
ffmpeg_path = None
|
ffmpeg_path = self._resolve_ffmpeg_path()
|
||||||
if self.config_manager:
|
if not ffmpeg_path:
|
||||||
ffmpeg_path = (
|
result['message'] = '未配置有效的ffmpeg_path,请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
|
||||||
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
|
return result
|
||||||
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
|
|
||||||
)
|
|
||||||
if not ffmpeg_path or not os.path.isfile(str(ffmpeg_path)):
|
|
||||||
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
|
||||||
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
|
|
||||||
if os.path.isfile(alt_path):
|
|
||||||
ffmpeg_path = alt_path
|
|
||||||
else:
|
|
||||||
result['message'] = '未配置有效的ffmpeg_path,请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
|
|
||||||
return result
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
str(ffmpeg_path),
|
str(ffmpeg_path),
|
||||||
@ -136,7 +154,7 @@ class RecordingManager:
|
|||||||
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
||||||
)
|
)
|
||||||
self._ffmpeg_processes['screen'] = proc
|
self._ffmpeg_processes['screen'] = proc
|
||||||
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
|
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'file_dir': file_dir, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
|
||||||
result['success'] = True
|
result['success'] = True
|
||||||
result['message'] = 'ffmpeg录制已启动'
|
result['message'] = 'ffmpeg录制已启动'
|
||||||
result['database_updates'] = {
|
result['database_updates'] = {
|
||||||
@ -151,6 +169,86 @@ class RecordingManager:
|
|||||||
result['message'] = f'ffmpeg启动失败: {e}'
|
result['message'] = f'ffmpeg启动失败: {e}'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def transcode_jpeg_sequence_async(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
frames_dir: str,
|
||||||
|
output_mp4_path: str,
|
||||||
|
fps: int,
|
||||||
|
on_done: Optional[Callable[[int], None]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if not frames_dir or not os.path.isdir(frames_dir):
|
||||||
|
return {'success': False, 'message': 'frames_dir不存在'}
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(output_mp4_path), exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ffmpeg_path = self._resolve_ffmpeg_path()
|
||||||
|
if not ffmpeg_path:
|
||||||
|
return {'success': False, 'message': 'ffmpeg_path无效'}
|
||||||
|
|
||||||
|
codec = (
|
||||||
|
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||||
|
'libx264'
|
||||||
|
)
|
||||||
|
preset = (
|
||||||
|
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||||
|
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||||
|
('p1' if codec == 'h264_nvenc' else 'ultrafast')
|
||||||
|
)
|
||||||
|
threads = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_threads', fallback='1') or '1')
|
||||||
|
bframes = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_bframes', fallback='0') or '0')
|
||||||
|
gop = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_gop', fallback=str(max(1, int(fps * 2)))) or str(max(1, int(fps * 2))))
|
||||||
|
|
||||||
|
input_pattern = os.path.join(frames_dir, 'frame_%06d.jpg')
|
||||||
|
cmd = [
|
||||||
|
str(ffmpeg_path),
|
||||||
|
'-y',
|
||||||
|
'-framerate', str(int(max(1, fps))),
|
||||||
|
'-start_number', '0',
|
||||||
|
'-i', input_pattern,
|
||||||
|
'-c:v', str(codec),
|
||||||
|
'-preset', str(preset),
|
||||||
|
'-bf', str(bframes),
|
||||||
|
'-g', str(gop),
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
|
'-threads', str(threads),
|
||||||
|
str(output_mp4_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
code = 1
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
||||||
|
)
|
||||||
|
self._ffmpeg_processes[name] = proc
|
||||||
|
code = proc.wait()
|
||||||
|
except Exception:
|
||||||
|
code = 1
|
||||||
|
finally:
|
||||||
|
self._ffmpeg_processes.pop(name, None)
|
||||||
|
self._transcode_threads.pop(name, None)
|
||||||
|
if on_done:
|
||||||
|
try:
|
||||||
|
on_done(int(code))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
t = threading.Thread(target=worker, name=f"FFmpegTranscode-{name}", daemon=True)
|
||||||
|
self._transcode_threads[name] = t
|
||||||
|
t.start()
|
||||||
|
return {'success': True, 'message': '转码任务已启动', 'name': name, 'output': output_mp4_path}
|
||||||
|
except Exception as e:
|
||||||
|
return {'success': False, 'message': str(e)}
|
||||||
|
|
||||||
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
|
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
|
||||||
result = {'success': False, 'message': ''}
|
result = {'success': False, 'message': ''}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -434,6 +434,8 @@ class ConfigManager:
|
|||||||
self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock']))
|
self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock']))
|
||||||
if 'mac_address' in config_data:
|
if 'mac_address' in config_data:
|
||||||
self.set_config_value('DEVICES', 'imu_mac_address', config_data['mac_address'])
|
self.set_config_value('DEVICES', 'imu_mac_address', config_data['mac_address'])
|
||||||
|
if 'ble_name' in config_data:
|
||||||
|
self.set_config_value('DEVICES', 'imu_ble_name', config_data['ble_name'])
|
||||||
|
|
||||||
results['imu'] = {
|
results['imu'] = {
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
|||||||
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
import base64
|
import base64
|
||||||
|
import shutil
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,85 +52,345 @@ class LicenseManager:
|
|||||||
def __init__(self, config_manager=None):
|
def __init__(self, config_manager=None):
|
||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
self._machine_id = None
|
self._machine_id = None
|
||||||
|
self._machine_id_candidates = None
|
||||||
self._license_cache = None
|
self._license_cache = None
|
||||||
self._cache_timestamp = None
|
self._cache_timestamp = None
|
||||||
|
|
||||||
|
def _get_backend_base_dir(self) -> str:
|
||||||
|
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
def _get_persistent_license_dir(self) -> str:
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
base = os.environ.get("PROGRAMDATA") or os.environ.get("ALLUSERSPROFILE") or ""
|
||||||
|
if not base:
|
||||||
|
base = os.path.expanduser("~\\AppData\\Local")
|
||||||
|
else:
|
||||||
|
base = os.path.expanduser("~/.config")
|
||||||
|
return os.path.join(base, "BodyCheck", "license")
|
||||||
|
|
||||||
|
def _get_configured_license_paths(self) -> Tuple[str, str, int]:
|
||||||
|
license_path = "data/license.json"
|
||||||
|
public_key_path = "backend/license_pub.pem"
|
||||||
|
grace_days = 3
|
||||||
|
if self.config_manager:
|
||||||
|
license_path = self.config_manager.get_config_value("LICENSE", "path", license_path)
|
||||||
|
public_key_path = self.config_manager.get_config_value("LICENSE", "public_key", public_key_path)
|
||||||
|
grace_days = int(self.config_manager.get_config_value("LICENSE", "grace_days", str(grace_days)))
|
||||||
|
|
||||||
|
if not os.path.isabs(license_path):
|
||||||
|
license_path = os.path.join(self._get_backend_base_dir(), license_path)
|
||||||
|
if not os.path.isabs(public_key_path):
|
||||||
|
public_key_path = os.path.join(self._get_backend_base_dir(), public_key_path)
|
||||||
|
return license_path, public_key_path, grace_days
|
||||||
|
|
||||||
|
def _get_persistent_license_paths(self) -> Tuple[str, str]:
|
||||||
|
pdir = self._get_persistent_license_dir()
|
||||||
|
return os.path.join(pdir, "license.json"), os.path.join(pdir, "license_public_key.pem")
|
||||||
|
|
||||||
|
def _resolve_license_paths(self) -> Tuple[str, str, int]:
|
||||||
|
cfg_license_path, cfg_public_key_path, grace_days = self._get_configured_license_paths()
|
||||||
|
p_license_path, p_public_key_path = self._get_persistent_license_paths()
|
||||||
|
|
||||||
|
effective_license_path = cfg_license_path
|
||||||
|
if not os.path.exists(effective_license_path) and os.path.exists(p_license_path):
|
||||||
|
effective_license_path = p_license_path
|
||||||
|
|
||||||
|
effective_public_key_path = cfg_public_key_path
|
||||||
|
if not os.path.exists(effective_public_key_path) and os.path.exists(p_public_key_path):
|
||||||
|
effective_public_key_path = p_public_key_path
|
||||||
|
|
||||||
|
return effective_license_path, effective_public_key_path, grace_days
|
||||||
|
|
||||||
|
def _mirror_license_assets(self, license_path: Optional[str] = None, public_key_path: Optional[str] = None) -> None:
|
||||||
|
p_license_path, p_public_key_path = self._get_persistent_license_paths()
|
||||||
|
pdir = os.path.dirname(p_license_path)
|
||||||
|
try:
|
||||||
|
os.makedirs(pdir, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if license_path and os.path.exists(license_path):
|
||||||
|
try:
|
||||||
|
shutil.copyfile(license_path, p_license_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if public_key_path and os.path.exists(public_key_path):
|
||||||
|
try:
|
||||||
|
shutil.copyfile(public_key_path, p_public_key_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install_license_file(self, source_license_path: str) -> Tuple[bool, str]:
|
||||||
|
try:
|
||||||
|
if not os.path.exists(source_license_path):
|
||||||
|
return False, "源授权文件不存在"
|
||||||
|
|
||||||
|
cfg_license_path, cfg_public_key_path, _ = self._get_configured_license_paths()
|
||||||
|
os.makedirs(os.path.dirname(cfg_license_path), exist_ok=True)
|
||||||
|
shutil.copyfile(source_license_path, cfg_license_path)
|
||||||
|
self._mirror_license_assets(license_path=cfg_license_path, public_key_path=cfg_public_key_path)
|
||||||
|
return True, cfg_license_path
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def mirror_license_dir(self, source_dir: str) -> None:
|
||||||
|
if not source_dir or not os.path.isdir(source_dir):
|
||||||
|
return
|
||||||
|
license_candidate = os.path.join(source_dir, "license.json")
|
||||||
|
pub_candidates = [
|
||||||
|
os.path.join(source_dir, "license_public_key.pem"),
|
||||||
|
os.path.join(source_dir, "license_pub.pem"),
|
||||||
|
os.path.join(source_dir, "public_key.pem"),
|
||||||
|
]
|
||||||
|
pub_path = ""
|
||||||
|
for c in pub_candidates:
|
||||||
|
if os.path.exists(c):
|
||||||
|
pub_path = c
|
||||||
|
break
|
||||||
|
self._mirror_license_assets(license_path=license_candidate if os.path.exists(license_candidate) else None, public_key_path=pub_path or None)
|
||||||
|
|
||||||
|
def _run_powershell(self, command: str, timeout: int = 10) -> str:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"powershell",
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
command,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
return (result.stdout or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_windows_cpu_id(self) -> str:
|
||||||
|
out = self._run_powershell(
|
||||||
|
"(Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 -ExpandProperty ProcessorId)"
|
||||||
|
)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
def _get_windows_baseboard_serial(self) -> str:
|
||||||
|
out = self._run_powershell(
|
||||||
|
"(Get-CimInstance -ClassName Win32_BaseBoard | Select-Object -First 1 -ExpandProperty SerialNumber)"
|
||||||
|
)
|
||||||
|
serial = out.strip()
|
||||||
|
if serial and serial != "To be filled by O.E.M.":
|
||||||
|
return serial
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_windows_disk_serials(self) -> list:
|
||||||
|
raw = self._run_powershell(
|
||||||
|
"$d=Get-CimInstance -ClassName Win32_DiskDrive | Select-Object SerialNumber,InterfaceType,PNPDeviceID,MediaType; $d | ConvertTo-Json -Compress"
|
||||||
|
)
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
disks = data if isinstance(data, list) else ([data] if isinstance(data, dict) else [])
|
||||||
|
serials = []
|
||||||
|
for d in disks:
|
||||||
|
serial = str((d.get("SerialNumber") or "")).strip()
|
||||||
|
iface = str((d.get("InterfaceType") or "")).strip().upper()
|
||||||
|
pnp = str((d.get("PNPDeviceID") or "")).strip().upper()
|
||||||
|
media = str((d.get("MediaType") or "")).strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
serials = sorted(set(serials))
|
||||||
|
return serials
|
||||||
|
|
||||||
|
def _get_windows_identifiers(self) -> Dict[str, Any]:
|
||||||
|
cpu_id = self._get_windows_cpu_id()
|
||||||
|
board_serial = self._get_windows_baseboard_serial()
|
||||||
|
disk_serials = self._get_windows_disk_serials()
|
||||||
|
|
||||||
|
if not cpu_id:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wmic", "cpu", "get", "ProcessorId", "/value"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
if "ProcessorId=" in line:
|
||||||
|
cpu_id = line.split("=", 1)[1].strip()
|
||||||
|
if cpu_id:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not board_serial:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wmic", "baseboard", "get", "SerialNumber", "/value"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
if "SerialNumber=" in line:
|
||||||
|
board_serial = line.split("=", 1)[1].strip()
|
||||||
|
if board_serial and board_serial != "To be filled by O.E.M.":
|
||||||
|
break
|
||||||
|
board_serial = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not disk_serials:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"wmic",
|
||||||
|
"path",
|
||||||
|
"Win32_DiskDrive",
|
||||||
|
"get",
|
||||||
|
"SerialNumber,InterfaceType,PNPDeviceID,MediaType",
|
||||||
|
"/value",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
block = {}
|
||||||
|
serials = []
|
||||||
|
for line in (result.stdout or "").split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
serial = (block.get("SerialNumber") or "").strip()
|
||||||
|
iface = (block.get("InterfaceType") or "").strip().upper()
|
||||||
|
pnp = (block.get("PNPDeviceID") or "").strip().upper()
|
||||||
|
media = (block.get("MediaType") or "").strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
block = {}
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
block[k] = v
|
||||||
|
if block:
|
||||||
|
serial = (block.get("SerialNumber") or "").strip()
|
||||||
|
iface = (block.get("InterfaceType") or "").strip().upper()
|
||||||
|
pnp = (block.get("PNPDeviceID") or "").strip().upper()
|
||||||
|
media = (block.get("MediaType") or "").strip().upper()
|
||||||
|
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
|
||||||
|
serials.append(serial)
|
||||||
|
disk_serials = sorted(set(serials))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mac = ""
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
mac = ":".join(
|
||||||
|
["{:02x}".format((uuid.getnode() >> elements) & 0xFF) for elements in range(0, 2 * 6, 2)][::-1]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
mac = ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cpu_id": cpu_id.strip() if cpu_id else "",
|
||||||
|
"board_serial": board_serial.strip() if board_serial else "",
|
||||||
|
"disk_serials": disk_serials or [],
|
||||||
|
"mac": mac,
|
||||||
|
"node": platform.node(),
|
||||||
|
"processor": platform.processor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _hash_core_info(self, core_info: list) -> str:
|
||||||
|
combined_info = "|".join(sorted(core_info))
|
||||||
|
return hashlib.sha256(combined_info.encode("utf-8")).hexdigest()[:16].upper()
|
||||||
|
|
||||||
|
def _build_machine_id_candidates(self) -> list:
|
||||||
|
system = platform.system()
|
||||||
|
info: Dict[str, Any] = {}
|
||||||
|
if system == "Windows":
|
||||||
|
info = self._get_windows_identifiers()
|
||||||
|
else:
|
||||||
|
info = {
|
||||||
|
"cpu_id": "",
|
||||||
|
"board_serial": "",
|
||||||
|
"disk_serials": [],
|
||||||
|
"mac": "",
|
||||||
|
"node": platform.node(),
|
||||||
|
"processor": platform.processor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu_id = info.get("cpu_id") or ""
|
||||||
|
board_serial = info.get("board_serial") or ""
|
||||||
|
disk_serials = info.get("disk_serials") or []
|
||||||
|
mac = info.get("mac") or ""
|
||||||
|
|
||||||
|
variants = []
|
||||||
|
|
||||||
|
full_core = []
|
||||||
|
if cpu_id:
|
||||||
|
full_core.append(f"CPU:{cpu_id}")
|
||||||
|
if board_serial:
|
||||||
|
full_core.append(f"BOARD:{board_serial}")
|
||||||
|
for s in disk_serials:
|
||||||
|
full_core.append(f"DISK:{s}")
|
||||||
|
if full_core:
|
||||||
|
variants.append(full_core)
|
||||||
|
|
||||||
|
no_disk = []
|
||||||
|
if cpu_id:
|
||||||
|
no_disk.append(f"CPU:{cpu_id}")
|
||||||
|
if board_serial:
|
||||||
|
no_disk.append(f"BOARD:{board_serial}")
|
||||||
|
if no_disk:
|
||||||
|
variants.append(no_disk)
|
||||||
|
|
||||||
|
if cpu_id:
|
||||||
|
variants.append([f"CPU:{cpu_id}"])
|
||||||
|
if board_serial:
|
||||||
|
variants.append([f"BOARD:{board_serial}"])
|
||||||
|
|
||||||
|
if mac:
|
||||||
|
variants.append([f"MAC:{mac}"])
|
||||||
|
if cpu_id and mac:
|
||||||
|
variants.append([f"CPU:{cpu_id}", f"MAC:{mac}"])
|
||||||
|
|
||||||
|
fallback_core = []
|
||||||
|
node = (info.get("node") or "").strip()
|
||||||
|
proc = (info.get("processor") or "").strip()
|
||||||
|
if node:
|
||||||
|
fallback_core.append(f"NODE:{node}")
|
||||||
|
if proc:
|
||||||
|
fallback_core.append(f"PROCESSOR:{proc}")
|
||||||
|
if fallback_core:
|
||||||
|
variants.append(fallback_core)
|
||||||
|
|
||||||
|
prefix = "W10-" if system == "Windows" else "FB-"
|
||||||
|
candidates = []
|
||||||
|
for core in variants:
|
||||||
|
try:
|
||||||
|
mid = f"{prefix}{self._hash_core_info(core)}"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if mid not in candidates:
|
||||||
|
candidates.append(mid)
|
||||||
|
return candidates
|
||||||
|
|
||||||
def get_machine_id(self) -> str:
|
def get_machine_id(self) -> str:
|
||||||
"""生成机器硬件指纹"""
|
"""生成机器硬件指纹"""
|
||||||
if self._machine_id:
|
if self._machine_id:
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
try:
|
try:
|
||||||
core_info = []
|
candidates = self._build_machine_id_candidates()
|
||||||
aux_info = []
|
if not candidates:
|
||||||
try:
|
raise RuntimeError("无法生成机器指纹")
|
||||||
if platform.system() == "Windows":
|
self._machine_id_candidates = candidates
|
||||||
result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'], capture_output=True, text=True, timeout=10)
|
self._machine_id = candidates[0]
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if 'ProcessorId=' in line:
|
|
||||||
cpu_id = line.split('=')[1].strip()
|
|
||||||
if cpu_id:
|
|
||||||
core_info.append(f"CPU:{cpu_id}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取CPU信息失败: {e}")
|
|
||||||
try:
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
result = subprocess.run(['wmic', 'baseboard', 'get', 'SerialNumber', '/value'], capture_output=True, text=True, timeout=10)
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
if 'SerialNumber=' in line:
|
|
||||||
board_serial = line.split('=')[1].strip()
|
|
||||||
if board_serial and board_serial != "To be filled by O.E.M.":
|
|
||||||
core_info.append(f"BOARD:{board_serial}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取主板信息失败: {e}")
|
|
||||||
try:
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
# 获取磁盘信息并过滤掉USB/可移动介质,收集所有内部磁盘序列号
|
|
||||||
result = subprocess.run(
|
|
||||||
['wmic', 'path', 'Win32_DiskDrive', 'get', 'SerialNumber,InterfaceType,PNPDeviceID,MediaType', '/value'],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
block = {}
|
|
||||||
for line in result.stdout.split('\n'):
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
# 结束一个块
|
|
||||||
serial = (block.get('SerialNumber') or '').strip()
|
|
||||||
iface = (block.get('InterfaceType') or '').strip().upper()
|
|
||||||
pnp = (block.get('PNPDeviceID') or '').strip().upper()
|
|
||||||
media = (block.get('MediaType') or '').strip().upper()
|
|
||||||
if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media:
|
|
||||||
core_info.append(f"DISK:{serial}")
|
|
||||||
block = {}
|
|
||||||
continue
|
|
||||||
if '=' in line:
|
|
||||||
k, v = line.split('=', 1)
|
|
||||||
block[k] = v
|
|
||||||
# 处理最后一个块
|
|
||||||
if block:
|
|
||||||
serial = (block.get('SerialNumber') or '').strip()
|
|
||||||
iface = (block.get('InterfaceType') or '').strip().upper()
|
|
||||||
pnp = (block.get('PNPDeviceID') or '').strip().upper()
|
|
||||||
media = (block.get('MediaType') or '').strip().upper()
|
|
||||||
if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media:
|
|
||||||
core_info.append(f"DISK:{serial}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取磁盘信息失败: {e}")
|
|
||||||
try:
|
|
||||||
import uuid
|
|
||||||
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1])
|
|
||||||
aux_info.append(f"MAC:{mac}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"获取MAC地址失败: {e}")
|
|
||||||
aux_info.append(f"OS:{platform.system()}")
|
|
||||||
aux_info.append(f"MACHINE:{platform.machine()}")
|
|
||||||
if len(core_info) < 1:
|
|
||||||
core_info.append(f"NODE:{platform.node()}")
|
|
||||||
core_info.append(f"PROCESSOR:{platform.processor()}")
|
|
||||||
combined_info = "|".join(sorted(core_info))
|
|
||||||
machine_id = hashlib.sha256(combined_info.encode('utf-8')).hexdigest()[:16].upper()
|
|
||||||
self._machine_id = f"W10-{machine_id}"
|
|
||||||
logger.info(f"生成机器指纹: {self._machine_id}")
|
logger.info(f"生成机器指纹: {self._machine_id}")
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -137,8 +398,14 @@ class LicenseManager:
|
|||||||
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
|
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
|
||||||
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
|
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
|
||||||
self._machine_id = f"FB-{fallback_id}"
|
self._machine_id = f"FB-{fallback_id}"
|
||||||
|
self._machine_id_candidates = [self._machine_id]
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
|
|
||||||
|
def get_machine_id_candidates(self) -> list:
|
||||||
|
if self._machine_id_candidates is None:
|
||||||
|
self.get_machine_id()
|
||||||
|
return list(self._machine_id_candidates or [])
|
||||||
|
|
||||||
def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
|
def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
|
||||||
"""加载授权文件"""
|
"""加载授权文件"""
|
||||||
try:
|
try:
|
||||||
@ -231,7 +498,13 @@ class LicenseManager:
|
|||||||
|
|
||||||
# 检查机器绑定
|
# 检查机器绑定
|
||||||
license_machine_id = license_data.get('machine_id', '')
|
license_machine_id = license_data.get('machine_id', '')
|
||||||
if license_machine_id != machine_id:
|
candidates = []
|
||||||
|
if machine_id:
|
||||||
|
candidates.append(machine_id)
|
||||||
|
for mid in self.get_machine_id_candidates():
|
||||||
|
if mid not in candidates:
|
||||||
|
candidates.append(mid)
|
||||||
|
if license_machine_id not in candidates:
|
||||||
return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
|
return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
|
||||||
|
|
||||||
# 检查有效期
|
# 检查有效期
|
||||||
@ -269,18 +542,7 @@ class LicenseManager:
|
|||||||
if not self.config_manager:
|
if not self.config_manager:
|
||||||
return LicenseStatus(valid=False, message="配置管理器未初始化")
|
return LicenseStatus(valid=False, message="配置管理器未初始化")
|
||||||
|
|
||||||
license_path = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json')
|
license_path, public_key_path, grace_days = self._resolve_license_paths()
|
||||||
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
|
|
||||||
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
|
|
||||||
|
|
||||||
# 转换为绝对路径
|
|
||||||
if not os.path.isabs(license_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
|
|
||||||
license_path = os.path.join(base_dir, license_path)
|
|
||||||
|
|
||||||
if not os.path.isabs(public_key_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
|
|
||||||
public_key_path = os.path.join(base_dir, public_key_path)
|
|
||||||
|
|
||||||
# 获取机器指纹
|
# 获取机器指纹
|
||||||
machine_id = self.get_machine_id()
|
machine_id = self.get_machine_id()
|
||||||
@ -309,6 +571,8 @@ class LicenseManager:
|
|||||||
self._cache_timestamp = datetime.now().timestamp()
|
self._cache_timestamp = datetime.now().timestamp()
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
self._mirror_license_assets(license_path=license_path, public_key_path=public_key_path)
|
||||||
|
|
||||||
# 检查有效性
|
# 检查有效性
|
||||||
is_valid, message = self.check_validity(license_data, machine_id, grace_days)
|
is_valid, message = self.check_validity(license_data, machine_id, grace_days)
|
||||||
|
|
||||||
@ -362,14 +626,7 @@ class LicenseManager:
|
|||||||
if not self.config_manager:
|
if not self.config_manager:
|
||||||
return False, "配置管理器未初始化"
|
return False, "配置管理器未初始化"
|
||||||
|
|
||||||
# 解析公钥路径与宽限期
|
_, public_key_path, grace_days = self._resolve_license_paths()
|
||||||
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
|
|
||||||
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
|
|
||||||
|
|
||||||
# 转换为绝对路径(相对backend目录)
|
|
||||||
if not os.path.isabs(public_key_path):
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
public_key_path = os.path.join(base_dir, public_key_path)
|
|
||||||
|
|
||||||
if not os.path.exists(license_path):
|
if not os.path.exists(license_path):
|
||||||
return False, f"授权文件不存在: {license_path}"
|
return False, f"授权文件不存在: {license_path}"
|
||||||
|
|||||||
125
backend/main.py
125
backend/main.py
@ -10,6 +10,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Flask, jsonify
|
from flask import Flask, jsonify
|
||||||
from flask import request as flask_request
|
from flask import request as flask_request
|
||||||
@ -378,6 +379,10 @@ class AppServer:
|
|||||||
'error': '授权管理器未初始化'
|
'error': '授权管理器未初始化'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
if self.license_manager:
|
||||||
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
@ -548,22 +553,14 @@ class AppServer:
|
|||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
# 覆盖系统授权文件为上传的文件
|
# 覆盖系统授权文件为上传的文件
|
||||||
try:
|
ok, result = self.license_manager.install_license_file(temp_path)
|
||||||
license_path_cfg = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json') if self.config_manager else 'data/license.json'
|
if not ok:
|
||||||
# 解析目标路径为绝对路径
|
self.logger.error(f'保存授权文件失败: {result}')
|
||||||
if not os.path.isabs(license_path_cfg):
|
return jsonify({'success': False, 'error': f'保存授权文件失败: {result}'}), 500
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
license_path_cfg = os.path.join(base_dir, license_path_cfg)
|
|
||||||
os.makedirs(os.path.dirname(license_path_cfg), exist_ok=True)
|
|
||||||
# 移动/覆盖授权文件
|
|
||||||
import shutil
|
|
||||||
shutil.copyfile(temp_path, license_path_cfg)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'保存授权文件失败: {e}')
|
|
||||||
return jsonify({'success': False, 'error': f'保存授权文件失败: {str(e)}'}), 500
|
|
||||||
|
|
||||||
# 更新授权状态(强制刷新)
|
# 更新授权状态(强制刷新)
|
||||||
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -631,8 +628,11 @@ class AppServer:
|
|||||||
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
|
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
|
||||||
zip_ref.extractall(target_dir)
|
zip_ref.extractall(target_dir)
|
||||||
|
|
||||||
|
self.license_manager.mirror_license_dir(target_dir)
|
||||||
|
|
||||||
# 刷新授权
|
# 刷新授权
|
||||||
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
self.license_status = self.license_manager.get_license_status(force_reload=True)
|
||||||
|
self.app.license_status = self.license_status
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -1449,6 +1449,30 @@ class AppServer:
|
|||||||
# 使用新的ffmpeg录制方法
|
# 使用新的ffmpeg录制方法
|
||||||
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location)
|
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location)
|
||||||
|
|
||||||
|
base_info = None
|
||||||
|
try:
|
||||||
|
base_info = self.recording_manager.get_active_video_base(session_id)
|
||||||
|
except Exception:
|
||||||
|
base_info = None
|
||||||
|
|
||||||
|
if base_info and base_info.get('base_path'):
|
||||||
|
base_path = base_info.get('base_path')
|
||||||
|
try:
|
||||||
|
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
if cam1 and getattr(cam1, 'is_streaming', False):
|
||||||
|
cam1_frames_dir = os.path.join(base_path, 'camera1_frames')
|
||||||
|
cam1.start_jpeg_recording(session_id=session_id, frames_dir=cam1_frames_dir)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'启动camera1 JPEG录制失败: {e}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
if cam2 and getattr(cam2, 'is_streaming', False):
|
||||||
|
cam2_frames_dir = os.path.join(base_path, 'camera2_frames')
|
||||||
|
cam2.start_jpeg_recording(session_id=session_id, frames_dir=cam2_frames_dir)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'启动camera2 JPEG录制失败: {e}')
|
||||||
|
|
||||||
# 处理录制管理器返回的数据库更新信息
|
# 处理录制管理器返回的数据库更新信息
|
||||||
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
||||||
db_updates = recording_response['database_updates']
|
db_updates = recording_response['database_updates']
|
||||||
@ -1456,7 +1480,10 @@ class AppServer:
|
|||||||
# 保存检测视频记录(映射到 detection_video 表字段)
|
# 保存检测视频记录(映射到 detection_video 表字段)
|
||||||
video_paths = db_updates.get('video_paths', {})
|
video_paths = db_updates.get('video_paths', {})
|
||||||
video_record = {
|
video_record = {
|
||||||
'screen_video_path': video_paths.get('screen_video_path')
|
'screen_video_path': video_paths.get('screen_video_path'),
|
||||||
|
'body_video_path': None,
|
||||||
|
'foot_video1_path': None,
|
||||||
|
'foot_video2_path': None
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1495,6 +1522,76 @@ class AppServer:
|
|||||||
except Exception as rec_e:
|
except Exception as rec_e:
|
||||||
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
base_info = None
|
||||||
|
try:
|
||||||
|
base_info = self.recording_manager.get_active_video_base(session_id)
|
||||||
|
except Exception:
|
||||||
|
base_info = None
|
||||||
|
|
||||||
|
if base_info and base_info.get('base_path') and base_info.get('file_dir'):
|
||||||
|
base_path = base_info.get('base_path')
|
||||||
|
file_dir = base_info.get('file_dir')
|
||||||
|
|
||||||
|
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
|
||||||
|
|
||||||
|
if cam1:
|
||||||
|
try:
|
||||||
|
cam1_stop = cam1.stop_jpeg_recording(session_id=session_id)
|
||||||
|
cam1_frames_dir = cam1_stop.get('frames_dir')
|
||||||
|
cam1_fps = int(cam1_stop.get('fps') or 10)
|
||||||
|
if cam1_frames_dir and os.path.isdir(cam1_frames_dir):
|
||||||
|
out1 = os.path.join(base_path, 'foot1.mp4')
|
||||||
|
rel1 = os.path.relpath(out1, file_dir).replace('\\', '/')
|
||||||
|
def _done1(code: int):
|
||||||
|
if code == 0:
|
||||||
|
try:
|
||||||
|
self.db_manager.update_detection_video_latest(session_id, {'foot_video1_path': rel1})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'更新foot_video1失败: {e}')
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cam1_frames_dir, ignore_errors=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'删除camera1_frames失败: {e}')
|
||||||
|
self.recording_manager.transcode_jpeg_sequence_async(
|
||||||
|
name=f'{session_id}-camera1',
|
||||||
|
frames_dir=cam1_frames_dir,
|
||||||
|
output_mp4_path=out1,
|
||||||
|
fps=cam1_fps,
|
||||||
|
on_done=_done1
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'停止/转码camera1录制失败: {e}')
|
||||||
|
|
||||||
|
if cam2:
|
||||||
|
try:
|
||||||
|
cam2_stop = cam2.stop_jpeg_recording(session_id=session_id)
|
||||||
|
cam2_frames_dir = cam2_stop.get('frames_dir')
|
||||||
|
cam2_fps = int(cam2_stop.get('fps') or 10)
|
||||||
|
if cam2_frames_dir and os.path.isdir(cam2_frames_dir):
|
||||||
|
out2 = os.path.join(base_path, 'foot2.mp4')
|
||||||
|
rel2 = os.path.relpath(out2, file_dir).replace('\\', '/')
|
||||||
|
def _done2(code: int):
|
||||||
|
if code == 0:
|
||||||
|
try:
|
||||||
|
self.db_manager.update_detection_video_latest(session_id, {'foot_video2_path': rel2})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'更新foot_video2失败: {e}')
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cam2_frames_dir, ignore_errors=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'删除camera2_frames失败: {e}')
|
||||||
|
self.recording_manager.transcode_jpeg_sequence_async(
|
||||||
|
name=f'{session_id}-camera2',
|
||||||
|
frames_dir=cam2_frames_dir,
|
||||||
|
output_mp4_path=out2,
|
||||||
|
fps=cam2_fps,
|
||||||
|
on_done=_done2
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'停止/转码camera2录制失败: {e}')
|
||||||
|
|
||||||
return jsonify({'success': True,'msg': '停止录制成功'})
|
return jsonify({'success': True,'msg': '停止录制成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
||||||
|
|||||||
64
backend/tests/test_license_manager_unit.py
Normal file
64
backend/tests/test_license_manager_unit.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class DummyConfigManager:
|
||||||
|
def __init__(self, values):
|
||||||
|
self._values = values
|
||||||
|
|
||||||
|
def get_config_value(self, section, key, fallback=None):
|
||||||
|
return self._values.get((section, key), fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_validity_accepts_candidate_machine_id(monkeypatch):
|
||||||
|
from devices.utils.license_manager import LicenseManager
|
||||||
|
|
||||||
|
lm = LicenseManager(config_manager=DummyConfigManager({}))
|
||||||
|
monkeypatch.setattr(lm, "get_machine_id_candidates", lambda: ["MID-PRIMARY", "MID-ALT"])
|
||||||
|
|
||||||
|
license_data = {
|
||||||
|
"product": "BodyBalanceEvaluation",
|
||||||
|
"license_id": "L1",
|
||||||
|
"license_type": "full",
|
||||||
|
"machine_id": "MID-ALT",
|
||||||
|
"issued_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"expires_at": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat().replace("+00:00", "Z"),
|
||||||
|
"signature": "x",
|
||||||
|
"features": {"export": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, msg = lm.check_validity(license_data, machine_id="MID-PRIMARY", grace_days=0)
|
||||||
|
assert ok is True, msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_license_paths_falls_back_to_persistent(monkeypatch, tmp_path):
|
||||||
|
from devices.utils.license_manager import LicenseManager
|
||||||
|
|
||||||
|
programdata = tmp_path / "programdata"
|
||||||
|
persistent_dir = programdata / "BodyCheck" / "license"
|
||||||
|
persistent_dir.mkdir(parents=True)
|
||||||
|
(persistent_dir / "license.json").write_text('{"a":1}', encoding="utf-8")
|
||||||
|
(persistent_dir / "license_public_key.pem").write_text("PUB", encoding="utf-8")
|
||||||
|
|
||||||
|
cfg_dir = tmp_path / "cfg"
|
||||||
|
cfg_license_path = str(cfg_dir / "license.json")
|
||||||
|
cfg_pub_path = str(cfg_dir / "license_public_key.pem")
|
||||||
|
|
||||||
|
cfg = DummyConfigManager(
|
||||||
|
{
|
||||||
|
("LICENSE", "path"): cfg_license_path,
|
||||||
|
("LICENSE", "public_key"): cfg_pub_path,
|
||||||
|
("LICENSE", "grace_days"): "3",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lm = LicenseManager(config_manager=cfg)
|
||||||
|
|
||||||
|
monkeypatch.setenv("PROGRAMDATA", str(programdata))
|
||||||
|
monkeypatch.setattr("platform.system", lambda: "Windows")
|
||||||
|
|
||||||
|
license_path, pub_path, grace_days = lm._resolve_license_paths()
|
||||||
|
assert license_path == str(persistent_dir / "license.json")
|
||||||
|
assert pub_path == str(persistent_dir / "license_public_key.pem")
|
||||||
|
assert grace_days == 3
|
||||||
@ -3,7 +3,7 @@ const path = require('path');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const { spawn } = require('child_process');
|
const { spawn, exec, execSync } = require('child_process');
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let localServer;
|
let localServer;
|
||||||
let backendProcess;
|
let backendProcess;
|
||||||
@ -11,6 +11,24 @@ let splashWindow;
|
|||||||
// app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
app.disableDomainBlockingFor3DAPIs();
|
app.disableDomainBlockingFor3DAPIs();
|
||||||
console.log('Electron version:', process.versions.electron);
|
console.log('Electron version:', process.versions.electron);
|
||||||
|
|
||||||
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotSingleInstanceLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
mainWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app.isReady()) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (!win) throw new Error('窗口未找到');
|
if (!win) throw new Error('窗口未找到');
|
||||||
@ -134,6 +152,21 @@ ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function startBackendService() {
|
function startBackendService() {
|
||||||
|
if (backendProcess) {
|
||||||
|
console.log('Backend service already started (tracked).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tasklistOut = execSync('tasklist /FI "IMAGENAME eq BodyBalanceBackend.exe" /NH', { windowsHide: true }).toString();
|
||||||
|
if (tasklistOut && tasklistOut.toLowerCase().includes('bodybalancebackend.exe')) {
|
||||||
|
console.log('Backend service already running (tasklist). Skip spawning a new instance.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Backend process detection failed, continue to spawn:', e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
|
||||||
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
||||||
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
||||||
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
||||||
@ -199,7 +232,6 @@ function stopBackendService() {
|
|||||||
|
|
||||||
// 强制杀死所有BodyBalanceBackend.exe进程
|
// 强制杀死所有BodyBalanceBackend.exe进程
|
||||||
try {
|
try {
|
||||||
const { exec } = require('child_process');
|
|
||||||
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
||||||
@ -300,6 +332,11 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startLocalServer(callback) {
|
function startLocalServer(callback) {
|
||||||
|
if (localServer) {
|
||||||
|
console.log('Local server already started on http://localhost:3000');
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const staticPath = path.join(__dirname, '../dist/');
|
const staticPath = path.join(__dirname, '../dist/');
|
||||||
|
|
||||||
localServer = http.createServer((req, res) => {
|
localServer = http.createServer((req, res) => {
|
||||||
@ -360,26 +397,28 @@ function startLocalServer(callback) {
|
|||||||
// 应用事件处理
|
// 应用事件处理
|
||||||
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
||||||
// app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
app.whenReady().then(createWindow);
|
if (gotSingleInstanceLock) {
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
if (localServer) {
|
if (localServer) {
|
||||||
localServer.close();
|
localServer.close();
|
||||||
|
}
|
||||||
|
// 关闭后端服务
|
||||||
|
stopBackendService();
|
||||||
|
app.quit();
|
||||||
}
|
}
|
||||||
// 关闭后端服务
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用退出前清理资源
|
||||||
|
app.on('before-quit', () => {
|
||||||
stopBackendService();
|
stopBackendService();
|
||||||
app.quit();
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用退出前清理资源
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
stopBackendService();
|
|
||||||
});
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
<Header />
|
<Header />
|
||||||
<div class="displaycontainer">
|
<div class="displaycontainer">
|
||||||
<div class="displayleft" style="width: 550px;">
|
<div class="displayleft" style="width: 550px;">
|
||||||
|
|
||||||
<img src="@/assets/detection/progress.png" alt="" style=" margin-left:10px;margin-right:15px">
|
<img src="@/assets/detection/progress.png" alt="" style=" margin-left:10px;margin-right:15px">
|
||||||
<div style="
|
<div style="
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@ -47,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width:100%;height: calc(100% - 115px);" ref="contenGridRef">
|
<div style="width:100%;height: calc(100% - 131px);" ref="contenGridRef">
|
||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
<el-row :gutter="15" style="padding: 10px;padding-top:0" >
|
<el-row :gutter="15" style="padding: 10px;padding-top:0" >
|
||||||
<el-col :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
|
<el-col :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
|
||||||
@ -192,93 +193,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body-footbottom-box" ref="pressureRef">
|
<div class="body-footbottom-box" ref="pressureRef">
|
||||||
<div style="width: 100%;height: calc(100% - 57px);display: flex; align-items: center;justify-content: center;">
|
<div class="body-footbottom-left">
|
||||||
<div class="body-footbottom-left">
|
<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="currencytext1">左前足</span>
|
<span class="currencytext2">
|
||||||
<span class="currencytext2">
|
{{ footPressure.left_front }}%
|
||||||
{{ footPressure.left_front }}%
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="body-footbottom-leftbox">
|
|
||||||
<span class="currencytext1">左后足</span>
|
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.left_rear }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="body-footbottom-leftbox">
|
|
||||||
<span class="currencytext1">左足总压力</span>
|
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.left_total}}%
|
|
||||||
</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="body-footbottom-leftbox">
|
||||||
<div class="body-footbottom-center">
|
<span class="currencytext1">左后足</span>
|
||||||
<div class="body-footbottom-topbox">
|
<span class="currencytext2">
|
||||||
<div class="currencytext1" style="font-size:22px;text-align:center;">左足</div>
|
{{ footPressure.left_rear }}%
|
||||||
<div class="currencytext1" style="font-size:22px;text-align:center;">右足</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="position: relative;width: 100%;height:calc(100% - 60px) ;"
|
<div class="body-footbottom-leftbox">
|
||||||
:class="(pressureStatus === '已连接' && footImgSrc)?'':'noImageSvg-bg'">
|
<span class="currencytext1">左足总压力</span>
|
||||||
|
<span class="currencytext2">
|
||||||
<img v-if="(pressureStatus === '已连接' && footImgSrc)" :src="footImgSrc" style="width: 100%;height: 100%;" alt="">
|
{{ footPressure.left_total}}%
|
||||||
<div v-else style="width:90px;height:60px">
|
</span>
|
||||||
<img :src="noImageSvg" style="margin-left: 15px;">
|
|
||||||
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
|
||||||
</div>
|
|
||||||
<div class="xline"></div>
|
|
||||||
<div class="yline"></div>
|
|
||||||
<!-- <div v-if="(pressureStatus === '已连接' && footImgSrc)" class="xline"></div>
|
|
||||||
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="yline"></div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="body-footbottom-left">
|
|
||||||
<div style="width:100%;height: 50px;"></div>
|
|
||||||
<div class="body-footbottom-leftbottom">
|
|
||||||
<div class="body-footbottom-leftbox">
|
|
||||||
<span class="currencytext1">右前足</span>
|
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_front }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="body-footbottom-leftbox">
|
|
||||||
<span class="currencytext1">右后足</span>
|
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_rear }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="body-footbottom-leftbox">
|
|
||||||
<span class="currencytext1">右足总压力</span>
|
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_total}}%
|
|
||||||
</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;justify-content: center; width: 100%;">
|
<div class="body-footbottom-center">
|
||||||
<div class="body-footbottom-leftbox" style="width:calc(22% + 2px)">
|
<div class="body-footbottom-topbox">
|
||||||
<span class="currencytext1">左足总压力</span>
|
<div class="currencytext1" style="font-size:22px;text-align:center;">左足</div>
|
||||||
<span class="currencytext2">
|
<div class="currencytext1" style="font-size:22px;text-align:center;">右足</div>
|
||||||
{{ footPressure.left_total}}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="body-footbottom-leftbox" style="width:calc(22% + 2px);margin-left: 20px">
|
<div style="position: relative;width: 100%;height:calc(100% - 60px) ;"
|
||||||
<span class="currencytext1">右足总压力</span>
|
:class="(pressureStatus === '已连接' && footImgSrc)?'':'noImageSvg-bg'">
|
||||||
<span class="currencytext2">
|
|
||||||
{{ footPressure.right_total}}%
|
<img v-if="(pressureStatus === '已连接' && footImgSrc)" :src="footImgSrc" style="width: 100%;height: 100%;" alt="">
|
||||||
</span>
|
<div v-else style="width:90px;height:60px">
|
||||||
|
<img :src="noImageSvg" style="margin-left: 15px;">
|
||||||
|
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="xline"></div>
|
||||||
|
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="yline"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body-footbottom-left">
|
||||||
|
<div style="width:100%;height: 50px;"></div>
|
||||||
|
<div class="body-footbottom-leftbottom">
|
||||||
|
<div class="body-footbottom-leftbox">
|
||||||
|
<span class="currencytext1">右前足</span>
|
||||||
|
<span class="currencytext2">
|
||||||
|
{{ footPressure.right_front }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="body-footbottom-leftbox">
|
||||||
|
<span class="currencytext1">右后足</span>
|
||||||
|
<span class="currencytext2">
|
||||||
|
{{ footPressure.right_rear }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="body-footbottom-leftbox">
|
||||||
|
<span class="currencytext1">右足总压力</span>
|
||||||
|
<span class="currencytext2">
|
||||||
|
{{ footPressure.right_total}}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-if=" false || camera1Status === '已连接' && camera2Status === '已连接'"
|
<el-col v-if="camera1Status === '已连接' && camera2Status === '已连接'"
|
||||||
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px); position: relative;">
|
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px); position: relative;">
|
||||||
<div class="body-userinfo-box" :class="isExpand == true?'body-userinfo-expandbox':''">
|
<div class="body-userinfo-box" :class="isExpand == true?'body-userinfo-expandbox':''">
|
||||||
<div class="body-title-display">
|
<div class="body-title-display">
|
||||||
@ -448,9 +430,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-if="true || camera1Status === '已连接' || camera2Status === '已连接'"
|
<el-col v-if="camera1Status === '已连接' || camera2Status === '已连接'"
|
||||||
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px);position: relative;">
|
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
|
||||||
<div class="body-userinfo-box1" :class="isExpand == true?'body-userinfo-expandbox':''">
|
<div class="body-userinfo-box1">
|
||||||
<div class="body-title-display">
|
<div class="body-title-display">
|
||||||
<div class="body-son-display">
|
<div class="body-son-display">
|
||||||
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
|
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
|
||||||
@ -462,7 +444,7 @@
|
|||||||
<div class="body-userinfo-content-top">
|
<div class="body-userinfo-content-top">
|
||||||
|
|
||||||
<img src="@/assets/detection/useredit.png" alt="" title="编辑患者信息"
|
<img src="@/assets/detection/useredit.png" alt="" title="编辑患者信息"
|
||||||
class="userinfo-edit-img" style="cursor: pointer;" @click="handleEditUserInfo">
|
class="userinfo-edit-img" style="cursor: pointer;" @click="handleEditUserInfo">
|
||||||
<div class="useravatar-box">
|
<div class="useravatar-box">
|
||||||
<img src="@/assets/detection/useravatar.svg" alt="">
|
<img src="@/assets/detection/useravatar.svg" alt="">
|
||||||
</div>
|
</div>
|
||||||
@ -479,116 +461,61 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="body-userinfo-content-bottom0" v-if="isExpand == false">
|
<div class="body-userinfo-content-bottom1">
|
||||||
<img src="@/assets/detection/userinfo.png" alt=""
|
<div class="userinfo-disyplaypadding4">
|
||||||
class="userinfo-edit-img" style="cursor: pointer;"
|
<div class="userinfo-text4">出生日期</div>
|
||||||
@click="viewClick(true)">
|
|
||||||
<div class="userinfo-disyplaypadding1 ">
|
|
||||||
<div class="userinfo-text4 padding10">出生日期</div>
|
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
<span v-if="patientInfo && patientInfo.birth_date">
|
<span v-if="patientInfo && patientInfo.birth_date">
|
||||||
{{ formatDate(patientInfo.birth_date) }}
|
{{ formatDate(patientInfo.birth_date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding2">
|
<div class="userinfo-disyplaypadding5">
|
||||||
<div class="userinfo-text4 padding10">身高</div>
|
<div class="userinfo-text4">身高</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
|
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding1">
|
<div class="userinfo-disyplaypadding4">
|
||||||
<div class="userinfo-text4 padding10">体重</div>
|
<div class="userinfo-text4">体重</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
|
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding2">
|
<div class="userinfo-disyplaypadding5">
|
||||||
<div class="userinfo-text4 padding10">鞋码</div>
|
<div class="userinfo-text4">鞋码</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
|
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding1">
|
<div class="userinfo-disyplaypadding4">
|
||||||
<div class="userinfo-text4 padding10">电话</div>
|
<div class="userinfo-text4">电话</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
|
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding2">
|
<div class="userinfo-disyplaypadding5">
|
||||||
<div class="userinfo-text4 padding10">民族</div>
|
<div class="userinfo-text4">民族</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
|
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding1">
|
<div class="userinfo-disyplaypadding4">
|
||||||
<div class="userinfo-text4 padding10">身份证号</div>
|
<div class="userinfo-text4">身份证号</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
|
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding2">
|
<div class="userinfo-disyplaypadding5">
|
||||||
<div class="userinfo-text4 padding10">职业</div>
|
<div class="userinfo-text4">职业</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
|
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="userinfo-disyplaypadding6">
|
||||||
<div class="body-userinfo-content-bottom2" v-if="isExpand == true">
|
<div class="userinfo-text4">居住地</div>
|
||||||
<img src="@/assets/detection/userinfo.png" alt=""
|
|
||||||
class="userinfo-edit-img" style="cursor: pointer;"
|
|
||||||
@click="viewClick(false)">
|
|
||||||
<div class="userinfo-disyplaypadding1 ">
|
|
||||||
<div class="userinfo-text4 padding10">出生日期</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
<span v-if="patientInfo && patientInfo.birth_date">
|
|
||||||
{{ formatDate(patientInfo.birth_date) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding2">
|
|
||||||
<div class="userinfo-text4 padding10">身高</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding1">
|
|
||||||
<div class="userinfo-text4 padding10">体重</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding2">
|
|
||||||
<div class="userinfo-text4 padding10">鞋码</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding1">
|
|
||||||
<div class="userinfo-text4 padding10">电话</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding2">
|
|
||||||
<div class="userinfo-text4 padding10">民族</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding1">
|
|
||||||
<div class="userinfo-text4 padding10">身份证号</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding2">
|
|
||||||
<div class="userinfo-text4 padding10">职业</div>
|
|
||||||
<div class="userinfo-text5">
|
|
||||||
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="userinfo-disyplaypadding3">
|
|
||||||
<div class="userinfo-text4 padding10">居住地</div>
|
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
|
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="userinfo-disyplaypadding3">
|
<div class="userinfo-disyplaypadding6">
|
||||||
<div class="userinfo-text4 padding10">邮箱</div>
|
<div class="userinfo-text4">邮箱</div>
|
||||||
<div class="userinfo-text5">
|
<div class="userinfo-text5">
|
||||||
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
|
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -596,8 +523,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body-video-box1" style="position: absolute; top: 522px; width: calc(100% - 15px);">
|
<div class="body-video-box1">
|
||||||
|
|
||||||
<div class="body-title-display">
|
<div class="body-title-display">
|
||||||
<div class="body-son-display">
|
<div class="body-son-display">
|
||||||
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
|
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
|
||||||
@ -612,14 +538,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="body-video-content" style="padding: 0;">
|
<div class="body-video-content">
|
||||||
<div v-show="camera1Status === '已连接'" class="body-video-imgbox3" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
|
<div v-show="camera1Status === '已连接'" class="body-video-imgbox3" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
|
||||||
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
|
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
|
||||||
@click="isBig1 = true" class="big-img">
|
@click="isBig1 = true" class="big-img">
|
||||||
<img src="@/assets/detection/big.png">
|
<img src="@/assets/detection/big.png">
|
||||||
</div>
|
</div>
|
||||||
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
|
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
|
||||||
style="width: 100%; height: 100%;object-fit:contain;" />
|
style="width: 100%; height: 100%;" />
|
||||||
<div v-else style="width:90px;height:60px">
|
<div v-else style="width:90px;height:60px">
|
||||||
<img :src="noImageSvg" style="margin-left: 15px;">
|
<img :src="noImageSvg" style="margin-left: 15px;">
|
||||||
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
||||||
@ -631,7 +557,7 @@
|
|||||||
<img src="@/assets/detection/big.png">
|
<img src="@/assets/detection/big.png">
|
||||||
</div>
|
</div>
|
||||||
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
|
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
|
||||||
style="width: 100%; height: 100%;object-fit:contain;" />
|
style="width: 100%; height: 100%;" />
|
||||||
<div v-else style="width:90px;height:60px">
|
<div v-else style="width:90px;height:60px">
|
||||||
<img :src="noImageSvg" style="margin-left: 15px;">
|
<img :src="noImageSvg" style="margin-left: 15px;">
|
||||||
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
|
||||||
@ -640,7 +566,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-if="false || camera1Status !== '已连接' && camera2Status !== '已连接'" :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
|
<el-col v-if="camera1Status !== '已连接' && camera2Status !== '已连接'" :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
|
||||||
<div class="body-userinfo-box3">
|
<div class="body-userinfo-box3">
|
||||||
<div class="body-title-display">
|
<div class="body-title-display">
|
||||||
<div class="body-son-display">
|
<div class="body-son-display">
|
||||||
@ -787,7 +713,6 @@
|
|||||||
<div class="pop-up-tip-text" v-if="!isVideoOperation">本次检测未截图或录像操作,不予存档记录!</div>
|
<div class="pop-up-tip-text" v-if="!isVideoOperation">本次检测未截图或录像操作,不予存档记录!</div>
|
||||||
<div class="pop-up-tip-text" v-if="isVideoOperation">本次检测未截图操作,存档记录不可生成报告!</div>
|
<div class="pop-up-tip-text" v-if="isVideoOperation">本次检测未截图操作,存档记录不可生成报告!</div>
|
||||||
<div class="tipconfirmbutton-box">
|
<div class="tipconfirmbutton-box">
|
||||||
<div class="tipclosebutton" @click="handleCancel">取消</div>
|
|
||||||
<el-button type="primary" class="tipconfirmbutton" @click="closeTipClick">确定</el-button>
|
<el-button type="primary" class="tipconfirmbutton" @click="closeTipClick">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2709,23 +2634,9 @@ const isPhotoAlbum = ref(false)
|
|||||||
function closePhotoAlbum(){
|
function closePhotoAlbum(){
|
||||||
isPhotoAlbum.value = false
|
isPhotoAlbum.value = false
|
||||||
}
|
}
|
||||||
function closecreatbox(e,info){
|
function closecreatbox(e){
|
||||||
if(e === '编辑'){
|
if(e === true){
|
||||||
patientInfo.value.age = info.age
|
loadPatientInfo()
|
||||||
patientInfo.value.birth_date = info.birth_date
|
|
||||||
patientInfo.value.email = info.email
|
|
||||||
patientInfo.value.gender = info.gender
|
|
||||||
patientInfo.value.height = info.height
|
|
||||||
patientInfo.value.id = info.id
|
|
||||||
patientInfo.value.idcode = info.idcode
|
|
||||||
patientInfo.value.name = info.name
|
|
||||||
patientInfo.value.nationality = info.nationality
|
|
||||||
patientInfo.value.occupation = info.occupation
|
|
||||||
patientInfo.value.phone = info.phone
|
|
||||||
patientInfo.value.residence = info.residence
|
|
||||||
patientInfo.value.shoe_size = info.shoe_size
|
|
||||||
patientInfo.value.weight = info.weight
|
|
||||||
// loadPatientInfo()
|
|
||||||
}
|
}
|
||||||
isCloseCreat.value = false
|
isCloseCreat.value = false
|
||||||
}
|
}
|
||||||
@ -2767,7 +2678,7 @@ function viewClick(e){
|
|||||||
|
|
||||||
.displaycontainer {
|
.displaycontainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 46px;
|
height: 62px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -2944,8 +2855,10 @@ function viewClick(e){
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.body-footbottom-box{
|
.body-footbottom-box{
|
||||||
height: calc(100% - 50px);
|
display: flex;
|
||||||
padding-bottom: 15px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(100% - 70px);
|
||||||
}
|
}
|
||||||
.body-footbottom-left{
|
.body-footbottom-left{
|
||||||
width: 28%;
|
width: 28%;
|
||||||
@ -2958,8 +2871,8 @@ function viewClick(e){
|
|||||||
.body-footbottom-leftbox{
|
.body-footbottom-leftbox{
|
||||||
min-width: 215px;
|
min-width: 215px;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
min-height: 57px;
|
min-height: 60px;
|
||||||
height: 57px;
|
height: 20%;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -2969,8 +2882,8 @@ function viewClick(e){
|
|||||||
padding: 0px 20px;
|
padding: 0px 20px;
|
||||||
}
|
}
|
||||||
.body-footbottom-center{
|
.body-footbottom-center{
|
||||||
width: 37%;
|
width: 40%;
|
||||||
height: calc(100% - 0px);
|
height: calc(100%);
|
||||||
}
|
}
|
||||||
.body-footbottom-topbox{
|
.body-footbottom-topbox{
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -3005,7 +2918,7 @@ function viewClick(e){
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-content:space-around;
|
align-content:space-between ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-userinfo-box{
|
.body-userinfo-box{
|
||||||
@ -3013,18 +2926,18 @@ function viewClick(e){
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 346px;
|
height: 346px;
|
||||||
|
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
|
||||||
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
||||||
border: 1px solid #242E3D;
|
border: 1px solid #242E3D;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.body-userinfo-expandbox{
|
.body-userinfo-expandbox{
|
||||||
height: 680px !important;
|
height: 638px ;
|
||||||
}
|
}
|
||||||
.body-userinfo-box1{
|
.body-userinfo-box1{
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 524px ;
|
height: 534px ;
|
||||||
|
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
|
||||||
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
||||||
border: 1px solid #242E3D;
|
border: 1px solid #242E3D;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -3050,7 +2963,7 @@ function viewClick(e){
|
|||||||
}
|
}
|
||||||
.body-video-box1{
|
.body-video-box1{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 534px - 4px) ;
|
height: calc(100% - 534px - 14px) ;
|
||||||
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
|
||||||
border: 1px solid #242E3D;
|
border: 1px solid #242E3D;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -3187,10 +3100,10 @@ function viewClick(e){
|
|||||||
}
|
}
|
||||||
.userinfo-disyplaypadding1{
|
.userinfo-disyplaypadding1{
|
||||||
width: calc(64%);
|
width: calc(64%);
|
||||||
padding-bottom: 20px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
.padding10{
|
.padding10{
|
||||||
padding-bottom: 10px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
.userinfo-disyplaypadding2{
|
.userinfo-disyplaypadding2{
|
||||||
width: calc(36%);
|
width: calc(36%);
|
||||||
@ -3425,25 +3338,6 @@ function viewClick(e){
|
|||||||
background:#14aaff;
|
background:#14aaff;
|
||||||
border:1px solid #14aaff;
|
border:1px solid #14aaff;
|
||||||
}
|
}
|
||||||
.tipclosebutton{
|
|
||||||
width: 80px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: #597194;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.tipclosebutton:hover{
|
|
||||||
background-color: #14aaff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.pop-up-tip-text{
|
.pop-up-tip-text{
|
||||||
width:100%;
|
width:100%;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user