diff --git a/backend/config.ini b/backend/config.ini
index a2da6209..35d88621 100644
--- a/backend/config.ini
+++ b/backend/config.ini
@@ -52,6 +52,7 @@ synchronized_images_only = False
[DEVICES]
imu_enable = True
imu_use_mock = False
+imu_ble_name = WT901BLE67
imu_mac_address = FA:E8:88:06:FE:F3
pressure_enable = False
pressure_use_mock = False
diff --git a/backend/devices/base_device.py b/backend/devices/base_device.py
index 85d17d44..e1c1bf86 100644
--- a/backend/devices/base_device.py
+++ b/backend/devices/base_device.py
@@ -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() # 可重入锁
@@ -361,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})>"
\ No newline at end of file
+ return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py
index a3279274..47e9f4b6 100644
--- a/backend/devices/camera_manager.py
+++ b/backend/devices/camera_manager.py
@@ -812,17 +812,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]:
"""
@@ -994,4 +996,4 @@ class CameraManager(BaseDevice):
-
\ No newline at end of file
+
diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py
index df73b117..de3f70d9 100644
--- a/backend/devices/device_coordinator.py
+++ b/backend/devices/device_coordinator.py
@@ -75,6 +75,8 @@ 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=8, thread_name_prefix="DeviceCoord")
@@ -117,12 +119,12 @@ class DeviceCoordinator:
if not self._initialize_devices():
self.logger.warning("设备初始化失败,将以降级模式继续运行")
- # 启动监控线程
- self._start_monitor()
-
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())})
@@ -191,7 +193,8 @@ class DeviceCoordinator:
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)
if result:
success_count += 1
self.logger.info(f"{device_name}设备初始化成功")
@@ -587,12 +590,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} 设备推流...")
@@ -609,6 +622,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'):
@@ -646,7 +664,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
# 第四步:重新创建设备实例
@@ -754,8 +772,6 @@ 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
@@ -797,6 +813,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):
"""
@@ -822,15 +840,18 @@ 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:
+ now = time.time()
+ if now - self._last_restart_ts[device_name] >= 15.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
@@ -1098,7 +1119,7 @@ if __name__ == "__main__":
# 执行测试
# 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
- success = test_restart_device('pressure')
+ success = test_restart_device('imu')
if success:
print("\n🎉 所有测试通过!")
diff --git a/backend/devices/device_model.py b/backend/devices/device_model.py
index 83804d73..20d7a110 100644
--- a/backend/devices/device_model.py
+++ b/backend/devices/device_model.py
@@ -1,9 +1,8 @@
# coding:UTF-8
-import threading
import time
-import struct
import bleak
import asyncio
+import logging
# 设备实例 Device instance
@@ -24,7 +23,8 @@ class DeviceModel:
# endregion
def __init__(self, deviceName, BLEDevice, callback_method):
- print("Initialize device model")
+ self.logger = logging.getLogger("device.imu.witmotion")
+ self.logger.info("初始化IMU设备模型")
# 设备名称(自定义) Device Name
self.deviceName = deviceName
self.BLEDevice = BLEDevice
@@ -57,10 +57,12 @@ class DeviceModel:
# 打开设备 open Device
async def openDevice(self):
- print("Opening device......")
- # 获取设备的服务和特征 Obtain the services and characteristic of the device
+ 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"
@@ -68,11 +70,28 @@ class DeviceModel:
target_characteristic_uuid_write = "0000ffe9-0000-1000-8000-00805f9a34fb"
notify_characteristic = None
- print("Matching services......")
- for service in client.services:
+ self.logger.info("正在匹配服务...")
+ services = None
+ for _ in range(3):
+ get_services = getattr(client, 'get_services', None)
+ if callable(get_services):
+ services = await get_services()
+ else:
+ backend = getattr(client, "_backend", None)
+ backend_get_services = getattr(backend, "get_services", None)
+ if callable(backend_get_services):
+ services = await backend_get_services()
+ else:
+ services = getattr(client, 'services', None)
+ if services:
+ break
+ await asyncio.sleep(0.2)
+ if not services:
+ services = []
+ for service in services:
if service.uuid == target_service_uuid:
- print(f"Service: {service}")
- print("Matching characteristic......")
+ self.logger.info(f"匹配到服务: {service}")
+ self.logger.info("正在匹配特征...")
for characteristic in service.characteristics:
if characteristic.uuid == target_characteristic_uuid_read:
notify_characteristic = characteristic
@@ -81,16 +100,12 @@ class DeviceModel:
if notify_characteristic:
break
- if self.writer_characteristic:
- # 读取磁场四元数 Reading magnetic field quaternions
- print("Reading magnetic field quaternions")
- time.sleep(3)
- asyncio.create_task(self.sendDataTh())
-
if notify_characteristic:
- print(f"Characteristic: {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:
@@ -102,19 +117,13 @@ class DeviceModel:
# 在退出时停止通知 Stop notification on exit
await client.stop_notify(notify_characteristic.uuid)
else:
- print("No matching services or characteristic found")
+ self.logger.warning("未找到匹配的服务或特征")
+ raise RuntimeError("未找到匹配的服务或特征")
# 关闭设备 close Device
def closeDevice(self):
self.isOpen = False
- print("The device is turned off")
-
- async def sendDataTh(self):
- while self.isOpen:
- await self.readReg(0x3A)
- time.sleep(0.1)
- await self.readReg(0x51)
- time.sleep(0.1)
+ self.logger.info("设备已关闭")
# region 数据解析 data analysis
# 串口数据处理 Serial port data processing
@@ -125,7 +134,7 @@ class DeviceModel:
if len(self.TempBytes) == 1 and self.TempBytes[0] != 0x55:
del self.TempBytes[0]
continue
- if len(self.TempBytes) == 2 and (self.TempBytes[1] != 0x61 and self.TempBytes[1] != 0x71):
+ if len(self.TempBytes) == 2 and self.TempBytes[1] != 0x61:
del self.TempBytes[0]
continue
if len(self.TempBytes) == 20:
@@ -134,47 +143,15 @@ class DeviceModel:
# 数据解析 data analysis
def processData(self, Bytes):
- if Bytes[1] == 0x61:
- Ax = self.getSignInt16(Bytes[3] << 8 | Bytes[2]) / 32768 * 16
- Ay = self.getSignInt16(Bytes[5] << 8 | Bytes[4]) / 32768 * 16
- Az = self.getSignInt16(Bytes[7] << 8 | Bytes[6]) / 32768 * 16
- Gx = self.getSignInt16(Bytes[9] << 8 | Bytes[8]) / 32768 * 2000
- Gy = self.getSignInt16(Bytes[11] << 8 | Bytes[10]) / 32768 * 2000
- Gz = self.getSignInt16(Bytes[13] << 8 | Bytes[12]) / 32768 * 2000
- 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("AccX", round(Ax, 3))
- self.set("AccY", round(Ay, 3))
- self.set("AccZ", round(Az, 3))
- self.set("AsX", round(Gx, 3))
- self.set("AsY", round(Gy, 3))
- self.set("AsZ", round(Gz, 3))
- self.set("AngX", round(AngX, 3))
- self.set("AngY", round(AngY, 3))
- self.set("AngZ", round(AngZ, 3))
- self.callback_method(self)
- else:
- # 磁场 magnetic field
- if Bytes[2] == 0x3A:
- Hx = self.getSignInt16(Bytes[5] << 8 | Bytes[4]) / 120
- Hy = self.getSignInt16(Bytes[7] << 8 | Bytes[6]) / 120
- Hz = self.getSignInt16(Bytes[9] << 8 | Bytes[8]) / 120
- self.set("HX", round(Hx, 3))
- self.set("HY", round(Hy, 3))
- self.set("HZ", round(Hz, 3))
- # 四元数 Quaternion
- elif Bytes[2] == 0x51:
- Q0 = self.getSignInt16(Bytes[5] << 8 | Bytes[4]) / 32768
- Q1 = self.getSignInt16(Bytes[7] << 8 | Bytes[6]) / 32768
- Q2 = self.getSignInt16(Bytes[9] << 8 | Bytes[8]) / 32768
- Q3 = self.getSignInt16(Bytes[11] << 8 | Bytes[10]) / 32768
- self.set("Q0", round(Q0, 5))
- self.set("Q1", round(Q1, 5))
- self.set("Q2", round(Q2, 5))
- self.set("Q3", round(Q3, 5))
- else:
- pass
+ if Bytes[1] != 0x61:
+ return
+ 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)
# 获得int16有符号数 Obtain int16 signed number
@staticmethod
@@ -191,7 +168,7 @@ class DeviceModel:
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:
- print(ex)
+ self.logger.warning(f"发送数据失败: {ex}")
# 读取寄存器 read register
async def readReg(self, regAddr):
diff --git a/backend/devices/femtobolt_manager.py b/backend/devices/femtobolt_manager.py
index 0715abe2..18bdf741 100644
--- a/backend/devices/femtobolt_manager.py
+++ b/backend/devices/femtobolt_manager.py
@@ -939,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,
@@ -949,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()
+ }
diff --git a/backend/devices/imu_manager.py b/backend/devices/imu_manager.py
index 69768263..54d61e68 100644
--- a/backend/devices/imu_manager.py
+++ b/backend/devices/imu_manager.py
@@ -23,8 +23,9 @@ logger = logging.getLogger(__name__)
class BleIMUDevice:
"""蓝牙IMU设备(WitMotion WT9011DCL-BT50),基于 device_model.py 官方接口"""
- def __init__(self, mac_address: str):
+ def __init__(self, mac_address: str, ble_name: str = ""):
self.mac_address = mac_address
+ self.ble_name = ble_name
self.loop = None
self.loop_thread = None
self.running = False
@@ -40,11 +41,14 @@ class BleIMUDevice:
self._connected = False
self._device_model = None
self._open_task = None
+ self._main_task = None
+ self._last_update_ts = None
try:
from . import device_model as wit_device_model
except Exception:
import device_model as wit_device_model
self._wit_device_model = wit_device_model
+ logger.info(f"BLE IMU实例创建: mac={self.mac_address}")
def set_calibration(self, calibration: Dict[str, Any]):
self.calibration_data = calibration
@@ -75,7 +79,21 @@ class BleIMUDevice:
self.running = False
try:
if self.loop:
- asyncio.run_coroutine_threadsafe(self._disconnect(), self.loop)
+ try:
+ if self._main_task is not None and not self._main_task.done():
+ self.loop.call_soon_threadsafe(self._main_task.cancel)
+ except Exception:
+ pass
+ try:
+ fut = asyncio.run_coroutine_threadsafe(self._disconnect(), self.loop)
+ fut.result(timeout=5.0)
+ except Exception:
+ pass
+ except Exception:
+ pass
+ try:
+ if self.loop_thread and self.loop_thread.is_alive():
+ self.loop_thread.join(timeout=6.0)
except Exception:
pass
@@ -96,18 +114,28 @@ class BleIMUDevice:
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
try:
- self.loop.run_until_complete(self._connect_and_listen())
+ self._main_task = self.loop.create_task(self._connect_and_listen())
+ self.loop.run_until_complete(self._main_task)
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f'BLE IMU事件循环异常: {e}', exc_info=True)
finally:
try:
+ try:
+ pending = asyncio.all_tasks(self.loop)
+ for t in pending:
+ t.cancel()
+ if pending:
+ self.loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
+ except Exception:
+ pass
if not self.loop.is_closed():
self.loop.stop()
- self.loop.close()
except Exception:
pass
+ self._main_task = None
+ self.loop = None
def _on_device_update(self, dm):
try:
@@ -120,6 +148,8 @@ class BleIMUDevice:
self.last_data['roll'] = float(roll)
self.last_data['pitch'] = float(pitch)
self.last_data['yaw'] = float(yaw)
+ self._last_update_ts = time.time()
+ self._connected = True
except Exception:
pass
@@ -130,6 +160,12 @@ class BleIMUDevice:
self._device_model.closeDevice()
except Exception:
pass
+ try:
+ client = getattr(self._device_model, "client", None)
+ if client is not None and getattr(client, "is_connected", False):
+ await client.disconnect()
+ except Exception:
+ pass
if self._open_task is not None and not self._open_task.done():
self._open_task.cancel()
try:
@@ -141,6 +177,7 @@ class BleIMUDevice:
finally:
self._open_task = None
self._device_model = None
+ self._last_update_ts = None
async def _connect_and_listen(self):
try:
@@ -150,39 +187,86 @@ class BleIMUDevice:
self.running = False
return
+ async def find_device() -> Optional[Any]:
+ scan_timeout_s = 30.0
+ if self.ble_name:
+ find_by_name = getattr(BleakScanner, "find_device_by_name", None)
+ if callable(find_by_name):
+ try:
+ device = await find_by_name(self.ble_name, timeout=scan_timeout_s)
+ if device is not None:
+ if self.mac_address and (getattr(device, "address", "") or "").lower() != self.mac_address.lower():
+ return None
+ return device
+ except Exception:
+ pass
+ try:
+ found = await BleakScanner.discover(timeout=scan_timeout_s)
+ except Exception:
+ found = []
+ if self.ble_name:
+ for d in found:
+ if (getattr(d, "name", None) or "") != self.ble_name:
+ continue
+ if self.mac_address and (getattr(d, "address", "") or "").lower() != self.mac_address.lower():
+ continue
+ return d
+ if self.mac_address:
+ target = self.mac_address.lower()
+ for d in found:
+ addr = getattr(d, "address", "") or ""
+ if addr.lower() == target:
+ return d
+ candidates = [d for d in found if (getattr(d, "name", "") or "").startswith("WT")]
+ if len(candidates) == 1:
+ return candidates[0]
+ return None
+
while self.running:
try:
- try:
- device = await BleakScanner.find_device_by_address(self.mac_address, timeout=20.0)
- except TypeError:
- device = await BleakScanner.find_device_by_address(self.mac_address, cb=dict(use_bdaddr=False))
+ attempt_ts = time.perf_counter()
+ logger.info(f"BLE IMU开始扫描并连接: name={self.ble_name}, mac={self.mac_address}")
+ device = await find_device()
if device is None:
+ logger.info(f"BLE IMU扫描未发现设备 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms)")
await asyncio.sleep(2.0)
continue
+ device_addr = getattr(device, "address", None)
+ device_name = getattr(device, "name", None)
+ logger.info(f"BLE IMU发现设备 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms, address={device_addr}, name={device_name})")
+ self._connected = False
+ self._last_update_ts = None
self._device_model = self._wit_device_model.DeviceModel("WitMotionBle5.0", device, self._on_device_update)
self._open_task = asyncio.create_task(self._device_model.openDevice())
- connected = False
- for _ in range(50):
- if not self.running:
+ ready = False
+ ready_timeout_s = 20.0
+ deadline = time.time() + ready_timeout_s
+ while self.running and time.time() < deadline:
+ if self._open_task is not None and self._open_task.done():
+ try:
+ exc = self._open_task.exception()
+ except Exception:
+ exc = None
+ if exc:
+ logger.warning(f"BLE IMU打开失败: {type(exc).__name__}: {repr(exc)}")
break
- if self._open_task.done():
+ if self.has_received_data:
+ ready = True
break
- client = getattr(self._device_model, "client", None)
- if client is not None and getattr(client, "is_connected", False):
- connected = True
- break
- await asyncio.sleep(0.2)
+ await asyncio.sleep(0.1)
- self._connected = connected
- if not connected:
+ if not ready:
+ logger.warning(f"BLE IMU未获取到姿态数据 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms)")
await self._disconnect()
self._connected = False
await asyncio.sleep(2.0)
continue
+ logger.info(f"BLE IMU连接并开始产出数据 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms)")
+
while self.running and self._open_task is not None and not self._open_task.done():
await asyncio.sleep(1.0)
@@ -199,7 +283,15 @@ class BleIMUDevice:
@property
def connected(self) -> bool:
return self._connected
-
+
+ @property
+ def has_received_data(self) -> bool:
+ return self._last_update_ts is not None
+
+ @property
+ def last_update_time(self):
+ return self._last_update_ts
+
class MockIMUDevice:
def __init__(self):
self.running = False
@@ -215,6 +307,7 @@ class MockIMUDevice:
'temperature': 25.0
}
self._phase = 0.0
+ self._last_update_ts = None
def set_calibration(self, calibration: Dict[str, Any]):
self.calibration_data = calibration
@@ -286,6 +379,7 @@ class MockIMUDevice:
self.last_data['roll'] = round(roll, 1)
# 温度模拟:以25℃为基准,叠加±0.5℃的轻微波动
self.last_data['temperature'] = round(25.0 + math.sin(self._phase * 0.2) * 0.5, 2)
+ self._last_update_ts = time.time()
# 控制输出频率为约60Hz
time.sleep(1.0 / 30.0)
except Exception:
@@ -296,6 +390,14 @@ class MockIMUDevice:
def connected(self) -> bool:
return self._connected
+ @property
+ def has_received_data(self) -> bool:
+ return self._last_update_ts is not None
+
+ @property
+ def last_update_time(self):
+ return self._last_update_ts
+
class IMUManager(BaseDevice):
"""IMU传感器管理器"""
@@ -319,11 +421,12 @@ class IMUManager(BaseDevice):
# 设备配置
self.use_mock = bool(config.get('use_mock', False))
self.mac_address = config.get('mac_address', '')
+ self.ble_name = config.get('ble_name', '')
# IMU设备实例
self.imu_device = None
+ self._init_abort = threading.Event()
# 推流相关
- self.imu_streaming = False
self.imu_thread = None
# 统计信息
@@ -338,7 +441,7 @@ class IMUManager(BaseDevice):
self.data_buffer = deque(maxlen=100)
self.last_valid_data = None
- self.logger.info(f"IMU管理器初始化完成 - use_mock: {self.use_mock}, MAC: {self.mac_address}")
+ self.logger.info(f"IMU管理器初始化完成 - use_mock: {self.use_mock}, BLE_NAME: {self.ble_name}, MAC: {self.mac_address}")
def initialize(self) -> bool:
"""
@@ -347,35 +450,62 @@ class IMUManager(BaseDevice):
Returns:
bool: 初始化是否成功
"""
+ self._initializing = True
+ self._init_abort.clear()
try:
+ start_ts = time.perf_counter()
self.logger.info(f"正在初始化IMU设备...")
# 使用构造函数中已加载的配置,避免并发读取配置文件
- self.logger.info(f"使用已加载配置: use_mock={self.use_mock}, mac={self.mac_address}")
+ self.logger.info(f"使用已加载配置: use_mock={self.use_mock}, name={self.ble_name}, mac={self.mac_address}")
# 根据配置选择设备类型
if not self.use_mock:
- if not self.mac_address:
- self.logger.error("IMU BLE设备未配置MAC地址")
+ if not self.mac_address and not self.ble_name:
+ self.logger.error("IMU BLE设备未配置蓝牙名称或MAC地址")
self.is_connected = False
return False
- self.logger.info(f"使用蓝牙IMU设备 - MAC: {self.mac_address}")
- self.imu_device = BleIMUDevice(self.mac_address)
+ self.logger.info(f"使用蓝牙IMU设备 - NAME: {self.ble_name}, MAC: {self.mac_address}")
+ try:
+ if self.imu_device and hasattr(self.imu_device, 'stop'):
+ self.imu_device.stop()
+ except Exception:
+ pass
+ self.imu_device = BleIMUDevice(self.mac_address, self.ble_name)
self.imu_device.start()
- # 使用set_connected方法来正确启动连接监控线程
- self.set_connected(True)
+ connect_timeout_s = float(self.config.get('connect_timeout', 40.0))
+ deadline = time.time() + max(0.1, connect_timeout_s)
+ while time.time() < deadline:
+ if self._init_abort.is_set():
+ self.logger.warning("IMU初始化被中断")
+ return False
+ if bool(getattr(self.imu_device, 'connected', False)):
+ break
+ time.sleep(0.1)
+ connected = bool(getattr(self.imu_device, 'connected', False))
+ self._start_connection_monitor()
+ self.set_connected(connected)
+ if not connected:
+ self.logger.error(f"IMU蓝牙连接超时(等待 {connect_timeout_s:.1f}s)")
+ try:
+ self.imu_device.stop()
+ except Exception:
+ pass
+ self.imu_device = None
+ self.set_connected(False)
+ return False
else:
self.logger.info("使用模拟IMU设备")
self.imu_device = MockIMUDevice()
self.imu_device.start()
- # 使用set_connected方法来正确启动连接监控线程
+ self._start_connection_monitor()
self.set_connected(True)
self._device_info.update({
'mac_address': self.mac_address,
})
- self.logger.info("IMU初始化成功")
+ self.logger.info(f"IMU初始化完成(耗时: {(time.perf_counter() - start_ts)*1000:.1f}ms,当前连接状态: {self.is_connected})")
return True
except Exception as e:
@@ -383,6 +513,8 @@ class IMUManager(BaseDevice):
self.is_connected = False
self.imu_device = None
return False
+ finally:
+ self._initializing = False
def _quick_calibrate_imu(self) -> Dict[str, Any]:
"""
@@ -464,20 +596,39 @@ class IMUManager(BaseDevice):
bool: 启动是否成功
"""
try:
+ if self.is_streaming:
+ self.logger.warning("IMU数据流已在运行")
+ return True
+
if not self.is_connected or not self.imu_device:
if not self.initialize():
return False
+
+ if not self.is_connected:
+ self.logger.error("IMU设备未连接")
+ return False
- if self.imu_streaming:
- self.logger.warning("IMU数据流已在运行")
- return True
+ first_data_timeout_s = float(self.config.get('first_data_timeout', 30.0))
+ deadline = time.time() + max(0.1, first_data_timeout_s)
+ while time.time() < deadline:
+ if not self.imu_device:
+ break
+ if hasattr(self.imu_device, 'connected') and not bool(getattr(self.imu_device, 'connected')):
+ break
+ if bool(getattr(self.imu_device, 'has_received_data', False)):
+ break
+ time.sleep(0.05)
+ if not self.imu_device or not bool(getattr(self.imu_device, 'has_received_data', False)):
+ self.logger.error(f"IMU未获取到有效数据(等待 {first_data_timeout_s:.1f}s)")
+ return False
# 启动前进行快速校准
if not self.is_calibrated:
self.logger.info("启动前进行快速零点校准...")
self._quick_calibrate_imu()
- self.imu_streaming = True
+ self.is_streaming = True
+ self.update_heartbeat()
self.imu_thread = threading.Thread(target=self._imu_streaming_thread, daemon=True)
self.imu_thread.start()
@@ -486,7 +637,7 @@ class IMUManager(BaseDevice):
except Exception as e:
self.logger.error(f"IMU数据流启动失败: {e}")
- self.imu_streaming = False
+ self.is_streaming = False
return False
def stop_streaming(self) -> bool:
@@ -497,7 +648,7 @@ class IMUManager(BaseDevice):
bool: 停止是否成功
"""
try:
- self.imu_streaming = False
+ self.is_streaming = False
if self.imu_thread and self.imu_thread.is_alive():
self.imu_thread.join(timeout=3.0)
@@ -522,19 +673,25 @@ class IMUManager(BaseDevice):
"""
self.logger.info("IMU数据流工作线程启动")
- while self.imu_streaming:
+ while self.is_streaming:
try:
if self.imu_device:
# 读取IMU数据
data = self.imu_device.read_data(apply_calibration=True)
- if data:
+ if data and isinstance(data, dict) and data.get('head_pose') is not None:
# 发送数据到前端
if self._socketio:
self._socketio.emit('imu_data', data, namespace='/devices')
# 更新统计
self.data_count += 1
+ self.update_heartbeat()
+ self.last_valid_data = data
+ try:
+ self.data_buffer.append(data)
+ except Exception:
+ pass
else:
self.error_count += 1
@@ -556,19 +713,19 @@ class IMUManager(BaseDevice):
Returns:
Dict[str, Any]: 设备状态信息
"""
- status = super().get_status()
- status.update({
- 'is_streaming': self.imu_streaming,
+ return {
+ 'device_type': 'mock' if self.use_mock else 'ble',
+ 'mac_address': self.mac_address,
+ 'is_connected': self.is_connected,
+ 'is_streaming': self.is_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 'ble',
- 'mac_address': self.mac_address
- })
- return status
+ 'device_info': self.get_device_info()
+ }
def get_latest_data(self) -> Optional[Dict[str, float]]:
"""
@@ -586,9 +743,15 @@ class IMUManager(BaseDevice):
断开IMU设备连接
"""
try:
+ self._init_abort.set()
self.stop_streaming()
if self.imu_device:
+ try:
+ if hasattr(self.imu_device, 'stop'):
+ self.imu_device.stop()
+ except Exception:
+ pass
self.imu_device = None
self.is_connected = False
@@ -643,7 +806,18 @@ class IMUManager(BaseDevice):
return False
if hasattr(self.imu_device, 'connected'):
- return bool(getattr(self.imu_device, 'connected'))
+ connected = bool(getattr(self.imu_device, 'connected'))
+ if connected:
+ last_ts = getattr(self.imu_device, 'last_update_time', None)
+ if last_ts:
+ try:
+ if time.time() - float(last_ts) <= float(self._connection_timeout):
+ self.update_heartbeat()
+ except Exception:
+ self.update_heartbeat()
+ else:
+ self.update_heartbeat()
+ return connected
if hasattr(self.imu_device, 'ser') and getattr(self.imu_device, 'ser', None):
if not self.imu_device.ser.is_open:
@@ -653,6 +827,7 @@ class IMUManager(BaseDevice):
self.imu_device.ser.timeout = 0.1
self.imu_device.ser.read(1)
self.imu_device.ser.timeout = original_timeout
+ self.update_heartbeat()
return True
except Exception:
return False
@@ -668,6 +843,7 @@ class IMUManager(BaseDevice):
清理资源
"""
try:
+ self._init_abort.set()
# 停止连接监控
self._cleanup_monitoring()
diff --git a/backend/devices/imu_test.py b/backend/devices/imu_test.py
index c5e4f6ae..e83716e1 100644
--- a/backend/devices/imu_test.py
+++ b/backend/devices/imu_test.py
@@ -1,69 +1,84 @@
+import argparse
import asyncio
+import time
+from statistics import mean
+
import bleak
-import device_model
-
-# 扫描到的设备 Scanned devices
-devices = []
-# 蓝牙设备 BLEDevice
-BLEDevice = None
-# 扫描蓝牙设备并过滤名称
-# Scan Bluetooth devices and filter names
-async def scan():
- global devices
- global BLEDevice
- find = []
- print("Searching for Bluetooth devices......")
+async def find_device_by_address(address: str, timeout_s: float):
try:
- devices = await bleak.BleakScanner.discover(timeout=20.0)
- print("Search ended")
- for d in devices:
- if d.name is not None and "WT" in d.name:
- find.append(d)
- print(d)
- if len(find) == 0:
- print("No devices found in this search!")
+ 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:
- user_input = input("Please enter the Mac address you want to connect to (e.g. DF:E9:1F:2C:BD:59):")
- for d in devices:
- if d.address == user_input:
- BLEDevice = d
- break
- except Exception as ex:
- print("Bluetooth search failed to start")
- print(ex)
+ 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)
-
-# 指定MAC地址搜索并连接设备
-# Specify MAC address to search and connect devices
-async def scanByMac(device_mac):
- global BLEDevice
- print("Searching for Bluetooth devices......")
- BLEDevice = await bleak.BleakScanner.find_device_by_address(device_mac, timeout=20)
-
-
-# 数据更新时会调用此方法 This method will be called when data is updated
-def updateData(DeviceModel):
- # 直接打印出设备数据字典 Directly print out the device data dictionary
- print(DeviceModel.deviceData)
- # 获得X轴加速度 Obtain X-axis acceleration
- # print(DeviceModel.get("AccX"))
-
-
-if __name__ == '__main__':
- # 方式一:广播搜索和连接蓝牙设备
- # # Method 1:Broadcast search and connect Bluetooth devices
- # asyncio.run(scan())
-
- # # 方式二:指定MAC地址搜索并连接设备
- # # Method 2: Specify MAC address to search and connect devices
- asyncio.run(scanByMac("FA:E8:88:06:FE:F3"))
-
- if BLEDevice is not None:
- # 创建设备 Create device
- device = device_model.DeviceModel("MyBle5.0", BLEDevice, updateData)
- # 开始连接设备 Start connecting devices
- asyncio.run(device.openDevice())
+ 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("This BLEDevice was not found!!")
+ 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"], default="both")
+ return parser.parse_args()
+
+
+async def main():
+ args = parse_args()
+
+ 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())
diff --git a/backend/devices/utils/config_manager.py b/backend/devices/utils/config_manager.py
index dce5a71d..298bec7e 100644
--- a/backend/devices/utils/config_manager.py
+++ b/backend/devices/utils/config_manager.py
@@ -98,6 +98,7 @@ class ConfigManager:
self.config['DEVICES'] = {
'imu_enable': 'False',
'imu_use_mock': 'False',
+ 'imu_ble_name': '',
'imu_mac_address': '',
'pressure_port': 'COM8',
'pressure_baudrate': '115200'
@@ -235,6 +236,7 @@ class ConfigManager:
return {
'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'),
}
diff --git a/backend/main.py b/backend/main.py
index dca45d05..bc44da7e 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -18,6 +18,7 @@ import logging
from flask_socketio import SocketIO, emit
import configparser
import argparse
+from collections import defaultdict
# 添加当前目录到路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@@ -81,6 +82,7 @@ class AppServer:
# 数据推送状态
self.is_pushing_data = False
+ self._device_op_locks = defaultdict(threading.RLock)
# 设备管理器
self.config_manager = None
@@ -97,6 +99,9 @@ class AppServer:
# 注册路由和事件
self._register_routes()
self._register_socketio_events()
+
+ def _get_device_lock(self, device_name: str):
+ return self._device_op_locks[device_name]
def _init_logging(self):
"""初始化日志配置"""
@@ -228,6 +233,10 @@ class AppServer:
self.logger.info('正在初始化设备协调器...')
self.device_coordinator = DeviceCoordinator(self.socketio)
+ # Flask应用启动后,异步初始化设备(如果尚未初始化)
+ if self.device_coordinator and not self.device_coordinator.is_initialized:
+ threading.Thread(target=self._initialize_devices, daemon=True).start()
+
# 初始化录制管理器
self.logger.info('正在初始化录制管理器...')
@@ -247,8 +256,8 @@ class AppServer:
if self.socketio:
self.socketio.run(self.app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
else:
- self.app.run(host=host, port=port, debug=debug)
-
+ self.app.run(host=host, port=port, debug=debug)
+
except Exception as e:
self.logger.error(f'应用初始化失败: {e}')
raise
@@ -755,9 +764,7 @@ class AppServer:
self.logger.info(f'用户 {username} 登录成功')
- # 登录成功后,异步初始化设备(如果尚未初始化)
- if self.device_coordinator and not self.device_coordinator.is_initialized:
- threading.Thread(target=self._initialize_devices, daemon=True).start()
+
return jsonify({
'success': True,
@@ -1964,7 +1971,8 @@ class AppServer:
self.logger.info(f'开始重启 {device_type} 设备...')
# 调用设备协调器的重启方法
- success = self.device_coordinator.restart_device(device_type)
+ with self._get_device_lock(device_type):
+ success = self.device_coordinator.restart_device(device_type)
if success:
self.logger.info(f'{device_type} 设备重启成功')
@@ -2023,6 +2031,25 @@ class AppServer:
def initialize_device(device_name, manager):
"""设备初始化工作函数"""
try:
+ with self._get_device_lock(device_name):
+ restarting = bool(getattr(self.device_coordinator, '_restart_in_progress', {}).get(device_name, False))
+ if restarting:
+ device_results[device_name] = False
+ self.logger.warning(f'{device_name}设备正在重启,跳过启动')
+ return
+
+ initializing = bool(getattr(manager, '_initializing', False))
+ if initializing:
+ wait_deadline = time.time() + (60.0 if device_name == 'imu' else 30.0)
+ while time.time() < wait_deadline and bool(getattr(manager, '_initializing', False)):
+ time.sleep(0.2)
+ if hasattr(manager, 'is_connected') and manager.is_connected:
+ if hasattr(manager, 'is_streaming') and not manager.is_streaming:
+ manager.start_streaming()
+ device_results[device_name] = True
+ self.logger.info(f'{device_name}设备已连接,启动成功')
+ return
+
# 检查设备是否已连接,避免重复初始化
if hasattr(manager, 'is_connected') and manager.is_connected:
print(f"[DEBUG] {device_name} 已连接,跳过初始化")
diff --git a/frontend/src/renderer/src/assets/glb/head.glb b/frontend/src/renderer/src/assets/glb/head.glb
new file mode 100644
index 00000000..400f67e3
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/head.glb differ
diff --git a/frontend/src/renderer/src/assets/glb/xyz-axes.png b/frontend/src/renderer/src/assets/glb/xyz-axes.png
new file mode 100644
index 00000000..68a27a0d
Binary files /dev/null and b/frontend/src/renderer/src/assets/glb/xyz-axes.png differ
diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue
index f1176f41..7c04259d 100644
--- a/frontend/src/renderer/src/views/Detection.vue
+++ b/frontend/src/renderer/src/views/Detection.vue
@@ -1774,47 +1774,50 @@ function handleIMUData(data) {
function updateHeadPoseMaxValues(headPose) {
try {
// 更新旋转角最值
- if (headPose.rotation < 0) {
+ if (headPose.rotation > 0) {
// 左旋(负值),取绝对值的最大值
headPoseMaxValues.value.rotationLeftMax = Math.max(
headPoseMaxValues.value.rotationLeftMax,
- Math.abs(headPose.rotation)
+ headPose.rotation
)
- } else if (headPose.rotation > 0) {
+ } else if (headPose.rotation < 0) {
// 右旋(正值)
headPoseMaxValues.value.rotationRightMax = Math.max(
headPoseMaxValues.value.rotationRightMax,
- headPose.rotation
+ Math.abs(headPose.rotation)
+
)
}
// 更新倾斜角最值
- if (headPose.tilt < 0) {
+ if (headPose.tilt > 0) {
// 左倾(负值),取绝对值的最大值
headPoseMaxValues.value.tiltLeftMax = Math.max(
headPoseMaxValues.value.tiltLeftMax,
- Math.abs(headPose.tilt)
+ headPose.tilt
+
)
- } else if (headPose.tilt > 0) {
+ } else if (headPose.tilt < 0) {
// 右倾(正值)
headPoseMaxValues.value.tiltRightMax = Math.max(
headPoseMaxValues.value.tiltRightMax,
- headPose.tilt
+ Math.abs(headPose.tilt)
)
}
// 更新俯仰角最值
- if (headPose.pitch < 0) {
+ if (headPose.pitch >0) {
// 下俯(负值),取绝对值的最大值
headPoseMaxValues.value.pitchDownMax = Math.max(
headPoseMaxValues.value.pitchDownMax,
- Math.abs(headPose.pitch)
+ headPose.pitch
+
)
- } else if (headPose.pitch > 0) {
+ } else if (headPose.pitch < 0) {
// 上仰(正值)
headPoseMaxValues.value.pitchUpMax = Math.max(
headPoseMaxValues.value.pitchUpMax,
- headPose.pitch
+ Math.abs(headPose.pitch)
)
}
diff --git a/frontend/src/renderer/src/views/model.vue b/frontend/src/renderer/src/views/model.vue
index b329a4a5..80ab22d2 100644
--- a/frontend/src/renderer/src/views/model.vue
+++ b/frontend/src/renderer/src/views/model.vue
@@ -1,13 +1,16 @@
-
+
+
![]()
+