Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c55f128c22 | |||
| 4137bc10d7 | |||
| 6e4ec82e17 | |||
| 42570d633a | |||
| 1321f557d2 | |||
| 5ed553b2a0 | |||
| 75c3380ff3 | |||
| def6304bd9 | |||
| e3e294694c | |||
| 68bbeb63fd | |||
| c71f07f931 | |||
| 6499926298 | |||
| cd35871476 | |||
| 5059ad6158 | |||
| 641bc9e8a0 | |||
| e95c771171 | |||
| d5d66a91a2 | |||
| 41f9b283c2 | |||
| 599d1f6807 | |||
| 653f4e1666 | |||
| 61a43a2800 | |||
| 6ff9e0c8ca | |||
| 79e0f6de87 | |||
| a368d24156 | |||
| 6dbde36852 | |||
| 0f0ca132d3 | |||
| 8ff04936da | |||
| 1ea823fe71 | |||
| 8a73144305 | |||
| e0131da087 | |||
| e39876a2ac | |||
| c3ced25fe1 | |||
| 6ec453a63d | |||
| c1cb2a0970 | |||
| e8ca34c34b | |||
| df63b22e7a | |||
| 5ed15ce017 | |||
| 685765e2b2 | |||
| 9f2ecf02c4 | |||
| 93e45aec19 | |||
| 513e9867a6 | |||
| 2accaa48a6 | |||
| c91f1d7481 | |||
| a08f666306 | |||
| 33d5e5a6e5 | |||
| 7eff717256 | |||
| 36c399c009 | |||
| 33a5d8b3a3 | |||
| 394e79d770 | |||
| d85b6feff9 | |||
| 99e35eba95 | |||
| 1ae2146ff0 | |||
| d2046fdc2e | |||
| b3ae7ec7b8 | |||
| a8654c4036 | |||
| 8e6a7518d2 | |||
|
|
c65a89ed6f | ||
|
|
23f0fc9bba | ||
|
|
4f27b86e76 | ||
| 1cfba3827c | |||
| f3e2df3c02 | |||
| 69aaaeac09 | |||
| 9c616fc467 | |||
| b014088d22 | |||
| c2cfb192d5 | |||
| b273732450 | |||
| 5de70f5dbb | |||
| ed0ae9f3ce | |||
| ff7f1b09dd | |||
| 6a45190e0b | |||
| 01062249e3 | |||
| 8928732f0a | |||
| ac9c10ce49 | |||
| 4ab00a7702 | |||
| 978ae089d1 | |||
| 4a01758e8b | |||
| 20ef0cef86 | |||
| 5692d87815 | |||
| fbe368ee20 | |||
| 1a842b93e0 | |||
| da041bd292 | |||
| 61b720fff4 | |||
| b74d9129fe | |||
| 5ec2adc833 | |||
| 74a20ea351 | |||
| c97cbbf7e1 | |||
| aeb6e9ab3f | |||
| d5eeb77f4f | |||
| 5fb58a6f7b | |||
| d9b6141d54 | |||
| 3a46cac625 | |||
| eec871fc05 | |||
| 1ed382f126 | |||
| 20ef65112d | |||
| d4d4b5bee5 | |||
| 268c9109d0 | |||
| 79b8268ff8 | |||
| 0b1af5bc78 | |||
| 68936a6ab3 | |||
| af1e5f6d18 | |||
| c00fdf19b6 | |||
| d5468ccc5b | |||
| 4173c3b862 | |||
| 3e3a0aef34 | |||
| b114ee6e0a | |||
| e8673b7115 | |||
| bec4b9a392 | |||
| ff46066fab | |||
| 2772a2a9f2 | |||
| 1e77e93d79 | |||
| 120d4a0e2f | |||
| 38851f4844 | |||
| bf359d82cd | |||
| ed72ce3cd1 | |||
| a30f5e3ac2 | |||
| 4ee9eb2eef | |||
| 58a135fe74 | |||
| d25ce01bb9 | |||
| 1b1bada016 | |||
| ef88e599dd | |||
| a7f48305be | |||
| d058f15488 | |||
| a04b2b5e36 | |||
| 6ec26bfeaa | |||
| 3b6280b7c5 | |||
| 95cdd967cc | |||
| 7ff97bd871 | |||
| c16dc20540 | |||
| 8dba853cf6 | |||
| 522685d8c1 | |||
| 69ec3e8d62 | |||
| 73191de5b8 | |||
| c4cccfd3c2 | |||
| 2b7c5b90e9 | |||
| c91a71a7da | |||
| 22e6a3f48a | |||
| 900ca4dd2c | |||
| 8f1eb56a47 | |||
| 94d52e4237 | |||
| da2dc38a51 | |||
| 79c6b5859b | |||
| 77d35fc4cc | |||
| fbe332a8a6 | |||
| fe79fe828a | |||
| 96ba7c098a |
23927
.gitignore
vendored
@ -1,15 +0,0 @@
|
||||
{
|
||||
"product": "BodyBalanceEvaluation",
|
||||
"version": "1.0.0",
|
||||
"machine_id": "W10-D13710C7BD317C29",
|
||||
"platform": "Windows",
|
||||
"request_time": "2025-11-04T05:35:19.472181+00:00",
|
||||
"hardware_info": {
|
||||
"system": "Windows",
|
||||
"machine": "AMD64",
|
||||
"processor": "Intel64 Family 6 Model 165 Stepping 2, GenuineIntel",
|
||||
"node": "MSI"
|
||||
},
|
||||
"company_name": "北京天宏博科技有限公司",
|
||||
"contact_info": "thb@163.com"
|
||||
}
|
||||
118
backend/app.spec
@ -1,118 +0,0 @@
|
||||
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=['D:/Trae_space/pyKinectAzure'],
|
||||
binaries=[
|
||||
# FemtoBolt相关库文件
|
||||
('dll/femtobolt/k4a.dll', 'dll/femtobolt'), # K4A动态库
|
||||
('dll/femtobolt/k4arecord.dll', 'dll/femtobolt'), # K4A录制库
|
||||
('dll/femtobolt/depthengine_2_0.dll', 'dll/femtobolt'), # 深度引擎
|
||||
('dll/femtobolt/OrbbecSDK.dll', 'dll/femtobolt'), # Orbbec SDK
|
||||
('dll/femtobolt/k4a.lib', 'dll/femtobolt'), # K4A静态库
|
||||
('dll/femtobolt/k4arecord.lib', 'dll/femtobolt'), # K4A录制静态库
|
||||
('dll/femtobolt/k4arecorder.exe', 'dll/femtobolt'), # K4A录制工具
|
||||
('dll/femtobolt/k4aviewer.exe', 'dll/femtobolt'), # K4A查看器
|
||||
# SMiTSense相关库文件
|
||||
('dll/smitsense/SMiTSenseUsb-F3.0.dll', 'dll/smitsense'), # SMiTSense传感器库
|
||||
('dll/smitsense/Wrapper.dll', 'dll/smitsense'), # SMiTSense传感器库包装类
|
||||
],
|
||||
hiddenimports=[
|
||||
'flask',
|
||||
'flask_socketio',
|
||||
'flask_cors',
|
||||
'cv2',
|
||||
'numpy',
|
||||
'pandas',
|
||||
'scipy',
|
||||
'matplotlib',
|
||||
'seaborn',
|
||||
'sklearn',
|
||||
'PIL',
|
||||
'reportlab',
|
||||
'sqlite3',
|
||||
'configparser',
|
||||
'logging',
|
||||
'threading',
|
||||
'queue',
|
||||
'base64',
|
||||
'psutil',
|
||||
'pykinect_azure',
|
||||
'pykinect_azure.k4a',
|
||||
'pykinect_azure.k4abt',
|
||||
'pykinect_azure.k4arecord',
|
||||
'pykinect_azure.pykinect',
|
||||
'pykinect_azure.utils',
|
||||
'pykinect_azure._k4a',
|
||||
'pykinect_azure._k4abt',
|
||||
'pyserial',
|
||||
'requests',
|
||||
'yaml',
|
||||
'click',
|
||||
'colorama',
|
||||
'tqdm',
|
||||
'database',
|
||||
'device_manager',
|
||||
'utils',
|
||||
'eventlet',
|
||||
'socketio',
|
||||
'engineio',
|
||||
'engineio.async_drivers.threading',
|
||||
'engineio.async_drivers.eventlet',
|
||||
'engineio.async_eventlet',
|
||||
'socketio.async_eventlet',
|
||||
'greenlet',
|
||||
'gevent',
|
||||
'gevent.socket',
|
||||
'gevent.select',
|
||||
'dns',
|
||||
'dns.resolver',
|
||||
'dns.reversename',
|
||||
'dns.e164',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='BodyBalanceBackend',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=None
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='BodyBalanceBackend'
|
||||
)
|
||||
@ -233,7 +233,7 @@ def copy_data_files():
|
||||
data_dir = os.path.join('dist', 'BodyBalanceBackend', 'data')
|
||||
|
||||
# 数据库文件列表 - 这些文件会在程序首次运行时自动创建
|
||||
db_files = ['body_balance.db']
|
||||
db_files = ['body_balance.db','common_items.json']
|
||||
|
||||
# 检查是否存在现有数据库文件,如果存在则复制
|
||||
copied_count = 0
|
||||
@ -276,6 +276,17 @@ def copy_config_files():
|
||||
else:
|
||||
print(f"⚠️ 配置文件不存在: {config_file}")
|
||||
|
||||
try:
|
||||
ffmpeg_src = 'ffmpeg'
|
||||
ffmpeg_dst = os.path.join(dist_dir, 'ffmpeg')
|
||||
if os.path.exists(ffmpeg_src):
|
||||
shutil.copytree(ffmpeg_src, ffmpeg_dst, dirs_exist_ok=True)
|
||||
print(f"✓ 已复制 ffmpeg 目录到 {ffmpeg_dst}")
|
||||
else:
|
||||
print("⚠️ 未找到 ffmpeg 源目录:ffmpeg")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 复制 ffmpeg 目录失败: {e}")
|
||||
|
||||
def install_build_dependencies():
|
||||
"""安装打包依赖"""
|
||||
print("检查并安装打包依赖...")
|
||||
@ -390,4 +401,4 @@ def main():
|
||||
input("按回车键退出...")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[APP]
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
version = 1.5.0
|
||||
debug = True
|
||||
log_level = INFO
|
||||
|
||||
@ -11,14 +11,15 @@ cors_origins = *
|
||||
|
||||
[DATABASE]
|
||||
path = D:/BodyCheck/data/body_balance.db
|
||||
common_items_path = D:/BodyCheck/data/common_items.json
|
||||
backup_interval = 24
|
||||
max_backups = 7
|
||||
|
||||
[FILEPATH]
|
||||
path = D:/BodyCheck/file/
|
||||
|
||||
[CAMERA]
|
||||
enabled = True
|
||||
[CAMERA1]
|
||||
enable = True
|
||||
device_index = 0
|
||||
width = 1280
|
||||
height = 720
|
||||
@ -27,8 +28,18 @@ buffer_size = 1
|
||||
fourcc = MJPG
|
||||
backend = directshow
|
||||
|
||||
[CAMERA2]
|
||||
enable = True
|
||||
device_index = 2
|
||||
width = 1280
|
||||
height = 720
|
||||
fps = 30
|
||||
buffer_size = 1
|
||||
fourcc = MJPG
|
||||
backend = directshow
|
||||
|
||||
[FEMTOBOLT]
|
||||
enabled = True
|
||||
enable = True
|
||||
algorithm_type = plt
|
||||
color_resolution = 1080P
|
||||
depth_mode = NFOV_2X2BINNED
|
||||
@ -39,17 +50,22 @@ fps = 15
|
||||
synchronized_images_only = False
|
||||
|
||||
[DEVICES]
|
||||
imu_enabled = True
|
||||
imu_device_type = ble
|
||||
imu_port = COM9
|
||||
imu_mac_address = ef:3c:1a:0a:fe:02
|
||||
imu_baudrate = 9600
|
||||
pressure_enabled = True
|
||||
pressure_device_type = real
|
||||
pressure_use_mock = False
|
||||
pressure_port = COM5
|
||||
imu_enable = True
|
||||
imu_use_mock = False
|
||||
imu_ble_name = WT901BLE67
|
||||
imu_mac_address = FA:E8:88:06:FE:F3
|
||||
pressure_enable = True
|
||||
pressure_use_mock = True
|
||||
pressure_port = COM3
|
||||
pressure_baudrate = 115200
|
||||
|
||||
[REMOTE]
|
||||
enable = False
|
||||
port = COM6
|
||||
baudrate = 115200
|
||||
timeout = 0.1
|
||||
strict_crc = False
|
||||
|
||||
[SYSTEM]
|
||||
log_level = INFO
|
||||
max_cache_size = 10
|
||||
@ -78,3 +94,13 @@ public_key = D:/BodyCheck/license/license_public_key.pem
|
||||
grace_days = 7
|
||||
dev_mode = False
|
||||
|
||||
[SCREEN_RECORDING]
|
||||
strategy = ffmpeg
|
||||
ffmpeg_path = D:/BodyCheck/ffmpeg/bin/ffmpeg.exe
|
||||
ffmpeg_codec = libx264
|
||||
ffmpeg_preset = ultrafast
|
||||
ffmpeg_threads = 2
|
||||
ffmpeg_bframes = 0
|
||||
ffmpeg_gop = 50
|
||||
ffmpeg_draw_mouse = 0
|
||||
|
||||
|
||||
1009
backend/database.py
@ -21,5 +21,5 @@ __all__ = [
|
||||
'DeviceCoordinator'
|
||||
]
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '1.5.0'
|
||||
__author__ = 'Body Balance Detection System'
|
||||
@ -33,6 +33,7 @@ class BaseDevice(ABC):
|
||||
self.config = config
|
||||
self.is_connected = False
|
||||
self.is_streaming = False
|
||||
self._initializing = False
|
||||
self.socket_namespace = f"/{device_name}"
|
||||
self.logger = logging.getLogger(f"device.{device_name}")
|
||||
self._lock = threading.RLock() # 可重入锁
|
||||
@ -246,10 +247,7 @@ class BaseDevice(ABC):
|
||||
Dict[str, Any]: 设备信息
|
||||
"""
|
||||
with self._lock:
|
||||
return self._device_info.copy()
|
||||
|
||||
|
||||
|
||||
return self._device_info.copy()
|
||||
|
||||
|
||||
def _set_error(self, error_msg: str):
|
||||
@ -311,7 +309,7 @@ class BaseDevice(ABC):
|
||||
try:
|
||||
# 检查硬件连接状态
|
||||
hardware_connected = self.check_hardware_connection()
|
||||
self.logger.info(f"检测到设备 {self.device_name} 硬件连接状态: {hardware_connected} is_connected:{self.is_connected}")
|
||||
# self.logger.info(f"检测到设备 {self.device_name} 硬件连接状态: {hardware_connected} is_connected:{self.is_connected}")
|
||||
# 如果硬件断开但软件状态仍为连接,则更新状态
|
||||
if not hardware_connected and self.is_connected:
|
||||
self.logger.warning(f"检测到设备 {self.device_name} 硬件连接断开")
|
||||
@ -364,4 +362,4 @@ class BaseDevice(ABC):
|
||||
self._stop_connection_monitor()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
|
||||
return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
|
||||
|
||||
@ -14,6 +14,7 @@ from typing import Optional, Dict, Any
|
||||
import logging
|
||||
import queue
|
||||
import gc
|
||||
import os
|
||||
|
||||
try:
|
||||
from .base_device import BaseDevice
|
||||
@ -26,34 +27,49 @@ except ImportError:
|
||||
class CameraManager(BaseDevice):
|
||||
"""普通相机管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None,
|
||||
device_name: str = "camera1",
|
||||
instance_config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
初始化相机管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
device_name: 设备名称(仅支持 'camera1' | 'camera2')
|
||||
instance_config: 覆盖默认配置的实例级配置(如 device_index、分辨率、fps 等)
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
config = self.config_manager.get_device_config('camera')
|
||||
# 校验设备名,仅允许 camera1/camera2
|
||||
if device_name not in ('camera1', 'camera2'):
|
||||
raise ValueError(f"不支持的设备名: {device_name},仅支持 'camera1'/'camera2'")
|
||||
# 根据设备名选择配置源:'camera1' 使用 [CAMERA1];'camera2' 使用 [CAMERA2]
|
||||
base_key = 'camera1' if device_name == 'camera1' else 'camera2'
|
||||
base_config = self.config_manager.get_device_config(base_key)
|
||||
# 合并实例覆盖配置
|
||||
if instance_config:
|
||||
try:
|
||||
base_config = {**base_config, **instance_config}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
super().__init__("camera", config)
|
||||
super().__init__(device_name, base_config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 相机相关属性
|
||||
self.cap = None
|
||||
self.device_index = config.get('device_index', 0)
|
||||
self.width = config.get('width', 1280)
|
||||
self.height = config.get('height', 720)
|
||||
self.fps = config.get('fps', 30)
|
||||
self.buffer_size = config.get('buffer_size', 1)
|
||||
self.fourcc = config.get('fourcc', 'MJPG')
|
||||
self.device_index = base_config.get('device_index', 0)
|
||||
self.width = base_config.get('width', 1280)
|
||||
self.height = base_config.get('height', 720)
|
||||
self.fps = base_config.get('fps', 30)
|
||||
self.buffer_size = base_config.get('buffer_size', 1)
|
||||
self.fourcc = base_config.get('fourcc', 'MJPG')
|
||||
|
||||
# OpenCV后端配置 (DirectShow性能最佳)
|
||||
backend_name = config.get('backend', 'directshow').lower()
|
||||
backend_name = base_config.get('backend', 'directshow').lower()
|
||||
self.backend_map = {
|
||||
'directshow': cv2.CAP_DSHOW,
|
||||
'dshow': cv2.CAP_DSHOW,
|
||||
@ -64,12 +80,12 @@ class CameraManager(BaseDevice):
|
||||
self.backend_name = backend_name
|
||||
|
||||
# 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输)
|
||||
self._tx_max_width = int(config.get('tx_max_width', 1920))
|
||||
self._tx_max_width = int(base_config.get('tx_max_width', 1920))
|
||||
|
||||
# 流控制
|
||||
self.streaming_thread = None
|
||||
# 减小缓存长度,保留最近2帧即可,避免累计占用
|
||||
self.frame_cache = queue.Queue(maxsize=int(config.get('frame_cache_len', 2)))
|
||||
self.frame_cache = queue.Queue(maxsize=int(base_config.get('frame_cache_len', 2)))
|
||||
self.last_frame = None
|
||||
self.frame_count = 0
|
||||
self.dropped_frames = 0
|
||||
@ -80,13 +96,14 @@ class CameraManager(BaseDevice):
|
||||
self.actual_fps = 0
|
||||
|
||||
# 重连与断连检测机制(-1 表示无限重连)
|
||||
self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', -1))
|
||||
self.reconnect_delay = float(config.get('reconnect_delay', 2.0))
|
||||
self.read_fail_threshold = int(config.get('read_fail_threshold', 30))
|
||||
self.max_reconnect_attempts = int(base_config.get('max_reconnect_attempts', -1))
|
||||
self.reconnect_delay = float(base_config.get('reconnect_delay', 2.0))
|
||||
self.read_fail_threshold = int(base_config.get('read_fail_threshold', 30))
|
||||
self._last_connected_state = None
|
||||
|
||||
# 设备标识和性能统计
|
||||
self.device_id = f"camera_{self.device_index}"
|
||||
# 使用设备名作为ID,便于前端区分
|
||||
self.device_id = device_name
|
||||
self.performance_stats = {
|
||||
'frames_processed': 0,
|
||||
'actual_fps': 0,
|
||||
@ -94,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 = {}
|
||||
@ -109,6 +138,130 @@ class CameraManager(BaseDevice):
|
||||
pass
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -138,6 +291,19 @@ class CameraManager(BaseDevice):
|
||||
|
||||
return True
|
||||
|
||||
def set_connected(self, is_connected: bool):
|
||||
"""
|
||||
设置连接状态并触发回调
|
||||
|
||||
Args:
|
||||
is_connected: 连接状态
|
||||
"""
|
||||
# 更新心跳时间,防止刚连接就被判定超时
|
||||
if is_connected:
|
||||
self.update_heartbeat()
|
||||
|
||||
super().set_connected(is_connected)
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化相机设备
|
||||
@ -206,6 +372,8 @@ class CameraManager(BaseDevice):
|
||||
# 使用set_connected方法来正确启动连接监控线程
|
||||
self.set_connected(True)
|
||||
self._last_connected_state = True
|
||||
# 更新心跳时间,防止连接监控线程刚启动就判定为超时
|
||||
self.update_heartbeat()
|
||||
self._device_info.update({
|
||||
'device_index': self.device_index,
|
||||
'resolution': f"{self.width}x{self.height}",
|
||||
@ -373,6 +541,9 @@ class CameraManager(BaseDevice):
|
||||
|
||||
total_config_time = (time.time() - config_start) * 1000
|
||||
|
||||
# 若未进行性能优化,确保变量存在
|
||||
optimization_time = locals().get('optimization_time', 0.0)
|
||||
|
||||
self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
|
||||
self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
|
||||
self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
|
||||
@ -474,6 +645,8 @@ class CameraManager(BaseDevice):
|
||||
|
||||
try:
|
||||
self.is_streaming = True
|
||||
# 更新心跳时间,确保流开始时不会被误判超时
|
||||
self.update_heartbeat()
|
||||
self.streaming_thread = threading.Thread(
|
||||
target=self._streaming_worker,
|
||||
name=f"Camera-{self.device_index}-Stream",
|
||||
@ -613,23 +786,6 @@ class CameraManager(BaseDevice):
|
||||
# 更新心跳时间,防止连接监控线程判定为超时
|
||||
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)
|
||||
|
||||
@ -704,6 +860,7 @@ class CameraManager(BaseDevice):
|
||||
|
||||
# 转换为bytes再做base64,减少中间numpy对象的长生命周期
|
||||
frame_bytes = buffer.tobytes()
|
||||
self._maybe_enqueue_recording(time.time(), frame_bytes)
|
||||
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
|
||||
|
||||
# 发送数据
|
||||
@ -776,17 +933,19 @@ class CameraManager(BaseDevice):
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
return {
|
||||
'device_type': 'camera',
|
||||
'is_connected': self.is_connected,
|
||||
'is_streaming': self.is_streaming,
|
||||
'device_index': self.device_index,
|
||||
'resolution': f"{self.width}x{self.height}",
|
||||
'target_fps': self.fps,
|
||||
'actual_fps': self.actual_fps,
|
||||
'frame_count': self.frame_count,
|
||||
'dropped_frames': self.dropped_frames,
|
||||
'has_frame': self.last_frame is not None
|
||||
})
|
||||
return status
|
||||
'has_frame': self.last_frame is not None,
|
||||
'device_info': self.get_device_info()
|
||||
}
|
||||
|
||||
def capture_image(self, save_path: Optional[str] = None) -> Optional[np.ndarray]:
|
||||
"""
|
||||
@ -852,8 +1011,9 @@ class CameraManager(BaseDevice):
|
||||
|
||||
|
||||
|
||||
# 获取最新配置
|
||||
config = self.config_manager.get_device_config('camera')
|
||||
# 获取最新配置(按设备名映射,已限制为 camera1/camera2)
|
||||
key = self.device_name
|
||||
config = self.config_manager.get_device_config(key)
|
||||
|
||||
# 更新配置属性
|
||||
self.device_index = config.get('device_index', 0)
|
||||
@ -880,8 +1040,8 @@ class CameraManager(BaseDevice):
|
||||
# 创建新队列
|
||||
self.frame_cache = queue.Queue(maxsize=frame_cache_len)
|
||||
|
||||
# 更新设备信息
|
||||
self.device_id = f"camera_{self.device_index}"
|
||||
# 更新设备信息(设备ID直接使用设备名)
|
||||
self.device_id = self.device_name
|
||||
|
||||
self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}")
|
||||
return True
|
||||
@ -899,7 +1059,7 @@ class CameraManager(BaseDevice):
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected:
|
||||
self.logger.info("相机未连接,检查连接状态")
|
||||
# self.logger.info("相机未连接,检查连接状态")
|
||||
return False
|
||||
|
||||
# 尝试读取一帧
|
||||
@ -908,7 +1068,7 @@ class CameraManager(BaseDevice):
|
||||
self.logger.error("相机连接已断开,读取失败")
|
||||
return False
|
||||
|
||||
self.logger.info("相机硬件连接正常")
|
||||
# self.logger.info("相机硬件连接正常")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -946,6 +1106,21 @@ class CameraManager(BaseDevice):
|
||||
self.frame_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
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
|
||||
|
||||
@ -957,4 +1132,4 @@ class CameraManager(BaseDevice):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -57,6 +57,10 @@ class DeviceCoordinator:
|
||||
self.is_initialized = False
|
||||
self.is_running = False
|
||||
self.coordinator_lock = threading.RLock()
|
||||
self._init_summary = {
|
||||
'initialized_at': None,
|
||||
'device_results': {},
|
||||
}
|
||||
|
||||
# 监控线程
|
||||
self.monitor_thread = None
|
||||
@ -75,9 +79,11 @@ class DeviceCoordinator:
|
||||
'device_errors': defaultdict(int),
|
||||
'reconnect_attempts': defaultdict(int)
|
||||
}
|
||||
self._last_restart_ts = defaultdict(float)
|
||||
self._restart_in_progress = defaultdict(bool)
|
||||
|
||||
# 线程池
|
||||
self.executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="DeviceCoord")
|
||||
self.executor = ThreadPoolExecutor(max_workers=8, thread_name_prefix="DeviceCoord")
|
||||
|
||||
self.logger.info("设备协调器初始化完成")
|
||||
|
||||
@ -113,19 +119,23 @@ class DeviceCoordinator:
|
||||
# 注册Socket.IO命名空间
|
||||
self._register_namespaces()
|
||||
|
||||
# 初始化设备
|
||||
if not self._initialize_devices():
|
||||
self.logger.warning("设备初始化失败,将以降级模式继续运行")
|
||||
|
||||
# 启动监控线程
|
||||
self._start_monitor()
|
||||
|
||||
# 初始化设备(失败则降级继续)
|
||||
init_ok = bool(self._initialize_devices())
|
||||
self._init_summary['initialized_at'] = time.time()
|
||||
if not init_ok:
|
||||
self.logger.warning("设备初始化失败(没有任何设备初始化成功)")
|
||||
self.is_initialized = False
|
||||
return False
|
||||
|
||||
self.is_initialized = True
|
||||
self.stats['start_time'] = time.time()
|
||||
|
||||
|
||||
# 启动监控线程
|
||||
self._start_monitor()
|
||||
|
||||
self.logger.info("设备协调器初始化成功")
|
||||
self._emit_event('coordinator_initialized', {'devices': list(self.devices.keys())})
|
||||
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -159,38 +169,48 @@ class DeviceCoordinator:
|
||||
futures = []
|
||||
|
||||
# FemtoBolt深度相机
|
||||
if self.device_configs.get('femtobolt', {}).get('enabled', False):
|
||||
if self.device_configs.get('femtobolt', {}).get('enable', False):
|
||||
future = self.executor.submit(self._init_femtobolt)
|
||||
futures.append(('femtobolt', future))
|
||||
|
||||
# 普通相机
|
||||
if self.device_configs.get('camera', {}).get('enabled', False):
|
||||
future = self.executor.submit(self._init_camera)
|
||||
futures.append(('camera', future))
|
||||
# 普通相机:初始化两个实例(camera1 与 camera2)
|
||||
# camera1 使用 [CAMERA1] 配置;camera2 使用 [CAMERA2](若不存在则回退为 device_index+1)
|
||||
if self.device_configs.get('camera1', {}).get('enable', False):
|
||||
futures.append(('camera1', self.executor.submit(self._init_camera_by_name, 'camera1', 'CAMERA1')))
|
||||
if self.device_configs.get('camera2', {}).get('enable', False):
|
||||
futures.append(('camera2', self.executor.submit(self._init_camera_by_name, 'camera2', 'CAMERA2')))
|
||||
|
||||
# IMU传感器
|
||||
if self.device_configs.get('imu', {}).get('enabled', False):
|
||||
if self.device_configs.get('imu', {}).get('enable', False):
|
||||
future = self.executor.submit(self._init_imu)
|
||||
futures.append(('imu', future))
|
||||
|
||||
# 压力传感器
|
||||
if self.device_configs.get('pressure', {}).get('enabled', False):
|
||||
if self.device_configs.get('pressure', {}).get('enable', False):
|
||||
future = self.executor.submit(self._init_pressure)
|
||||
futures.append(('pressure', future))
|
||||
|
||||
# 遥控器
|
||||
if self.device_configs.get('remote', {}).get('enable', False):
|
||||
future = self.executor.submit(self._init_remote)
|
||||
futures.append(('remote', future))
|
||||
|
||||
|
||||
|
||||
# 等待所有设备初始化完成
|
||||
success_count = 0
|
||||
for device_name, future in futures:
|
||||
try:
|
||||
result = future.result(timeout=30) # 30秒超时
|
||||
timeout_s = 45 if device_name == 'imu' else 30
|
||||
result = future.result(timeout=timeout_s)
|
||||
self._init_summary['device_results'][device_name] = bool(result)
|
||||
if result:
|
||||
success_count += 1
|
||||
self.logger.info(f"{device_name}设备初始化成功")
|
||||
else:
|
||||
self.logger.error(f"{device_name}设备初始化失败")
|
||||
except Exception as e:
|
||||
self._init_summary['device_results'][device_name] = False
|
||||
self.logger.error(f"{device_name}设备初始化异常: {e}")
|
||||
|
||||
# 至少需要一个设备初始化成功
|
||||
@ -204,22 +224,181 @@ class DeviceCoordinator:
|
||||
except Exception as e:
|
||||
self.logger.error(f"设备初始化失败: {e}")
|
||||
return False
|
||||
|
||||
def get_enabled_devices(self) -> List[str]:
|
||||
enabled = []
|
||||
try:
|
||||
for name, cfg in (self.device_configs or {}).items():
|
||||
if isinstance(cfg, dict) and cfg.get('enable', False):
|
||||
enabled.append(name)
|
||||
except Exception:
|
||||
pass
|
||||
return enabled
|
||||
|
||||
def get_required_devices_for_detection(self) -> List[str]:
|
||||
enabled = self.get_enabled_devices()
|
||||
required = [d for d in enabled if d not in ('remote',)]
|
||||
return required
|
||||
|
||||
def get_device_readiness(self, device_name: str) -> Dict[str, Any]:
|
||||
enabled = bool(self.device_configs.get(device_name, {}).get('enable', False))
|
||||
device = self.devices.get(device_name)
|
||||
readiness = {
|
||||
'device_name': device_name,
|
||||
'enabled': enabled,
|
||||
'exists': device is not None,
|
||||
'initializing': False,
|
||||
'is_connected': False,
|
||||
'is_streaming': False,
|
||||
'ready': False,
|
||||
}
|
||||
|
||||
if not device:
|
||||
return readiness
|
||||
status = None
|
||||
try:
|
||||
if hasattr(device, 'get_status'):
|
||||
status = device.get_status()
|
||||
except Exception:
|
||||
status = None
|
||||
|
||||
try:
|
||||
readiness['initializing'] = bool(getattr(device, '_initializing', False))
|
||||
except Exception:
|
||||
readiness['initializing'] = False
|
||||
try:
|
||||
if isinstance(status, dict) and 'is_connected' in status:
|
||||
readiness['is_connected'] = bool(status.get('is_connected'))
|
||||
else:
|
||||
readiness['is_connected'] = bool(getattr(device, 'is_connected', False))
|
||||
except Exception:
|
||||
readiness['is_connected'] = False
|
||||
try:
|
||||
if isinstance(status, dict) and 'is_streaming' in status:
|
||||
readiness['is_streaming'] = bool(status.get('is_streaming'))
|
||||
else:
|
||||
readiness['is_streaming'] = bool(getattr(device, 'is_streaming', False))
|
||||
except Exception:
|
||||
readiness['is_streaming'] = False
|
||||
|
||||
ok = readiness['is_connected'] and (not readiness['initializing'])
|
||||
readiness['ready'] = bool(ok)
|
||||
return readiness
|
||||
|
||||
def get_readiness_snapshot(self, required_devices: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
required = required_devices if required_devices is not None else self.get_required_devices_for_detection()
|
||||
devices = {}
|
||||
for name in required:
|
||||
devices[name] = self.get_device_readiness(name)
|
||||
|
||||
all_ready = bool(self.is_initialized) and all(devices[name].get('ready', False) for name in devices)
|
||||
return {
|
||||
'coordinator': {
|
||||
'is_initialized': bool(self.is_initialized),
|
||||
'is_running': bool(self.is_running),
|
||||
'enabled_devices': self.get_enabled_devices(),
|
||||
'required_devices': required,
|
||||
'init_summary': self._init_summary,
|
||||
},
|
||||
'devices': devices,
|
||||
'all_ready': all_ready,
|
||||
}
|
||||
|
||||
def wait_until_ready_for_detection(self, timeout_s: float = 10.0, poll_interval_s: float = 0.2) -> Dict[str, Any]:
|
||||
deadline = time.time() + max(0.0, float(timeout_s))
|
||||
last_snapshot = self.get_readiness_snapshot()
|
||||
while time.time() < deadline:
|
||||
last_snapshot = self.get_readiness_snapshot()
|
||||
if last_snapshot.get('all_ready', False):
|
||||
return last_snapshot
|
||||
time.sleep(max(0.05, float(poll_interval_s)))
|
||||
last_snapshot['timeout'] = True
|
||||
last_snapshot['timeout_s'] = float(timeout_s)
|
||||
return last_snapshot
|
||||
|
||||
def _init_camera(self) -> bool:
|
||||
def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool:
|
||||
"""
|
||||
初始化普通相机
|
||||
|
||||
按名称初始化相机,支持 camera1/camera2 并覆盖配置段
|
||||
|
||||
Args:
|
||||
device_name: 设备名称(如 'camera1'、'camera2')
|
||||
section: 配置段名称('CAMERA1' 或 'CAMERA2')
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
camera = CameraManager(self.socketio, self.config_manager)
|
||||
self.devices['camera'] = camera
|
||||
if camera.initialize():
|
||||
# 构造实例覆盖配置:优先读取目标配置段,否则回退到 [CAMERA1]
|
||||
cfg = {}
|
||||
parser = getattr(self.config_manager, 'config', None)
|
||||
base_cam = self.config_manager.get_device_config('camera1')
|
||||
if parser and parser.has_section(section):
|
||||
# 读取所有相关键
|
||||
def get_opt(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.get(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_int(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getint(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_bool(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getboolean(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
enable = get_bool(section, 'enable', True)
|
||||
if not enable:
|
||||
self.logger.info(f"{device_name} 未启用,跳过初始化")
|
||||
return False
|
||||
# 填充覆盖项
|
||||
idx2 = get_int(section, 'device_index', None)
|
||||
if idx2 is not None:
|
||||
cfg['device_index'] = idx2
|
||||
w = get_int(section, 'width', None)
|
||||
h = get_int(section, 'height', None)
|
||||
f = get_int(section, 'fps', None)
|
||||
buf = get_int(section, 'buffer_size', None)
|
||||
fourcc = get_opt(section, 'fourcc', None)
|
||||
backend = get_opt(section, 'backend', None)
|
||||
if w is not None: cfg['width'] = w
|
||||
if h is not None: cfg['height'] = h
|
||||
if f is not None: cfg['fps'] = f
|
||||
if buf is not None: cfg['buffer_size'] = buf
|
||||
if fourcc is not None: cfg['fourcc'] = fourcc
|
||||
if backend is not None: cfg['backend'] = backend
|
||||
else:
|
||||
# section 不存在时:camera2 默认使用 device_index+1
|
||||
if device_name.lower() == 'camera2':
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
|
||||
else:
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0))
|
||||
|
||||
camera = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
|
||||
self.devices[device_name] = camera
|
||||
if camera.initialize():
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化相机失败: {e}")
|
||||
self.logger.error(f"初始化{device_name}失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_remote(self) -> bool:
|
||||
"""
|
||||
初始化串口遥控器
|
||||
"""
|
||||
try:
|
||||
from .remote_control_manager import RemoteControlManager
|
||||
remote = RemoteControlManager(self.socketio, self.config_manager)
|
||||
self.devices['remote'] = remote
|
||||
if not remote.initialize():
|
||||
return False
|
||||
if hasattr(remote, 'start_streaming'):
|
||||
return bool(remote.start_streaming())
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化遥控器失败: {e}")
|
||||
return False
|
||||
|
||||
def _init_imu(self) -> bool:
|
||||
@ -441,8 +620,8 @@ class DeviceCoordinator:
|
||||
success_count = 0
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
# 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流
|
||||
if device_name in ['femtobolt', 'camera',"imu"]:
|
||||
# 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控
|
||||
if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
|
||||
continue
|
||||
|
||||
if hasattr(device, '_start_connection_monitor'):
|
||||
@ -475,19 +654,9 @@ class DeviceCoordinator:
|
||||
success_count = 0
|
||||
for device_name, device in self.devices.items():
|
||||
try:
|
||||
# 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流
|
||||
if device_name in ['femtobolt', 'camera',"imu"]:
|
||||
self.logger.info(f"停止{device_name}设备推流")
|
||||
|
||||
# # 调用设备的cleanup方法清理资源,停止推流
|
||||
# if hasattr(device, 'cleanup'):
|
||||
# if device.cleanup():
|
||||
# success_count += 1
|
||||
# self.logger.info(f"{device_name}设备推流已停止")
|
||||
# else:
|
||||
# self.logger.warning(f"{device_name}设备推流停止失败")
|
||||
# else:
|
||||
# self.logger.warning(f"{device_name}设备不支持推流停止")
|
||||
# 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控停止
|
||||
if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
|
||||
self.logger.info(f"停止{device_name}设备推流")
|
||||
continue
|
||||
|
||||
if hasattr(device, '_stop_connection_monitor'):
|
||||
@ -524,12 +693,22 @@ class DeviceCoordinator:
|
||||
was_streaming = False
|
||||
|
||||
try:
|
||||
if self._restart_in_progress[device_name]:
|
||||
self.logger.warning(f"{device_name} 设备正在重启中,跳过重复重启请求")
|
||||
return False
|
||||
self._restart_in_progress[device_name] = True
|
||||
self._last_restart_ts[device_name] = time.time()
|
||||
self.logger.info(f"开始彻底重启设备: {device_name}")
|
||||
|
||||
# 第一步:检查并停止数据流
|
||||
stop_start = time.time()
|
||||
if hasattr(device, 'is_streaming'):
|
||||
was_streaming = device.is_streaming
|
||||
try:
|
||||
if hasattr(device, 'get_status'):
|
||||
was_streaming = bool((device.get_status() or {}).get('is_streaming', False))
|
||||
elif hasattr(device, 'is_streaming'):
|
||||
was_streaming = bool(device.is_streaming)
|
||||
except Exception:
|
||||
was_streaming = False
|
||||
|
||||
if hasattr(device, 'stop_streaming') and was_streaming:
|
||||
self.logger.info(f"正在停止 {device_name} 设备推流...")
|
||||
@ -546,6 +725,11 @@ class DeviceCoordinator:
|
||||
# 第二步:断开连接并彻底清理资源
|
||||
cleanup_start = time.time()
|
||||
self.logger.info(f"正在彻底清理 {device_name} 设备...")
|
||||
try:
|
||||
if hasattr(device, '_init_abort'):
|
||||
device._init_abort.set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 断开连接但暂时不广播状态变化,避免重启过程中的状态冲突
|
||||
if hasattr(device, 'disconnect'):
|
||||
@ -583,7 +767,7 @@ class DeviceCoordinator:
|
||||
self.logger.info(f"{device_name} 设备实例已销毁")
|
||||
|
||||
# 短暂等待,确保资源完全释放
|
||||
time.sleep(0.2)
|
||||
time.sleep(1.5 if device_name == 'imu' else 0.2)
|
||||
destroy_time = (time.time() - destroy_start) * 1000
|
||||
|
||||
# 第四步:重新创建设备实例
|
||||
@ -592,13 +776,58 @@ class DeviceCoordinator:
|
||||
|
||||
new_device = None
|
||||
try:
|
||||
# 根据设备类型重新创建实例
|
||||
if device_name == 'camera':
|
||||
# 根据设备类型重新创建实例(仅支持 camera1/camera2)
|
||||
if device_name in ('camera1', 'camera2'):
|
||||
try:
|
||||
from .camera_manager import CameraManager
|
||||
except ImportError:
|
||||
from camera_manager import CameraManager
|
||||
new_device = CameraManager(self.socketio, self.config_manager)
|
||||
# 为 camera1/camera2 构造实例配置
|
||||
section = 'CAMERA1' if device_name == 'camera1' else 'CAMERA2'
|
||||
cfg = {}
|
||||
parser = getattr(self.config_manager, 'config', None)
|
||||
base_cam = self.config_manager.get_device_config('camera1')
|
||||
if parser and parser.has_section(section):
|
||||
def get_opt(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.get(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_int(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getint(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
def get_bool(sec, key, fallback=None):
|
||||
try:
|
||||
return parser.getboolean(sec, key)
|
||||
except Exception:
|
||||
return fallback
|
||||
enable = get_bool(section, 'enable', True)
|
||||
if not enable:
|
||||
raise Exception(f"{device_name} 未启用")
|
||||
idx2 = get_int(section, 'device_index', None)
|
||||
if idx2 is not None:
|
||||
cfg['device_index'] = idx2
|
||||
w = get_int(section, 'width', None)
|
||||
h = get_int(section, 'height', None)
|
||||
f = get_int(section, 'fps', None)
|
||||
buf = get_int(section, 'buffer_size', None)
|
||||
fourcc = get_opt(section, 'fourcc', None)
|
||||
backend = get_opt(section, 'backend', None)
|
||||
if w is not None: cfg['width'] = w
|
||||
if h is not None: cfg['height'] = h
|
||||
if f is not None: cfg['fps'] = f
|
||||
if buf is not None: cfg['buffer_size'] = buf
|
||||
if fourcc is not None: cfg['fourcc'] = fourcc
|
||||
if backend is not None: cfg['backend'] = backend
|
||||
else:
|
||||
# section 不存在时:camera2 默认使用 [CAMERA1] 的 device_index + 1
|
||||
if device_name == 'camera2':
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
|
||||
else:
|
||||
cfg['device_index'] = int(base_cam.get('device_index', 0))
|
||||
new_device = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
|
||||
elif device_name == 'imu':
|
||||
try:
|
||||
from .imu_manager import IMUManager
|
||||
@ -617,6 +846,12 @@ class DeviceCoordinator:
|
||||
except ImportError:
|
||||
from femtobolt_manager import FemtoBoltManager
|
||||
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:
|
||||
raise ValueError(f"未知的设备类型: {device_name}")
|
||||
|
||||
@ -646,13 +881,22 @@ class DeviceCoordinator:
|
||||
if not new_device.initialize():
|
||||
init_time = (time.time() - init_start) * 1000
|
||||
self.logger.error(f"{device_name} 设备初始化失败 (耗时: {init_time:.1f}ms)")
|
||||
# 初始化失败,从设备字典中移除
|
||||
self.devices.pop(device_name, None)
|
||||
return False
|
||||
|
||||
init_time = (time.time() - init_start) * 1000
|
||||
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)触发了状态变化通知
|
||||
# 但为了确保状态一致性,我们再次确认状态
|
||||
@ -689,6 +933,8 @@ class DeviceCoordinator:
|
||||
error_msg = f"彻底重启设备 {device_name} 异常: {e} (耗时: {total_time:.1f}ms)"
|
||||
self.logger.error(error_msg)
|
||||
return False
|
||||
finally:
|
||||
self._restart_in_progress[device_name] = False
|
||||
|
||||
def _start_monitor(self):
|
||||
"""
|
||||
@ -714,18 +960,29 @@ class DeviceCoordinator:
|
||||
while self.is_initialized:
|
||||
try:
|
||||
# 检查设备健康状态
|
||||
for device_name, device in self.devices.items():
|
||||
for device_name, device in list(self.devices.items()):
|
||||
try:
|
||||
if self._restart_in_progress.get(device_name, False) or getattr(device, '_initializing', False):
|
||||
continue
|
||||
status = device.get_status()
|
||||
if not status.get('is_connected', False):
|
||||
self.logger.warning(f"设备 {device_name} 连接丢失")
|
||||
self.stats['device_errors'][device_name] += 1
|
||||
|
||||
# 尝试重连
|
||||
if self.stats['device_errors'][device_name] <= 3:
|
||||
if self.stats['reconnect_attempts'][device_name] >= 3:
|
||||
continue
|
||||
|
||||
now = time.time()
|
||||
if now - self._last_restart_ts[device_name] >= 50.0:
|
||||
self._last_restart_ts[device_name] = now
|
||||
self.logger.info(f"尝试重连设备: {device_name}")
|
||||
if self.restart_device(device_name):
|
||||
self.stats['device_errors'][device_name] = 0
|
||||
self.stats['reconnect_attempts'][device_name] = 0
|
||||
else:
|
||||
self.stats['reconnect_attempts'][device_name] += 1
|
||||
if self.stats['reconnect_attempts'][device_name] >= 3:
|
||||
self.logger.error(f"设备 {device_name} 重连失败已达3次,停止自动重试")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查设备 {device_name} 状态异常: {e}")
|
||||
@ -812,7 +1069,12 @@ class DeviceCoordinator:
|
||||
self.executor.shutdown(wait=True)
|
||||
|
||||
# 清理Socket管理器
|
||||
self.socket_manager.cleanup()
|
||||
try:
|
||||
self.socket_manager.cleanup_all()
|
||||
except Exception:
|
||||
# 兼容旧接口
|
||||
if hasattr(self.socket_manager, 'cleanup'):
|
||||
self.socket_manager.cleanup()
|
||||
|
||||
self.logger.info("设备协调器已关闭")
|
||||
|
||||
@ -837,7 +1099,7 @@ def test_restart_device(device_name=None):
|
||||
|
||||
Args:
|
||||
device_name (str, optional): 指定要测试的设备名称。如果为None,则自动选择第一个可用设备。
|
||||
可选值: 'camera', 'imu', 'pressure', 'femtobolt'
|
||||
可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
@ -847,22 +1109,13 @@ def test_restart_device(device_name=None):
|
||||
print("设备协调器重启功能测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建模拟的SocketIO和配置管理器
|
||||
# 创建模拟的SocketIO(使用真实配置文件)
|
||||
mock_socketio = Mock()
|
||||
mock_config_manager = Mock()
|
||||
|
||||
# 模拟配置数据
|
||||
mock_config_manager.get_device_config.return_value = {
|
||||
'camera': {'enabled': True, 'device_id': 0, 'fps': 30},
|
||||
'imu': {'enabled': True, 'device_type': 'mock'},
|
||||
'pressure': {'enabled': True, 'device_type': 'mock'},
|
||||
'femtobolt': {'enabled': False}
|
||||
}
|
||||
|
||||
try:
|
||||
# 创建设备协调器实例
|
||||
print("1. 创建设备协调器...")
|
||||
coordinator = DeviceCoordinator(mock_socketio, mock_config_manager)
|
||||
coordinator = DeviceCoordinator(mock_socketio)
|
||||
|
||||
# 初始化设备协调器
|
||||
print("2. 初始化设备协调器...")
|
||||
@ -882,13 +1135,14 @@ def test_restart_device(device_name=None):
|
||||
print("❌ 没有可用的设备进行测试")
|
||||
return False
|
||||
|
||||
# 根据参数选择测试设备
|
||||
# 根据参数选择测试设备(仅支持 camera1/camera2/imu/pressure/femtobolt)
|
||||
if device_name:
|
||||
if device_name in available_devices:
|
||||
allowed = {'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'}
|
||||
if device_name in available_devices and device_name in allowed:
|
||||
test_device = device_name
|
||||
print(f"3. 使用指定的测试设备: {test_device}")
|
||||
else:
|
||||
print(f"❌ 指定的设备 '{device_name}' 不存在")
|
||||
print(f"❌ 指定的设备 '{device_name}' 不存在或不受支持")
|
||||
print(f" 可用设备: {available_devices}")
|
||||
return False
|
||||
else:
|
||||
@ -992,8 +1246,8 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
# 执行测试
|
||||
# 可选值: 'camera', 'imu', 'pressure', 'femtobolt'
|
||||
success = test_restart_device('pressure')
|
||||
# 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
|
||||
success = test_restart_device('imu')
|
||||
|
||||
if success:
|
||||
print("\n🎉 所有测试通过!")
|
||||
@ -1005,4 +1259,4 @@ if __name__ == "__main__":
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试启动失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
traceback.print_exc()
|
||||
|
||||
335
backend/devices/device_model.py
Normal file
@ -0,0 +1,335 @@
|
||||
# coding:UTF-8
|
||||
import time
|
||||
import bleak
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
# 设备实例 Device instance
|
||||
class DeviceModel:
|
||||
# region 属性 attribute
|
||||
# 设备名称 deviceName
|
||||
deviceName = "我的设备"
|
||||
|
||||
# 设备数据字典 Device Data Dictionary
|
||||
deviceData = {}
|
||||
|
||||
# 设备是否开启
|
||||
isOpen = False
|
||||
|
||||
# 临时数组 Temporary array
|
||||
TempBytes = []
|
||||
|
||||
# endregion
|
||||
|
||||
def __init__(self, deviceName, BLEDevice, callback_method):
|
||||
self.logger = logging.getLogger("device.imu.witmotion")
|
||||
self.logger.info("初始化IMU设备模型")
|
||||
# 设备名称(自定义) Device Name
|
||||
self.deviceName = deviceName
|
||||
self.BLEDevice = BLEDevice
|
||||
self.client = None
|
||||
self.writer_characteristic = None
|
||||
self.isOpen = False
|
||||
self.callback_method = callback_method
|
||||
self.deviceData = {}
|
||||
self._battery_ts = 0.0
|
||||
|
||||
@staticmethod
|
||||
def _battery_percent_from_reg(reg_value: int) -> int:
|
||||
try:
|
||||
v = int(reg_value)
|
||||
except Exception:
|
||||
return 0
|
||||
if v > 396:
|
||||
return 100
|
||||
if v >= 393:
|
||||
return 90
|
||||
if v >= 387:
|
||||
return 75
|
||||
if v >= 382:
|
||||
return 60
|
||||
if v >= 379:
|
||||
return 50
|
||||
if v >= 377:
|
||||
return 40
|
||||
if v >= 373:
|
||||
return 30
|
||||
if v >= 370:
|
||||
return 20
|
||||
if v >= 368:
|
||||
return 15
|
||||
if v >= 350:
|
||||
return 10
|
||||
if v >= 340:
|
||||
return 5
|
||||
return 0
|
||||
|
||||
# region 获取设备数据 Obtain device data
|
||||
# 设置设备数据 Set device data
|
||||
def set(self, key, value):
|
||||
# 将设备数据存到键值 Saving device data to key values
|
||||
self.deviceData[key] = value
|
||||
|
||||
# 获得设备数据 Obtain device data
|
||||
def get(self, key):
|
||||
# 从键值中获取数据,没有则返回None Obtaining data from key values
|
||||
if key in self.deviceData:
|
||||
return self.deviceData[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
# 删除设备数据 Delete device data
|
||||
def remove(self, key):
|
||||
# 删除设备键值
|
||||
del self.deviceData[key]
|
||||
|
||||
# endregion
|
||||
|
||||
# 打开设备 open Device
|
||||
async def openDevice(self):
|
||||
start_ts = time.perf_counter()
|
||||
self.logger.info("正在打开蓝牙IMU设备...")
|
||||
connect_start = time.perf_counter()
|
||||
async with bleak.BleakClient(self.BLEDevice, timeout=15) as client:
|
||||
self.client = client
|
||||
self.logger.info(f"蓝牙连接建立完成(耗时: {(time.perf_counter() - connect_start)*1000:.1f}ms)")
|
||||
self.isOpen = True
|
||||
# 设备UUID常量 Device UUID constant
|
||||
target_service_uuid = "0000ffe5-0000-1000-8000-00805f9a34fb"
|
||||
target_characteristic_uuid_read = "0000ffe4-0000-1000-8000-00805f9a34fb"
|
||||
target_characteristic_uuid_write = "0000ffe9-0000-1000-8000-00805f9a34fb"
|
||||
notify_characteristic = None
|
||||
|
||||
self.logger.info("正在匹配服务...")
|
||||
await asyncio.sleep(0.3)
|
||||
services = []
|
||||
for i in range(10):
|
||||
tmp_services = None
|
||||
get_services = getattr(client, 'get_services', None)
|
||||
if callable(get_services):
|
||||
try:
|
||||
tmp_services = await get_services()
|
||||
except Exception:
|
||||
tmp_services = None
|
||||
if not tmp_services:
|
||||
backend = getattr(client, "_backend", None)
|
||||
backend_get_services = getattr(backend, "get_services", None)
|
||||
if callable(backend_get_services):
|
||||
try:
|
||||
tmp_services = await backend_get_services()
|
||||
except Exception:
|
||||
tmp_services = None
|
||||
if not tmp_services:
|
||||
tmp_services = getattr(client, 'services', None)
|
||||
|
||||
try:
|
||||
services = list(tmp_services) if tmp_services else []
|
||||
except Exception:
|
||||
services = []
|
||||
|
||||
for service in services:
|
||||
try:
|
||||
svc_uuid = str(getattr(service, "uuid", "") or "").lower()
|
||||
except Exception:
|
||||
svc_uuid = ""
|
||||
if svc_uuid == str(target_service_uuid).lower():
|
||||
self.logger.info(f"匹配到服务: {service}")
|
||||
self.logger.info("正在匹配特征...")
|
||||
chars = []
|
||||
try:
|
||||
chars = list(getattr(service, "characteristics", None) or [])
|
||||
except Exception:
|
||||
chars = []
|
||||
for characteristic in chars:
|
||||
try:
|
||||
chr_uuid = str(getattr(characteristic, "uuid", "") or "").lower()
|
||||
except Exception:
|
||||
chr_uuid = ""
|
||||
if chr_uuid == str(target_characteristic_uuid_read).lower():
|
||||
notify_characteristic = characteristic
|
||||
if chr_uuid == str(target_characteristic_uuid_write).lower():
|
||||
self.writer_characteristic = characteristic
|
||||
if notify_characteristic:
|
||||
break
|
||||
if notify_characteristic:
|
||||
break
|
||||
await asyncio.sleep(0.2 + i * 0.1)
|
||||
|
||||
if notify_characteristic:
|
||||
self.logger.info(f"匹配到特征: {notify_characteristic}")
|
||||
# 设置通知以接收数据 Set up notifications to receive data
|
||||
await client.start_notify(notify_characteristic.uuid, self.onDataReceived)
|
||||
self.logger.info("开始接收姿态数据(XYZ欧拉角)")
|
||||
self.logger.info(f"设备打开完成(耗时: {(time.perf_counter() - start_ts)*1000:.1f}ms)")
|
||||
|
||||
# 保持连接打开 Keep connected and open
|
||||
try:
|
||||
while self.isOpen:
|
||||
try:
|
||||
if not bool(getattr(client, "is_connected", False)):
|
||||
self.isOpen = False
|
||||
break
|
||||
except Exception:
|
||||
self.isOpen = False
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
# 在退出时停止通知 Stop notification on exit
|
||||
try:
|
||||
await client.stop_notify(notify_characteristic.uuid)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
svc_list = []
|
||||
for s in services:
|
||||
try:
|
||||
svc_list.append(str(getattr(s, "uuid", "") or ""))
|
||||
except Exception:
|
||||
pass
|
||||
self.logger.warning(f"未找到匹配的服务或特征,可用服务: {svc_list}")
|
||||
except Exception:
|
||||
self.logger.warning("未找到匹配的服务或特征")
|
||||
raise RuntimeError("未找到匹配的服务或特征")
|
||||
|
||||
# 关闭设备 close Device
|
||||
def closeDevice(self):
|
||||
self.isOpen = False
|
||||
self.logger.info("设备已关闭")
|
||||
|
||||
# region 数据解析 data analysis
|
||||
# 串口数据处理 Serial port data processing
|
||||
def onDataReceived(self, sender, data):
|
||||
tempdata = bytes.fromhex(data.hex())
|
||||
for var in tempdata:
|
||||
self.TempBytes.append(var)
|
||||
if len(self.TempBytes) == 1 and self.TempBytes[0] != 0x55:
|
||||
del self.TempBytes[0]
|
||||
continue
|
||||
if len(self.TempBytes) == 20:
|
||||
self.processData(self.TempBytes)
|
||||
self.TempBytes.clear()
|
||||
|
||||
# 数据解析 data analysis
|
||||
def processData(self, Bytes):
|
||||
if Bytes[1] == 0x61:
|
||||
AngX = self.getSignInt16(Bytes[15] << 8 | Bytes[14]) / 32768 * 180
|
||||
AngY = self.getSignInt16(Bytes[17] << 8 | Bytes[16]) / 32768 * 180
|
||||
AngZ = self.getSignInt16(Bytes[19] << 8 | Bytes[18]) / 32768 * 180
|
||||
self.set("AngX", round(AngX, 3))
|
||||
self.set("AngY", round(AngY, 3))
|
||||
self.set("AngZ", round(AngZ, 3))
|
||||
self.callback_method(self)
|
||||
return
|
||||
|
||||
if Bytes[1] == 0x71:
|
||||
start_reg = int(Bytes[2])
|
||||
regs = []
|
||||
for i in range(8):
|
||||
base = 4 + i * 2
|
||||
if base + 1 >= len(Bytes):
|
||||
break
|
||||
regs.append(int(Bytes[base]) | (int(Bytes[base + 1]) << 8))
|
||||
for idx, val in enumerate(regs):
|
||||
self.set(f"Reg_{start_reg + idx:02X}", val)
|
||||
if regs and start_reg == 0x64:
|
||||
raw = int(regs[0])
|
||||
self.set("BatteryRaw", raw)
|
||||
self.set("BatteryVoltage", round(raw / 100.0, 2))
|
||||
self.set("BatteryPercent", int(self._battery_percent_from_reg(raw)))
|
||||
self._battery_ts = time.time()
|
||||
self.set("BatteryTS", self._battery_ts)
|
||||
self.callback_method(self)
|
||||
return
|
||||
|
||||
# 获得int16有符号数 Obtain int16 signed number
|
||||
@staticmethod
|
||||
def getSignInt16(num):
|
||||
if num >= pow(2, 15):
|
||||
num -= pow(2, 16)
|
||||
return num
|
||||
|
||||
# endregion
|
||||
|
||||
# 发送串口数据 Sending serial port data
|
||||
async def sendData(self, data):
|
||||
try:
|
||||
if self.client.is_connected and self.writer_characteristic is not None:
|
||||
await self.client.write_gatt_char(self.writer_characteristic.uuid, bytes(data))
|
||||
except Exception as ex:
|
||||
self.logger.warning(f"发送数据失败: {ex}")
|
||||
|
||||
# 读取寄存器 read register
|
||||
async def readReg(self, regAddr):
|
||||
# 封装读取指令并向串口发送数据 Encapsulate read instructions and send data to the serial port
|
||||
await self.sendData(self.get_readBytes(regAddr))
|
||||
|
||||
async def readBattery(self, timeout: float = 2.0):
|
||||
if not bool(self.isOpen):
|
||||
return None
|
||||
client = getattr(self, "client", None)
|
||||
if client is None or not bool(getattr(client, "is_connected", False)):
|
||||
return None
|
||||
prev_ts = float(self.deviceData.get("BatteryTS", 0.0) or 0.0)
|
||||
await self.readReg(0x64)
|
||||
deadline = time.time() + max(0.1, float(timeout))
|
||||
while time.time() < deadline:
|
||||
cur_ts = float(self.deviceData.get("BatteryTS", 0.0) or 0.0)
|
||||
if cur_ts > prev_ts:
|
||||
return {
|
||||
"raw": self.deviceData.get("BatteryRaw"),
|
||||
"voltage": self.deviceData.get("BatteryVoltage"),
|
||||
"percent": self.deviceData.get("BatteryPercent"),
|
||||
}
|
||||
await asyncio.sleep(0.05)
|
||||
return None
|
||||
|
||||
# 写入寄存器 Write Register
|
||||
async def writeReg(self, regAddr, sValue):
|
||||
# 解锁 unlock
|
||||
self.unlock()
|
||||
# 延迟100ms Delay 100ms
|
||||
time.sleep(0.1)
|
||||
# 封装写入指令并向串口发送数据
|
||||
await self.sendData(self.get_writeBytes(regAddr, sValue))
|
||||
# 延迟100ms Delay 100ms
|
||||
time.sleep(0.1)
|
||||
# 保存 save
|
||||
self.save()
|
||||
|
||||
# 读取指令封装 Read instruction encapsulation
|
||||
@staticmethod
|
||||
def get_readBytes(regAddr):
|
||||
# 初始化
|
||||
tempBytes = [None] * 5
|
||||
tempBytes[0] = 0xff
|
||||
tempBytes[1] = 0xaa
|
||||
tempBytes[2] = 0x27
|
||||
tempBytes[3] = regAddr
|
||||
tempBytes[4] = 0
|
||||
return tempBytes
|
||||
|
||||
# 写入指令封装 Write instruction encapsulation
|
||||
@staticmethod
|
||||
def get_writeBytes(regAddr, rValue):
|
||||
# 初始化
|
||||
tempBytes = [None] * 5
|
||||
tempBytes[0] = 0xff
|
||||
tempBytes[1] = 0xaa
|
||||
tempBytes[2] = regAddr
|
||||
tempBytes[3] = rValue & 0xff
|
||||
tempBytes[4] = rValue >> 8
|
||||
return tempBytes
|
||||
|
||||
# 解锁 unlock
|
||||
def unlock(self):
|
||||
cmd = self.get_writeBytes(0x69, 0xb588)
|
||||
self.sendData(cmd)
|
||||
|
||||
# 保存 save
|
||||
def save(self):
|
||||
cmd = self.get_writeBytes(0x00, 0x0000)
|
||||
self.sendData(cmd)
|
||||
@ -249,21 +249,20 @@ class FemtoBoltManager(BaseDevice):
|
||||
|
||||
try:
|
||||
# 设置图形背景色和边距
|
||||
self.fig.patch.set_facecolor((50/255, 50/255, 50/255)) # 设置深灰色背景 rgb(50, 50, 50)
|
||||
self.fig.patch.set_facecolor((38/255, 48/255, 64/255))
|
||||
self.fig.tight_layout(pad=0) # 移除所有边距
|
||||
|
||||
# 清除之前的绘图
|
||||
self.ax.clear()
|
||||
self.ax.set_facecolor((50/255, 50/255, 50/255)) # 设置坐标区域背景色为黑色
|
||||
self.ax.set_facecolor((38/255, 48/255, 64/255))
|
||||
|
||||
# 深度数据过滤(与display_x.py完全一致)
|
||||
depth[depth > self.depth_range_max] = 0
|
||||
depth[depth < self.depth_range_min] = 0
|
||||
|
||||
# 背景图(深灰色背景)
|
||||
# 创建RGB格式的背景图,确保颜色准确性
|
||||
background_gray_value = 50 # RGB(50, 50, 50)
|
||||
background = np.full((*depth.shape, 3), background_gray_value, dtype=np.uint8)
|
||||
# 背景图(#263040)
|
||||
background = np.zeros((*depth.shape, 3), dtype=np.uint8)
|
||||
background[:] = (38, 48, 64)
|
||||
|
||||
# 使用 np.ma.masked_equal() 来屏蔽深度图中的零值
|
||||
depth = np.ma.masked_equal(depth, 0)
|
||||
@ -940,8 +939,10 @@ class FemtoBoltManager(BaseDevice):
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
return {
|
||||
'device_type': 'femtobolt',
|
||||
'is_connected': self.is_connected,
|
||||
'is_streaming': self.is_streaming,
|
||||
'color_resolution': self.color_resolution,
|
||||
'depth_mode': self.depth_mode,
|
||||
'target_fps': self.fps,
|
||||
@ -950,9 +951,9 @@ class FemtoBoltManager(BaseDevice):
|
||||
'dropped_frames': self.dropped_frames,
|
||||
'depth_range': f"{self.depth_range_min}-{self.depth_range_max}mm",
|
||||
'has_depth_frame': self.last_depth_frame is not None,
|
||||
'has_color_frame': self.last_color_frame is not None
|
||||
})
|
||||
return status
|
||||
'has_color_frame': self.last_color_frame is not None,
|
||||
'device_info': self.get_device_info()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,649 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
IMU传感器管理器
|
||||
负责IMU传感器的连接、校准和头部姿态数据采集
|
||||
"""
|
||||
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
import numpy as np
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
import logging
|
||||
from collections import deque
|
||||
import struct
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from .base_device import BaseDevice
|
||||
from .utils.socket_manager import SocketManager
|
||||
from .utils.config_manager import ConfigManager
|
||||
except ImportError:
|
||||
from base_device import BaseDevice
|
||||
from utils.socket_manager import SocketManager
|
||||
from utils.config_manager import ConfigManager
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RealIMUDevice:
|
||||
"""真实IMU设备,通过串口读取姿态数据"""
|
||||
def __init__(self, port, baudrate):
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.ser = None
|
||||
self.buffer = bytearray()
|
||||
self.calibration_data = None
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
self.last_data = {
|
||||
'roll': 0.0,
|
||||
'pitch': 0.0,
|
||||
'yaw': 0.0,
|
||||
'temperature': 25.0
|
||||
}
|
||||
logger.debug(f'RealIMUDevice 初始化: port={self.port}, baudrate={self.baudrate}')
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
logger.debug(f'尝试打开串口: {self.port} @ {self.baudrate}')
|
||||
self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
|
||||
if hasattr(self.ser, 'reset_input_buffer'):
|
||||
try:
|
||||
self.ser.reset_input_buffer()
|
||||
logger.debug('已清空串口输入缓冲区')
|
||||
except Exception as e:
|
||||
logger.debug(f'重置串口输入缓冲区失败: {e}')
|
||||
logger.info(f'IMU设备连接成功: {self.port} @ {self.baudrate}bps')
|
||||
except Exception as e:
|
||||
# logger.error(f'IMU设备连接失败: {e}', exc_info=True)
|
||||
self.ser = None
|
||||
|
||||
def set_calibration(self, calibration: Dict[str, Any]):
|
||||
self.calibration_data = calibration
|
||||
if 'head_pose_offset' in calibration:
|
||||
self.head_pose_offset = calibration['head_pose_offset']
|
||||
logger.debug(f'应用IMU校准数据: {self.head_pose_offset}')
|
||||
def apply_calibration(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""应用校准:将当前姿态减去初始偏移,得到相对于初始姿态的变化量"""
|
||||
if not raw_data or 'head_pose' not in raw_data:
|
||||
return raw_data
|
||||
|
||||
# 应用校准偏移
|
||||
calibrated_data = raw_data.copy()
|
||||
head_pose = raw_data['head_pose'].copy()
|
||||
angle=head_pose['rotation'] - self.head_pose_offset['rotation']
|
||||
# 减去基准值(零点偏移)
|
||||
head_pose['rotation'] = ((angle + 180) % 360) - 180
|
||||
head_pose['tilt'] = head_pose['tilt'] - self.head_pose_offset['tilt']
|
||||
head_pose['pitch'] = head_pose['pitch'] - self.head_pose_offset['pitch']
|
||||
|
||||
calibrated_data['head_pose'] = head_pose
|
||||
return calibrated_data
|
||||
@staticmethod
|
||||
def _checksum(data: bytes) -> int:
|
||||
return sum(data[:-1]) & 0xFF
|
||||
|
||||
def _parse_packet(self, data: bytes) -> Optional[Dict[str, float]]:
|
||||
if len(data) != 11:
|
||||
logger.debug(f'无效数据包长度: {len(data)}')
|
||||
return None
|
||||
if data[0] != 0x55:
|
||||
logger.debug(f'错误的包头: 0x{data[0]:02X}')
|
||||
return None
|
||||
if self._checksum(data) != data[-1]:
|
||||
logger.debug(f'校验和错误: 期望{self._checksum(data):02X}, 实际{data[-1]:02X}')
|
||||
return None
|
||||
packet_type = data[1]
|
||||
vals = [int.from_bytes(data[i:i+2], 'little', signed=True) for i in range(2, 10, 2)]
|
||||
if packet_type == 0x53: # 姿态角,单位0.01°
|
||||
pitchl, rxl, yawl, temp = vals # 注意这里 vals 已经是有符号整数
|
||||
# 使用第一段代码里的比例系数
|
||||
k_angle = 180.0
|
||||
roll = -round(rxl / 32768.0 * k_angle,2)
|
||||
pitch = -round(pitchl / 32768.0 * k_angle,2)
|
||||
yaw = -round(yawl / 32768.0 * k_angle,2)
|
||||
temp = temp / 100.0
|
||||
self.last_data = {
|
||||
'roll': roll,
|
||||
'pitch': pitch,
|
||||
'yaw': yaw,
|
||||
'temperature': temp
|
||||
}
|
||||
# print(f'解析姿态角包: roll={roll}, pitch={pitch}, yaw={yaw}, temp={temp}')
|
||||
return self.last_data
|
||||
else:
|
||||
# logger.debug(f'忽略的数据包类型: 0x{packet_type:02X}')
|
||||
return None
|
||||
|
||||
def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]:
|
||||
if not self.ser or not getattr(self.ser, 'is_open', False):
|
||||
# logger.warning('IMU串口未连接,尝试重新连接...')
|
||||
self._connect()
|
||||
return {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
try:
|
||||
bytes_waiting = self.ser.in_waiting
|
||||
if bytes_waiting:
|
||||
# logger.debug(f'串口缓冲区待读字节: {bytes_waiting}')
|
||||
chunk = self.ser.read(bytes_waiting)
|
||||
# logger.debug(f'读取到字节: {len(chunk)}')
|
||||
self.buffer.extend(chunk)
|
||||
while len(self.buffer) >= 11:
|
||||
if self.buffer[0] != 0x55:
|
||||
dropped = self.buffer.pop(0)
|
||||
logger.debug(f'丢弃无效字节: 0x{dropped:02X}')
|
||||
continue
|
||||
packet = bytes(self.buffer[:11])
|
||||
parsed = self._parse_packet(packet)
|
||||
del self.buffer[:11]
|
||||
if parsed is not None:
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': parsed['yaw'], # rotation = roll
|
||||
'tilt': parsed['roll'], # tilt = yaw
|
||||
'pitch': parsed['pitch'] # pitch = pitch
|
||||
},
|
||||
'temperature': parsed['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
# logger.debug(f'映射后的头部姿态: {raw}')
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
except Exception as e:
|
||||
logger.error(f'IMU数据读取异常: {e}', exc_info=True)
|
||||
raw = {
|
||||
'head_pose': {
|
||||
'rotation': self.last_data['yaw'],
|
||||
'tilt': self.last_data['roll'],
|
||||
'pitch': self.last_data['pitch']
|
||||
},
|
||||
'temperature': self.last_data['temperature'],
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
return self.apply_calibration(raw) if apply_calibration else raw
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self.ser and getattr(self.ser, 'is_open', False):
|
||||
self.ser.close()
|
||||
logger.info('IMU设备串口已关闭')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class MockIMUDevice:
|
||||
"""模拟IMU设备"""
|
||||
|
||||
def __init__(self):
|
||||
self.noise_level = 0.1
|
||||
self.calibration_data = None # 校准数据
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0} # 头部姿态零点偏移
|
||||
|
||||
def set_calibration(self, calibration: Dict[str, Any]):
|
||||
"""设置校准数据"""
|
||||
self.calibration_data = calibration
|
||||
if 'head_pose_offset' in calibration:
|
||||
self.head_pose_offset = calibration['head_pose_offset']
|
||||
|
||||
def apply_calibration(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""应用校准:将当前姿态减去初始偏移,得到相对姿态"""
|
||||
if not raw_data or 'head_pose' not in raw_data:
|
||||
return raw_data
|
||||
|
||||
calibrated_data = raw_data.copy()
|
||||
head_pose = raw_data['head_pose'].copy()
|
||||
head_pose['rotation'] = head_pose['rotation'] - self.head_pose_offset['rotation']
|
||||
head_pose['tilt'] = head_pose['tilt'] - self.head_pose_offset['tilt']
|
||||
head_pose['pitch'] = head_pose['pitch'] - self.head_pose_offset['pitch']
|
||||
calibrated_data['head_pose'] = head_pose
|
||||
return calibrated_data
|
||||
|
||||
def read_data(self, apply_calibration: bool = True) -> Dict[str, Any]:
|
||||
"""读取IMU数据"""
|
||||
# 生成头部姿态角度数据,角度范围(-90°, +90°)
|
||||
# 使用正弦波模拟自然的头部运动,添加随机噪声
|
||||
import time
|
||||
current_time = time.time()
|
||||
|
||||
# 旋转角(左旋为负,右旋为正)
|
||||
rotation_angle = 30 * np.sin(current_time * 0.5) + np.random.normal(0, self.noise_level * 5)
|
||||
rotation_angle = np.clip(rotation_angle, -90, 90)
|
||||
|
||||
# 倾斜角(左倾为负,右倾为正)
|
||||
tilt_angle = 20 * np.sin(current_time * 0.3 + np.pi/4) + np.random.normal(0, self.noise_level * 5)
|
||||
tilt_angle = np.clip(tilt_angle, -90, 90)
|
||||
|
||||
# 俯仰角(俯角为负,仰角为正)
|
||||
pitch_angle = 15 * np.sin(current_time * 0.7 + np.pi/2) + np.random.normal(0, self.noise_level * 5)
|
||||
pitch_angle = np.clip(pitch_angle, -90, 90)
|
||||
|
||||
# 生成原始数据
|
||||
raw_data = {
|
||||
'head_pose': {
|
||||
'rotation': rotation_angle, # 旋转角:左旋(-), 右旋(+)
|
||||
'tilt': tilt_angle, # 倾斜角:左倾(-), 右倾(+)
|
||||
'pitch': pitch_angle # 俯仰角:俯角(-), 仰角(+)
|
||||
},
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
# 应用校准并返回
|
||||
return self.apply_calibration(raw_data) if apply_calibration else raw_data
|
||||
|
||||
|
||||
class IMUManager(BaseDevice):
|
||||
"""IMU传感器管理器"""
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
"""
|
||||
初始化IMU管理器
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例
|
||||
config_manager: 配置管理器实例
|
||||
"""
|
||||
# 配置管理
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
config = self.config_manager.get_device_config('imu')
|
||||
|
||||
super().__init__("imu", config)
|
||||
|
||||
# 保存socketio实例
|
||||
self._socketio = socketio
|
||||
|
||||
# 设备配置
|
||||
self.port = config.get('port', 'COM7')
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self.device_type = config.get('device_type', 'mock') # 'real' 或 'mock'
|
||||
self.use_mock = config.get('use_mock', False) # 保持向后兼容
|
||||
# IMU设备实例
|
||||
self.imu_device = None
|
||||
|
||||
# 推流相关
|
||||
self.imu_streaming = False
|
||||
self.imu_thread = None
|
||||
|
||||
# 统计信息
|
||||
self.data_count = 0
|
||||
self.error_count = 0
|
||||
|
||||
# 校准相关
|
||||
self.is_calibrated = False
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
|
||||
# 数据缓存
|
||||
self.data_buffer = deque(maxlen=100)
|
||||
self.last_valid_data = None
|
||||
|
||||
self.logger.info(f"IMU管理器初始化完成 - 端口: {self.port}, 设备类型: {self.device_type}")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
初始化IMU设备
|
||||
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"正在初始化IMU设备...")
|
||||
|
||||
# 使用构造函数中已加载的配置,避免并发读取配置文件
|
||||
self.logger.info(f"使用已加载配置: port={self.port}, baudrate={self.baudrate}, device_type={self.device_type}")
|
||||
|
||||
# 根据配置选择真实设备或模拟设备
|
||||
# 优先使用device_type配置,如果没有则使用use_mock配置(向后兼容)
|
||||
use_real_device = (self.device_type == 'real') or (not self.use_mock)
|
||||
|
||||
if use_real_device:
|
||||
self.logger.info(f"使用真实IMU设备 - 端口: {self.port}, 波特率: {self.baudrate}")
|
||||
self.imu_device = RealIMUDevice(self.port, self.baudrate)
|
||||
|
||||
# 检查真实设备是否连接成功
|
||||
if self.imu_device.ser is None:
|
||||
self.logger.error(f"IMU设备连接失败: 无法打开串口 {self.port}")
|
||||
self.is_connected = False
|
||||
self.imu_device = None
|
||||
return False
|
||||
else:
|
||||
self.logger.info("使用模拟IMU设备")
|
||||
self.imu_device = MockIMUDevice()
|
||||
|
||||
self.is_connected = True
|
||||
self._device_info.update({
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'use_mock': self.use_mock
|
||||
})
|
||||
|
||||
self.logger.info("IMU初始化成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU初始化失败: {e}")
|
||||
self.is_connected = False
|
||||
self.imu_device = None
|
||||
return False
|
||||
|
||||
def _quick_calibrate_imu(self) -> Dict[str, Any]:
|
||||
"""
|
||||
快速IMU零点校准(以当前姿态为基准)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 校准结果
|
||||
"""
|
||||
try:
|
||||
if not self.imu_device:
|
||||
return {'status': 'error', 'error': 'IMU设备未初始化'}
|
||||
|
||||
self.logger.info('开始IMU快速零点校准...')
|
||||
|
||||
# 直接读取一次原始数据作为校准偏移量
|
||||
raw_data = self.imu_device.read_data(apply_calibration=False)
|
||||
if not raw_data or 'head_pose' not in raw_data:
|
||||
return {'status': 'error', 'error': '无法读取IMU原始数据'}
|
||||
|
||||
# 使用当前姿态作为零点偏移
|
||||
self.head_pose_offset = {
|
||||
'rotation': raw_data['head_pose']['rotation'],
|
||||
'tilt': raw_data['head_pose']['tilt'],
|
||||
'pitch': raw_data['head_pose']['pitch']
|
||||
}
|
||||
|
||||
# 应用校准到设备
|
||||
calibration_data = {'head_pose_offset': self.head_pose_offset}
|
||||
self.imu_device.set_calibration(calibration_data)
|
||||
|
||||
self.logger.info(f'IMU快速校准完成: {self.head_pose_offset}')
|
||||
return {
|
||||
'status': 'success',
|
||||
'head_pose_offset': self.head_pose_offset
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f'IMU快速校准失败: {e}')
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
|
||||
def calibrate(self) -> bool:
|
||||
"""
|
||||
校准IMU传感器
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.imu_device:
|
||||
if not self.initialize():
|
||||
self.logger.error("IMU设备未连接")
|
||||
return False
|
||||
|
||||
# 使用快速校准方法
|
||||
result = self._quick_calibrate_imu()
|
||||
|
||||
if result['status'] == 'success':
|
||||
self.is_calibrated = True
|
||||
self.logger.info("IMU校准成功")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"IMU校准失败: {result.get('error', '未知错误')}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU校准失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
"""
|
||||
开始IMU数据流
|
||||
|
||||
Args:
|
||||
socketio: SocketIO实例,用于数据推送
|
||||
|
||||
Returns:
|
||||
bool: 启动是否成功
|
||||
"""
|
||||
try:
|
||||
if not self.is_connected or not self.imu_device:
|
||||
if not self.initialize():
|
||||
return False
|
||||
|
||||
if self.imu_streaming:
|
||||
self.logger.warning("IMU数据流已在运行")
|
||||
return True
|
||||
|
||||
# 启动前进行快速校准
|
||||
if not self.is_calibrated:
|
||||
self.logger.info("启动前进行快速零点校准...")
|
||||
self._quick_calibrate_imu()
|
||||
|
||||
self.imu_streaming = True
|
||||
self.imu_thread = threading.Thread(target=self._imu_streaming_thread, daemon=True)
|
||||
self.imu_thread.start()
|
||||
|
||||
self.logger.info("IMU数据流启动成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU数据流启动失败: {e}")
|
||||
self.imu_streaming = False
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
"""
|
||||
停止IMU数据流
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
try:
|
||||
self.imu_streaming = False
|
||||
|
||||
if self.imu_thread and self.imu_thread.is_alive():
|
||||
self.imu_thread.join(timeout=3.0)
|
||||
|
||||
self.logger.info("IMU数据流已停止")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止IMU数据流失败: {e}")
|
||||
return False
|
||||
|
||||
def _imu_streaming_thread(self):
|
||||
"""
|
||||
IMU数据流工作线程
|
||||
"""
|
||||
self.logger.info("IMU数据流工作线程启动")
|
||||
|
||||
while self.imu_streaming:
|
||||
try:
|
||||
if self.imu_device:
|
||||
# 读取IMU数据
|
||||
data = self.imu_device.read_data(apply_calibration=True)
|
||||
|
||||
if data:
|
||||
# 缓存数据
|
||||
# self.data_buffer.append(data)
|
||||
# self.last_valid_data = data
|
||||
|
||||
# 发送数据到前端
|
||||
if self._socketio:
|
||||
self._socketio.emit('imu_data', data, namespace='/devices')
|
||||
|
||||
# 更新统计
|
||||
self.data_count += 1
|
||||
else:
|
||||
self.error_count += 1
|
||||
|
||||
time.sleep(0.02) # 50Hz采样率
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"IMU数据流处理异常: {e}")
|
||||
self.error_count += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
self.logger.info("IMU数据流工作线程结束")
|
||||
|
||||
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
status = super().get_status()
|
||||
status.update({
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'is_streaming': self.imu_streaming,
|
||||
'is_calibrated': self.is_calibrated,
|
||||
'data_count': self.data_count,
|
||||
'error_count': self.error_count,
|
||||
'buffer_size': len(self.data_buffer),
|
||||
'has_data': self.last_valid_data is not None,
|
||||
'head_pose_offset': self.head_pose_offset,
|
||||
'device_type': 'mock' if self.use_mock else 'real'
|
||||
})
|
||||
return status
|
||||
|
||||
def get_latest_data(self) -> Optional[Dict[str, float]]:
|
||||
"""
|
||||
获取最新的IMU数据
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, float]]: 最新数据,无数据返回None
|
||||
"""
|
||||
return self.last_valid_data.copy() if self.last_valid_data else None
|
||||
|
||||
def collect_head_pose_data(self, duration: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
收集头部姿态数据
|
||||
|
||||
Args:
|
||||
duration: 收集时长(秒)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 收集到的数据列表
|
||||
"""
|
||||
collected_data = []
|
||||
|
||||
if not self.is_connected or not self.imu_device:
|
||||
self.logger.error("IMU设备未连接")
|
||||
return collected_data
|
||||
|
||||
self.logger.info(f"开始收集头部姿态数据,时长: {duration}秒")
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < duration:
|
||||
try:
|
||||
data = self.imu_device.read_data(apply_calibration=True)
|
||||
if data:
|
||||
# 添加时间戳
|
||||
data['timestamp'] = time.time()
|
||||
collected_data.append(data)
|
||||
|
||||
time.sleep(0.02) # 50Hz采样率
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"数据收集异常: {e}")
|
||||
break
|
||||
|
||||
self.logger.info(f"头部姿态数据收集完成,共收集 {len(collected_data)} 个样本")
|
||||
return collected_data
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
断开IMU设备连接
|
||||
"""
|
||||
try:
|
||||
self.stop_streaming()
|
||||
|
||||
if self.imu_device:
|
||||
self.imu_device = None
|
||||
|
||||
self.is_connected = False
|
||||
self.logger.info("IMU设备已断开连接")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开IMU设备连接失败: {e}")
|
||||
|
||||
def reload_config(self) -> bool:
|
||||
"""
|
||||
重新加载设备配置
|
||||
|
||||
Returns:
|
||||
bool: 重新加载是否成功
|
||||
"""
|
||||
try:
|
||||
self.logger.info("正在重新加载IMU配置...")
|
||||
|
||||
|
||||
|
||||
# 获取最新配置
|
||||
config = self.config_manager.get_device_config('imu')
|
||||
|
||||
# 更新配置属性
|
||||
self.port = config.get('port', 'COM7')
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self.device_type = config.get('device_type', 'mock')
|
||||
self.use_mock = config.get('use_mock', False)
|
||||
|
||||
# 更新数据缓存队列大小
|
||||
buffer_size = config.get('buffer_size', 100)
|
||||
if buffer_size != self.data_buffer.maxlen:
|
||||
# 保存当前数据
|
||||
current_data = list(self.data_buffer)
|
||||
# 创建新缓冲区
|
||||
self.data_buffer = deque(maxlen=buffer_size)
|
||||
# 恢复数据(保留最新的数据)
|
||||
for data in current_data[-buffer_size:]:
|
||||
self.data_buffer.append(data)
|
||||
|
||||
self.logger.info(f"IMU配置重新加载成功 - 端口: {self.port}, 波特率: {self.baudrate}, 设备类型: {self.device_type}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"重新加载IMU配置失败: {e}")
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
清理资源
|
||||
"""
|
||||
try:
|
||||
self.disconnect()
|
||||
|
||||
# 清理缓冲区
|
||||
self.data_buffer.clear()
|
||||
|
||||
# 重置状态
|
||||
self.is_calibrated = False
|
||||
self.last_valid_data = None
|
||||
self.head_pose_offset = {'rotation': 0, 'tilt': 0, 'pitch': 0}
|
||||
|
||||
super().cleanup()
|
||||
self.logger.info("IMU资源清理完成")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"清理IMU资源失败: {e}")
|
||||
177
backend/devices/imu_test.py
Normal file
@ -0,0 +1,177 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import time
|
||||
from statistics import mean
|
||||
|
||||
import bleak
|
||||
import device_model
|
||||
|
||||
|
||||
async def find_device_by_address(address: str, timeout_s: float):
|
||||
try:
|
||||
return await bleak.BleakScanner.find_device_by_address(address, timeout=timeout_s)
|
||||
except TypeError:
|
||||
return await bleak.BleakScanner.find_device_by_address(address, cb=dict(use_bdaddr=False))
|
||||
|
||||
|
||||
async def find_device_by_name(name: str, timeout_s: float):
|
||||
scanner_fn = getattr(bleak.BleakScanner, "find_device_by_name", None)
|
||||
if callable(scanner_fn):
|
||||
return await scanner_fn(name, timeout=timeout_s)
|
||||
devices = await bleak.BleakScanner.discover(timeout=timeout_s)
|
||||
for d in devices:
|
||||
if (getattr(d, "name", None) or "") == name:
|
||||
return d
|
||||
return None
|
||||
|
||||
|
||||
async def run_trials(label: str, finder, runs: int, cooldown_s: float):
|
||||
ok_times = []
|
||||
fail = 0
|
||||
|
||||
for i in range(1, runs + 1):
|
||||
start = time.perf_counter()
|
||||
device = await finder()
|
||||
ms = (time.perf_counter() - start) * 1000
|
||||
if device is None:
|
||||
fail += 1
|
||||
print(f"[{label}] [{i:03d}] FAIL {ms:.1f}ms")
|
||||
else:
|
||||
addr = getattr(device, "address", None)
|
||||
name = getattr(device, "name", None)
|
||||
ok_times.append(ms)
|
||||
print(f"[{label}] [{i:03d}] OK {ms:.1f}ms address={addr} name={name}")
|
||||
if cooldown_s > 0:
|
||||
await asyncio.sleep(cooldown_s)
|
||||
|
||||
if ok_times:
|
||||
print(f"[{label}] runs={runs} success={len(ok_times)} fail={fail} avg={mean(ok_times):.1f}ms min={min(ok_times):.1f}ms max={max(ok_times):.1f}ms")
|
||||
else:
|
||||
print(f"[{label}] runs={runs} success=0 fail={fail}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--address", default="FA:E8:88:06:FE:F3")
|
||||
parser.add_argument("--name", default="WT901BLE67")
|
||||
parser.add_argument("--runs", type=int, default=10)
|
||||
parser.add_argument("--timeout", type=float, default=30.0)
|
||||
parser.add_argument("--cooldown", type=float, default=0.3)
|
||||
parser.add_argument("--mode", choices=["mac", "name", "both", "isopen", "battery"], default="both")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def main():
|
||||
args = parse_args()
|
||||
|
||||
if args.mode == "isopen":
|
||||
device = await find_device_by_name(args.name, args.timeout)
|
||||
if device is None:
|
||||
device = await find_device_by_address(args.address, args.timeout)
|
||||
if device is None:
|
||||
print("FAIL: 未发现设备")
|
||||
return
|
||||
addr = getattr(device, "address", None)
|
||||
name = getattr(device, "name", None)
|
||||
if args.address and addr and addr.lower() != args.address.lower():
|
||||
print(f"FAIL: 发现设备地址不匹配 found={addr} expected={args.address}")
|
||||
return
|
||||
print(f"FOUND address={addr} name={name}")
|
||||
|
||||
first_frame = asyncio.Event()
|
||||
|
||||
def on_update(dm):
|
||||
if not first_frame.is_set():
|
||||
first_frame.set()
|
||||
|
||||
dm = device_model.DeviceModel("imu_test", device, on_update)
|
||||
task = asyncio.create_task(dm.openDevice())
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(first_frame.wait(), timeout=30.0)
|
||||
print(f"OPENED isOpen={dm.isOpen} client_connected={bool(getattr(getattr(dm, 'client', None), 'is_connected', False))}")
|
||||
except Exception:
|
||||
print(f"OPEN_TIMEOUT isOpen={dm.isOpen} client_connected={bool(getattr(getattr(dm, 'client', None), 'is_connected', False))}")
|
||||
|
||||
while True:
|
||||
client_connected = bool(getattr(getattr(dm, "client", None), "is_connected", False))
|
||||
print(f"STATUS isOpen={dm.isOpen} client_connected={client_connected}")
|
||||
if not dm.isOpen:
|
||||
break
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(task, timeout=5.0)
|
||||
except Exception:
|
||||
try:
|
||||
task.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
print("DONE")
|
||||
return
|
||||
|
||||
if args.mode == "battery":
|
||||
device = await find_device_by_name(args.name, args.timeout)
|
||||
if device is None:
|
||||
device = await find_device_by_address(args.address, args.timeout)
|
||||
if device is None:
|
||||
print("FAIL: 未发现设备")
|
||||
return
|
||||
addr = getattr(device, "address", None)
|
||||
name = getattr(device, "name", None)
|
||||
if args.address and addr and addr.lower() != args.address.lower():
|
||||
print(f"FAIL: 发现设备地址不匹配 found={addr} expected={args.address}")
|
||||
return
|
||||
print(f"FOUND address={addr} name={name}")
|
||||
|
||||
first_frame = asyncio.Event()
|
||||
|
||||
def on_update(dm):
|
||||
if not first_frame.is_set():
|
||||
first_frame.set()
|
||||
|
||||
dm = device_model.DeviceModel("imu_test", device, on_update)
|
||||
task = asyncio.create_task(dm.openDevice())
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(first_frame.wait(), timeout=30.0)
|
||||
print("OPENED")
|
||||
except Exception:
|
||||
print("OPEN_TIMEOUT")
|
||||
|
||||
info = await dm.readBattery(timeout=3.0)
|
||||
print(f"BATTERY {info}")
|
||||
|
||||
try:
|
||||
dm.closeDevice()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await asyncio.wait_for(task, timeout=5.0)
|
||||
except Exception:
|
||||
try:
|
||||
task.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
print("DONE")
|
||||
return
|
||||
|
||||
if args.mode in ("mac", "both"):
|
||||
await run_trials(
|
||||
"mac",
|
||||
lambda: find_device_by_address(args.address, args.timeout),
|
||||
args.runs,
|
||||
args.cooldown,
|
||||
)
|
||||
|
||||
if args.mode in ("name", "both"):
|
||||
await run_trials(
|
||||
"name",
|
||||
lambda: find_device_by_name(args.name, args.timeout),
|
||||
args.runs,
|
||||
args.cooldown,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -463,7 +463,193 @@ class RealPressureDevice:
|
||||
"""析构函数,确保资源清理"""
|
||||
self.close()
|
||||
|
||||
class MockPressureDevice:
|
||||
def __init__(self, rows: int = 32, cols: int = 32, seed: Optional[int] = None):
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.is_connected = True
|
||||
self._rng = np.random.RandomState(seed if seed is not None else (int(time.time()) & 0xFFFF))
|
||||
self._phase = 0.0
|
||||
|
||||
def read_data(self) -> Dict[str, Any]:
|
||||
try:
|
||||
if not self.is_connected:
|
||||
return self._get_empty_data()
|
||||
raw_data = self._generate_raw_frame()
|
||||
zones = self._calculate_foot_pressure_zones(raw_data)
|
||||
image_base64 = self._generate_pressure_image(
|
||||
zones['left_front'], zones['left_rear'], zones['right_front'], zones['right_rear'], raw_data
|
||||
)
|
||||
return {
|
||||
'foot_pressure': {
|
||||
'left_front': round(zones['left_front'], 2),
|
||||
'left_rear': round(zones['left_rear'], 2),
|
||||
'right_front': round(zones['right_front'], 2),
|
||||
'right_rear': round(zones['right_rear'], 2),
|
||||
'left_total': round(zones['left_total'], 2),
|
||||
'right_total': round(zones['right_total'], 2)
|
||||
},
|
||||
'pressure_image': image_base64,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception:
|
||||
return self._get_empty_data()
|
||||
|
||||
def _generate_raw_frame(self) -> np.ndarray:
|
||||
rows, cols = self.rows, self.cols
|
||||
gy, gx = np.meshgrid(np.arange(rows), np.arange(cols), indexing='ij')
|
||||
gy = gy.astype(np.float64)
|
||||
gx = gx.astype(np.float64)
|
||||
self._phase += 0.15
|
||||
lf_cy = rows * 0.30 + 0.6 * np.sin(self._phase)
|
||||
lf_cx = cols * 0.25 + 0.3 * np.cos(self._phase * 0.7)
|
||||
lr_cy = rows * 0.75 + 0.5 * np.sin(self._phase * 0.8)
|
||||
lr_cx = cols * 0.25 + 0.2 * np.sin(self._phase * 0.6)
|
||||
rf_cy = rows * 0.30 + 0.6 * np.cos(self._phase * 0.9)
|
||||
rf_cx = cols * 0.75 + 0.3 * np.sin(self._phase)
|
||||
rr_cy = rows * 0.75 + 0.5 * np.cos(self._phase * 0.5)
|
||||
rr_cx = cols * 0.75 + 0.2 * np.cos(self._phase * 0.4)
|
||||
sy = rows * 0.10
|
||||
sx = cols * 0.10
|
||||
def gauss(cy: float, cx: float, amp: float) -> np.ndarray:
|
||||
return amp * np.exp(-(((gy - cy) ** 2) / (2 * sy * sy) + ((gx - cx) ** 2) / (2 * sx * sx)))
|
||||
lf = gauss(lf_cy, lf_cx, 300.0 + 120.0 * self._rng.rand())
|
||||
lr = gauss(lr_cy, lr_cx, 280.0 + 120.0 * self._rng.rand())
|
||||
rf = gauss(rf_cy, rf_cx, 300.0 + 120.0 * self._rng.rand())
|
||||
rr = gauss(rr_cy, rr_cx, 280.0 + 120.0 * self._rng.rand())
|
||||
base = lf + lr + rf + rr
|
||||
noise = self._rng.normal(0.0, 5.0, size=(rows, cols))
|
||||
frame = base + noise
|
||||
frame = np.clip(frame, 0, 65535).astype(np.uint16)
|
||||
return frame
|
||||
|
||||
def _calculate_foot_pressure_zones(self, raw_data: np.ndarray) -> Dict[str, Any]:
|
||||
try:
|
||||
rd = np.asarray(raw_data, dtype=np.float64)
|
||||
rows, cols = rd.shape if rd.ndim == 2 else (0, 0)
|
||||
if rows == 0 or cols == 0:
|
||||
raise ValueError
|
||||
mid_r = rows // 2
|
||||
mid_c = cols // 2
|
||||
left_front = float(np.sum(rd[:mid_r, :mid_c], dtype=np.float64))
|
||||
left_rear = float(np.sum(rd[mid_r:, :mid_c], dtype=np.float64))
|
||||
right_front = float(np.sum(rd[:mid_r, mid_c:], dtype=np.float64))
|
||||
right_rear = float(np.sum(rd[mid_r:, mid_c:], dtype=np.float64))
|
||||
left_total_abs = left_front + left_rear
|
||||
right_total_abs = right_front + right_rear
|
||||
total_abs = left_total_abs + right_total_abs
|
||||
left_total_pct = float((left_total_abs / total_abs * 100) if total_abs > 0 else 0)
|
||||
right_total_pct = float((right_total_abs / total_abs * 100) if total_abs > 0 else 0)
|
||||
left_front_pct = float((left_front / total_abs * 100) if total_abs > 0 else 0)
|
||||
left_rear_pct = float((left_rear / total_abs * 100) if total_abs > 0 else 0)
|
||||
right_front_pct = float((right_front / total_abs * 100) if total_abs > 0 else 0)
|
||||
right_rear_pct = float((right_rear / total_abs * 100) if total_abs > 0 else 0)
|
||||
return {
|
||||
'left_front': round(left_front_pct),
|
||||
'left_rear': round(left_rear_pct),
|
||||
'right_front': round(right_front_pct),
|
||||
'right_rear': round(right_rear_pct),
|
||||
'left_total': round(left_total_pct),
|
||||
'right_total': round(right_total_pct),
|
||||
'total_pressure': round(total_abs)
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
'left_front': 0, 'left_rear': 0, 'right_front': 0, 'right_rear': 0,
|
||||
'left_total': 0, 'right_total': 0, 'total_pressure': 0
|
||||
}
|
||||
|
||||
def _generate_pressure_image(self, left_front: float, left_rear: float, right_front: float, right_rear: float, raw_data: Optional[np.ndarray] = None) -> str:
|
||||
try:
|
||||
if MATPLOTLIB_AVAILABLE and raw_data is not None:
|
||||
return self._generate_heatmap_image(raw_data)
|
||||
else:
|
||||
return self._generate_simple_pressure_image(left_front, left_rear, right_front, right_rear)
|
||||
except Exception:
|
||||
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
|
||||
def _generate_heatmap_image(self, raw_data: np.ndarray) -> str:
|
||||
try:
|
||||
# 底值阈值(小于等于该值的区域作为背景)
|
||||
vmin = 10
|
||||
# 归一化到 [0,255],避免 dmax==dmin 时除零
|
||||
dmin, dmax = np.min(raw_data), np.max(raw_data)
|
||||
norm = np.clip((raw_data - dmin) / max(dmax - dmin, 1) * 255, 0, 255).astype(np.uint8)
|
||||
# 应用伪彩色(JET)以增强对比
|
||||
heatmap = cv2.applyColorMap(norm, cv2.COLORMAP_JET)
|
||||
# 将低值区域设置为背景色 #263040;OpenCV 使用 BGR 通道顺序 -> (64, 48, 38)
|
||||
heatmap[raw_data <= vmin] = (64, 48, 38)
|
||||
# 放大显示,保持像素边界清晰
|
||||
rows, cols = raw_data.shape
|
||||
heatmap = cv2.resize(heatmap, (cols * 4, rows * 4), interpolation=cv2.INTER_NEAREST)
|
||||
# 转换为 RGB 交给 PIL 编码
|
||||
heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
|
||||
from PIL import Image
|
||||
buffer = BytesIO()
|
||||
Image.fromarray(heatmap_rgb).save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
# 输出 data URL 便于前端直接显示
|
||||
image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
return f"data:image/png;base64,{image_base64}"
|
||||
except Exception:
|
||||
return self._generate_simple_pressure_image(0, 0, 0, 0)
|
||||
|
||||
def _generate_simple_pressure_image(self, left_front: float, left_rear: float, right_front: float, right_rear: float) -> str:
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
fig, ax = plt.subplots(1, 1, figsize=(6, 8))
|
||||
ax.set_xlim(0, 10)
|
||||
ax.set_ylim(0, 12)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
m = max(left_front, left_rear, right_front, right_rear)
|
||||
if m > 0:
|
||||
lf_c = plt.cm.Reds(left_front / m)
|
||||
lr_c = plt.cm.Reds(left_rear / m)
|
||||
rf_c = plt.cm.Reds(right_front / m)
|
||||
rr_c = plt.cm.Reds(right_rear / m)
|
||||
else:
|
||||
lf_c = lr_c = rf_c = rr_c = 'lightgray'
|
||||
ax.add_patch(patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=lf_c))
|
||||
ax.add_patch(patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=lr_c))
|
||||
ax.add_patch(patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=rf_c))
|
||||
ax.add_patch(patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=rr_c))
|
||||
ax.text(2, 8, f'{left_front:.1f}', ha='center', va='center', fontsize=10, weight='bold')
|
||||
ax.text(2, 4, f'{left_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold')
|
||||
ax.text(8, 8, f'{right_front:.1f}', ha='center', va='center', fontsize=10, weight='bold')
|
||||
ax.text(8, 4, f'{right_rear:.1f}', ha='center', va='center', fontsize=10, weight='bold')
|
||||
ax.text(2, 0.5, '左足', ha='center', va='center', fontsize=12, weight='bold')
|
||||
ax.text(8, 0.5, '右足', ha='center', va='center', fontsize=12, weight='bold')
|
||||
fig.patch.set_facecolor('black')
|
||||
ax.set_facecolor('black')
|
||||
buffer = BytesIO()
|
||||
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, facecolor='black')
|
||||
buffer.seek(0)
|
||||
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
plt.close(fig)
|
||||
return f"data:image/png;base64,{image_base64}"
|
||||
except Exception:
|
||||
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
|
||||
def _get_empty_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'foot_pressure': {
|
||||
'left_front': 0.0,
|
||||
'left_rear': 0.0,
|
||||
'right_front': 0.0,
|
||||
'right_rear': 0.0,
|
||||
'left_total': 0.0,
|
||||
'right_total': 0.0
|
||||
},
|
||||
'pressure_image': "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def close(self):
|
||||
self.is_connected = False
|
||||
|
||||
class PressureManager(BaseDevice):
|
||||
"""压力板管理器"""
|
||||
@ -487,7 +673,7 @@ class PressureManager(BaseDevice):
|
||||
|
||||
# 设备实例
|
||||
self.device = None
|
||||
self.device_type = self.config.get('device_type', 'mock') # 'real' 或 'mock'
|
||||
self.use_mock = bool(self.config.get('use_mock', False))
|
||||
|
||||
# 数据流相关
|
||||
self.streaming_thread = None
|
||||
@ -509,7 +695,7 @@ class PressureManager(BaseDevice):
|
||||
self.read_fail_threshold = int(self.config.get('read_fail_threshold', 30))
|
||||
self._last_connected_state = None # 去抖动状态广播
|
||||
|
||||
self.logger.info(f"压力板管理器初始化完成 - 设备类型: {self.device_type}")
|
||||
self.logger.info(f"压力板管理器初始化完成 - use_mock: {self.use_mock}")
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
@ -518,26 +704,42 @@ class PressureManager(BaseDevice):
|
||||
Returns:
|
||||
bool: 初始化是否成功
|
||||
"""
|
||||
self._initializing = True
|
||||
try:
|
||||
self.logger.info(f"正在初始化压力板设备...")
|
||||
|
||||
# 使用构造函数中已加载的配置,避免并发读取配置文件
|
||||
self.logger.info(f"使用已加载配置: device_type={self.device_type}, stream_interval={self.stream_interval}")
|
||||
self.logger.info(f"使用已加载配置: use_mock={self.use_mock}, stream_interval={self.stream_interval}")
|
||||
|
||||
# 根据设备类型创建设备实例
|
||||
if self.device_type == 'real':
|
||||
if not self.use_mock:
|
||||
self.device = RealPressureDevice()
|
||||
else:
|
||||
self.device = MockPressureDevice()
|
||||
|
||||
connected = False
|
||||
try:
|
||||
if self.use_mock:
|
||||
connected = True
|
||||
elif hasattr(self.device, 'is_connected'):
|
||||
connected = bool(self.device.is_connected)
|
||||
else:
|
||||
connected = bool(self.check_hardware_connection())
|
||||
except Exception:
|
||||
connected = False
|
||||
|
||||
# 使用set_connected方法启动连接监控线程
|
||||
self.set_connected(True)
|
||||
self.set_connected(bool(connected))
|
||||
self._device_info.update({
|
||||
'device_type': self.device_type,
|
||||
'device_type': 'mock' if self.use_mock else 'real',
|
||||
'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown'
|
||||
})
|
||||
|
||||
self.logger.info(f"压力板初始化成功 - 设备类型: {self.device_type}")
|
||||
if not connected:
|
||||
self.logger.warning("压力板初始化完成但硬件未连接")
|
||||
return False
|
||||
|
||||
self.logger.info(f"压力板初始化成功 - use_mock: {self.use_mock}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -546,6 +748,8 @@ class PressureManager(BaseDevice):
|
||||
self.set_connected(False)
|
||||
self.device = None
|
||||
return False
|
||||
finally:
|
||||
self._initializing = False
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
|
||||
@ -704,7 +908,7 @@ class PressureManager(BaseDevice):
|
||||
Dict[str, Any]: 设备状态信息
|
||||
"""
|
||||
return {
|
||||
'device_type': self.device_type,
|
||||
'device_type': 'mock' if self.use_mock else 'real',
|
||||
'is_connected': self.is_connected,
|
||||
'is_streaming': self.is_streaming,
|
||||
'is_calibrated': self.is_calibrated,
|
||||
@ -780,14 +984,14 @@ class PressureManager(BaseDevice):
|
||||
|
||||
# 更新配置属性
|
||||
self.config = new_config
|
||||
self.device_type = new_config.get('device_type', 'mock')
|
||||
self.use_mock = bool(new_config.get('use_mock', False))
|
||||
self.stream_interval = new_config.get('stream_interval', 0.1)
|
||||
# 动态更新重连参数
|
||||
self.max_reconnect_attempts = int(new_config.get('max_reconnect_attempts', self.max_reconnect_attempts))
|
||||
self.reconnect_delay = float(new_config.get('reconnect_delay', self.reconnect_delay))
|
||||
self.read_fail_threshold = int(new_config.get('read_fail_threshold', self.read_fail_threshold))
|
||||
|
||||
self.logger.info(f"压力板配置重新加载成功 - 设备类型: {self.device_type}, 流间隔: {self.stream_interval}")
|
||||
self.logger.info(f"压力板配置重新加载成功 - use_mock: {self.use_mock}, 流间隔: {self.stream_interval}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -899,4 +1103,4 @@ class PressureManager(BaseDevice):
|
||||
self.disconnect()
|
||||
self.logger.info("压力板设备资源清理完成")
|
||||
except Exception as e:
|
||||
self.logger.error(f"压力板设备资源清理失败: {e}")
|
||||
self.logger.error(f"压力板设备资源清理失败: {e}")
|
||||
|
||||
282
backend/devices/remote_control_manager.py
Normal file
@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
import re
|
||||
try:
|
||||
import serial # pyserial
|
||||
except Exception:
|
||||
serial = None
|
||||
|
||||
try:
|
||||
from .base_device import BaseDevice
|
||||
from .utils.config_manager import ConfigManager
|
||||
except ImportError:
|
||||
from base_device import BaseDevice
|
||||
from utils.config_manager import ConfigManager
|
||||
|
||||
|
||||
def _modbus_crc16(data: bytes) -> int:
|
||||
crc = 0xFFFF
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
if crc & 0x0001:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
|
||||
class RemoteControlManager(BaseDevice):
|
||||
|
||||
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
port = self.config_manager.get_config_value('REMOTE', 'port', fallback='COM6')
|
||||
baudrate = int(self.config_manager.get_config_value('REMOTE', 'baudrate', fallback='115200'))
|
||||
timeout = float(self.config_manager.get_config_value('REMOTE', 'timeout', fallback='0.1'))
|
||||
|
||||
instance_config: Dict[str, Any] = {
|
||||
'enable': True,
|
||||
'port': port,
|
||||
'baudrate': baudrate,
|
||||
'timeout': timeout,
|
||||
'strict_crc': bool(str(self.config_manager.get_config_value('REMOTE', 'strict_crc', fallback='False')).lower() == 'true'),
|
||||
}
|
||||
|
||||
super().__init__("remote", instance_config)
|
||||
self._socketio = socketio
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.timeout = timeout
|
||||
self.bytesize = getattr(serial, 'EIGHTBITS', 8)
|
||||
self.parity = getattr(serial, 'PARITY_NONE', 'N')
|
||||
self.stopbits = getattr(serial, 'STOPBITS_ONE', 1)
|
||||
self.strict_crc = instance_config['strict_crc']
|
||||
|
||||
self._ser: Optional[serial.Serial] = None
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._running = False
|
||||
self._buffer = bytearray()
|
||||
|
||||
def initialize(self) -> bool:
|
||||
try:
|
||||
self.logger.info(f"初始化遥控器串口: {self.port}, {self.baudrate}bps, 8N1")
|
||||
self.set_connected(False)
|
||||
self._device_info['initialized_at'] = time.time()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"遥控器初始化失败: {e}")
|
||||
self.set_connected(False)
|
||||
return False
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
try:
|
||||
if self._running:
|
||||
return True
|
||||
if serial is None:
|
||||
raise RuntimeError("pyserial 未安装或不可用")
|
||||
self._ser = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
bytesize=self.bytesize,
|
||||
parity=self.parity,
|
||||
stopbits=self.stopbits,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
self.set_connected(True)
|
||||
self.update_heartbeat()
|
||||
self.is_streaming = True
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._worker_loop, daemon=True)
|
||||
self._thread.start()
|
||||
self.logger.info("遥控器串口监听已启动")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"启动遥控器监听失败: {e}")
|
||||
self._running = False
|
||||
try:
|
||||
if self._ser and self._ser.is_open:
|
||||
self._ser.close()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def stop_streaming(self) -> bool:
|
||||
try:
|
||||
self._running = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=2.0)
|
||||
if self._ser and self._ser.is_open:
|
||||
self._ser.close()
|
||||
self._ser = None
|
||||
self.set_connected(False)
|
||||
self.is_streaming = False
|
||||
self.logger.info("遥控器串口监听已停止")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"停止遥控器监听失败: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
try:
|
||||
self.stop_streaming()
|
||||
self.set_connected(False)
|
||||
except Exception as e:
|
||||
self.logger.error(f"断开遥控器失败: {e}")
|
||||
|
||||
def reload_config(self) -> bool:
|
||||
try:
|
||||
self.logger.info("重新加载遥控器配置")
|
||||
self.port = self.config_manager.get_config_value('REMOTE', 'port', fallback=self.port) or self.port
|
||||
self.baudrate = int(self.config_manager.get_config_value('REMOTE', 'baudrate', fallback=self.baudrate))
|
||||
self.timeout = float(self.config_manager.get_config_value('REMOTE', 'timeout', fallback=self.timeout))
|
||||
self._device_info.update({
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'timeout': self.timeout,
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"重新加载遥控器配置失败: {e}")
|
||||
return False
|
||||
|
||||
def calibrate(self) -> Dict[str, Any]:
|
||||
return {'status': 'success'}
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.device_name,
|
||||
'port': self.port,
|
||||
'baudrate': self.baudrate,
|
||||
'timeout': self.timeout,
|
||||
'is_connected': self.is_connected,
|
||||
'is_streaming': self._running,
|
||||
'has_serial': bool(self._ser and getattr(self._ser, 'is_open', False)),
|
||||
'last_error': self._device_info.get('last_error')
|
||||
}
|
||||
|
||||
def cleanup(self) -> None:
|
||||
try:
|
||||
self.stop_streaming()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def check_hardware_connection(self) -> bool:
|
||||
try:
|
||||
return bool(self._ser and self._ser.is_open)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _emit_code(self, key_code: int):
|
||||
code_hex = f"{key_code:02X}"
|
||||
name_map = {
|
||||
0x11: "start",
|
||||
0x14: "stop",
|
||||
0x13: "up",
|
||||
0x15: "down",
|
||||
0x12: "center",
|
||||
0x0E: "power",
|
||||
0x0F: "screenshot",
|
||||
}
|
||||
name = name_map.get(key_code, 'unknown')
|
||||
payload = {
|
||||
'code': code_hex,
|
||||
'name': name,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
try:
|
||||
msg = f"接收到遥控器按键: code={code_hex}, name={name}"
|
||||
print(msg)
|
||||
self.logger.info(msg)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self._socketio:
|
||||
self._socketio.emit('remote_control', payload, namespace='/devices')
|
||||
except Exception as e:
|
||||
self.logger.error(f"推送遥控器事件失败: {e}")
|
||||
|
||||
def _try_parse_frames(self):
|
||||
while True:
|
||||
if len(self._buffer) < 7:
|
||||
break
|
||||
idx = self._buffer.find(b'\x01\x04\x02\x00')
|
||||
if idx >= 0 and len(self._buffer) - idx >= 7:
|
||||
frame = bytes(self._buffer[idx:idx + 7])
|
||||
calc_crc = _modbus_crc16(frame[:5])
|
||||
recv_crc = frame[5] | (frame[6] << 8)
|
||||
if calc_crc == recv_crc:
|
||||
key_code = frame[4]
|
||||
self._emit_code(key_code)
|
||||
del self._buffer[:idx + 7]
|
||||
continue
|
||||
else:
|
||||
if not self.strict_crc and len(frame) >= 7:
|
||||
key_code = frame[4]
|
||||
self._emit_code(key_code)
|
||||
del self._buffer[:idx + 7]
|
||||
continue
|
||||
del self._buffer[idx:idx + 1]
|
||||
continue
|
||||
# ASCII HEX fallback
|
||||
try:
|
||||
text = bytes(self._buffer).decode(errors='ignore')
|
||||
except Exception:
|
||||
break
|
||||
m = re.search(r'01\s*04\s*02\s*00\s*([0-9A-Fa-f]{2})\s*([0-9A-Fa-f]{2})\s*([0-9A-Fa-f]{2})', text)
|
||||
if m:
|
||||
k = int(m.group(1), 16)
|
||||
crcL = int(m.group(2), 16)
|
||||
crcH = int(m.group(3), 16)
|
||||
calc = _modbus_crc16(bytes.fromhex(f'01 04 02 00 {m.group(1)}'))
|
||||
recv = crcL | (crcH << 8)
|
||||
if calc == recv:
|
||||
self._emit_code(k)
|
||||
elif not self.strict_crc:
|
||||
self._emit_code(k)
|
||||
self._buffer.clear()
|
||||
break
|
||||
# no header; trim
|
||||
del self._buffer[:max(0, len(self._buffer) - 3)]
|
||||
break
|
||||
|
||||
def _worker_loop(self):
|
||||
self.logger.info("遥控器串口线程启动")
|
||||
last_rx_ts = time.time()
|
||||
while self._running:
|
||||
try:
|
||||
if not self._ser or not self._ser.is_open:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
chunk = self._ser.read(64)
|
||||
self.update_heartbeat()
|
||||
if chunk:
|
||||
try:
|
||||
hexstr = ' '.join(f'{b:02X}' for b in chunk)
|
||||
except Exception:
|
||||
pass
|
||||
self._buffer.extend(chunk)
|
||||
self._try_parse_frames()
|
||||
last_rx_ts = time.time()
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
if time.time() - last_rx_ts > 5.0:
|
||||
self.logger.debug("遥控器串口暂无数据")
|
||||
except Exception as 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)
|
||||
self.logger.info("遥控器串口线程结束")
|
||||
@ -84,7 +84,8 @@ class DeviceTestServer:
|
||||
|
||||
# 设备管理器和模拟数据生成器
|
||||
self.device_managers = {
|
||||
'camera': CameraManager(self.socketio, self.config_manager),
|
||||
'camera1': CameraManager(self.socketio, self.config_manager, device_name='camera1'),
|
||||
'camera2': CameraManager(self.socketio, self.config_manager, device_name='camera2'),
|
||||
'femtobolt': FemtoBoltManager(self.socketio, self.config_manager),
|
||||
'imu': IMUManager(self.socketio, self.config_manager),
|
||||
'pressure': PressureManager(self.socketio, self.config_manager)
|
||||
@ -340,7 +341,8 @@ class DeviceTestServer:
|
||||
def _get_event_name(self, device_name: str) -> str:
|
||||
"""获取设备对应的事件名称"""
|
||||
event_map = {
|
||||
'camera': 'camera_frame',
|
||||
'camera1': 'camera_frame',
|
||||
'camera2': 'camera_frame',
|
||||
'femtobolt': 'femtobolt_frame',
|
||||
'imu': 'imu_data',
|
||||
'pressure': 'pressure_data'
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-family: 'Noto Sans SC';
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
|
||||
@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
配置API测试脚本
|
||||
用于测试设备配置HTTP API的功能
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class ConfigAPITester:
|
||||
"""配置API测试器"""
|
||||
|
||||
def __init__(self, base_url="http://localhost:5002"):
|
||||
"""
|
||||
初始化测试器
|
||||
|
||||
Args:
|
||||
base_url: API基础URL
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.api_url = f"{base_url}/api/config"
|
||||
|
||||
def test_get_all_configs(self):
|
||||
"""测试获取所有设备配置"""
|
||||
print("\n=== 测试获取所有设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 获取所有设备配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_get_single_config(self, device_name):
|
||||
"""测试获取单个设备配置"""
|
||||
print(f"\n=== 测试获取{device_name}设备配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/devices/{device_name}")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print(f"✓ 获取{device_name}配置成功")
|
||||
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 获取失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_imu_config(self):
|
||||
"""测试设置IMU配置"""
|
||||
print("\n=== 测试设置IMU配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"port": "COM6",
|
||||
"baudrate": 9600
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/imu", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置IMU配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_pressure_config(self):
|
||||
"""测试设置压力板配置"""
|
||||
print("\n=== 测试设置压力板配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_type": "real",
|
||||
"use_mock": False,
|
||||
"port": "COM5",
|
||||
"baudrate": 115200
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/pressure", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置压力板配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_camera_config(self):
|
||||
"""测试设置相机配置"""
|
||||
print("\n=== 测试设置相机配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"device_index": 0,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 30
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/camera", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置相机配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_set_femtobolt_config(self):
|
||||
"""测试设置FemtoBolt配置"""
|
||||
print("\n=== 测试设置FemtoBolt配置 ===")
|
||||
try:
|
||||
data = {
|
||||
"color_resolution": "1080P",
|
||||
"depth_mode": "NFOV_UNBINNED",
|
||||
"fps": 30,
|
||||
"depth_range_min": 1200,
|
||||
"depth_range_max": 1500
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/devices/femtobolt", json=data)
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 设置FemtoBolt配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
print("更新后的配置:")
|
||||
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"✗ 设置失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_validate_config(self):
|
||||
"""测试验证配置"""
|
||||
print("\n=== 测试验证配置 ===")
|
||||
try:
|
||||
response = requests.get(f"{self.api_url}/validate")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 配置验证成功")
|
||||
validation_result = result['data']
|
||||
print(f"配置有效性: {validation_result['valid']}")
|
||||
if validation_result['errors']:
|
||||
print(f"错误: {validation_result['errors']}")
|
||||
if validation_result['warnings']:
|
||||
print(f"警告: {validation_result['warnings']}")
|
||||
else:
|
||||
print(f"✗ 验证失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def test_reload_config(self):
|
||||
"""测试重新加载配置"""
|
||||
print("\n=== 测试重新加载配置 ===")
|
||||
try:
|
||||
response = requests.post(f"{self.api_url}/reload")
|
||||
result = response.json()
|
||||
|
||||
if result['success']:
|
||||
print("✓ 重新加载配置成功")
|
||||
print(f"消息: {result['message']}")
|
||||
else:
|
||||
print(f"✗ 重新加载失败: {result['message']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 请求异常: {e}")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
print("开始配置API功能测试...")
|
||||
print(f"API地址: {self.api_url}")
|
||||
|
||||
# 等待服务启动
|
||||
print("\n等待API服务启动...")
|
||||
time.sleep(2)
|
||||
|
||||
# 运行测试
|
||||
self.test_get_all_configs()
|
||||
|
||||
# 测试获取单个设备配置
|
||||
for device in ['imu', 'pressure', 'camera', 'femtobolt']:
|
||||
self.test_get_single_config(device)
|
||||
|
||||
# 测试设置配置
|
||||
self.test_set_imu_config()
|
||||
self.test_set_pressure_config()
|
||||
self.test_set_camera_config()
|
||||
self.test_set_femtobolt_config()
|
||||
|
||||
# 测试其他功能
|
||||
self.test_validate_config()
|
||||
self.test_reload_config()
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建测试器并运行测试
|
||||
tester = ConfigAPITester()
|
||||
tester.run_all_tests()
|
||||
@ -96,14 +96,16 @@ class ConfigManager:
|
||||
|
||||
# 默认设备配置
|
||||
self.config['DEVICES'] = {
|
||||
'imu_port': 'COM7',
|
||||
'imu_baudrate': '9600',
|
||||
'imu_enable': 'False',
|
||||
'imu_use_mock': 'False',
|
||||
'imu_ble_name': '',
|
||||
'imu_mac_address': '',
|
||||
'pressure_port': 'COM8',
|
||||
'pressure_baudrate': '115200'
|
||||
}
|
||||
|
||||
# 默认相机配置
|
||||
self.config['CAMERA'] = {
|
||||
# 默认相机1配置
|
||||
self.config['CAMERA1'] = {
|
||||
'device_index': '0',
|
||||
'width': '1280',
|
||||
'height': '720',
|
||||
@ -111,6 +113,15 @@ class ConfigManager:
|
||||
'backend': 'directshow'
|
||||
}
|
||||
|
||||
# 默认相机2配置
|
||||
self.config['CAMERA2'] = {
|
||||
'device_index': '1',
|
||||
'width': '1280',
|
||||
'height': '720',
|
||||
'fps': '30',
|
||||
'backend': 'directshow'
|
||||
}
|
||||
|
||||
# 默认FemtoBolt配置
|
||||
self.config['FEMTOBOLT'] = {
|
||||
'color_resolution': '1080P',
|
||||
@ -134,7 +145,7 @@ class ConfigManager:
|
||||
获取设备配置
|
||||
|
||||
Args:
|
||||
device_name: 设备名称 (camera, femtobolt, imu, pressure)
|
||||
device_name: 设备名称 (camera1, camera2, femtobolt, imu, pressure, remote)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设备配置字典
|
||||
@ -144,14 +155,18 @@ class ConfigManager:
|
||||
|
||||
config = {}
|
||||
|
||||
if device_name == 'camera':
|
||||
config = self._get_camera_config()
|
||||
if device_name == 'camera1':
|
||||
config = self._get_camera1_config()
|
||||
elif device_name == 'camera2':
|
||||
config = self._get_camera2_config()
|
||||
elif device_name == 'femtobolt':
|
||||
config = self._get_femtobolt_config()
|
||||
elif device_name == 'imu':
|
||||
config = self._get_imu_config()
|
||||
elif device_name == 'pressure':
|
||||
config = self._get_pressure_config()
|
||||
elif device_name == 'remote':
|
||||
config = self._get_remote_config()
|
||||
else:
|
||||
self.logger.warning(f"未知设备类型: {device_name}")
|
||||
|
||||
@ -159,7 +174,7 @@ class ConfigManager:
|
||||
self._device_configs[device_name] = config
|
||||
return config.copy()
|
||||
|
||||
def _get_camera_config(self) -> Dict[str, Any]:
|
||||
def _get_camera1_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取相机配置
|
||||
|
||||
@ -167,16 +182,32 @@ class ConfigManager:
|
||||
Dict[str, Any]: 相机配置
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('CAMERA', 'enabled', fallback=True),
|
||||
'device_index': self.config.getint('CAMERA', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA', 'backend', fallback='directshow')
|
||||
'enable': self.config.getboolean('CAMERA1', 'enable', fallback=False),
|
||||
'device_index': self.config.getint('CAMERA1', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA1', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA1', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA1', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA1', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA1', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA1', 'backend', fallback='directshow')
|
||||
}
|
||||
def _get_camera2_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取相机配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
"""
|
||||
return {
|
||||
'enable': self.config.getboolean('CAMERA2', 'enable', fallback=False),
|
||||
'device_index': self.config.getint('CAMERA2', 'device_index', fallback=0),
|
||||
'width': self.config.getint('CAMERA2', 'width', fallback=1280),
|
||||
'height': self.config.getint('CAMERA2', 'height', fallback=720),
|
||||
'fps': self.config.getint('CAMERA2', 'fps', fallback=30),
|
||||
'buffer_size': self.config.getint('CAMERA2', 'buffer_size', fallback=1),
|
||||
'fourcc': self.config.get('CAMERA2', 'fourcc', fallback='MJPG'),
|
||||
'backend': self.config.get('CAMERA2', 'backend', fallback='directshow')
|
||||
}
|
||||
def _get_femtobolt_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取FemtoBolt配置
|
||||
@ -185,7 +216,7 @@ class ConfigManager:
|
||||
Dict[str, Any]: FemtoBolt配置
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('FEMTOBOLT', 'enabled', fallback=True),
|
||||
'enable': self.config.getboolean('FEMTOBOLT', 'enable', fallback=False),
|
||||
'algorithm_type': self.config.get('FEMTOBOLT', 'algorithm_type', fallback='opencv'),
|
||||
'color_resolution': self.config.get('FEMTOBOLT', 'color_resolution', fallback='1080P'),
|
||||
'depth_mode': self.config.get('FEMTOBOLT', 'depth_mode', fallback='NFOV_UNBINNED'),
|
||||
@ -203,13 +234,10 @@ class ConfigManager:
|
||||
Dict[str, Any]: IMU配置
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('DEVICES', 'imu_enabled', fallback=True),
|
||||
'device_type': self.config.get('DEVICES', 'imu_device_type', fallback='mock'),
|
||||
'port': self.config.get('DEVICES', 'imu_port', fallback='COM7'),
|
||||
'baudrate': self.config.getint('DEVICES', 'imu_baudrate', fallback=9600),
|
||||
'timeout': self.config.getfloat('DEVICES', 'imu_timeout', fallback=1.0),
|
||||
'calibration_samples': self.config.getint('DEVICES', 'imu_calibration_samples', fallback=100),
|
||||
'mac_address': self.config.get('DEVICES', 'imu_mac_address', fallback='ef:3c:1a:0a:fe:02'),
|
||||
'enable': self.config.getboolean('DEVICES', 'imu_enable', fallback=False),
|
||||
'use_mock': self.config.getboolean('DEVICES', 'imu_use_mock', fallback=False),
|
||||
'ble_name': self.config.get('DEVICES', 'imu_ble_name', fallback=''),
|
||||
'mac_address': self.config.get('DEVICES', 'imu_mac_address', fallback='FA:E8:88:06:FE:F3'),
|
||||
}
|
||||
|
||||
def _get_pressure_config(self) -> Dict[str, Any]:
|
||||
@ -220,12 +248,25 @@ class ConfigManager:
|
||||
Dict[str, Any]: 压力传感器配置
|
||||
"""
|
||||
return {
|
||||
'enabled': self.config.getboolean('DEVICES', 'pressure_enabled', fallback=True),
|
||||
'device_type': self.config.get('DEVICES', 'pressure_device_type', fallback='mock'),
|
||||
'enable': self.config.getboolean('DEVICES', 'pressure_enable', fallback=False),
|
||||
'use_mock': self.config.getboolean('DEVICES', 'pressure_use_mock', fallback=False),
|
||||
'port': self.config.get('DEVICES', 'pressure_port', fallback='COM8'),
|
||||
'baudrate': self.config.getint('DEVICES', 'pressure_baudrate', fallback=115200),
|
||||
'timeout': self.config.getfloat('DEVICES', 'pressure_timeout', fallback=1.0),
|
||||
'calibration_samples': self.config.getint('DEVICES', 'pressure_calibration_samples', fallback=50)
|
||||
'baudrate': self.config.getint('DEVICES', 'pressure_baudrate', fallback=115200)
|
||||
}
|
||||
|
||||
def _get_remote_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取远程控制配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 远程控制配置
|
||||
"""
|
||||
return {
|
||||
'enable': self.config.getboolean('REMOTE', 'enable', fallback=False),
|
||||
'port': self.config.get('REMOTE', 'port', fallback='COM6'),
|
||||
'baudrate': self.config.getint('REMOTE', 'baudrate', fallback=115200),
|
||||
'timeout': self.config.getfloat('REMOTE', 'timeout', fallback=0.1),
|
||||
'strict_crc': self.config.getboolean('REMOTE', 'strict_crc', fallback=False)
|
||||
}
|
||||
|
||||
def get_system_config(self) -> Dict[str, Any]:
|
||||
@ -332,7 +373,7 @@ class ConfigManager:
|
||||
warnings = []
|
||||
|
||||
# 验证必需的配置段
|
||||
required_sections = ['DEVICES', 'CAMERA', 'FEMTOBOLT', 'SYSTEM']
|
||||
required_sections = ['DEVICES', 'CAMERA1', 'CAMERA2', 'FEMTOBOLT', 'REMOTE']
|
||||
for section in required_sections:
|
||||
if not self.config.has_section(section):
|
||||
errors.append(f"缺少必需的配置段: {section}")
|
||||
@ -340,8 +381,8 @@ class ConfigManager:
|
||||
# 验证设备配置
|
||||
try:
|
||||
imu_config = self.get_device_config('imu')
|
||||
if not imu_config.get('port'):
|
||||
warnings.append("IMU串口未配置")
|
||||
if imu_config.get('enable') and not imu_config.get('use_mock') and not imu_config.get('mac_address'):
|
||||
warnings.append("IMU未配置MAC地址")
|
||||
except Exception as e:
|
||||
errors.append(f"IMU配置验证失败: {e}")
|
||||
|
||||
@ -351,185 +392,7 @@ class ConfigManager:
|
||||
'valid': len(errors) == 0
|
||||
}
|
||||
|
||||
# HTTP接口设备参数设置方法
|
||||
def set_imu_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置IMU设备配置
|
||||
|
||||
Args:
|
||||
config_data: IMU配置数据
|
||||
{
|
||||
'device_type': 'real' | 'mock' | 'ble',
|
||||
'port': 'COM6',
|
||||
'baudrate': 9600,
|
||||
'mac_address': 'ef:3c:1a:0a:fe:02'
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_device_type', config_data['device_type'])
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_baudrate', str(config_data['baudrate']))
|
||||
if 'mac_address' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_mac_address', config_data['mac_address'])
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"IMU配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'IMU配置更新成功',
|
||||
'config': self.get_device_config('imu')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置IMU配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置IMU配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_pressure_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置压力板设备配置
|
||||
|
||||
Args:
|
||||
config_data: 压力板配置数据
|
||||
{
|
||||
'device_type': 'real' | 'mock',
|
||||
'use_mock': False,
|
||||
'port': 'COM5',
|
||||
'baudrate': 115200
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_device_type', config_data['device_type'])
|
||||
if 'use_mock' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_use_mock', str(config_data['use_mock']))
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_baudrate', str(config_data['baudrate']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"压力板配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '压力板配置更新成功',
|
||||
'config': self.get_device_config('pressure')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置压力板配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置压力板配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_camera_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置相机设备配置
|
||||
|
||||
Args:
|
||||
config_data: 相机配置数据
|
||||
{
|
||||
'device_index': 1,
|
||||
'width': 1280,
|
||||
'height': 720,
|
||||
'fps': 30
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA', 'fps', str(config_data['fps']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA', 'backend', str(config_data['backend']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': self.get_device_config('camera')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置相机配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置相机配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
设置FemtoBolt设备配置
|
||||
|
||||
Args:
|
||||
config_data: FemtoBolt配置数据
|
||||
{
|
||||
'color_resolution': '1080P',
|
||||
'depth_mode': 'NFOV_UNBINNED',
|
||||
'fps': 30,
|
||||
'depth_range_min': 1200,
|
||||
'depth_range_max': 1500
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 设置结果
|
||||
"""
|
||||
try:
|
||||
# 验证必需参数
|
||||
if 'algorithm_type' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'algorithm_type', config_data['algorithm_type'])
|
||||
if 'color_resolution' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'color_resolution', config_data['color_resolution'])
|
||||
if 'depth_mode' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_mode', config_data['depth_mode'])
|
||||
if 'camera_fps' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'camera_fps', str(config_data['camera_fps']))
|
||||
if 'depth_range_min' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_range_min', str(config_data['depth_range_min']))
|
||||
if 'depth_range_max' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'depth_range_max', str(config_data['depth_range_max']))
|
||||
|
||||
# 保存配置
|
||||
self.save_config()
|
||||
|
||||
self.logger.info(f"FemtoBolt配置已更新: {config_data}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'FemtoBolt配置更新成功',
|
||||
'config': self.get_device_config('femtobolt')
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"设置FemtoBolt配置失败: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'设置FemtoBolt配置失败: {str(e)}'
|
||||
}
|
||||
|
||||
|
||||
def get_all_device_configs(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@ -541,8 +404,10 @@ class ConfigManager:
|
||||
return {
|
||||
'imu': self.get_device_config('imu'),
|
||||
'pressure': self.get_device_config('pressure'),
|
||||
'camera': self.get_device_config('camera'),
|
||||
'femtobolt': self.get_device_config('femtobolt')
|
||||
'camera1': self.get_device_config('camera1'),
|
||||
'camera2': self.get_device_config('camera2'),
|
||||
'femtobolt': self.get_device_config('femtobolt'),
|
||||
'remote': self.get_device_config('remote')
|
||||
}
|
||||
|
||||
def _batch_update_device_configs(self, configs: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
||||
@ -563,14 +428,14 @@ class ConfigManager:
|
||||
if 'imu' in configs:
|
||||
try:
|
||||
config_data = configs['imu']
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_device_type', config_data['device_type'])
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_enable', str(config_data['enable']))
|
||||
if 'use_mock' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock']))
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('DEVICES', 'imu_baudrate', str(config_data['baudrate']))
|
||||
if 'mac_address' in config_data:
|
||||
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'] = {
|
||||
'success': True,
|
||||
@ -588,8 +453,8 @@ class ConfigManager:
|
||||
if 'pressure' in configs:
|
||||
try:
|
||||
config_data = configs['pressure']
|
||||
if 'device_type' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_device_type', config_data['device_type'])
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_enable', str(config_data['enable']))
|
||||
if 'use_mock' in config_data:
|
||||
self.set_config_value('DEVICES', 'pressure_use_mock', str(config_data['use_mock']))
|
||||
if 'port' in config_data:
|
||||
@ -610,27 +475,29 @@ class ConfigManager:
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# 相机配置
|
||||
if 'camera' in configs:
|
||||
if 'camera1' in configs:
|
||||
try:
|
||||
config_data = configs['camera']
|
||||
config_data = configs['camera1']
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('CAMERA1', 'enable', str(config_data['enable']))
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index']))
|
||||
self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA', 'width', str(config_data['width']))
|
||||
self.set_config_value('CAMERA1', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA', 'height', str(config_data['height']))
|
||||
self.set_config_value('CAMERA1', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA', 'fps', str(config_data['fps']))
|
||||
self.set_config_value('CAMERA1', 'fps', str(config_data['fps']))
|
||||
if 'buffer_size' in config_data:
|
||||
self.set_config_value('CAMERA', 'buffer_size', str(config_data['buffer_size']))
|
||||
self.set_config_value('CAMERA1', 'buffer_size', str(config_data['buffer_size']))
|
||||
if 'fourcc' in config_data:
|
||||
self.set_config_value('CAMERA', 'fourcc', config_data['fourcc'])
|
||||
self.set_config_value('CAMERA1', 'fourcc', config_data['fourcc'])
|
||||
if 'tx_max_width' in config_data:
|
||||
self.set_config_value('CAMERA', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
self.set_config_value('CAMERA1', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA', 'backend', str(config_data['backend']))
|
||||
self.set_config_value('CAMERA1', 'backend', str(config_data['backend']))
|
||||
|
||||
results['camera'] = {
|
||||
results['camera1'] = {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': config_data
|
||||
@ -638,14 +505,50 @@ class ConfigManager:
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
except Exception as e:
|
||||
error_msg = f'设置相机配置失败: {str(e)}'
|
||||
results['camera'] = {'success': False, 'message': error_msg}
|
||||
results['camera1'] = {'success': False, 'message': error_msg}
|
||||
errors.append(f"相机: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
if 'camera2' in configs:
|
||||
try:
|
||||
config_data = configs['camera2']
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('CAMERA2', 'enable', str(config_data['enable']))
|
||||
if 'device_index' in config_data:
|
||||
self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index']))
|
||||
if 'width' in config_data:
|
||||
self.set_config_value('CAMERA2', 'width', str(config_data['width']))
|
||||
if 'height' in config_data:
|
||||
self.set_config_value('CAMERA2', 'height', str(config_data['height']))
|
||||
if 'fps' in config_data:
|
||||
self.set_config_value('CAMERA2', 'fps', str(config_data['fps']))
|
||||
if 'buffer_size' in config_data:
|
||||
self.set_config_value('CAMERA2', 'buffer_size', str(config_data['buffer_size']))
|
||||
if 'fourcc' in config_data:
|
||||
self.set_config_value('CAMERA2', 'fourcc', config_data['fourcc'])
|
||||
if 'tx_max_width' in config_data:
|
||||
self.set_config_value('CAMERA2', 'tx_max_width', str(config_data['tx_max_width']))
|
||||
if 'backend' in config_data:
|
||||
self.set_config_value('CAMERA2', 'backend', str(config_data['backend']))
|
||||
|
||||
results['camera2'] = {
|
||||
'success': True,
|
||||
'message': '相机配置更新成功',
|
||||
'config': config_data
|
||||
}
|
||||
self.logger.info(f"相机配置已更新: {config_data}")
|
||||
except Exception as e:
|
||||
error_msg = f'设置相机配置失败: {str(e)}'
|
||||
results['camera2'] = {'success': False, 'message': error_msg}
|
||||
errors.append(f"相机2: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# FemtoBolt配置
|
||||
if 'femtobolt' in configs:
|
||||
try:
|
||||
config_data = configs['femtobolt']
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'enable', str(config_data['enable']))
|
||||
if 'algorithm_type' in config_data:
|
||||
self.set_config_value('FEMTOBOLT', 'algorithm_type', config_data['algorithm_type'])
|
||||
if 'color_resolution' in config_data:
|
||||
@ -677,6 +580,33 @@ class ConfigManager:
|
||||
errors.append(f"FemtoBolt: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# Remote配置
|
||||
if 'remote' in configs:
|
||||
try:
|
||||
config_data = configs['remote']
|
||||
if 'enable' in config_data:
|
||||
self.set_config_value('REMOTE', 'enable', str(config_data['enable']))
|
||||
if 'port' in config_data:
|
||||
self.set_config_value('REMOTE', 'port', config_data['port'])
|
||||
if 'baudrate' in config_data:
|
||||
self.set_config_value('REMOTE', 'baudrate', str(config_data['baudrate']))
|
||||
if 'timeout' in config_data:
|
||||
self.set_config_value('REMOTE', 'timeout', str(config_data['timeout']))
|
||||
if 'strict_crc' in config_data:
|
||||
self.set_config_value('REMOTE', 'strict_crc', str(config_data['strict_crc']))
|
||||
|
||||
results['remote'] = {
|
||||
'success': True,
|
||||
'message': '遥控器配置更新成功',
|
||||
'config': config_data
|
||||
}
|
||||
self.logger.info(f"遥控器配置已更新: {config_data}")
|
||||
except Exception as e:
|
||||
error_msg = f'设置遥控器配置失败: {str(e)}'
|
||||
results['remote'] = {'success': False, 'message': error_msg}
|
||||
errors.append(f"Remote: {error_msg}")
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# 一次性保存所有配置
|
||||
if results: # 只有在有配置更新时才保存
|
||||
self.save_config()
|
||||
@ -701,10 +631,12 @@ class ConfigManager:
|
||||
Args:
|
||||
configs: 所有设备配置数据
|
||||
{
|
||||
'imu': {'device_type': 'real', 'port': 'COM7', 'baudrate': 9600},
|
||||
'pressure': {'device_type': 'real', 'port': 'COM8', 'baudrate': 115200},
|
||||
'camera': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15}
|
||||
'imu': {'enable': True, 'use_mock': False, 'mac_address': 'FA:E8:88:06:FE:F3'},
|
||||
'pressure': {'enable': True, 'device_type': 'real', 'use_mock': False, 'port': 'COM8', 'baudrate': 115200},
|
||||
'camera1': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'camera2': {'device_index': 1, 'width': 1280, 'height': 720, 'fps': 30},
|
||||
'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15},
|
||||
'remote': {'enable': True, 'port': 'COM6', 'baudrate': 115200, 'timeout': 0.1, 'strict_crc': False}
|
||||
}
|
||||
|
||||
Returns:
|
||||
@ -743,4 +675,4 @@ class ConfigManager:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'批量设置设备配置失败: {str(e)}'
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
import base64
|
||||
import shutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -51,96 +52,359 @@ class LicenseManager:
|
||||
def __init__(self, config_manager=None):
|
||||
self.config_manager = config_manager
|
||||
self._machine_id = None
|
||||
self._machine_id_candidates = None
|
||||
self._license_cache = 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:
|
||||
"""生成机器硬件指纹"""
|
||||
if self._machine_id:
|
||||
return self._machine_id
|
||||
|
||||
try:
|
||||
# 收集硬件信息
|
||||
hardware_info = []
|
||||
|
||||
# CPU信息
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'ProcessorId=' in line:
|
||||
cpu_id = line.split('=')[1].strip()
|
||||
if cpu_id:
|
||||
hardware_info.append(f"CPU:{cpu_id}")
|
||||
break
|
||||
else:
|
||||
# Linux/Mac 可以使用其他方法获取CPU信息
|
||||
pass
|
||||
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.":
|
||||
hardware_info.append(f"BOARD:{board_serial}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"获取主板信息失败: {e}")
|
||||
|
||||
# 磁盘信息
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(['wmic', 'diskdrive', 'get', 'SerialNumber', '/value'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'SerialNumber=' in line:
|
||||
disk_serial = line.split('=')[1].strip()
|
||||
if disk_serial:
|
||||
hardware_info.append(f"DISK:{disk_serial}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"获取磁盘信息失败: {e}")
|
||||
|
||||
# MAC地址
|
||||
try:
|
||||
import uuid
|
||||
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
|
||||
for elements in range(0,2*6,2)][::-1])
|
||||
hardware_info.append(f"MAC:{mac}")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取MAC地址失败: {e}")
|
||||
|
||||
# 系统信息作为补充
|
||||
hardware_info.append(f"OS:{platform.system()}")
|
||||
hardware_info.append(f"MACHINE:{platform.machine()}")
|
||||
|
||||
# 如果没有获取到足够的硬件信息,使用系统信息作为fallback
|
||||
if len(hardware_info) < 2:
|
||||
hardware_info.append(f"NODE:{platform.node()}")
|
||||
hardware_info.append(f"PROCESSOR:{platform.processor()}")
|
||||
|
||||
# 生成指纹哈希
|
||||
combined_info = "|".join(sorted(hardware_info))
|
||||
machine_id = hashlib.sha256(combined_info.encode('utf-8')).hexdigest()[:16].upper()
|
||||
|
||||
self._machine_id = f"W10-{machine_id}"
|
||||
candidates = self._build_machine_id_candidates()
|
||||
if not candidates:
|
||||
raise RuntimeError("无法生成机器指纹")
|
||||
self._machine_id_candidates = candidates
|
||||
self._machine_id = candidates[0]
|
||||
logger.info(f"生成机器指纹: {self._machine_id}")
|
||||
return self._machine_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成机器指纹失败: {e}")
|
||||
# 使用fallback方案
|
||||
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
|
||||
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
|
||||
self._machine_id = f"FB-{fallback_id}"
|
||||
self._machine_id_candidates = [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]]:
|
||||
"""加载授权文件"""
|
||||
@ -234,7 +498,13 @@ class LicenseManager:
|
||||
|
||||
# 检查机器绑定
|
||||
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})"
|
||||
|
||||
# 检查有效期
|
||||
@ -271,19 +541,8 @@ class LicenseManager:
|
||||
# 获取配置
|
||||
if not self.config_manager:
|
||||
return LicenseStatus(valid=False, message="配置管理器未初始化")
|
||||
|
||||
license_path = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json')
|
||||
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)
|
||||
|
||||
license_path, public_key_path, grace_days = self._resolve_license_paths()
|
||||
|
||||
# 获取机器指纹
|
||||
machine_id = self.get_machine_id()
|
||||
@ -311,6 +570,8 @@ class LicenseManager:
|
||||
self._license_cache = status
|
||||
self._cache_timestamp = datetime.now().timestamp()
|
||||
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)
|
||||
@ -365,14 +626,7 @@ class LicenseManager:
|
||||
if not self.config_manager:
|
||||
return False, "配置管理器未初始化"
|
||||
|
||||
# 解析公钥路径与宽限期
|
||||
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)
|
||||
_, public_key_path, grace_days = self._resolve_license_paths()
|
||||
|
||||
if not os.path.exists(license_path):
|
||||
return False, f"授权文件不存在: {license_path}"
|
||||
@ -401,7 +655,7 @@ class LicenseManager:
|
||||
|
||||
request_data = {
|
||||
"product": "BodyBalanceEvaluation",
|
||||
"version": "1.0.0",
|
||||
"version": "1.5.0",
|
||||
"machine_id": machine_id,
|
||||
"platform": platform.system(),
|
||||
"request_time": datetime.now(timezone.utc).isoformat(),
|
||||
@ -429,4 +683,4 @@ class LicenseManager:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成激活请求失败: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
BIN
backend/ffmpeg/bin/ffmpeg.exe
Normal file
BIN
backend/ffmpeg/bin/ffplay.exe
Normal file
BIN
backend/ffmpeg/bin/ffprobe.exe
Normal file
906
backend/main.py
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
|
||||
@ -46,7 +46,7 @@ class Config:
|
||||
# 应用配置
|
||||
self.config['APP'] = {
|
||||
'name': 'Body Balance Evaluation System',
|
||||
'version': '1.0.0',
|
||||
'version': '1.5.0',
|
||||
'debug': 'false',
|
||||
'log_level': 'INFO'
|
||||
}
|
||||
@ -218,20 +218,19 @@ class DataValidator:
|
||||
name = data['name'].strip()
|
||||
if len(name) < 2 or len(name) > 50:
|
||||
errors.append('姓名长度应在2-50个字符之间')
|
||||
data['name'] = name
|
||||
data['name'] = name
|
||||
|
||||
# 性别验证
|
||||
if data.get('gender'):
|
||||
if data['gender'] not in ['male', 'female', 'other']:
|
||||
errors.append('性别值无效')
|
||||
|
||||
# 出生日期验证
|
||||
if data.get('birth_date'):
|
||||
try:
|
||||
birth_date = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00'))
|
||||
if birth_date > datetime.now():
|
||||
birth_dt = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00'))
|
||||
birth = birth_dt.date()
|
||||
today = datetime.now().date()
|
||||
lower = datetime(1900, 1, 1).date()
|
||||
if birth > today:
|
||||
errors.append('出生日期不能是未来时间')
|
||||
if birth_date < datetime(1900, 1, 1):
|
||||
if birth < lower:
|
||||
errors.append('出生日期过早')
|
||||
except ValueError:
|
||||
errors.append('出生日期格式无效')
|
||||
@ -518,4 +517,4 @@ class ResponseFormatter:
|
||||
config = Config()
|
||||
|
||||
# 性能监控实例
|
||||
performance_monitor = PerformanceMonitor()
|
||||
performance_monitor = PerformanceMonitor()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[APP]
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
version = 1.5.0
|
||||
debug = false
|
||||
log_level = INFO
|
||||
|
||||
@ -35,7 +35,7 @@ chart_dpi = 300
|
||||
export_format = csv
|
||||
|
||||
[SECURITY]
|
||||
secret_key = f50c705c26a963701a4832ae3d69a091674f587a4b02da8b1c59909c0bd312fe
|
||||
secret_key = 855842922ac3d1747493bcf40f0b2534387ac6304b903c901cf980e4059d5150
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
|
||||
259
document/Web接口调用说明.md
Normal file
@ -0,0 +1,259 @@
|
||||
# Web 接口调用说明
|
||||
|
||||
本文档基于 `backend/main.py` 中注册的路由,整理对外提供的 Web API 调用方式、参数与返回示例,便于前端或第三方系统集成。
|
||||
|
||||
## 总览
|
||||
|
||||
- 基础与健康检查
|
||||
- `GET /health`
|
||||
- `GET /api/health`
|
||||
- 授权相关
|
||||
- `GET /api/license/info`
|
||||
- `POST /api/license/activation-request`
|
||||
- `POST /api/license/verify`
|
||||
- `POST /api/license/activate-package`
|
||||
- 认证与用户
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/register`
|
||||
- `POST /api/auth/logout`
|
||||
- `GET /api/auth/verify`
|
||||
- `POST /api/auth/forgot-password`
|
||||
- `GET /api/users`
|
||||
- `POST /api/users/<user_id>/approve`
|
||||
- `DELETE /api/users/<user_id>`
|
||||
- 设备与配置
|
||||
- `GET /api/devices/status`
|
||||
- `POST /api/devices/refresh`
|
||||
- `GET /api/config/devices`
|
||||
- `POST /api/config/devices/all`
|
||||
- `POST /api/devices/calibrate`
|
||||
- `POST /api/devices/calibrate/imu`
|
||||
- 患者管理
|
||||
- `GET /api/patients`
|
||||
- `GET|PUT|DELETE /api/patients/<patient_id>`
|
||||
- 检测流程
|
||||
- `POST /api/detection/start`
|
||||
- `GET /api/detection/<session_id>/has_data`
|
||||
- `POST /api/detection/<session_id>/start_record`
|
||||
- `POST /api/detection/<session_id>/stop_record`
|
||||
- `POST /api/detection/<session_id>/save-data`
|
||||
- `POST /api/detection/<session_id>/save-info`
|
||||
- `GET /api/detection/<session_id>/status`
|
||||
- `POST /api/detection/<session_id>/stop`
|
||||
- 历史与数据查询
|
||||
- `GET /api/history/sessions`
|
||||
- `GET /api/history/sessions/<session_id>`
|
||||
- `GET /api/detection/data/details?ids=<id1,id2,...>`
|
||||
- 删除操作
|
||||
- `DELETE /api/detection/data/<data_id[,data_id2,...]>`
|
||||
- `DELETE /api/detection/video/<video_id[,video_id2,...]>`
|
||||
- `DELETE /api/detection/sessions/<session_id>`
|
||||
|
||||
## 统一响应约定
|
||||
|
||||
- 成功:`{ "success": true, ... }`
|
||||
- 失败:`{ "success": false, "error": "错误信息" }`
|
||||
- 时间字段统一使用 ISO 文本或 `YYYY-MM-DD HH:mm:ss` 字符串。
|
||||
|
||||
## 基础与授权
|
||||
|
||||
### GET /health | GET /api/health
|
||||
- 功能:健康检查与服务存活状态。
|
||||
- 示例响应:
|
||||
```json
|
||||
{ "status": "healthy", "timestamp": "2024-01-01T12:00:00", "version": "1.5.0" }
|
||||
```
|
||||
|
||||
### GET /api/license/info
|
||||
- 功能:获取授权状态与基础信息。
|
||||
- 响应字段:`valid`, `message`, `license_type`, `license_id`, `expires_at`, `features`, `machine_id`。
|
||||
|
||||
### POST /api/license/activation-request
|
||||
- 功能:生成离线激活请求文件。
|
||||
- Body:`{ "company_name": "公司名", "contact_info": "联系方式" }`
|
||||
- 返回:`request_file` 路径与 `content` 文本。
|
||||
|
||||
### POST /api/license/verify
|
||||
- 功能:上传并验证授权文件。
|
||||
- Form-Data:`license_file`(文件)。
|
||||
- 返回:授权验证结果与解析出的授权信息。
|
||||
|
||||
### POST /api/license/activate-package
|
||||
- 功能:上传激活包进行离线激活。
|
||||
- 细节见服务端实现,返回激活状态与信息。
|
||||
|
||||
## 认证与用户
|
||||
|
||||
### POST /api/auth/login
|
||||
- 功能:用户登录。
|
||||
- Body:`{ "username": "string", "password": "string" }`
|
||||
|
||||
### POST /api/auth/register
|
||||
- 功能:用户注册。
|
||||
- Body:包含用户名、密码、手机号等注册信息。
|
||||
|
||||
### POST /api/auth/logout
|
||||
- 功能:登出当前会话。
|
||||
|
||||
### GET /api/auth/verify
|
||||
- 功能:登录状态校验。
|
||||
|
||||
### POST /api/auth/forgot-password
|
||||
- 功能:忘记密码,根据用户名和手机号找回。
|
||||
- Body:`{ "username": "string", "phone": "string" }`
|
||||
|
||||
### GET /api/users
|
||||
- 功能:获取用户列表。
|
||||
- Query 可选:分页参数。
|
||||
|
||||
### POST /api/users/<user_id>/approve
|
||||
- 功能:批准用户,使其 `is_active = 1`。
|
||||
- Body 可选:`{ "approved_by": 123 }`
|
||||
|
||||
### DELETE /api/users/<user_id>
|
||||
- 功能:删除用户。
|
||||
|
||||
## 患者管理
|
||||
|
||||
### GET /api/patients
|
||||
- 功能:获取患者列表。
|
||||
- Query 可选:分页筛选。
|
||||
|
||||
### POST /api/patients
|
||||
- 功能:创建新患者。
|
||||
- Body:患者基本信息字段(姓名、性别、出生日期等)。
|
||||
- 必填字段:`name`(姓名)、`gender`(性别)、`birth_date`(出生日期)
|
||||
- 可选字段:`phone`(电话)、`email`(邮箱)、`height`(身高)、`weight`(体重)、`nationality`(民族)、`residence`(居住地)、`occupation`(职业)、`workplace`(工作单位)、`idcode`(身份证号)、`medical_history`(病史)、`notes`(备注)
|
||||
|
||||
### GET|PUT|DELETE /api/patients/<patient_id>
|
||||
- 功能:获取/更新/删除单个患者。
|
||||
- PUT Body:患者基本信息字段(姓名、性别、出生日期、联系方式等)。
|
||||
|
||||
## 设备与配置
|
||||
|
||||
### GET /api/devices/status
|
||||
- 功能:获取各设备连接与工作状态。
|
||||
|
||||
### POST /api/devices/refresh
|
||||
- 功能:刷新设备状态(重扫/重连)。
|
||||
|
||||
### GET /api/config/devices
|
||||
- 功能:获取当前设备配置。
|
||||
|
||||
### POST /api/config/devices/all
|
||||
- 功能:批量设置设备配置。
|
||||
- Body:设备配置对象集合。
|
||||
|
||||
### POST /api/devices/calibrate
|
||||
- 功能:触发设备标定(通用)。
|
||||
|
||||
### POST /api/devices/calibrate/imu
|
||||
- 功能:仅触发 IMU 设备标定。
|
||||
|
||||
## 检测流程
|
||||
|
||||
### POST /api/detection/start
|
||||
- 功能:创建检测会话并启动设备连接监控。
|
||||
- Body:`{ "patient_id": "string", "creator_id": "string" }`
|
||||
- 返回:`session_id`。
|
||||
|
||||
### GET /api/detection/<session_id>/has_data
|
||||
- 功能:会话是否存在检测数据,用于判断单次检测是否有效。
|
||||
- 返回:`{ "has_data": true|false }`
|
||||
### GET /api/detection/<session_id>/status
|
||||
- 功能:获取会话最新状态与聚合数据(源自 `get_session_data`)。
|
||||
|
||||
### POST /api/detection/<session_id>/stop
|
||||
- 功能:结束检测并写入总结信息(自动计算时长与结束时间)。
|
||||
- Body 可选:
|
||||
```json
|
||||
{
|
||||
"diagnosis_info": "string",
|
||||
"treatment_info": "string",
|
||||
"remark_info": "string"
|
||||
}
|
||||
```
|
||||
- 说明:服务端已统一走 `update_session_endcheck` 数据库流程。
|
||||
### POST /api/detection/<session_id>/start_record
|
||||
- 功能:开始同步录制(屏幕/相机/设备流)。
|
||||
- Body 示例:
|
||||
```json
|
||||
{
|
||||
"patient_id": "p001",
|
||||
"screen_location": [0,0,1920,1080],
|
||||
"femtobolt_location": [0,0,640,480],
|
||||
"camera1_location": [0,0,640,480],
|
||||
"camera2_location": [0,0,640,480]
|
||||
}
|
||||
```
|
||||
- 返回:`recording` 含 `database_updates.video_paths`;服务端会自动调用 `save_detection_video` 记录视频路径(`screen_video_path`, `femtobolt_video_path`, `camera1_video_path`, `camera2_video_path`)。
|
||||
|
||||
### POST /api/detection/<session_id>/stop_record
|
||||
- 功能:停止同步录制。
|
||||
|
||||
### POST /api/detection/<session_id>/save-data
|
||||
- 功能:采集检测数据与截图并持久化。
|
||||
- Body:检测数据载荷(结构由实际采集模块决定)。
|
||||
- 返回:`timestamp` 与是否采集成功。
|
||||
|
||||
### POST /api/detection/<session_id>/save-info
|
||||
- 功能:保存会话信息(诊断、处理、建议、状态)。
|
||||
- Body:
|
||||
```json
|
||||
{
|
||||
"diagnosis_info": "string",
|
||||
"treatment_info": "string",
|
||||
"remark_info": "string",
|
||||
"status": "completed|running|..."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 历史与数据查询
|
||||
|
||||
### GET /api/history/sessions
|
||||
- 功能:分页获取检测会话历史。
|
||||
- Query:`page`(默认 1)、`size`(默认 10)、`patient_id`(可选)。
|
||||
- 返回:`sessions` 与 `total`。
|
||||
|
||||
### GET /api/history/sessions/<session_id>
|
||||
- 功能:获取会话详细数据(聚合会话、检测数据、视频)。
|
||||
|
||||
### GET /api/detection/data/details?ids=<id1,id2,...>
|
||||
- 功能:根据多个逗号分隔的 ID 批量获取检测数据详情。
|
||||
- Query:`ids` 逗号分隔字符串。
|
||||
|
||||
## 删除操作
|
||||
|
||||
### DELETE /api/detection/data/<data_id[,data_id2,...]>
|
||||
- 功能:删除检测数据记录,支持多个 ID 逗号分隔。
|
||||
- 返回:`deleted_ids` 列表。
|
||||
|
||||
### DELETE /api/detection/video/<video_id[,video_id2,...]>
|
||||
- 功能:删除检测视频记录,支持多个 ID 逗号分隔。
|
||||
- 返回:`deleted_ids` 列表。
|
||||
|
||||
### DELETE /api/detection/sessions/<session_id>
|
||||
- 功能:删除检测会话,同时清理关联的检测数据与视频记录。
|
||||
|
||||
## 错误码与常见失败
|
||||
|
||||
- 400:缺少必要参数或请求体格式错误。
|
||||
- 403:授权校验失败或未授权功能。
|
||||
- 404:会话或资源不存在。
|
||||
- 500:服务内部错误(设备不可用、数据库失败等)。
|
||||
|
||||
## 典型调用序列(示例)
|
||||
|
||||
1. 创建会话:`POST /api/detection/start`
|
||||
2. 开始录制:`POST /api/detection/<session_id>/start_record`
|
||||
3. 采集数据:多次 `POST /api/detection/<session_id>/save-data`
|
||||
4. 停止录制:`POST /api/detection/<session_id>/stop_record`
|
||||
5. 保存诊断建议:`POST /api/detection/<session_id>/save-info`
|
||||
6. 结束检测:`POST /api/detection/<session_id>/stop`
|
||||
7. 校验有效性:`GET /api/detection/<session_id>/has_data`
|
||||
8. 查询历史:`GET /api/history/sessions`
|
||||
9. 查看详情:`GET /api/history/sessions/<session_id>`
|
||||
|
||||
> 注:具体字段与数据结构以采集模块与数据库模型为准,本文档以主干流程为纲要,建议结合实际返回进行前端适配与校验。
|
||||
70
document/串口遥控器遥控界面操作说明.md
Normal file
@ -0,0 +1,70 @@
|
||||
# 串口遥控器遥控界面操作说明
|
||||
|
||||
## 概述
|
||||
- 通过串口接收遥控器报文,解析键码并通过 WebSocket 推送到前端,实现对检测页面的远程控制。
|
||||
- 后端设备名为 `remote`,事件名为 `remote_control`,命名空间为 `/devices`。
|
||||
|
||||
## 串口配置
|
||||
- 配置文件位置:backend/config.ini
|
||||
- 读取段与键:
|
||||
- [REMOTE] port,缺省 COM6
|
||||
- [REMOTE] baudrate,缺省 115200
|
||||
- [REMOTE] timeout,缺省 0.1 秒
|
||||
- [DEVICES] remote_enable(是否启用),缺省 true
|
||||
- 串口参数:115200 bps,8 数据位,1 停止位,无校验(8N1)。
|
||||
|
||||
## 报文格式
|
||||
- 参照 Modbus RTU 协议中功能码 0x04(读输入寄存器)的应答帧格式:
|
||||
- 帧结构:`01 04 02 00 [键码] crcL crcH`
|
||||
- 固定头:`01 04 02 00`
|
||||
- 第 5 字节为键码(KeyCode)
|
||||
- CRC16:Modbus RTU 小端(crcL, crcH),计算范围为前 5 个字节
|
||||
- 报文由接收器主动上传,无需主机轮询
|
||||
- 除键码与 CRC 外,前 4 字节保持不变
|
||||
|
||||
## 键码约定
|
||||
- 左:`11`
|
||||
- 右:`14`
|
||||
- 上:`13`
|
||||
- 下:`15`
|
||||
- 中:`12`
|
||||
- 电源:`0E`
|
||||
- 抓屏:`0F`
|
||||
|
||||
## 后端实现
|
||||
- 代码文件:`backend/devices/remote_control_manager.py`
|
||||
- 主要逻辑:
|
||||
- 打开串口并启动后台线程读取数据
|
||||
- 在缓冲区中查找帧头 `01 04 02 00`,截取 7 字节帧
|
||||
- 计算前 5 字节 Modbus CRC16(多项式 0xA001,初值 0xFFFF),校验通过后解析键码
|
||||
- 通过 Socket.IO 向 `/devices` 命名空间推送事件 `remote_control`,载荷示例:
|
||||
- `{ "code": "0F", "name": "screenshot", "timestamp": 1731234567.89 }`
|
||||
|
||||
## 前端对接
|
||||
- 页面:`frontend/src/renderer/src/views/Detection.vue`
|
||||
- 统一设备命名空间 Socket:`devicesSocket = io(BACKEND_URL + '/devices', ...)`
|
||||
- 事件监听与映射:
|
||||
- 监听:`devicesSocket.on('remote_control', handler)`
|
||||
- 根据编码触发页面方法:
|
||||
- `11` → `startVideoClick()`(开始录像)
|
||||
- `14` → `stopVideoClick()`(结束录像)
|
||||
- `0F` → `saveDetectionData()`(截图)
|
||||
- 页面中相关按钮:
|
||||
- 截图按钮:调用 `saveDetectionData`
|
||||
- 开始录像按钮:调用 `startVideoClick`
|
||||
- 结束录像按钮:调用 `stopVideoClick`
|
||||
|
||||
## 运行与验证
|
||||
- 打包后 Electron 主进程会在窗口创建前启动后端服务
|
||||
- 打开检测页面,确保设备命名空间连接成功
|
||||
- 使用遥控器按键,观察页面动作对应触发
|
||||
|
||||
## 常见问题
|
||||
- 无法接收到事件:
|
||||
- 检查后端串口配置是否正确(端口被占用或不存在)
|
||||
- 确认遥控器接收器已连接且在串口管理器线程持续读取
|
||||
- 确认前端已连接到 `/devices` 命名空间并注册了事件监听
|
||||
- CRC 错误:
|
||||
- 检查物理连接和电气参数
|
||||
- 若报文格式与约定不一致,请提供示例报文以调整解析逻辑
|
||||
|
||||
113
document/前端生成PDF并上传后端技术方案.md
Normal file
@ -0,0 +1,113 @@
|
||||
# 前端生成 PDF 并上传后端技术方案
|
||||
|
||||
## 概述
|
||||
- 目标:诊断报告页面在前端生成高保真 PDF,上传到 Python 后端持久化,并把文件路径写入对应检测会话。
|
||||
- 推荐:使用 Electron 主进程 `webContents.printToPDF` 生成 PDF,忠实度高、分页与中文字体友好;作为短期备选,提供 `html2canvas` 截图转 PDF 的实现。
|
||||
|
||||
## 架构流程
|
||||
- 前端渲染进程:用户在报告页面点击“生成报告”→ 发送 IPC 请求至主进程生成 PDF → 主进程返回 PDF Buffer 给渲染进程 → 渲染进程上传后端。
|
||||
- Python 后端:接收 PDF 文件流,写入 `backend/static/reports/<YYYY-MM-DD>/<session_id>.pdf`,记录相对路径到 `detection_sessions` 的报告字段。
|
||||
|
||||
## 前端实现(Electron 渲染)
|
||||
### 报告页面打印版式
|
||||
- 报告根容器,例如 `#report-root`。
|
||||
- 打印样式建议:
|
||||
- `@page { size: A4; margin: 12mm }`
|
||||
- 固定宽度设计为 A4 比例,非交互元素隐藏,保留背景。
|
||||
- 通过添加类名(例如 `.print-mode`)切换打印样式。
|
||||
|
||||
### 渲染进程触发生成
|
||||
```ts
|
||||
// renderer: 诊断报告视图中
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
async function generatePdf(sessionId: string) {
|
||||
const pdfBuffer: ArrayBuffer = await ipcRenderer.invoke('generate-report-pdf', {
|
||||
selector: '#report-root',
|
||||
pageSize: 'A4',
|
||||
printBackground: true
|
||||
})
|
||||
const blob = new Blob([pdfBuffer], { type: 'application/pdf' })
|
||||
const form = new FormData()
|
||||
form.append('file', blob, `${sessionId}.pdf`)
|
||||
|
||||
const res = await fetch(`${getBackendUrl()}/api/reports/${sessionId}/upload`, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
const json = await res.json()
|
||||
if (!json.success) throw new Error(json.error || '上传失败')
|
||||
}
|
||||
```
|
||||
|
||||
### 主进程生成 PDF
|
||||
```ts
|
||||
// main: 注册 IPC 处理
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||
const win = event.sender.getOwnerBrowserWindow()
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){
|
||||
const root = document.querySelector('${payload.selector}')
|
||||
if (!root) throw new Error('报告根节点缺失')
|
||||
document.body.classList.add('print-mode')
|
||||
return true
|
||||
})()
|
||||
`)
|
||||
const pdf = await win.webContents.printToPDF({
|
||||
pageSize: payload.pageSize || 'A4',
|
||||
printBackground: payload.printBackground !== false,
|
||||
marginsType: 0
|
||||
})
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){ document.body.classList.remove('print-mode'); return true })()
|
||||
`)
|
||||
return pdf
|
||||
})
|
||||
```
|
||||
|
||||
### 更新数据库记录
|
||||
```py
|
||||
# database.py 片段
|
||||
def update_session_report_path(self, session_id: str, report_path: str):
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE detection_sessions SET detection_report = ? WHERE id = ?', (report_path, session_id))
|
||||
conn.commit()
|
||||
```
|
||||
|
||||
## 接口设计
|
||||
- 上传 PDF:`POST /api/reports/<session_id>/upload`
|
||||
- 请求体:`multipart/form-data`,字段 `file`
|
||||
- 返回:`{ success, path }`
|
||||
|
||||
## 权限与安全
|
||||
- 校验 `session_id` 存在且归属当前登录用户(医生)。
|
||||
- 限制最大文件大小(例如 20MB),拒绝超限上传。
|
||||
- 文件名使用 `session_id.pdf`,避免任意文件名写盘。
|
||||
|
||||
## 版式与质量建议
|
||||
- 中文字体:在渲染环境预装中文字体或通过 `@font-face` 引入。
|
||||
- 分页:使用 `@page` 与 `page-break-before/after` 控制分页。
|
||||
- 背景:Electron 打印需开启 `printBackground: true`;确保 CSS 背景不被打印忽略。
|
||||
|
||||
## 错误与排查
|
||||
- 空白 PDF:确保打印前切入 `.print-mode` 并等待资源加载;检查选择器是否找到根节点。
|
||||
- 字体方框:安装中文字体或嵌入字体文件。
|
||||
- 资源丢失:使用相对路径或 base64;`html2canvas` 时启用 `useCORS` 且服务器设置跨域头。
|
||||
- 过大或截断:分页控制,或拆页生成。
|
||||
|
||||
## 测试用例
|
||||
- 单页报告:生成并上传,后端返回路径;数据库记录更新;历史档案中可下载或预览。
|
||||
- 多页报告:存在分页断点,打印后为多页;每页标题和页码正常。
|
||||
- 异常:网络断开、文件超限、会话不存在;前端提示具体错误。
|
||||
|
||||
## 部署说明
|
||||
- Electron 打包:已使用 `electron-builder`;生成 exe 后功能一致。
|
||||
- 后端存储:Windows 下路径统一使用 `/` 展示;静态文件由后端提供下载或前端拼接 `BACKEND_URL + '/' + path` 访问。
|
||||
|
||||
## 后续扩展
|
||||
- 加页眉页脚(时间、患者信息、页码)。
|
||||
- 报告水印与签章。
|
||||
- 历史报告下载与比对视图。
|
||||
|
Before Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 135 KiB |
BIN
document/登录页.png
|
Before Width: | Height: | Size: 63 KiB |
@ -1 +0,0 @@
|
||||
pip
|
||||
@ -1,139 +0,0 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: cryptography
|
||||
Version: 46.0.3
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: BSD
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
||||
Classifier: Topic :: Security :: Cryptography
|
||||
Requires-Dist: cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy'
|
||||
Requires-Dist: cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy'
|
||||
Requires-Dist: typing-extensions>=4.13.2 ; python_full_version < '3.11'
|
||||
Requires-Dist: bcrypt>=3.1.5 ; extra == 'ssh'
|
||||
Requires-Dist: nox[uv]>=2024.4.15 ; extra == 'nox'
|
||||
Requires-Dist: cryptography-vectors==46.0.3 ; extra == 'test'
|
||||
Requires-Dist: pytest>=7.4.0 ; extra == 'test'
|
||||
Requires-Dist: pytest-benchmark>=4.0 ; extra == 'test'
|
||||
Requires-Dist: pytest-cov>=2.10.1 ; extra == 'test'
|
||||
Requires-Dist: pytest-xdist>=3.5.0 ; extra == 'test'
|
||||
Requires-Dist: pretend>=0.7 ; extra == 'test'
|
||||
Requires-Dist: certifi>=2024 ; extra == 'test'
|
||||
Requires-Dist: pytest-randomly ; extra == 'test-randomorder'
|
||||
Requires-Dist: sphinx>=5.3.0 ; extra == 'docs'
|
||||
Requires-Dist: sphinx-rtd-theme>=3.0.0 ; extra == 'docs'
|
||||
Requires-Dist: sphinx-inline-tabs ; extra == 'docs'
|
||||
Requires-Dist: pyenchant>=3 ; extra == 'docstest'
|
||||
Requires-Dist: readme-renderer>=30.0 ; extra == 'docstest'
|
||||
Requires-Dist: sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest'
|
||||
Requires-Dist: build>=1.0.0 ; extra == 'sdist'
|
||||
Requires-Dist: ruff>=0.11.11 ; extra == 'pep8test'
|
||||
Requires-Dist: mypy>=1.14 ; extra == 'pep8test'
|
||||
Requires-Dist: check-sdist ; extra == 'pep8test'
|
||||
Requires-Dist: click>=8.0.1 ; extra == 'pep8test'
|
||||
Provides-Extra: ssh
|
||||
Provides-Extra: nox
|
||||
Provides-Extra: test
|
||||
Provides-Extra: test-randomorder
|
||||
Provides-Extra: docs
|
||||
Provides-Extra: docstest
|
||||
Provides-Extra: sdist
|
||||
Provides-Extra: pep8test
|
||||
License-File: LICENSE
|
||||
License-File: LICENSE.APACHE
|
||||
License-File: LICENSE.BSD
|
||||
Summary: cryptography is a package which provides cryptographic recipes and primitives to Python developers.
|
||||
Author-email: The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>
|
||||
License-Expression: Apache-2.0 OR BSD-3-Clause
|
||||
Requires-Python: >=3.8, !=3.9.0, !=3.9.1
|
||||
Description-Content-Type: text/x-rst; charset=UTF-8
|
||||
Project-URL: homepage, https://github.com/pyca/cryptography
|
||||
Project-URL: documentation, https://cryptography.io/
|
||||
Project-URL: source, https://github.com/pyca/cryptography/
|
||||
Project-URL: issues, https://github.com/pyca/cryptography/issues
|
||||
Project-URL: changelog, https://cryptography.io/en/latest/changelog/
|
||||
|
||||
pyca/cryptography
|
||||
=================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/cryptography.svg
|
||||
:target: https://pypi.org/project/cryptography/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://readthedocs.org/projects/cryptography/badge/?version=latest
|
||||
:target: https://cryptography.io
|
||||
:alt: Latest Docs
|
||||
|
||||
.. image:: https://github.com/pyca/cryptography/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/pyca/cryptography/actions/workflows/ci.yml?query=branch%3Amain
|
||||
|
||||
``cryptography`` is a package which provides cryptographic recipes and
|
||||
primitives to Python developers. Our goal is for it to be your "cryptographic
|
||||
standard library". It supports Python 3.8+ and PyPy3 7.3.11+.
|
||||
|
||||
``cryptography`` includes both high level recipes and low level interfaces to
|
||||
common cryptographic algorithms such as symmetric ciphers, message digests, and
|
||||
key derivation functions. For example, to encrypt something with
|
||||
``cryptography``'s high level symmetric encryption recipe:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from cryptography.fernet import Fernet
|
||||
>>> # Put this somewhere safe!
|
||||
>>> key = Fernet.generate_key()
|
||||
>>> f = Fernet(key)
|
||||
>>> token = f.encrypt(b"A really secret message. Not for prying eyes.")
|
||||
>>> token
|
||||
b'...'
|
||||
>>> f.decrypt(token)
|
||||
b'A really secret message. Not for prying eyes.'
|
||||
|
||||
You can find more information in the `documentation`_.
|
||||
|
||||
You can install ``cryptography`` with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install cryptography
|
||||
|
||||
For full details see `the installation documentation`_.
|
||||
|
||||
Discussion
|
||||
~~~~~~~~~~
|
||||
|
||||
If you run into bugs, you can file them in our `issue tracker`_.
|
||||
|
||||
We maintain a `cryptography-dev`_ mailing list for development discussion.
|
||||
|
||||
You can also join ``#pyca`` on ``irc.libera.chat`` to ask questions or get
|
||||
involved.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
Need to report a security issue? Please consult our `security reporting`_
|
||||
documentation.
|
||||
|
||||
|
||||
.. _`documentation`: https://cryptography.io/
|
||||
.. _`the installation documentation`: https://cryptography.io/en/latest/installation/
|
||||
.. _`issue tracker`: https://github.com/pyca/cryptography/issues
|
||||
.. _`cryptography-dev`: https://mail.python.org/mailman/listinfo/cryptography-dev
|
||||
.. _`security reporting`: https://cryptography.io/en/latest/security/
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
cryptography-46.0.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
cryptography-46.0.3.dist-info/METADATA,sha256=bx2LyCEmOVUC8FH5hsGEZewWPiZoIIYTq0hM9mu9r4s,5748
|
||||
cryptography-46.0.3.dist-info/RECORD,,
|
||||
cryptography-46.0.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cryptography-46.0.3.dist-info/WHEEL,sha256=8hEf8NzM1FnmM77AjVt5h8nDuYkN3UqZ79LoIAHXeRE,95
|
||||
cryptography-46.0.3.dist-info/licenses/LICENSE,sha256=Pgx8CRqUi4JTO6mP18u0BDLW8amsv4X1ki0vmak65rs,197
|
||||
cryptography-46.0.3.dist-info/licenses/LICENSE.APACHE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
|
||||
cryptography-46.0.3.dist-info/licenses/LICENSE.BSD,sha256=YCxMdILeZHndLpeTzaJ15eY9dz2s0eymiSMqtwCPtPs,1532
|
||||
cryptography/__about__.py,sha256=QCLxNH_Abbygdc9RQGpUmrK14Wp3Cl_SEiB2byLwyxo,445
|
||||
cryptography/__init__.py,sha256=mthuUrTd4FROCpUYrTIqhjz6s6T9djAZrV7nZ1oMm2o,364
|
||||
cryptography/__pycache__/__about__.cpython-313.pyc,,
|
||||
cryptography/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/__pycache__/exceptions.cpython-313.pyc,,
|
||||
cryptography/__pycache__/fernet.cpython-313.pyc,,
|
||||
cryptography/__pycache__/utils.cpython-313.pyc,,
|
||||
cryptography/exceptions.py,sha256=835EWILc2fwxw-gyFMriciC2SqhViETB10LBSytnDIc,1087
|
||||
cryptography/fernet.py,sha256=3Cvxkh0KJSbX8HbnCHu4wfCW7U0GgfUA3v_qQ8a8iWc,6963
|
||||
cryptography/hazmat/__init__.py,sha256=5IwrLWrVp0AjEr_4FdWG_V057NSJGY_W4egNNsuct0g,455
|
||||
cryptography/hazmat/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/__pycache__/_oid.cpython-313.pyc,,
|
||||
cryptography/hazmat/_oid.py,sha256=p8ThjwJB56Ci_rAIrjyJ1f8VjgD6e39es2dh8JIUBOw,17240
|
||||
cryptography/hazmat/asn1/__init__.py,sha256=hS_EWx3wVvZzfbCcNV8hzcDnyMM8H-BhIoS1TipUosk,293
|
||||
cryptography/hazmat/asn1/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/asn1/__pycache__/asn1.cpython-313.pyc,,
|
||||
cryptography/hazmat/asn1/asn1.py,sha256=eMEThEXa19LQjcyVofgHsW6tsZnjp3ddH7bWkkcxfLM,3860
|
||||
cryptography/hazmat/backends/__init__.py,sha256=O5jvKFQdZnXhKeqJ-HtulaEL9Ni7mr1mDzZY5kHlYhI,361
|
||||
cryptography/hazmat/backends/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/backends/openssl/__init__.py,sha256=p3jmJfnCag9iE5sdMrN6VvVEu55u46xaS_IjoI0SrmA,305
|
||||
cryptography/hazmat/backends/openssl/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/backends/openssl/__pycache__/backend.cpython-313.pyc,,
|
||||
cryptography/hazmat/backends/openssl/backend.py,sha256=tV5AxBoFJ2GfA0DMWSY-0TxQJrpQoexzI9R4Kybb--4,10215
|
||||
cryptography/hazmat/bindings/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
||||
cryptography/hazmat/bindings/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/bindings/_rust.pyd,sha256=kvWLtPAadaDvTdlCXcKpbd_iX8k_2dwR6o8NBbek8IU,9245696
|
||||
cryptography/hazmat/bindings/_rust/__init__.pyi,sha256=KhqLhXFPArPzzJ7DYO9Fl8FoXB_BagAd_r4Dm_Ze9Xo,1257
|
||||
cryptography/hazmat/bindings/_rust/_openssl.pyi,sha256=mpNJLuYLbCVrd5i33FBTmWwL_55Dw7JPkSLlSX9Q7oI,230
|
||||
cryptography/hazmat/bindings/_rust/asn1.pyi,sha256=BrGjC8J6nwuS-r3EVcdXJB8ndotfY9mbQYOfpbPG0HA,354
|
||||
cryptography/hazmat/bindings/_rust/declarative_asn1.pyi,sha256=2ECFmYue1EPkHEE2Bm7aLwkjB0mSUTpr23v9MN4pri4,892
|
||||
cryptography/hazmat/bindings/_rust/exceptions.pyi,sha256=exXr2xw_0pB1kk93cYbM3MohbzoUkjOms1ZMUi0uQZE,640
|
||||
cryptography/hazmat/bindings/_rust/ocsp.pyi,sha256=VPVWuKHI9EMs09ZLRYAGvR0Iz0mCMmEzXAkgJHovpoM,4020
|
||||
cryptography/hazmat/bindings/_rust/openssl/__init__.pyi,sha256=iOAMDyHoNwwCSZfZzuXDr64g4GpGUeDgEN-LjXqdrBM,1522
|
||||
cryptography/hazmat/bindings/_rust/openssl/aead.pyi,sha256=4Nddw6-ynzIB3w2W86WvkGKTLlTDk_6F5l54RHCuy3E,2688
|
||||
cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi,sha256=LhPzHWSXJq4grAJXn6zSvSSdV-aYIIscHDwIPlJGGPs,1315
|
||||
cryptography/hazmat/bindings/_rust/openssl/cmac.pyi,sha256=nPH0X57RYpsAkRowVpjQiHE566ThUTx7YXrsadmrmHk,564
|
||||
cryptography/hazmat/bindings/_rust/openssl/dh.pyi,sha256=Z3TC-G04-THtSdAOPLM1h2G7ml5bda1ElZUcn5wpuhk,1564
|
||||
cryptography/hazmat/bindings/_rust/openssl/dsa.pyi,sha256=qBtkgj2albt2qFcnZ9UDrhzoNhCVO7HTby5VSf1EXMI,1299
|
||||
cryptography/hazmat/bindings/_rust/openssl/ec.pyi,sha256=zJy0pRa5n-_p2dm45PxECB_-B6SVZyNKfjxFDpPqT38,1691
|
||||
cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi,sha256=VXfXd5G6hUivg399R1DYdmW3eTb0EebzDTqjRC2gaRw,532
|
||||
cryptography/hazmat/bindings/_rust/openssl/ed448.pyi,sha256=Yx49lqdnjsD7bxiDV1kcaMrDktug5evi5a6zerMiy2s,514
|
||||
cryptography/hazmat/bindings/_rust/openssl/hashes.pyi,sha256=OWZvBx7xfo_HJl41Nc--DugVyCVPIprZ3HlOPTSWH9g,984
|
||||
cryptography/hazmat/bindings/_rust/openssl/hmac.pyi,sha256=BXZn7NDjL3JAbYW0SQ8pg1iyC5DbQXVhUAiwsi8DFR8,702
|
||||
cryptography/hazmat/bindings/_rust/openssl/kdf.pyi,sha256=xXfFBb9QehHfDtEaxV_65Z0YK7NquOVIChpTLkgAs_k,2029
|
||||
cryptography/hazmat/bindings/_rust/openssl/keys.pyi,sha256=teIt8M6ZEMJrn4s3W0UnW0DZ-30Jd68WnSsKKG124l0,912
|
||||
cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi,sha256=_SW9NtQ5FDlAbdclFtWpT4lGmxKIKHpN-4j8J2BzYfQ,585
|
||||
cryptography/hazmat/bindings/_rust/openssl/rsa.pyi,sha256=2OQCNSXkxgc-3uw1xiCCloIQTV6p9_kK79Yu0rhZgPc,1364
|
||||
cryptography/hazmat/bindings/_rust/openssl/x25519.pyi,sha256=ewn4GpQyb7zPwE-ni7GtyQgMC0A1mLuqYsSyqv6nI_s,523
|
||||
cryptography/hazmat/bindings/_rust/openssl/x448.pyi,sha256=juTZTmli8jO_5Vcufg-vHvx_tCyezmSLIh_9PU3TczI,505
|
||||
cryptography/hazmat/bindings/_rust/pkcs12.pyi,sha256=vEEd5wDiZvb8ZGFaziLCaWLzAwoG_tvPUxLQw5_uOl8,1605
|
||||
cryptography/hazmat/bindings/_rust/pkcs7.pyi,sha256=txGBJijqZshEcqra6byPNbnisIdlxzOSIHP2hl9arPs,1601
|
||||
cryptography/hazmat/bindings/_rust/test_support.pyi,sha256=PPhld-WkO743iXFPebeG0LtgK0aTzGdjcIsay1Gm5GE,757
|
||||
cryptography/hazmat/bindings/_rust/x509.pyi,sha256=n9X0IQ6ICbdIi-ExdCFZoBgeY6njm3QOVAVZwDQdnbk,9784
|
||||
cryptography/hazmat/bindings/openssl/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
||||
cryptography/hazmat/bindings/openssl/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/bindings/openssl/__pycache__/_conditional.cpython-313.pyc,,
|
||||
cryptography/hazmat/bindings/openssl/__pycache__/binding.cpython-313.pyc,,
|
||||
cryptography/hazmat/bindings/openssl/_conditional.py,sha256=DMOpA_XN4l70zTc5_J9DpwlbQeUBRTWpfIJ4yRIn1-U,5791
|
||||
cryptography/hazmat/bindings/openssl/binding.py,sha256=x8eocEmukO4cm7cHqfVmOoYY7CCXdoF1v1WhZQt9neo,4610
|
||||
cryptography/hazmat/decrepit/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
|
||||
cryptography/hazmat/decrepit/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/decrepit/ciphers/__init__.py,sha256=wHCbWfaefa-fk6THSw9th9fJUsStJo7245wfFBqmduA,216
|
||||
cryptography/hazmat/decrepit/ciphers/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/decrepit/ciphers/__pycache__/algorithms.cpython-313.pyc,,
|
||||
cryptography/hazmat/decrepit/ciphers/algorithms.py,sha256=YrKgHS4MfwWaMmPBYRymRRlC0phwWp9ycICFezeJPGk,2595
|
||||
cryptography/hazmat/primitives/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
||||
cryptography/hazmat/primitives/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/_asymmetric.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/_cipheralgorithm.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/_serialization.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/cmac.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/constant_time.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/hashes.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/hmac.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/keywrap.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/padding.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/__pycache__/poly1305.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/_asymmetric.py,sha256=RhgcouUB6HTiFDBrR1LxqkMjpUxIiNvQ1r_zJjRG6qQ,532
|
||||
cryptography/hazmat/primitives/_cipheralgorithm.py,sha256=Eh3i7lwedHfi0eLSsH93PZxQKzY9I6lkK67vL4V5tOc,1522
|
||||
cryptography/hazmat/primitives/_serialization.py,sha256=chgPCSF2jxI2Cr5gB-qbWXOvOfupBh4CARS0KAhv9AM,5123
|
||||
cryptography/hazmat/primitives/asymmetric/__init__.py,sha256=s9oKCQ2ycFdXoERdS1imafueSkBsL9kvbyfghaauZ9Y,180
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/dh.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/dsa.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/ec.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/ed25519.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/ed448.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/padding.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/rsa.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/types.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/utils.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/x25519.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/__pycache__/x448.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/asymmetric/dh.py,sha256=0v_vEFFz5pQ1QG-FkWDyvgv7IfuVZSH5Q6LyFI5A8rg,3645
|
||||
cryptography/hazmat/primitives/asymmetric/dsa.py,sha256=Ld_bbbqQFz12dObHxIkzEQzX0SWWP41RLSWkYSaKhqE,4213
|
||||
cryptography/hazmat/primitives/asymmetric/ec.py,sha256=Vf5ig2PcS3PVnsb5N49Kx1uIkFBJyhg4BWXThDz5cug,12999
|
||||
cryptography/hazmat/primitives/asymmetric/ed25519.py,sha256=jZW5cs472wXXV3eB0sE1b8w64gdazwwU0_MT5UOTiXs,3700
|
||||
cryptography/hazmat/primitives/asymmetric/ed448.py,sha256=yAetgn2f2JYf0BO8MapGzXeThsvSMG5LmUCrxVOidAA,3729
|
||||
cryptography/hazmat/primitives/asymmetric/padding.py,sha256=vQ6l6gOg9HqcbOsvHrSiJRVLdEj9L4m4HkRGYziTyFA,2854
|
||||
cryptography/hazmat/primitives/asymmetric/rsa.py,sha256=ZnKOo2f34MCCOupC03Y1uR-_jiSG5IrelHEmxaME3D4,8303
|
||||
cryptography/hazmat/primitives/asymmetric/types.py,sha256=LnsOJym-wmPUJ7Knu_7bCNU3kIiELCd6krOaW_JU08I,2996
|
||||
cryptography/hazmat/primitives/asymmetric/utils.py,sha256=DPTs6T4F-UhwzFQTh-1fSEpQzazH2jf2xpIro3ItF4o,790
|
||||
cryptography/hazmat/primitives/asymmetric/x25519.py,sha256=_4nQeZ3yJ3Lg0RpXnaqA-1yt6vbx1F-wzLcaZHwSpeE,3613
|
||||
cryptography/hazmat/primitives/asymmetric/x448.py,sha256=WKBLtuVfJqiBRro654fGaQAlvsKbqbNkK7c4A_ZCdV0,3642
|
||||
cryptography/hazmat/primitives/ciphers/__init__.py,sha256=eyEXmjk6_CZXaOPYDr7vAYGXr29QvzgWL2-4CSolLFs,680
|
||||
cryptography/hazmat/primitives/ciphers/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/ciphers/__pycache__/aead.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/ciphers/__pycache__/algorithms.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/ciphers/__pycache__/base.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/ciphers/__pycache__/modes.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/ciphers/aead.py,sha256=Fzlyx7w8KYQakzDp1zWgJnIr62zgZrgVh1u2h4exB54,634
|
||||
cryptography/hazmat/primitives/ciphers/algorithms.py,sha256=Q7ZJwcsx83Mgxv5y7r6CyJKSdsOwC-my-5A67-ma2vw,3407
|
||||
cryptography/hazmat/primitives/ciphers/base.py,sha256=aBC7HHBBoixebmparVr0UlODs3VD0A7B6oz_AaRjDv8,4253
|
||||
cryptography/hazmat/primitives/ciphers/modes.py,sha256=20stpwhDtbAvpH0SMf9EDHIciwmTF-JMBUOZ9bU8WiQ,8318
|
||||
cryptography/hazmat/primitives/cmac.py,sha256=sz_s6H_cYnOvx-VNWdIKhRhe3Ymp8z8J0D3CBqOX3gg,338
|
||||
cryptography/hazmat/primitives/constant_time.py,sha256=xdunWT0nf8OvKdcqUhhlFKayGp4_PgVJRU2W1wLSr_A,422
|
||||
cryptography/hazmat/primitives/hashes.py,sha256=M8BrlKB3U6DEtHvWTV5VRjpteHv1kS3Zxm_Bsk04cr8,5184
|
||||
cryptography/hazmat/primitives/hmac.py,sha256=RpB3z9z5skirCQrm7zQbtnp9pLMnAjrlTUvKqF5aDDc,423
|
||||
cryptography/hazmat/primitives/kdf/__init__.py,sha256=4XibZnrYq4hh5xBjWiIXzaYW6FKx8hPbVaa_cB9zS64,750
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/argon2.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/concatkdf.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/hkdf.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/kbkdf.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/pbkdf2.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/scrypt.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/__pycache__/x963kdf.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/kdf/argon2.py,sha256=UFDNXG0v-rw3DqAQTB1UQAsQC2M5Ejg0k_6OCyhLKus,460
|
||||
cryptography/hazmat/primitives/kdf/concatkdf.py,sha256=Ua8KoLXXnzgsrAUmHpyKymaPt8aPRP0EHEaBz7QCQ9I,3737
|
||||
cryptography/hazmat/primitives/kdf/hkdf.py,sha256=M0lAEfRoc4kpp4-nwDj9yB-vNZukIOYEQrUlWsBNn9o,543
|
||||
cryptography/hazmat/primitives/kdf/kbkdf.py,sha256=oZepvo4evhKkkJQWRDwaPoIbyTaFmDc5NPimxg6lfKg,9165
|
||||
cryptography/hazmat/primitives/kdf/pbkdf2.py,sha256=1WIwhELR0w8ztTpTu8BrFiYWmK3hUfJq08I79TxwieE,1957
|
||||
cryptography/hazmat/primitives/kdf/scrypt.py,sha256=XyWUdUUmhuI9V6TqAPOvujCSMGv1XQdg0a21IWCmO-U,590
|
||||
cryptography/hazmat/primitives/kdf/x963kdf.py,sha256=zLTcF665QFvXX2f8TS7fmBZTteXpFjKahzfjjQcCJyw,1999
|
||||
cryptography/hazmat/primitives/keywrap.py,sha256=XV4Pj2fqSeD-RqZVvY2cA3j5_7RwJSFygYuLfk2ujCo,5650
|
||||
cryptography/hazmat/primitives/padding.py,sha256=QT-U-NvV2eQGO1wVPbDiNGNSc9keRDS-ig5cQOrLz0E,1865
|
||||
cryptography/hazmat/primitives/poly1305.py,sha256=P5EPQV-RB_FJPahpg01u0Ts4S_PnAmsroxIGXbGeRRo,355
|
||||
cryptography/hazmat/primitives/serialization/__init__.py,sha256=Q7uTgDlt7n3WfsMT6jYwutC6DIg_7SEeoAm1GHZ5B5E,1705
|
||||
cryptography/hazmat/primitives/serialization/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/serialization/__pycache__/base.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/serialization/__pycache__/pkcs12.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/serialization/__pycache__/pkcs7.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/serialization/__pycache__/ssh.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/serialization/base.py,sha256=ikq5MJIwp_oUnjiaBco_PmQwOTYuGi-XkYUYHKy8Vo0,615
|
||||
cryptography/hazmat/primitives/serialization/pkcs12.py,sha256=mS9cFNG4afzvseoc5e1MWoY2VskfL8N8Y_OFjl67luY,5104
|
||||
cryptography/hazmat/primitives/serialization/pkcs7.py,sha256=5OR_Tkysxaprn4FegvJIfbep9rJ9wok6FLWvWwQ5-Mg,13943
|
||||
cryptography/hazmat/primitives/serialization/ssh.py,sha256=hPV5obFznz0QhFfXFPOeQ8y6MsurA0xVMQiLnLESEs8,53700
|
||||
cryptography/hazmat/primitives/twofactor/__init__.py,sha256=tmMZGB-g4IU1r7lIFqASU019zr0uPp_wEBYcwdDCKCA,258
|
||||
cryptography/hazmat/primitives/twofactor/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/twofactor/__pycache__/hotp.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/twofactor/__pycache__/totp.cpython-313.pyc,,
|
||||
cryptography/hazmat/primitives/twofactor/hotp.py,sha256=ivZo5BrcCGWLsqql4nZV0XXCjyGPi_iHfDFltGlOJwk,3256
|
||||
cryptography/hazmat/primitives/twofactor/totp.py,sha256=m5LPpRL00kp4zY8gTjr55Hfz9aMlPS53kHmVkSQCmdY,1652
|
||||
cryptography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
cryptography/utils.py,sha256=bZAjFC5KVpfmF29qS_18vvpW3mKxmdiRALcusHhTTkg,4301
|
||||
cryptography/x509/__init__.py,sha256=xloN0swseNx-m2WFZmCA17gOoxQWqeU82UVjEdJBePQ,8257
|
||||
cryptography/x509/__pycache__/__init__.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/base.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/certificate_transparency.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/extensions.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/general_name.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/name.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/ocsp.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/oid.cpython-313.pyc,,
|
||||
cryptography/x509/__pycache__/verification.cpython-313.pyc,,
|
||||
cryptography/x509/base.py,sha256=OrmTw3y8B6AE_nGXQPN8x9kq-d7rDWeH13gCq6T6D6U,27997
|
||||
cryptography/x509/certificate_transparency.py,sha256=JqoOIDhlwInrYMFW6IFn77WJ0viF-PB_rlZV3vs9MYc,797
|
||||
cryptography/x509/extensions.py,sha256=QxYrqR6SF1qzR9ZraP8wDiIczlEVlAFuwDRVcltB6Tk,77724
|
||||
cryptography/x509/general_name.py,sha256=sP_rV11Qlpsk4x3XXGJY_Mv0Q_s9dtjeLckHsjpLQoQ,7836
|
||||
cryptography/x509/name.py,sha256=ty0_xf0LnHwZAdEf-d8FLO1K4hGqx_7DsD3CHwoLJiY,15101
|
||||
cryptography/x509/ocsp.py,sha256=Yey6NdFV1MPjop24Mj_VenjEpg3kUaMopSWOK0AbeBs,12699
|
||||
cryptography/x509/oid.py,sha256=BUzgXXGVWilkBkdKPTm9R4qElE9gAGHgdYPMZAp7PJo,931
|
||||
cryptography/x509/verification.py,sha256=gR2C2c-XZQtblZhT5T5vjSKOtCb74ef2alPVmEcwFlM,958
|
||||
@ -1,4 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: maturin (1.9.4)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp311-abi3-win_amd64
|
||||
@ -1,3 +0,0 @@
|
||||
This software is made available under the terms of *either* of the licenses
|
||||
found in LICENSE.APACHE or LICENSE.BSD. Contributions to cryptography are made
|
||||
under the terms of *both* these licenses.
|
||||
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@ -1,27 +0,0 @@
|
||||
Copyright (c) Individual contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of PyCA Cryptography nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -1,41 +0,0 @@
|
||||
[APP]
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
debug = false
|
||||
log_level = INFO
|
||||
|
||||
[SERVER]
|
||||
host = 0.0.0.0
|
||||
port = 5000
|
||||
cors_origins = *
|
||||
|
||||
[DATABASE]
|
||||
path = backend/data/body_balance.db
|
||||
backup_interval = 24
|
||||
max_backups = 7
|
||||
|
||||
[DEVICES]
|
||||
camera_index = 0
|
||||
camera_width = 640
|
||||
camera_height = 480
|
||||
camera_fps = 30
|
||||
imu_port = COM3
|
||||
pressure_port = COM4
|
||||
|
||||
[DETECTION]
|
||||
default_duration = 60
|
||||
sampling_rate = 30
|
||||
balance_threshold = 0.2
|
||||
posture_threshold = 5.0
|
||||
|
||||
[DATA_PROCESSING]
|
||||
filter_window = 5
|
||||
outlier_threshold = 2.0
|
||||
chart_dpi = 300
|
||||
export_format = csv
|
||||
|
||||
[SECURITY]
|
||||
secret_key = 579012d21afe892d663698a0875c78112bb7e73e949a0d9f591515cd7fce183b
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
@ -43,7 +43,7 @@ const viteProcess = spawn('npm', ['run', 'dev'], {
|
||||
// 等待Vite服务器启动后启动Electron
|
||||
setTimeout(() => {
|
||||
console.log('\n⚡ 启动Electron应用...');
|
||||
const electronProcess = spawn('npx', ['electron', mainPath], {
|
||||
const electronProcess = spawn('\"d:\\\\electron-v36.4.0-win32-x64\\\\electron.exe\"', [mainPath], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
cwd: __dirname
|
||||
@ -76,4 +76,4 @@ process.on('SIGTERM', () => {
|
||||
console.log('\n🛑 收到终止信号,正在关闭...');
|
||||
viteProcess.kill();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,8 +8,9 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: 'Noto Sans SC';
|
||||
/* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
|
||||
background:#191d28;
|
||||
}
|
||||
#app {
|
||||
height: 100vh;
|
||||
|
||||
@ -1,14 +1,172 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, exec, execSync } = require('child_process');
|
||||
let mainWindow;
|
||||
let localServer;
|
||||
let backendProcess;
|
||||
let splashWindow;
|
||||
// app.disableHardwareAcceleration();
|
||||
app.disableDomainBlockingFor3DAPIs();
|
||||
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) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) throw new Error('窗口未找到');
|
||||
|
||||
let prevWinBg = null;
|
||||
try {
|
||||
if (typeof win.getBackgroundColor === 'function') {
|
||||
prevWinBg = win.getBackgroundColor();
|
||||
}
|
||||
if (typeof win.setBackgroundColor === 'function') {
|
||||
win.setBackgroundColor('#FFFFFFFF');
|
||||
}
|
||||
if (win.webContents && typeof win.webContents.setBackgroundColor === 'function') {
|
||||
win.webContents.setBackgroundColor('#FFFFFFFF');
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 1. 准备打印环境:克隆节点到独立的打印容器,确保流式布局
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){
|
||||
const selector = '${(payload && payload.selector) ? payload.selector : '#report-root'}';
|
||||
const root = document.querySelector(selector);
|
||||
if (!root) throw new Error('报告根节点缺失');
|
||||
|
||||
// 创建打印容器
|
||||
const printContainer = document.createElement('div');
|
||||
printContainer.id = 'electron-print-container';
|
||||
|
||||
// 样式设置:绝对定位覆盖全屏,背景白,z-index最高
|
||||
printContainer.style.position = 'absolute';
|
||||
printContainer.style.top = '0';
|
||||
printContainer.style.left = '0';
|
||||
printContainer.style.width = '100%';
|
||||
printContainer.style.minHeight = '100vh';
|
||||
printContainer.style.background = '#ffffff';
|
||||
printContainer.style.zIndex = '2147483647';
|
||||
printContainer.style.display = 'block';
|
||||
printContainer.style.overflow = 'visible'; // 关键:允许内容溢出以触发分页
|
||||
|
||||
// 克隆目标节点
|
||||
const cloned = root.cloneNode(true);
|
||||
|
||||
// 强制重置克隆节点的关键样式,确保它是流式布局
|
||||
cloned.style.position = 'static'; // 必须是 static 或 relative
|
||||
cloned.style.display = 'block';
|
||||
cloned.style.width = '100%';
|
||||
cloned.style.height = 'auto';
|
||||
cloned.style.overflow = 'visible';
|
||||
cloned.style.margin = '0';
|
||||
cloned.style.transform = 'none'; // 移除可能影响打印的变换
|
||||
|
||||
// 将克隆节点加入容器
|
||||
printContainer.appendChild(cloned);
|
||||
|
||||
// 将容器加入 body
|
||||
document.body.appendChild(printContainer);
|
||||
|
||||
// 关键修复:打印时只保留我们的打印容器可见,其他所有元素隐藏
|
||||
// 我们创建一个 style 标签来强制隐藏除了 printContainer 以外的所有 body 直接子元素
|
||||
const style = document.createElement('style');
|
||||
style.id = 'print-style-override';
|
||||
style.innerHTML = \`
|
||||
@media print {
|
||||
html, body {
|
||||
background: #ffffff !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
body > *:not(#electron-print-container) {
|
||||
display: none !important;
|
||||
}
|
||||
#electron-print-container {
|
||||
display: block !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
}
|
||||
\`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
|
||||
try {
|
||||
const pdf = await win.webContents.printToPDF({
|
||||
pageSize: (payload && payload.pageSize) ? payload.pageSize : 'A4',
|
||||
landscape: !!(payload && payload.landscape),
|
||||
printBackground: !(payload && payload.printBackground === false),
|
||||
marginsType: 0,
|
||||
displayHeaderFooter: true, // 启用页眉页脚
|
||||
headerTemplate: '<div></div>', // 空页眉
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; text-align: center; font-size: 10px; font-family: 'Microsoft YaHei', sans-serif; padding-top: 5px; padding-bottom: 5px; background: #ffffff;">
|
||||
第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页
|
||||
</div>
|
||||
`
|
||||
});
|
||||
return pdf;
|
||||
} finally {
|
||||
// 3. 清理环境
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){
|
||||
const container = document.getElementById('electron-print-container');
|
||||
if (container) container.remove();
|
||||
const style = document.getElementById('print-style-override');
|
||||
if (style) style.remove();
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
try {
|
||||
if (prevWinBg && typeof win.setBackgroundColor === 'function') {
|
||||
win.setBackgroundColor(prevWinBg);
|
||||
}
|
||||
if (prevWinBg && win.webContents && typeof win.webContents.setBackgroundColor === 'function') {
|
||||
win.webContents.setBackgroundColor(prevWinBg);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
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目录
|
||||
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
||||
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
|
||||
@ -74,7 +232,6 @@ function stopBackendService() {
|
||||
|
||||
// 强制杀死所有BodyBalanceBackend.exe进程
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// 如果没有找到进程,taskkill会返回错误,这是正常的
|
||||
@ -91,6 +248,16 @@ function stopBackendService() {
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
backgroundColor: '#000000',
|
||||
show: true
|
||||
});
|
||||
splashWindow.loadFile(path.join(__dirname, 'resources/loading.html'));
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@ -98,6 +265,7 @@ function createWindow() {
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
// sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
icon: path.join(__dirname, '../public/logo.png'),
|
||||
@ -127,21 +295,25 @@ function createWindow() {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 窗口就绪后再显示,避免白屏/闪烁
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// 监听页面加载完成事件
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
console.log('Page loaded completely');
|
||||
if (splashWindow) {
|
||||
splashWindow.close();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// 添加加载失败的处理
|
||||
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
|
||||
console.log('Failed to load:', errorDescription, 'URL:', validatedURL);
|
||||
if (splashWindow) {
|
||||
splashWindow.close();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show(); // 即使加载失败也显示窗口
|
||||
});
|
||||
|
||||
@ -152,10 +324,19 @@ function createWindow() {
|
||||
}
|
||||
// 关闭后端服务
|
||||
stopBackendService();
|
||||
if (splashWindow) {
|
||||
splashWindow.close();
|
||||
splashWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startLocalServer(callback) {
|
||||
if (localServer) {
|
||||
console.log('Local server already started on http://localhost:3000');
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const staticPath = path.join(__dirname, '../dist/');
|
||||
|
||||
localServer = http.createServer((req, res) => {
|
||||
@ -215,27 +396,29 @@ function startLocalServer(callback) {
|
||||
|
||||
// 应用事件处理
|
||||
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
|
||||
app.disableHardwareAcceleration();
|
||||
app.whenReady().then(createWindow);
|
||||
// app.disableHardwareAcceleration();
|
||||
if (gotSingleInstanceLock) {
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
if (localServer) {
|
||||
localServer.close();
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
if (localServer) {
|
||||
localServer.close();
|
||||
}
|
||||
// 关闭后端服务
|
||||
stopBackendService();
|
||||
app.quit();
|
||||
}
|
||||
// 关闭后端服务
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// 应用退出前清理资源
|
||||
app.on('before-quit', () => {
|
||||
stopBackendService();
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// 应用退出前清理资源
|
||||
app.on('before-quit', () => {
|
||||
stopBackendService();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
const { contextBridge } = require('electron');
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
generateReportPdf: (payload) => ipcRenderer.invoke('generate-report-pdf', payload),
|
||||
getBackendUrl: () => process.env.BACKEND_URL || 'http://localhost:5000',
|
||||
getSocketTransports: () => {
|
||||
const allowPolling = process.env.ALLOW_POLLING === '1'
|
||||
return allowPolling ? ['websocket', 'polling'] : ['websocket']
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
40
frontend/src/renderer/main/resources/loading.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Loading</title>
|
||||
<style>
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
font-family: "Microsoft YaHei", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", sans-serif;
|
||||
}
|
||||
.wrap { display: flex; flex-direction: column; align-items: center; }
|
||||
.spinner {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 0deg, #ffffff 0deg, rgba(255,255,255,0.15) 90deg, rgba(255,255,255,0.15) 360deg);
|
||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.text { font-size: 30px; opacity: 0.9; letter-spacing: 0.5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="spinner"></div>
|
||||
<div class="text">系统启动中...</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
211
frontend/src/renderer/package-lock.json
generated
@ -1,20 +1,23 @@
|
||||
{
|
||||
"name": "body-balance-renderer",
|
||||
"version": "1.0.0",
|
||||
"name": "body-balance-system",
|
||||
"version": "1.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "body-balance-renderer",
|
||||
"version": "1.0.0",
|
||||
"name": "body-balance-system",
|
||||
"version": "1.5.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"echarts": "^5.4.3",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.3.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.4",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4"
|
||||
@ -63,10 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
||||
"dev": true,
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -1167,6 +1169,12 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz",
|
||||
@ -1179,6 +1187,13 @@
|
||||
"xmlbuilder": ">=11.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
@ -1189,6 +1204,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.11",
|
||||
"resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz",
|
||||
@ -1731,7 +1753,7 @@
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -1982,6 +2004,26 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
|
||||
@ -2012,6 +2054,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/china-division": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/china-division/-/china-division-2.7.0.tgz",
|
||||
"integrity": "sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz",
|
||||
@ -2253,6 +2301,18 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.47.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
|
||||
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@ -2342,7 +2402,7 @@
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2608,6 +2668,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz",
|
||||
@ -2984,6 +3054,15 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/element-china-area-data": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/element-china-area-data/-/element-china-area-data-6.1.0.tgz",
|
||||
"integrity": "sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"china-division": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.10.7",
|
||||
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.7.tgz",
|
||||
@ -3278,6 +3357,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
@ -3288,6 +3378,12 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz",
|
||||
@ -3893,7 +3989,7 @@
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -4025,6 +4121,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
@ -4234,6 +4336,23 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/junk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz",
|
||||
@ -4756,6 +4875,12 @@
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parse-author": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz",
|
||||
@ -4863,6 +4988,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -5016,6 +5148,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rcedit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz",
|
||||
@ -5103,6 +5245,13 @@
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@ -5170,6 +5319,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz",
|
||||
@ -5542,6 +5701,16 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/stat-mode": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||
@ -5696,6 +5865,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz",
|
||||
@ -5783,13 +5962,19 @@
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.160.1",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
|
||||
"integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
|
||||
@ -5931,7 +6116,7 @@
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"dev:electron": "electron .",
|
||||
"build": "npm run build:renderer && npm run build:electron",
|
||||
"build:renderer": "vite build",
|
||||
"build:electron": "electron-builder --config ./build/electron-builder.install.json",
|
||||
"build:electron": "set HTTP_PROXY=&& set HTTPS_PROXY=&&electron-builder --config ./build/electron-builder.install.json",
|
||||
"pack": "electron-packager . electron-browser --platform=win32 --arch=x64 --out=dist --overwrite",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@ -18,10 +18,13 @@
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"echarts": "^5.4.3",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.3.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.4",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4"
|
||||
@ -34,5 +37,5 @@
|
||||
"electron-packager": "^17.1.2",
|
||||
"vite": "^4.4.9",
|
||||
"wait-on": "^7.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/src/renderer/public/Model.glb
Normal file
BIN
frontend/src/renderer/public/logo.ico
Normal file
|
After Width: | Height: | Size: 104 KiB |
28
frontend/src/renderer/public/pdf.worker.min.js
vendored
Normal file
BIN
frontend/src/renderer/src/assets/archive/bi.png
Normal file
|
After Width: | Height: | Size: 334 B |
BIN
frontend/src/renderer/src/assets/archive/bi2.png
Normal file
|
After Width: | Height: | Size: 334 B |
BIN
frontend/src/renderer/src/assets/archive/del.png
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
frontend/src/renderer/src/assets/archive/del2.png
Normal file
|
After Width: | Height: | Size: 401 B |
BIN
frontend/src/renderer/src/assets/archive/download.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
frontend/src/renderer/src/assets/archive/history3.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
frontend/src/renderer/src/assets/archive/leftbutton.png
Normal file
|
After Width: | Height: | Size: 221 B |
BIN
frontend/src/renderer/src/assets/archive/noimg.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/src/renderer/src/assets/archive/noimg2.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src/renderer/src/assets/archive/pitch.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src/renderer/src/assets/archive/rightbutton.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
frontend/src/renderer/src/assets/archive/roll.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src/renderer/src/assets/archive/selectbottom.png
Normal file
|
After Width: | Height: | Size: 362 B |
BIN
frontend/src/renderer/src/assets/archive/table.png
Normal file
|
After Width: | Height: | Size: 445 B |
BIN
frontend/src/renderer/src/assets/archive/usericon.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
frontend/src/renderer/src/assets/archive/yaw.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
frontend/src/renderer/src/assets/bg.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 89 KiB |
BIN
frontend/src/renderer/src/assets/close.png
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
frontend/src/renderer/src/assets/detection/archive.png
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
frontend/src/renderer/src/assets/detection/big.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
frontend/src/renderer/src/assets/detection/calibration.png
Normal file
|
After Width: | Height: | Size: 447 B |
BIN
frontend/src/renderer/src/assets/detection/close.png
Normal file
|
After Width: | Height: | Size: 271 B |
BIN
frontend/src/renderer/src/assets/detection/conduct.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/src/renderer/src/assets/detection/endvideo.png
Normal file
|
After Width: | Height: | Size: 670 B |
BIN
frontend/src/renderer/src/assets/detection/no-image.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
16
frontend/src/renderer/src/assets/detection/photoalbum.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="20px" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="BGPattern" patternUnits="userSpaceOnUse" alignment="0 0" imageRepeat="None" />
|
||||
<mask fill="white" id="Clip290">
|
||||
<path d="M 21 3.905716464285714 C 21.27038787627078 3.245957928706087 20.735890223733122 2.702498517209308 20.064990224827696 3 L 1.195525299907394 3 C 0.5249081520192851 2.7031374543769244 -0.009213485708160033 3.246214555555484 0 3.9057164642857147 L 0 18.777907668189368 C -0.00921348570815974 19.4374095769196 0.5249081520192849 19.980486678098156 1.195525299907394 20 L 20.064990224827696 20 C 20.735607380518275 19.980486688686625 21.26972903018495 19.43740958438617 21 18.777907668189368 L 21 3.905716464285714 Z M 2.39690712331663 5.118813301910299 L 18.86958541889791 5.118813301910299 L 18.86958541889791 16.06607198089701 L 16.124935967333503 12.69479119975083 C 16.025639205909982 12.57265347420645 15.845591317420697 12.55026222871889 15.718498342050971 12.64424551038206 L 12.529158297758109 15.00226127450166 L 7.056595075330296 9.606801454734219 C 7.001168958709137 9.55195013470927 6.925781695757877 9.521105478442415 6.847145988179413 9.521105478442415 C 6.748976872404263 9.521105478442415 6.6572273760629495 9.56908313075721 6.602341250664903 9.649118786960134 L 2.39690712331663 15.751042965116277 L 2.39690712331663 5.118813301910299 Z M 11.676834668311601 9.671452941029901 L 11.679225491182653 9.671452941029901 C 11.686397920097628 8.591185500830566 12.561434247724588 7.720153745847176 13.6349077620998 7.726031146594684 C 14.707185884888577 7.730733078903654 15.573854372165034 8.60881770307309 15.571463549293984 9.689085162790699 C 15.569072746272022 10.769352602990034 14.698818044538072 11.642735314368773 13.625344510313774 11.642735314368773 C 12.547089370045551 11.639208870016613 11.67444386528964 10.757597781976747 11.676834668311601 9.671452941029901 Z M 23.00927227457588 15.588826524916941 L 23.01166309744693 15.588826524916941 C 22.513068917471912 15.581809171361453 22.11262253697901 15.182360556580665 22.11262253697901 14.69202604424181 C 22.11262253697901 14.687685151925008 22.112654585514925 14.683344316806666 22 14.6790038820598 L 22 2 L 3.442886333467469 2 C 2.938287533708241 1.8228303246589928 2.5338485057905884 1.4161889726150498 2.5391603100296973 0.9199936054817273 C 2.5391603100296973 0.416887551079734 2.94440254372579 0.010170962624584555 3.442886333467469 0 L 23.01166309744693 0 C 23.51400873914463 0.018545126845467025 23.914873644129443 0.42481727570425176 24 0.9188181175249168 L 24 14.68017937001661 C 23.914204343625748 15.174824428285286 23.51227526361774 15.581091721295216 23.00927227457588 15.588826524916941 Z " fill-rule="evenodd" />
|
||||
</mask>
|
||||
</defs>
|
||||
<g transform="matrix(1 0 0 1 -876 -78 )">
|
||||
<path d="M 21 3.905716464285714 C 21.27038787627078 3.245957928706087 20.735890223733122 2.702498517209308 20.064990224827696 3 L 1.195525299907394 3 C 0.5249081520192851 2.7031374543769244 -0.009213485708160033 3.246214555555484 0 3.9057164642857147 L 0 18.777907668189368 C -0.00921348570815974 19.4374095769196 0.5249081520192849 19.980486678098156 1.195525299907394 20 L 20.064990224827696 20 C 20.735607380518275 19.980486688686625 21.26972903018495 19.43740958438617 21 18.777907668189368 L 21 3.905716464285714 Z M 2.39690712331663 5.118813301910299 L 18.86958541889791 5.118813301910299 L 18.86958541889791 16.06607198089701 L 16.124935967333503 12.69479119975083 C 16.025639205909982 12.57265347420645 15.845591317420697 12.55026222871889 15.718498342050971 12.64424551038206 L 12.529158297758109 15.00226127450166 L 7.056595075330296 9.606801454734219 C 7.001168958709137 9.55195013470927 6.925781695757877 9.521105478442415 6.847145988179413 9.521105478442415 C 6.748976872404263 9.521105478442415 6.6572273760629495 9.56908313075721 6.602341250664903 9.649118786960134 L 2.39690712331663 15.751042965116277 L 2.39690712331663 5.118813301910299 Z M 11.676834668311601 9.671452941029901 L 11.679225491182653 9.671452941029901 C 11.686397920097628 8.591185500830566 12.561434247724588 7.720153745847176 13.6349077620998 7.726031146594684 C 14.707185884888577 7.730733078903654 15.573854372165034 8.60881770307309 15.571463549293984 9.689085162790699 C 15.569072746272022 10.769352602990034 14.698818044538072 11.642735314368773 13.625344510313774 11.642735314368773 C 12.547089370045551 11.639208870016613 11.67444386528964 10.757597781976747 11.676834668311601 9.671452941029901 Z M 23.00927227457588 15.588826524916941 L 23.01166309744693 15.588826524916941 C 22.513068917471912 15.581809171361453 22.11262253697901 15.182360556580665 22.11262253697901 14.69202604424181 C 22.11262253697901 14.687685151925008 22.112654585514925 14.683344316806666 22 14.6790038820598 L 22 2 L 3.442886333467469 2 C 2.938287533708241 1.8228303246589928 2.5338485057905884 1.4161889726150498 2.5391603100296973 0.9199936054817273 C 2.5391603100296973 0.416887551079734 2.94440254372579 0.010170962624584555 3.442886333467469 0 L 23.01166309744693 0 C 23.51400873914463 0.018545126845467025 23.914873644129443 0.42481727570425176 24 0.9188181175249168 L 24 14.68017937001661 C 23.914204343625748 15.174824428285286 23.51227526361774 15.581091721295216 23.00927227457588 15.588826524916941 Z " fill-rule="nonzero" fill="rgba(255, 255, 255, 1)" stroke="none" transform="matrix(1 0 0 1 876 78 )" class="fill" />
|
||||
<path d="M 21 3.905716464285714 C 21.27038787627078 3.245957928706087 20.735890223733122 2.702498517209308 20.064990224827696 3 L 1.195525299907394 3 C 0.5249081520192851 2.7031374543769244 -0.009213485708160033 3.246214555555484 0 3.9057164642857147 L 0 18.777907668189368 C -0.00921348570815974 19.4374095769196 0.5249081520192849 19.980486678098156 1.195525299907394 20 L 20.064990224827696 20 C 20.735607380518275 19.980486688686625 21.26972903018495 19.43740958438617 21 18.777907668189368 L 21 3.905716464285714 Z " stroke-width="0" stroke-dasharray="0" stroke="rgba(255, 255, 255, 0)" fill="none" transform="matrix(1 0 0 1 876 78 )" class="stroke" mask="url(#Clip290)" />
|
||||
<path d="M 2.39690712331663 5.118813301910299 L 18.86958541889791 5.118813301910299 L 18.86958541889791 16.06607198089701 L 16.124935967333503 12.69479119975083 C 16.025639205909982 12.57265347420645 15.845591317420697 12.55026222871889 15.718498342050971 12.64424551038206 L 12.529158297758109 15.00226127450166 L 7.056595075330296 9.606801454734219 C 7.001168958709137 9.55195013470927 6.925781695757877 9.521105478442415 6.847145988179413 9.521105478442415 C 6.748976872404263 9.521105478442415 6.6572273760629495 9.56908313075721 6.602341250664903 9.649118786960134 L 2.39690712331663 15.751042965116277 L 2.39690712331663 5.118813301910299 Z " stroke-width="0" stroke-dasharray="0" stroke="rgba(255, 255, 255, 0)" fill="none" transform="matrix(1 0 0 1 876 78 )" class="stroke" mask="url(#Clip290)" />
|
||||
<path d="M 11.676834668311601 9.671452941029901 L 11.679225491182653 9.671452941029901 C 11.686397920097628 8.591185500830566 12.561434247724588 7.720153745847176 13.6349077620998 7.726031146594684 C 14.707185884888577 7.730733078903654 15.573854372165034 8.60881770307309 15.571463549293984 9.689085162790699 C 15.569072746272022 10.769352602990034 14.698818044538072 11.642735314368773 13.625344510313774 11.642735314368773 C 12.547089370045551 11.639208870016613 11.67444386528964 10.757597781976747 11.676834668311601 9.671452941029901 Z " stroke-width="0" stroke-dasharray="0" stroke="rgba(255, 255, 255, 0)" fill="none" transform="matrix(1 0 0 1 876 78 )" class="stroke" mask="url(#Clip290)" />
|
||||
<path d="M 23.00927227457588 15.588826524916941 L 23.01166309744693 15.588826524916941 C 22.513068917471912 15.581809171361453 22.11262253697901 15.182360556580665 22.11262253697901 14.69202604424181 C 22.11262253697901 14.687685151925008 22.112654585514925 14.683344316806666 22 14.6790038820598 L 22 2 L 3.442886333467469 2 C 2.938287533708241 1.8228303246589928 2.5338485057905884 1.4161889726150498 2.5391603100296973 0.9199936054817273 C 2.5391603100296973 0.416887551079734 2.94440254372579 0.010170962624584555 3.442886333467469 0 L 23.01166309744693 0 C 23.51400873914463 0.018545126845467025 23.914873644129443 0.42481727570425176 24 0.9188181175249168 L 24 14.68017937001661 C 23.914204343625748 15.174824428285286 23.51227526361774 15.581091721295216 23.00927227457588 15.588826524916941 Z " stroke-width="0" stroke-dasharray="0" stroke="rgba(255, 255, 255, 0)" fill="none" transform="matrix(1 0 0 1 876 78 )" class="stroke" mask="url(#Clip290)" />
|
||||
</g>
|
||||
</svg>
|
||||
BIN
frontend/src/renderer/src/assets/detection/progress.png
Normal file
|
After Width: | Height: | Size: 452 B |
BIN
frontend/src/renderer/src/assets/detection/refresh.png
Normal file
|
After Width: | Height: | Size: 470 B |
BIN
frontend/src/renderer/src/assets/detection/refresh_active.png
Normal file
|
After Width: | Height: | Size: 504 B |
BIN
frontend/src/renderer/src/assets/detection/screenshot.png
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
frontend/src/renderer/src/assets/detection/set.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/src/renderer/src/assets/detection/settings.png
Normal file
|
After Width: | Height: | Size: 433 B |
BIN
frontend/src/renderer/src/assets/detection/startvideo.png
Normal file
|
After Width: | Height: | Size: 674 B |
BIN
frontend/src/renderer/src/assets/detection/title1.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
frontend/src/renderer/src/assets/detection/title2.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
frontend/src/renderer/src/assets/detection/title3.png
Normal file
|
After Width: | Height: | Size: 546 B |
BIN
frontend/src/renderer/src/assets/detection/title4.png
Normal file
|
After Width: | Height: | Size: 460 B |