908 lines
36 KiB
Python
908 lines
36 KiB
Python
#!/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不可用,将使用简化的压力图像生成")
|
||
|
||
|
||
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路径 - 使用正确的DLL文件名
|
||
if dll_path is None:
|
||
# 尝试多个可能的DLL文件名
|
||
dll_candidates = [
|
||
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'SMiTSenseUsbWrapper.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.WinDLL(self.dll_path)
|
||
logger.info(f"成功加载DLL: {self.dll_path}")
|
||
|
||
# 设置函数签名(基于testsmit.py的工作代码)
|
||
self.dll.SMiTSenseUsb_Init.argtypes = [ctypes.c_int]
|
||
self.dll.SMiTSenseUsb_Init.restype = ctypes.c_int
|
||
|
||
self.dll.SMiTSenseUsb_ScanDevices.argtypes = [ctypes.POINTER(ctypes.c_int)]
|
||
self.dll.SMiTSenseUsb_ScanDevices.restype = ctypes.c_int
|
||
|
||
self.dll.SMiTSenseUsb_OpenAndStart.argtypes = [
|
||
ctypes.c_int,
|
||
ctypes.POINTER(ctypes.c_uint16),
|
||
ctypes.POINTER(ctypes.c_uint16)
|
||
]
|
||
self.dll.SMiTSenseUsb_OpenAndStart.restype = ctypes.c_int
|
||
|
||
self.dll.SMiTSenseUsb_GetLatestFrame.argtypes = [
|
||
ctypes.POINTER(ctypes.c_uint16),
|
||
ctypes.c_int
|
||
]
|
||
self.dll.SMiTSenseUsb_GetLatestFrame.restype = ctypes.c_int
|
||
|
||
self.dll.SMiTSenseUsb_StopAndClose.argtypes = []
|
||
self.dll.SMiTSenseUsb_StopAndClose.restype = ctypes.c_int
|
||
|
||
logger.info("DLL函数签名设置完成")
|
||
|
||
except Exception as e:
|
||
logger.error(f"加载DLL失败: {e}")
|
||
raise
|
||
|
||
def _initialize_device(self):
|
||
"""初始化设备连接"""
|
||
try:
|
||
# 初始化USB连接
|
||
ret = self.dll.SMiTSenseUsb_Init(0)
|
||
if ret != 0:
|
||
raise RuntimeError(f"USB初始化失败: {ret}")
|
||
|
||
# 扫描设备
|
||
count = ctypes.c_int()
|
||
ret = self.dll.SMiTSenseUsb_ScanDevices(ctypes.byref(count))
|
||
if ret != 0 or count.value == 0:
|
||
raise RuntimeError(f"设备扫描失败或未找到设备: {ret}, count: {count.value}")
|
||
|
||
logger.info(f"发现 {count.value} 个SMiTSense设备")
|
||
|
||
# 打开并启动第一个设备
|
||
rows = ctypes.c_uint16()
|
||
cols = ctypes.c_uint16()
|
||
ret = self.dll.SMiTSenseUsb_OpenAndStart(0, ctypes.byref(rows), ctypes.byref(cols))
|
||
if ret != 0:
|
||
raise RuntimeError(f"设备启动失败: {ret}")
|
||
|
||
self.rows = rows.value
|
||
self.cols = cols.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()
|
||
|
||
# 读取原始压力数据
|
||
ret = self.dll.SMiTSenseUsb_GetLatestFrame(self.buf, self.frame_size)
|
||
if ret != 0:
|
||
logger.warning(f"读取数据帧失败: {ret}")
|
||
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 / 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': 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实现,固定范围映射,效果与matplotlib一致)"""
|
||
try:
|
||
import cv2
|
||
import numpy as np
|
||
import base64
|
||
from io import BytesIO
|
||
from PIL import Image
|
||
|
||
# 固定映射范围(与 matplotlib vmin/vmax 一致)
|
||
vmin, vmax = 0, 1000
|
||
norm_data = np.clip((raw_data - vmin) / (vmax - vmin) * 255, 0, 255).astype(np.uint8)
|
||
|
||
# 应用 jet 颜色映射
|
||
heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET)
|
||
|
||
# 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')
|
||
|
||
# 保存为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:
|
||
self.dll.SMiTSenseUsb_StopAndClose()
|
||
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:
|
||
# 更新统计信息
|
||
self.packet_count += 1
|
||
self.last_data_time = time.time()
|
||
|
||
# 发送数据到前端
|
||
if self._socketio:
|
||
self._socketio.emit('pressure_data', pressure_data, namespace='/pressure')
|
||
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}") |