This commit is contained in:
limengnan 2026-01-22 18:18:46 +08:00
commit 641bc9e8a0
8 changed files with 450 additions and 83 deletions

View File

@ -29,8 +29,8 @@ fourcc = MJPG
backend = directshow
[CAMERA2]
enable = False
device_index = 2
enable = True
device_index = 1
width = 1280
height = 720
fps = 30

View File

@ -849,12 +849,20 @@ class DeviceCoordinator:
self.logger.warning(f"设备 {device_name} 连接丢失")
self.stats['device_errors'][device_name] += 1
if self.stats['reconnect_attempts'][device_name] >= 3:
continue
now = time.time()
if now - self._last_restart_ts[device_name] >= 15.0:
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}")

View File

@ -33,6 +33,37 @@ class DeviceModel:
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
@ -71,34 +102,59 @@ class DeviceModel:
notify_characteristic = None
self.logger.info("正在匹配服务...")
services = None
for _ in range(3):
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):
services = await get_services()
else:
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):
services = await backend_get_services()
else:
services = getattr(client, 'services', None)
if services:
break
await asyncio.sleep(0.2)
if not 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:
if service.uuid == target_service_uuid:
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("正在匹配特征...")
for characteristic in service.characteristics:
if characteristic.uuid == target_characteristic_uuid_read:
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 characteristic.uuid == target_characteristic_uuid_write:
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}")
@ -110,13 +166,32 @@ class DeviceModel:
# 保持连接打开 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("未找到匹配的服务或特征")
@ -134,17 +209,13 @@ 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:
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:
return
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
@ -152,6 +223,27 @@ class DeviceModel:
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
@ -175,6 +267,26 @@ class DeviceModel:
# 封装读取指令并向串口发送数据 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

View File

@ -42,7 +42,9 @@ class BleIMUDevice:
self._device_model = None
self._open_task = None
self._main_task = None
self._battery_task = None
self._last_update_ts = None
self._last_ble_device = None
try:
from . import device_model as wit_device_model
except Exception:
@ -106,7 +108,8 @@ class BleIMUDevice:
'pitch': self.last_data['roll'] # pitch 对应俯仰
},
'temperature': self.last_data.get('temperature', 25.0),
'timestamp': datetime.now().isoformat()
'timestamp': datetime.now().isoformat(),
'battery': self.last_data.get('battery'),
}
return self.apply_calibration(raw) if apply_calibration else raw
@ -155,6 +158,14 @@ class BleIMUDevice:
async def _disconnect(self):
try:
if self._battery_task is not None and not self._battery_task.done():
self._battery_task.cancel()
try:
await self._battery_task
except asyncio.CancelledError:
pass
except Exception:
pass
if self._device_model is not None:
try:
self._device_model.closeDevice()
@ -175,9 +186,38 @@ class BleIMUDevice:
except Exception:
pass
finally:
self._battery_task = None
self._open_task = None
self._device_model = None
self._last_update_ts = None
self._connected = False
async def _battery_poll_loop(self, interval_s: float = 30.0):
try:
await asyncio.sleep(1.0)
while self.running:
dm = self._device_model
if dm is None or not bool(getattr(dm, "isOpen", False)):
await asyncio.sleep(1.0)
continue
try:
battery = await dm.readBattery(timeout=2.0)
except asyncio.CancelledError:
raise
except Exception:
battery = None
if isinstance(battery, dict):
battery_payload = {
"raw": battery.get("raw"),
"voltage": battery.get("voltage"),
"percent": battery.get("percent"),
"timestamp": datetime.now().isoformat(),
}
with self._lock:
self.last_data["battery"] = battery_payload
await asyncio.sleep(max(1.0, float(interval_s)))
except asyncio.CancelledError:
pass
async def _connect_and_listen(self):
try:
@ -187,23 +227,29 @@ class BleIMUDevice:
self.running = False
return
async def find_device() -> Optional[Any]:
scan_timeout_s = 30.0
if self.ble_name:
async def find_device(total_timeout_s: float = 30.0) -> Optional[Any]:
if self._last_ble_device is not None:
return self._last_ble_device
find_by_address = getattr(BleakScanner, "find_device_by_address", None)
find_by_name = getattr(BleakScanner, "find_device_by_name", None)
if callable(find_by_name):
deadline = time.time() + max(0.1, total_timeout_s)
attempt = 0
async def _match_discover(timeout_s: float) -> Optional[Any]:
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)
found = await BleakScanner.discover(timeout=timeout_s)
except Exception:
found = []
if self.mac_address:
target = self.mac_address.lower()
for d in found:
addr = (getattr(d, "address", "") or "").lower()
if addr == target:
return d
if self.ble_name:
for d in found:
if (getattr(d, "name", None) or "") != self.ble_name:
@ -211,22 +257,59 @@ class BleIMUDevice:
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 time.time() < deadline and self.running:
attempt += 1
remaining = deadline - time.time()
per_try = min(5.0, max(0.2, remaining))
strat = attempt % 3
device = None
if strat == 1 and self.ble_name and callable(find_by_name):
try:
device = await find_by_name(self.ble_name, timeout=per_try)
except Exception:
device = None
if device is None and strat == 2 and self.mac_address and callable(find_by_address):
try:
device = await find_by_address(self.mac_address, timeout=per_try)
except TypeError:
try:
device = await find_by_address(self.mac_address, cb=dict(use_bdaddr=False))
except Exception:
device = None
except Exception:
device = None
if device is None:
device = await _match_discover(per_try)
if device is not None:
if self.mac_address and (getattr(device, "address", "") or "").lower() != self.mac_address.lower():
device = None
elif self.ble_name and getattr(device, "name", None) not in (None, "") and getattr(device, "name", None) != self.ble_name:
device = None
if device is not None:
self._last_ble_device = device
return device
await asyncio.sleep(0.2)
return None
while self.running:
try:
attempt_ts = time.perf_counter()
logger.info(f"BLE IMU开始扫描并连接: name={self.ble_name}, mac={self.mac_address}")
device = await find_device()
device = await find_device(total_timeout_s=30.0)
if device is None:
logger.info(f"BLE IMU扫描未发现设备 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms)")
@ -236,13 +319,14 @@ class BleIMUDevice:
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._last_ble_device = device
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())
ready = False
ready_timeout_s = 20.0
ready_timeout_s = 30.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():
@ -266,23 +350,42 @@ class BleIMUDevice:
continue
logger.info(f"BLE IMU连接并开始产出数据 (耗时: {(time.perf_counter()-attempt_ts)*1000:.1f}ms)")
try:
if self._battery_task is None or self._battery_task.done():
self._battery_task = asyncio.create_task(self._battery_poll_loop(interval_s=30.0))
except Exception:
pass
while self.running and self._open_task is not None and not self._open_task.done():
await asyncio.sleep(1.0)
await self._disconnect()
self._connected = False
self._last_ble_device = None
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"BLE IMU连接/监听失败: {e}", exc_info=True)
self._connected = False
self._last_ble_device = None
await asyncio.sleep(2.0)
@property
def connected(self) -> bool:
return self._connected
dm = self._device_model
if dm is None:
return False
if not bool(getattr(dm, 'isOpen', False)):
return False
client = getattr(dm, 'client', None)
if client is not None:
try:
if not bool(getattr(client, 'is_connected', False)):
return False
except Exception:
return False
return True
@property
def has_received_data(self) -> bool:
@ -304,7 +407,13 @@ class MockIMUDevice:
'roll': 0.0,
'pitch': 0.0,
'yaw': 0.0,
'temperature': 25.0
'temperature': 25.0,
'battery': {
"raw": None,
"voltage": None,
"percent": 100,
"timestamp": datetime.now().isoformat(),
},
}
self._phase = 0.0
self._last_update_ts = None
@ -352,7 +461,8 @@ class MockIMUDevice:
'pitch': self.last_data['roll']
},
'temperature': self.last_data.get('temperature', 25.0),
'timestamp': datetime.now().isoformat()
'timestamp': datetime.now().isoformat(),
'battery': self.last_data.get('battery'),
}
return self.apply_calibration(raw) if apply_calibration else raw
@ -815,8 +925,6 @@ class IMUManager(BaseDevice):
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):

View File

@ -4,6 +4,7 @@ import time
from statistics import mean
import bleak
import device_model
async def find_device_by_address(address: str, timeout_s: float):
@ -56,13 +57,105 @@ def parse_args():
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")
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",

View File

@ -270,6 +270,10 @@ class AppServer:
self.logger.info('开始初始化设备...')
if self.device_coordinator.initialize():
self.logger.info('设备协调器初始化完成')
try:
self.device_coordinator.set_status_change_callback(self._on_device_status_change)
except Exception as e:
self.logger.error(f'注册设备状态变化回调失败: {e}')
# 获取设备管理器实例
self.device_managers = self.device_coordinator.get_device_managers()
@ -1969,6 +1973,10 @@ class AppServer:
try:
self.logger.info(f'开始重启 {device_type} 设备...')
try:
self.broadcast_device_status(device_type, False)
except Exception:
pass
# 调用设备协调器的重启方法
with self._get_device_lock(device_type):
@ -1976,6 +1984,10 @@ class AppServer:
if success:
self.logger.info(f'{device_type} 设备重启成功')
try:
self.broadcast_device_status(device_type, True)
except Exception:
pass
# 发送重启成功事件到前端
self.socketio.emit('device_restart_message', {
@ -1986,6 +1998,10 @@ class AppServer:
return True
else:
self.logger.error(f'{device_type} 设备重启失败!')
try:
self.broadcast_device_status(device_type, False)
except Exception:
pass
# 发送重启失败事件到前端
self.socketio.emit('device_restart_message', {
'device_type': device_type,
@ -2255,7 +2271,7 @@ def main():
"""主函数"""
# 解析命令行参数
parser = argparse.ArgumentParser(description='Body Balance Evaluation System Backend')
parser.add_argument('--host', default='localhost', help='Host address to bind to')
parser.add_argument('--host', default='0.0.0.0', help='Host address to bind to')
parser.add_argument('--port', type=int, default=5000, help='Port number to bind to')
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
args = parser.parse_args()

View File

@ -35,7 +35,7 @@ chart_dpi = 300
export_format = csv
[SECURITY]
secret_key = f50c705c26a963701a4832ae3d69a091674f587a4b02da8b1c59909c0bd312fe
secret_key = 855842922ac3d1747493bcf40f0b2534387ac6304b903c901cf980e4059d5150
session_timeout = 3600
max_login_attempts = 5

View File

@ -83,7 +83,10 @@
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title2.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">头部姿态</div>
<div class="body-posture-text">
头部姿态
<span v-if="imuBatteryPercent !== null" class="imu-battery-percent">电量{{ imuBatteryPercent }}%</span>
</div>
<div class="calibration-zero" @click="calibrationClick" style="margin-left:20px">
<img src="@/assets/detection/calibration.png" style="margin-right:7px">
校准
@ -1230,6 +1233,7 @@ const headlist = ref({
tilt: 0,
pitch: 0
})
const imuBatteryPercent = ref(null)
//
const startTimer = () => {
if (isRunning.value) return;
@ -1782,6 +1786,14 @@ function handleIMUData(data) {
try {
if (!data) return
const batteryPercent = data && data.battery && data.battery.percent
if (batteryPercent !== undefined && batteryPercent !== null) {
const v = Number(batteryPercent)
if (!Number.isNaN(v) && Number.isFinite(v)) {
imuBatteryPercent.value = Math.max(0, Math.min(100, Math.round(v)))
}
}
//
// 1) { rotation, tilt, pitch }
// 2) { head_pose: { rotation, tilt, pitch } }
@ -2620,17 +2632,28 @@ function refreshClick(type) {
ElMessage.warning(`🚀 发送重启设备请求...`)
if (devicesSocket && devicesSocket.connected) {
if(type == 'camera1'){
camera1Status.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'camera1' })
}else if(type == 'camera2'){
camera2Status.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'camera2' })
}else if(type == 'femtobolt'){
femtoboltStatus.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'femtobolt' })
}else if(type == 'imu'){
imuStatus.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'imu' })
}else if(type == 'pressure'){
pressureStatus.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'pressure' })
}else if(type == 'remote'){
remoteStatus.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'remote' })
}else if(type == 'camera'){
camera1Status.value = '未连接'
camera2Status.value = '未连接'
devicesSocket.emit('restart_device', { device_type: 'camera1' })
devicesSocket.emit('restart_device', { device_type: 'camera2' })
}
} else {
console.warn('⚠️ Socket服务未连接无法重启设备')
@ -2841,6 +2864,13 @@ function viewClick(e){
font-style: normal;
font-size: 14px;
}
.imu-battery-percent{
margin-left: 10px;
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}
.body-header-bottombox{
width: 100%;