BodyBalanceEvaluation/backend/devices/device_model.py

336 lines
12 KiB
Python
Raw Normal View History

2026-01-12 15:21:44 +08:00
# coding:UTF-8
import time
import bleak
import asyncio
import logging
2026-01-12 15:21:44 +08:00
# 设备实例 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设备模型")
2026-01-12 15:21:44 +08:00
# 设备名称(自定义) 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
2026-01-12 15:21:44 +08:00
# 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()
2026-01-12 15:21:44 +08:00
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")
2026-01-12 15:21:44 +08:00
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)
2026-01-12 15:21:44 +08:00
if notify_characteristic:
self.logger.info(f"匹配到特征: {notify_characteristic}")
2026-01-12 15:21:44 +08:00
# 设置通知以接收数据 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")
2026-01-12 15:21:44 +08:00
# 保持连接打开 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
2026-01-12 15:21:44 +08:00
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
finally:
# 在退出时停止通知 Stop notification on exit
try:
await client.stop_notify(notify_characteristic.uuid)
except Exception:
pass
2026-01-12 15:21:44 +08:00
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("未找到匹配的服务或特征")
2026-01-12 15:21:44 +08:00
# 关闭设备 close Device
def closeDevice(self):
self.isOpen = False
self.logger.info("设备已关闭")
2026-01-12 15:21:44 +08:00
# 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
2026-01-12 15:21:44 +08:00
# 获得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}")
2026-01-12 15:21:44 +08:00
# 读取寄存器 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
2026-01-12 15:21:44 +08:00
# 写入寄存器 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)