BodyBalanceEvaluation/backend/devices/pressure_manager.py
2025-08-18 18:30:49 +08:00

980 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
压力板管理器
负责压力传感器的连接、校准和足部压力数据采集
"""
import os
import ctypes
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 cv2
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from io import BytesIO
import base64
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__)
# 检查matplotlib可用性
try:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as patches
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
logger.warning("matplotlib不可用将使用简化的压力图像生成")
# 定义 C 结构体
class FPMS_DEVICE_INFO(ctypes.Structure):
_fields_ = [
("mn", ctypes.c_uint16),
("sn", ctypes.c_char * 64),
("fwVersion", ctypes.c_uint16),
("protoVer", ctypes.c_uint8),
("pid", ctypes.c_uint16),
("vid", ctypes.c_uint16),
("rows", ctypes.c_uint16),
("cols", ctypes.c_uint16),
]
class RealPressureDevice:
"""真实SMiTSense压力传感器设备"""
def __init__(self, dll_path=None):
"""
初始化SMiTSense压力传感器
Args:
dll_path: DLL文件路径如果为None则使用默认路径
"""
self.dll = None
self.device_handle = None
self.is_connected = False
self.rows = 0
self.cols = 0
self.frame_size = 0
self.buf = None
# 设置DLL路径 - 使用Wrapper.dll
if dll_path is None:
# 尝试多个可能的DLL文件名
dll_candidates = [
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'Wrapper.dll'),
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'SMiTSenseUsb-F3.0.dll')
]
dll_path = None
for candidate in dll_candidates:
if os.path.exists(candidate):
dll_path = candidate
break
if dll_path is None:
raise FileNotFoundError(f"未找到SMiTSense DLL文件检查路径: {dll_candidates}")
self.dll_path = dll_path
logger.info(f'初始化真实压力传感器设备DLL路径: {dll_path}')
try:
self._load_dll()
self._initialize_device()
except Exception as e:
logger.error(f'压力传感器初始化失败: {e}')
# 如果真实设备初始化失败,可以选择降级为模拟设备
raise
def _load_dll(self):
"""加载SMiTSense DLL并设置函数签名"""
try:
if not os.path.exists(self.dll_path):
raise FileNotFoundError(f"DLL文件未找到: {self.dll_path}")
# 加载DLL
self.dll = ctypes.CDLL(self.dll_path)
logger.info(f"成功加载DLL: {self.dll_path}")
# 设置函数签名基于test22new.py的工作代码
self.dll.fpms_usb_init_wrap.argtypes = [ctypes.c_int]
self.dll.fpms_usb_init_wrap.restype = ctypes.c_int
self.dll.fpms_usb_get_device_list_wrap.argtypes = [ctypes.POINTER(FPMS_DEVICE_INFO), ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
self.dll.fpms_usb_get_device_list_wrap.restype = ctypes.c_int
self.dll.fpms_usb_open_wrap.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_uint64)]
self.dll.fpms_usb_open_wrap.restype = ctypes.c_int
self.dll.fpms_usb_read_frame_wrap.argtypes = [ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t]
self.dll.fpms_usb_read_frame_wrap.restype = ctypes.c_int
self.dll.fpms_usb_close_wrap.argtypes = [ctypes.c_uint64]
self.dll.fpms_usb_close_wrap.restype = ctypes.c_int
logger.info("DLL函数签名设置完成")
except Exception as e:
logger.error(f"加载DLL失败: {e}")
raise
def _initialize_device(self):
"""初始化设备连接"""
try:
# 初始化USB连接
if self.dll.fpms_usb_init_wrap(0) != 0:
raise RuntimeError("初始化失败")
# 获取设备列表
count = ctypes.c_int()
devs = (FPMS_DEVICE_INFO * 10)()
r = self.dll.fpms_usb_get_device_list_wrap(devs, 10, ctypes.byref(count))
if r != 0 or count.value == 0:
raise RuntimeError(f"未检测到设备: {r}, count: {count.value}")
logger.info(f"检测到设备数量: {count.value}")
dev = devs[0]
self.rows, self.cols = dev.rows, dev.cols
logger.info(f"使用设备 SN={dev.sn.decode(errors='ignore')} {self.rows}x{self.cols}")
# 打开设备
self.device_handle = ctypes.c_uint64()
r = self.dll.fpms_usb_open_wrap(0, ctypes.byref(self.device_handle))
if r != 0:
raise RuntimeError("设备打开失败")
logger.info(f"设备已打开, 句柄 = {self.device_handle.value}")
# 准备数据缓冲区
self.frame_size = self.rows * self.cols
self.buf_type = ctypes.c_uint16 * self.frame_size
self.buf = self.buf_type()
self.is_connected = True
logger.info(f"SMiTSense压力传感器初始化成功: {self.rows}行 x {self.cols}")
except Exception as e:
logger.error(f"设备初始化失败: {e}")
raise
def read_data(self) -> Dict[str, Any]:
"""读取压力数据并转换为与MockPressureDevice兼容的格式"""
try:
if not self.is_connected or not self.dll:
logger.error("设备未连接")
return self._get_empty_data()
# 读取原始压力数据
r = self.dll.fpms_usb_read_frame_wrap(self.device_handle.value, self.buf, self.frame_size)
if r != 0:
logger.warning(f"读取帧失败, code= {r}")
return self._get_empty_data()
# 转换为numpy数组
raw_data = np.frombuffer(self.buf, dtype=np.uint16).reshape((self.rows, self.cols))
# 计算足部区域压力 (基于传感器的实际布局)
foot_zones = self._calculate_foot_pressure_zones(raw_data)
# 生成压力图像
pressure_image_base64 = self._generate_pressure_image(
foot_zones['left_front'],
foot_zones['left_rear'],
foot_zones['right_front'],
foot_zones['right_rear'],
raw_data
)
return {
'foot_pressure': {
'left_front': round(foot_zones['left_front'], 2),
'left_rear': round(foot_zones['left_rear'], 2),
'right_front': round(foot_zones['right_front'], 2),
'right_rear': round(foot_zones['right_rear'], 2),
'left_total': round(foot_zones['left_total'], 2),
'right_total': round(foot_zones['right_total'], 2)
},
'pressure_image': pressure_image_base64,
'timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"读取压力数据异常: {e}")
return self._get_empty_data()
def _calculate_foot_pressure_zones(self, raw_data):
"""计算足部区域压力,返回百分比:
- 左足、右足:相对于双足总压的百分比
- 左前、左后:相对于左足总压的百分比
- 右前、右后:相对于右足总压的百分比
基于原始矩阵按行列各等分为四象限(上半部为前、下半部为后,左半部为左、右半部为右)。
"""
try:
# 防护:空数据
if raw_data is None:
raise ValueError("raw_data is None")
# 转为浮点以避免 uint16 溢出
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("raw_data has invalid shape")
# 行列对半分(上=前,下=后;左=左,右=右)
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 as e:
logger.error(f"计算足部区域压力异常: {e}")
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, left_rear, right_front, right_rear, raw_data=None) -> str:
"""生成足部压力图片的base64数据"""
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 as e:
logger.warning(f"生成压力图片失败: {e}")
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
def _generate_heatmap_image(self, raw_data) -> str:
"""生成基于原始数据的热力图OpenCV实现自适应归一化黑色背景"""
try:
import cv2
import numpy as np
import base64
from io import BytesIO
from PIL import Image
# 自适应归一化基于test22new.py的方法2
vmin = 10 # 最小阈值,低于此值显示为黑色
dmin, dmax = np.min(raw_data), np.max(raw_data)
norm_data = np.clip((raw_data - dmin) / max(dmax - dmin, 1) * 255, 0, 255).astype(np.uint8)
# 应用 jet 颜色映射
heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET)
# 将低于阈值的区域设置为黑色
heatmap[raw_data <= vmin] = (0, 0, 0)
# 放大图像以便更好地显示细节
rows, cols = raw_data.shape
heatmap = cv2.resize(heatmap, (cols*4, rows*4), interpolation=cv2.INTER_NEAREST)
# OpenCV 生成的是 BGR转成 RGB
heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
# 转成 Pillow Image
img = Image.fromarray(heatmap_rgb)
# 输出为 Base64 PNG
buffer = BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return f"data:image/png;base64,{image_base64}"
except Exception as e:
logger.warning(f"生成热力图失败: {e}")
return self._generate_simple_pressure_image(0, 0, 0, 0)
def _generate_simple_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str:
"""生成简单的足部压力区域图"""
try:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from io import BytesIO
# 创建图形
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')
# 定义颜色映射
max_pressure = max(left_front, left_rear, right_front, right_rear)
if max_pressure > 0:
left_front_color = plt.cm.Reds(left_front / max_pressure)
left_rear_color = plt.cm.Reds(left_rear / max_pressure)
right_front_color = plt.cm.Reds(right_front / max_pressure)
right_rear_color = plt.cm.Reds(right_rear / max_pressure)
else:
left_front_color = left_rear_color = right_front_color = right_rear_color = 'lightgray'
# 绘制足部区域
left_front_rect = patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=left_front_color)
left_rear_rect = patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=left_rear_color)
right_front_rect = patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=right_front_color)
right_rear_rect = patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=right_rear_color)
ax.add_patch(left_front_rect)
ax.add_patch(left_rear_rect)
ax.add_patch(right_front_rect)
ax.add_patch(right_rear_rect)
# 添加标签
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')
# 保存为base64
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 as e:
logger.warning(f"生成简单压力图片失败: {e}")
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
def _get_empty_data(self):
"""返回空的压力数据"""
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):
"""显式关闭压力传感器连接"""
try:
if self.is_connected and self.dll and self.device_handle:
self.dll.fpms_usb_close_wrap(self.device_handle.value)
self.is_connected = False
logger.info('SMiTSense压力传感器连接已关闭')
except Exception as e:
logger.error(f'关闭压力传感器连接异常: {e}')
def __del__(self):
"""析构函数,确保资源清理"""
self.close()
class MockPressureDevice:
"""模拟压力传感器设备模拟真实SMiTSense设备的行为"""
def __init__(self):
self.base_pressure = 500 # 基础压力值
self.noise_level = 10
self.rows = 4 # 模拟传感器矩阵行数
self.cols = 4 # 模拟传感器矩阵列数
self.time_offset = np.random.random() * 10 # 随机时间偏移,让每个实例的波形不同
def read_data(self) -> Dict[str, Any]:
"""读取压力数据,模拟基于矩阵数据的真实设备行为"""
try:
# 生成模拟的传感器矩阵数据
raw_data = self._generate_simulated_matrix_data()
# 使用与真实设备相同的计算逻辑
foot_zones = self._calculate_foot_pressure_zones(raw_data)
# 生成压力图像
pressure_image_base64 = self._generate_pressure_image(
foot_zones['left_front'],
foot_zones['left_rear'],
foot_zones['right_front'],
foot_zones['right_rear'],
raw_data
)
return {
'foot_pressure': {
'left_front': round(foot_zones['left_front'], 2),
'left_rear': round(foot_zones['left_rear'], 2),
'right_front': round(foot_zones['right_front'], 2),
'right_rear': round(foot_zones['right_rear'], 2),
'left_total': round(foot_zones['left_total'], 2),
'right_total': round(foot_zones['right_total'], 2)
},
'pressure_image': pressure_image_base64,
'timestamp': datetime.now().isoformat()
}
except Exception as e:
logger.error(f"模拟压力设备读取数据异常: {e}")
return self._get_empty_data()
def _generate_simulated_matrix_data(self):
"""生成模拟的传感器矩阵数据,模拟真实的足部压力分布"""
import time
current_time = time.time() + self.time_offset
# 创建4x4的传感器矩阵
matrix_data = np.zeros((self.rows, self.cols))
# 模拟动态的压力分布,使用正弦波叠加噪声
for i in range(self.rows):
for j in range(self.cols):
# 基础压力值,根据传感器位置不同
base_value = self.base_pressure * (0.3 + 0.7 * np.random.random())
# 添加时间变化(模拟人体重心变化)
time_variation = np.sin(current_time * 0.5 + i * 0.5 + j * 0.3) * 0.3
# 添加噪声
noise = np.random.normal(0, self.noise_level)
# 确保压力值非负
matrix_data[i, j] = max(0, base_value * (1 + time_variation) + noise)
return matrix_data
def _calculate_foot_pressure_zones(self, raw_data):
"""计算足部区域压力,返回百分比:
- 左足、右足:相对于双足总压的百分比
- 左前、左后:相对于左足总压的百分比
- 右前、右后:相对于右足总压的百分比
基于原始矩阵按行列各等分为四象限(上半部为前、下半部为后,左半部为左、右半部为右)。
"""
try:
# 防护:空数据
if raw_data is None:
raise ValueError("raw_data is None")
# 转为浮点以避免 uint16 溢出
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("raw_data has invalid shape")
# 行列对半分(上=前,下=后;左=左,右=右)
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 / left_total_abs * 100) if left_total_abs > 0 else 0)
left_rear_pct = float((left_rear / left_total_abs * 100) if left_total_abs > 0 else 0)
right_front_pct = float((right_front / right_total_abs * 100) if right_total_abs > 0 else 0)
right_rear_pct = float((right_rear / right_total_abs * 100) if right_total_abs > 0 else 0)
return {
'left_front': left_front_pct,
'left_rear': left_rear_pct,
'right_front': right_front_pct,
'right_rear': right_rear_pct,
'left_total': left_total_pct,
'right_total': right_total_pct,
'total_pressure': float(total_abs)
}
except Exception as e:
logger.error(f"计算足部区域压力异常: {e}")
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, left_rear, right_front, right_rear, raw_data=None) -> str:
"""生成足部压力图片的base64数据"""
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 as e:
logger.warning(f"生成模拟压力图片失败: {e}")
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
def _generate_heatmap_image(self, raw_data) -> str:
"""生成基于原始数据的热力图"""
try:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from io import BytesIO
# 参考 tests/testsmit.py 的渲染方式:使用 jet 色图、nearest 插值、固定范围并关闭坐标轴
fig, ax = plt.subplots()
im = ax.imshow(raw_data, cmap='jet', interpolation='nearest', vmin=0, vmax=1000)
ax.axis('off')
# 紧凑布局并导出为 base64
from io import BytesIO
buffer = BytesIO()
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, pad_inches=0)
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 as e:
logger.warning(f"生成热力图失败: {e}")
return self._generate_simple_pressure_image(0, 0, 0, 0)
def _generate_simple_pressure_image(self, left_front, left_rear, right_front, right_rear) -> str:
"""生成简单的足部压力区域图"""
try:
import matplotlib
matplotlib.use('Agg') # 设置非交互式后端避免Tkinter错误
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from io import BytesIO
# 临时禁用PIL的调试日志
pil_logger = logging.getLogger('PIL')
original_level = pil_logger.level
pil_logger.setLevel(logging.WARNING)
# 创建图形
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')
# 定义颜色映射(根据压力值)
max_pressure = max(left_front, left_rear, right_front, right_rear)
if max_pressure > 0:
left_front_color = plt.cm.Reds(left_front / max_pressure)
left_rear_color = plt.cm.Reds(left_rear / max_pressure)
right_front_color = plt.cm.Reds(right_front / max_pressure)
right_rear_color = plt.cm.Reds(right_rear / max_pressure)
else:
left_front_color = left_rear_color = right_front_color = right_rear_color = 'lightgray'
# 绘制左脚
left_front_rect = patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=left_front_color)
left_rear_rect = patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=left_rear_color)
# 绘制右脚
right_front_rect = patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=right_front_color)
right_rear_rect = patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=right_rear_color)
# 添加到图形
ax.add_patch(left_front_rect)
ax.add_patch(left_rear_rect)
ax.add_patch(right_front_rect)
ax.add_patch(right_rear_rect)
# 添加标签
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')
# 保存为base64
buffer = BytesIO()
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100, facecolor='white')
buffer.seek(0)
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
plt.close(fig)
# 恢复PIL的日志级别
pil_logger.setLevel(original_level)
return f"data:image/png;base64,{image_base64}"
except Exception as e:
# 确保在异常情况下也恢复PIL的日志级别
try:
pil_logger.setLevel(original_level)
except:
pass
logger.warning(f"生成压力图片失败: {e}")
# 返回一个简单的占位符base64图片
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
def _get_empty_data(self):
"""返回空的压力数据"""
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()
}
class PressureManager(BaseDevice):
"""压力板管理器"""
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None):
"""
初始化压力板管理器
Args:
socketio: SocketIO实例
config_manager: 配置管理器实例
"""
# 配置管理
self.config_manager = config_manager or ConfigManager()
self.config = self.config_manager.get_device_config('pressure')
super().__init__("pressure", self.config)
# 保存socketio实例
self._socketio = socketio
# 设备实例
self.device = None
self.device_type = self.config.get('device_type', 'mock') # 'real' 或 'mock'
# 数据流相关
self.streaming_thread = None
self.is_streaming = False
self.stream_interval = self.config.get('stream_interval', 0.1) # 100ms间隔
# 校准相关
self.is_calibrated = False
self.calibration_data = None
# 性能统计
self.packet_count = 0
self.error_count = 0
self.last_data_time = None
self.logger.info(f"压力板管理器初始化完成 - 设备类型: {self.device_type}")
def initialize(self) -> bool:
"""
初始化压力板设备
Returns:
bool: 初始化是否成功
"""
try:
self.logger.info(f"正在初始化压力板设备 - 类型: {self.device_type}")
# 根据设备类型创建设备实例
if self.device_type == 'real':
self.device = RealPressureDevice()
else:
self.device = MockPressureDevice()
self.is_connected = True
self._device_info.update({
'device_type': self.device_type,
'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown'
})
self.logger.info(f"压力板初始化成功 - 设备类型: {self.device_type}")
return True
except Exception as e:
self.logger.error(f"压力板初始化失败: {e}")
self.is_connected = False
self.device = None
return False
def start_streaming(self) -> bool:
"""
开始压力数据流
Args:
socketio: SocketIO实例
Returns:
bool: 启动是否成功
"""
try:
if not self.is_connected or not self.device:
self.logger.error("设备未连接,无法启动数据流")
return False
if self.is_streaming:
self.logger.warning("压力数据流已在运行")
return True
self.is_streaming = True
self.streaming_thread = threading.Thread(target=self._pressure_streaming_thread, daemon=True)
self.streaming_thread.start()
self.logger.info("压力数据流启动成功")
return True
except Exception as e:
self.logger.error(f"启动压力数据流失败: {e}")
self.is_streaming = False
return False
def stop_streaming(self) -> bool:
"""
停止压力数据流
Returns:
bool: 停止是否成功
"""
try:
if not self.is_streaming:
return True
self.is_streaming = False
if self.streaming_thread and self.streaming_thread.is_alive():
self.streaming_thread.join(timeout=2.0)
self.logger.info("压力数据流已停止")
return True
except Exception as e:
self.logger.error(f"停止压力数据流失败: {e}")
return False
def _pressure_streaming_thread(self):
"""
压力数据流处理线程
"""
self.logger.info("压力数据流线程启动")
try:
while self.is_streaming and self.is_connected:
try:
# 从设备读取数据
if self.device:
pressure_data = self.device.read_data()
if pressure_data and 'foot_pressure' in pressure_data:
foot_pressure = pressure_data['foot_pressure']
# 获取各区域压力值
left_front = foot_pressure['left_front']
left_rear = foot_pressure['left_rear']
right_front = foot_pressure['right_front']
right_rear = foot_pressure['right_rear']
left_total = foot_pressure['left_total']
right_total = foot_pressure['right_total']
# 计算总压力
total_pressure = left_total + right_total
# 计算平衡比例(左脚压力占总压力的比例)
balance_ratio = left_total / total_pressure if total_pressure > 0 else 0.5
# 计算压力中心偏移
pressure_center_offset = (balance_ratio - 0.5) * 100 # 转换为百分比
# 计算前后足压力分布
left_front_ratio = left_front / left_total if left_total > 0 else 0.5
right_front_ratio = right_front / right_total if right_total > 0 else 0.5
# 构建完整的足部压力数据
complete_pressure_data = {
# 分区压力值
'pressure_zones': {
'left_front': left_front,
'left_rear': left_rear,
'right_front': right_front,
'right_rear': right_rear,
'left_total': left_total,
'right_total': right_total,
'total_pressure': total_pressure
},
# 平衡分析
'balance_analysis': {
'balance_ratio': round(balance_ratio, 3),
'pressure_center_offset': round(pressure_center_offset, 2),
'balance_status': 'balanced' if abs(pressure_center_offset) < 10 else 'unbalanced',
'left_front_ratio': round(left_front_ratio, 3),
'right_front_ratio': round(right_front_ratio, 3)
},
# 压力图片
'pressure_image': pressure_data.get('pressure_image', ''),
'timestamp': pressure_data['timestamp']
}
# 更新统计信息
self.packet_count += 1
self.last_data_time = time.time()
# 发送数据到前端
if self._socketio:
self._socketio.emit('pressure_data', {
'foot_pressure': complete_pressure_data,
'timestamp': datetime.now().isoformat()
}, namespace='/devices')
else:
self.logger.warning("SocketIO实例为空无法发送压力数据")
time.sleep(self.stream_interval)
except Exception as e:
self.error_count += 1
self.logger.error(f"压力数据流处理异常: {e}")
time.sleep(0.1)
except Exception as e:
self.logger.error(f"压力数据流线程异常: {e}")
finally:
self.logger.info("压力数据流线程结束")
def get_status(self) -> Dict[str, Any]:
"""
获取设备状态
Returns:
Dict[str, Any]: 设备状态信息
"""
return {
'device_type': self.device_type,
'is_connected': self.is_connected,
'is_streaming': self.is_streaming,
'is_calibrated': self.is_calibrated,
'packet_count': self.packet_count,
'error_count': self.error_count,
'last_data_time': self.last_data_time,
'device_info': self.get_device_info()
}
def calibrate(self) -> bool:
"""
校准压力传感器
Returns:
bool: 校准是否成功
"""
try:
self.logger.info("开始压力传感器校准...")
# 这里可以添加具体的校准逻辑
# 目前简单设置为已校准状态
self.is_calibrated = True
self.calibration_data = {
'timestamp': datetime.now().isoformat(),
'baseline': 'calibrated'
}
self.logger.info("压力传感器校准完成")
return True
except Exception as e:
self.logger.error(f"压力传感器校准失败: {e}")
return False
def disconnect(self) -> bool:
"""
断开设备连接
Returns:
bool: 断开是否成功
"""
try:
# 停止数据流
self.stop_streaming()
# 关闭设备连接
if self.device and hasattr(self.device, 'close'):
self.device.close()
self.device = None
self.is_connected = False
self.logger.info("压力板设备连接已断开")
return True
except Exception as e:
self.logger.error(f"断开压力板设备连接失败: {e}")
return False
def cleanup(self) -> None:
"""清理资源"""
try:
self.stop_streaming()
self.disconnect()
self.logger.info("压力板设备资源清理完成")
except Exception as e:
self.logger.error(f"压力板设备资源清理失败: {e}")