2025-08-17 12:48:10 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
压力板管理器
|
|
|
|
|
|
负责压力传感器的连接、校准和足部压力数据采集
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import json
|
2026-08-11 18:17:13 +08:00
|
|
|
|
import zlib
|
|
|
|
|
|
import multiprocessing.shared_memory as shared_memory
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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不可用,将使用简化的压力图像生成")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 定义 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),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
class RealPressureDevice:
|
|
|
|
|
|
"""真实SMiTSense压力传感器设备"""
|
|
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 类级别的USB初始化状态跟踪
|
|
|
|
|
|
_usb_initialized = False
|
|
|
|
|
|
_usb_init_lock = threading.Lock()
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 设置DLL路径 - 使用Wrapper.dll
|
2025-08-17 12:48:10 +08:00
|
|
|
|
if dll_path is None:
|
|
|
|
|
|
# 尝试多个可能的DLL文件名
|
|
|
|
|
|
dll_candidates = [
|
2025-08-18 18:30:49 +08:00
|
|
|
|
os.path.join(os.path.dirname(__file__), '..', 'dll', 'smitsense', 'Wrapper.dll'),
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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
|
2025-08-18 18:30:49 +08:00
|
|
|
|
self.dll = ctypes.CDLL(self.dll_path)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
logger.info(f"成功加载DLL: {self.dll_path}")
|
|
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 设置函数签名(基于test22new.py的工作代码)
|
|
|
|
|
|
self.dll.fpms_usb_init_wrap.argtypes = [ctypes.c_int]
|
|
|
|
|
|
self.dll.fpms_usb_init_wrap.restype = ctypes.c_int
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
self.dll.fpms_usb_close_wrap.argtypes = [ctypes.c_uint64]
|
|
|
|
|
|
self.dll.fpms_usb_close_wrap.restype = ctypes.c_int
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
logger.info("DLL函数签名设置完成")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"加载DLL失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _initialize_device(self):
|
|
|
|
|
|
"""初始化设备连接"""
|
|
|
|
|
|
try:
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 使用类级别锁确保USB子系统只初始化一次
|
|
|
|
|
|
with RealPressureDevice._usb_init_lock:
|
|
|
|
|
|
if not RealPressureDevice._usb_initialized:
|
|
|
|
|
|
# 初始化USB连接
|
|
|
|
|
|
if self.dll.fpms_usb_init_wrap(0) != 0:
|
|
|
|
|
|
raise RuntimeError("USB子系统初始化失败")
|
|
|
|
|
|
RealPressureDevice._usb_initialized = True
|
|
|
|
|
|
logger.info("USB子系统初始化成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info("USB子系统已初始化,跳过重复初始化")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 获取设备列表
|
2025-08-17 12:48:10 +08:00
|
|
|
|
count = ctypes.c_int()
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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}")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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}")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 打开设备
|
|
|
|
|
|
self.device_handle = ctypes.c_uint64()
|
|
|
|
|
|
r = self.dll.fpms_usb_open_wrap(0, ctypes.byref(self.device_handle))
|
|
|
|
|
|
if r != 0:
|
|
|
|
|
|
raise RuntimeError("设备打开失败")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
logger.info(f"设备已打开, 句柄 = {self.device_handle.value}")
|
|
|
|
|
|
|
|
|
|
|
|
# 准备数据缓冲区
|
2025-08-17 12:48:10 +08:00
|
|
|
|
self.frame_size = self.rows * self.cols
|
|
|
|
|
|
self.buf_type = ctypes.c_uint16 * self.frame_size
|
|
|
|
|
|
self.buf = self.buf_type()
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 设置连接状态
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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:
|
2025-09-18 09:07:09 +08:00
|
|
|
|
if not self.is_connected or not self.dll:
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
# 检查device_handle是否有效
|
|
|
|
|
|
if not self.device_handle:
|
2025-08-17 12:48:10 +08:00
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
# 读取原始压力数据
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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}")
|
2025-09-11 17:40:03 +08:00
|
|
|
|
# 如果返回负数,多半表示物理断开或严重错误,标记断连并关闭句柄,触发上层重连
|
|
|
|
|
|
if r < 0:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.device_handle:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.dll.fpms_usb_close_wrap(self.device_handle.value)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2025-09-18 09:07:09 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
self.device_handle = None
|
2025-09-11 17:40:03 +08:00
|
|
|
|
except Exception:
|
2025-09-18 09:07:09 +08:00
|
|
|
|
pass
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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()
|
2025-09-11 17:40:03 +08:00
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# 前后占比(相对于各自单足总压)
|
2025-08-18 18:30:49 +08:00
|
|
|
|
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)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
2025-08-18 18:30:49 +08:00
|
|
|
|
"""生成基于原始数据的热力图(OpenCV实现,自适应归一化,黑色背景)"""
|
2025-08-17 12:48:10 +08:00
|
|
|
|
try:
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import base64
|
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 自适应归一化(基于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)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 应用 jet 颜色映射
|
|
|
|
|
|
heatmap = cv2.applyColorMap(norm_data, cv2.COLORMAP_JET)
|
2025-08-18 18:30:49 +08:00
|
|
|
|
|
|
|
|
|
|
# 将低于阈值的区域设置为黑色
|
|
|
|
|
|
heatmap[raw_data <= vmin] = (0, 0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 放大图像以便更好地显示细节
|
|
|
|
|
|
rows, cols = raw_data.shape
|
|
|
|
|
|
heatmap = cv2.resize(heatmap, (cols*4, rows*4), interpolation=cv2.INTER_NEAREST)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 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')
|
|
|
|
|
|
|
2025-08-18 18:30:49 +08:00
|
|
|
|
# 设置图形背景为黑色
|
|
|
|
|
|
fig.patch.set_facecolor('black')
|
|
|
|
|
|
ax.set_facecolor('black')
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
# 保存为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:
|
2025-08-18 18:30:49 +08:00
|
|
|
|
if self.is_connected and self.dll and self.device_handle:
|
|
|
|
|
|
self.dll.fpms_usb_close_wrap(self.device_handle.value)
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 重置设备句柄
|
|
|
|
|
|
self.device_handle = None
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 设置连接状态为断开
|
2025-08-17 12:48:10 +08:00
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
logger.info('SMiTSense压力传感器连接已关闭')
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f'关闭压力传感器连接异常: {e}')
|
|
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def reset_usb_state(cls):
|
|
|
|
|
|
"""重置USB初始化状态(用于设备完全断开后的重新初始化)"""
|
|
|
|
|
|
with cls._usb_init_lock:
|
|
|
|
|
|
cls._usb_initialized = False
|
|
|
|
|
|
logger.info("USB子系统状态已重置")
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
def __del__(self):
|
|
|
|
|
|
"""析构函数,确保资源清理"""
|
|
|
|
|
|
self.close()
|
|
|
|
|
|
|
2025-12-02 08:53:04 +08:00
|
|
|
|
class MockPressureDevice:
|
|
|
|
|
|
def __init__(self, rows: int = 32, cols: int = 32, seed: Optional[int] = None):
|
|
|
|
|
|
self.rows = rows
|
|
|
|
|
|
self.cols = cols
|
|
|
|
|
|
self.is_connected = True
|
|
|
|
|
|
self._rng = np.random.RandomState(seed if seed is not None else (int(time.time()) & 0xFFFF))
|
|
|
|
|
|
self._phase = 0.0
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2025-12-02 08:53:04 +08:00
|
|
|
|
def read_data(self) -> Dict[str, Any]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not self.is_connected:
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
raw_data = self._generate_raw_frame()
|
|
|
|
|
|
zones = self._calculate_foot_pressure_zones(raw_data)
|
|
|
|
|
|
image_base64 = self._generate_pressure_image(
|
|
|
|
|
|
zones['left_front'], zones['left_rear'], zones['right_front'], zones['right_rear'], raw_data
|
|
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
'foot_pressure': {
|
|
|
|
|
|
'left_front': round(zones['left_front'], 2),
|
|
|
|
|
|
'left_rear': round(zones['left_rear'], 2),
|
|
|
|
|
|
'right_front': round(zones['right_front'], 2),
|
|
|
|
|
|
'right_rear': round(zones['right_rear'], 2),
|
|
|
|
|
|
'left_total': round(zones['left_total'], 2),
|
|
|
|
|
|
'right_total': round(zones['right_total'], 2)
|
|
|
|
|
|
},
|
|
|
|
|
|
'pressure_image': image_base64,
|
|
|
|
|
|
'timestamp': datetime.now().isoformat()
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_raw_frame(self) -> np.ndarray:
|
|
|
|
|
|
rows, cols = self.rows, self.cols
|
|
|
|
|
|
gy, gx = np.meshgrid(np.arange(rows), np.arange(cols), indexing='ij')
|
|
|
|
|
|
gy = gy.astype(np.float64)
|
|
|
|
|
|
gx = gx.astype(np.float64)
|
|
|
|
|
|
self._phase += 0.15
|
|
|
|
|
|
lf_cy = rows * 0.30 + 0.6 * np.sin(self._phase)
|
|
|
|
|
|
lf_cx = cols * 0.25 + 0.3 * np.cos(self._phase * 0.7)
|
|
|
|
|
|
lr_cy = rows * 0.75 + 0.5 * np.sin(self._phase * 0.8)
|
|
|
|
|
|
lr_cx = cols * 0.25 + 0.2 * np.sin(self._phase * 0.6)
|
|
|
|
|
|
rf_cy = rows * 0.30 + 0.6 * np.cos(self._phase * 0.9)
|
|
|
|
|
|
rf_cx = cols * 0.75 + 0.3 * np.sin(self._phase)
|
|
|
|
|
|
rr_cy = rows * 0.75 + 0.5 * np.cos(self._phase * 0.5)
|
|
|
|
|
|
rr_cx = cols * 0.75 + 0.2 * np.cos(self._phase * 0.4)
|
|
|
|
|
|
sy = rows * 0.10
|
|
|
|
|
|
sx = cols * 0.10
|
|
|
|
|
|
def gauss(cy: float, cx: float, amp: float) -> np.ndarray:
|
|
|
|
|
|
return amp * np.exp(-(((gy - cy) ** 2) / (2 * sy * sy) + ((gx - cx) ** 2) / (2 * sx * sx)))
|
|
|
|
|
|
lf = gauss(lf_cy, lf_cx, 300.0 + 120.0 * self._rng.rand())
|
|
|
|
|
|
lr = gauss(lr_cy, lr_cx, 280.0 + 120.0 * self._rng.rand())
|
|
|
|
|
|
rf = gauss(rf_cy, rf_cx, 300.0 + 120.0 * self._rng.rand())
|
|
|
|
|
|
rr = gauss(rr_cy, rr_cx, 280.0 + 120.0 * self._rng.rand())
|
|
|
|
|
|
base = lf + lr + rf + rr
|
|
|
|
|
|
noise = self._rng.normal(0.0, 5.0, size=(rows, cols))
|
|
|
|
|
|
frame = base + noise
|
|
|
|
|
|
frame = np.clip(frame, 0, 65535).astype(np.uint16)
|
|
|
|
|
|
return frame
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_foot_pressure_zones(self, raw_data: np.ndarray) -> Dict[str, Any]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
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
|
|
|
|
|
|
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:
|
|
|
|
|
|
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: float, left_rear: float, right_front: float, right_rear: float, raw_data: Optional[np.ndarray] = None) -> str:
|
|
|
|
|
|
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:
|
|
|
|
|
|
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_heatmap_image(self, raw_data: np.ndarray) -> str:
|
|
|
|
|
|
try:
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 底值阈值(小于等于该值的区域作为背景)
|
2025-12-02 08:53:04 +08:00
|
|
|
|
vmin = 10
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 归一化到 [0,255],避免 dmax==dmin 时除零
|
2025-12-02 08:53:04 +08:00
|
|
|
|
dmin, dmax = np.min(raw_data), np.max(raw_data)
|
|
|
|
|
|
norm = np.clip((raw_data - dmin) / max(dmax - dmin, 1) * 255, 0, 255).astype(np.uint8)
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 应用伪彩色(JET)以增强对比
|
2025-12-02 08:53:04 +08:00
|
|
|
|
heatmap = cv2.applyColorMap(norm, cv2.COLORMAP_JET)
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 将低值区域设置为背景色 #263040;OpenCV 使用 BGR 通道顺序 -> (64, 48, 38)
|
|
|
|
|
|
heatmap[raw_data <= vmin] = (64, 48, 38)
|
|
|
|
|
|
# 放大显示,保持像素边界清晰
|
2025-12-02 08:53:04 +08:00
|
|
|
|
rows, cols = raw_data.shape
|
|
|
|
|
|
heatmap = cv2.resize(heatmap, (cols * 4, rows * 4), interpolation=cv2.INTER_NEAREST)
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 转换为 RGB 交给 PIL 编码
|
2025-12-02 08:53:04 +08:00
|
|
|
|
heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
buffer = BytesIO()
|
|
|
|
|
|
Image.fromarray(heatmap_rgb).save(buffer, format="PNG")
|
|
|
|
|
|
buffer.seek(0)
|
2025-12-12 19:02:04 +08:00
|
|
|
|
# 输出 data URL 便于前端直接显示
|
2025-12-02 08:53:04 +08:00
|
|
|
|
image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
|
|
|
|
return f"data:image/png;base64,{image_base64}"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return self._generate_simple_pressure_image(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_simple_pressure_image(self, left_front: float, left_rear: float, right_front: float, right_rear: float) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
import matplotlib
|
|
|
|
|
|
matplotlib.use('Agg')
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
import matplotlib.patches as patches
|
|
|
|
|
|
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')
|
|
|
|
|
|
m = max(left_front, left_rear, right_front, right_rear)
|
|
|
|
|
|
if m > 0:
|
|
|
|
|
|
lf_c = plt.cm.Reds(left_front / m)
|
|
|
|
|
|
lr_c = plt.cm.Reds(left_rear / m)
|
|
|
|
|
|
rf_c = plt.cm.Reds(right_front / m)
|
|
|
|
|
|
rr_c = plt.cm.Reds(right_rear / m)
|
|
|
|
|
|
else:
|
|
|
|
|
|
lf_c = lr_c = rf_c = rr_c = 'lightgray'
|
|
|
|
|
|
ax.add_patch(patches.Rectangle((1, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=lf_c))
|
|
|
|
|
|
ax.add_patch(patches.Rectangle((1, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=lr_c))
|
|
|
|
|
|
ax.add_patch(patches.Rectangle((7, 6), 2, 4, linewidth=1, edgecolor='black', facecolor=rf_c))
|
|
|
|
|
|
ax.add_patch(patches.Rectangle((7, 2), 2, 4, linewidth=1, edgecolor='black', facecolor=rr_c))
|
|
|
|
|
|
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')
|
|
|
|
|
|
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:
|
|
|
|
|
|
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
|
|
|
|
|
|
|
|
|
|
|
def _get_empty_data(self) -> Dict[str, Any]:
|
|
|
|
|
|
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):
|
|
|
|
|
|
self.is_connected = False
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2026-08-11 18:17:13 +08:00
|
|
|
|
|
|
|
|
|
|
class SharedMemoryPressureDevice:
|
|
|
|
|
|
"""X2 共享内存压力板设备读取器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
shared_memory_name: str = "x2_pressure",
|
|
|
|
|
|
rows: int = 288,
|
|
|
|
|
|
cols: int = 64,
|
|
|
|
|
|
header_bytes: int = 72,
|
|
|
|
|
|
dtype_name: str = "float32",
|
|
|
|
|
|
crop_rows: int = 0,
|
|
|
|
|
|
low_percentile: float = 5.0,
|
|
|
|
|
|
high_percentile: float = 98.0,
|
|
|
|
|
|
gamma: float = 1.0,
|
|
|
|
|
|
ema_alpha: float = 0.1,
|
|
|
|
|
|
display_equalize_aspect: bool = True,
|
|
|
|
|
|
display_scale: int = 4,
|
|
|
|
|
|
display_max_side: int = 384,
|
|
|
|
|
|
image_emit_interval_s: float = 0.2,
|
|
|
|
|
|
rotate_90_cw: bool = False,
|
|
|
|
|
|
stale_frame_timeout_s: float = 1.2,
|
|
|
|
|
|
stale_reconnect_interval_s: float = 2.0,
|
|
|
|
|
|
sync_retry_times: int = 2,
|
|
|
|
|
|
sync_retry_sleep_s: float = 0.001,
|
|
|
|
|
|
):
|
|
|
|
|
|
self.shared_memory_name = shared_memory_name
|
|
|
|
|
|
self.rows = int(rows)
|
|
|
|
|
|
self.cols = int(cols)
|
|
|
|
|
|
self.header_bytes = int(header_bytes)
|
|
|
|
|
|
self.crop_rows = int(crop_rows) if int(crop_rows) > 0 else 0
|
|
|
|
|
|
self.dtype_name = str(dtype_name).lower()
|
|
|
|
|
|
self.dtype = np.float32 if self.dtype_name == "float32" else np.uint16
|
|
|
|
|
|
self.low_percentile = float(low_percentile)
|
|
|
|
|
|
self.high_percentile = float(high_percentile)
|
|
|
|
|
|
self.gamma = float(gamma)
|
|
|
|
|
|
self.ema_alpha = float(ema_alpha)
|
|
|
|
|
|
self.display_equalize_aspect = bool(display_equalize_aspect)
|
|
|
|
|
|
self.display_scale = int(display_scale) if int(display_scale) > 0 else 1
|
|
|
|
|
|
self.display_max_side = int(display_max_side) if int(display_max_side) > 0 else 384
|
|
|
|
|
|
self.image_emit_interval_s = max(0.05, float(image_emit_interval_s))
|
|
|
|
|
|
self.rotate_90_cw = bool(rotate_90_cw)
|
|
|
|
|
|
self.stale_frame_timeout_s = max(0.3, float(stale_frame_timeout_s))
|
|
|
|
|
|
self.stale_reconnect_interval_s = max(0.5, float(stale_reconnect_interval_s))
|
|
|
|
|
|
self.sync_retry_times = max(1, int(sync_retry_times))
|
|
|
|
|
|
self.sync_retry_sleep_s = max(0.0, float(sync_retry_sleep_s))
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
self.shm = None
|
|
|
|
|
|
self._last_warn_ts = 0.0
|
|
|
|
|
|
self._viz_vmin = None
|
|
|
|
|
|
self._viz_vmax = None
|
|
|
|
|
|
self._last_image_emit_ts = 0.0
|
|
|
|
|
|
self._last_frame_fingerprint = None
|
|
|
|
|
|
self._last_frame_change_ts = time.time()
|
|
|
|
|
|
self._last_stale_warn_ts = 0.0
|
|
|
|
|
|
self._last_stale_reconnect_ts = 0.0
|
|
|
|
|
|
self._last_header_tick = None
|
|
|
|
|
|
self._last_crc = None
|
|
|
|
|
|
self._diag_last_log_ts = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
self.connect()
|
|
|
|
|
|
|
|
|
|
|
|
def connect(self) -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.shm is not None:
|
|
|
|
|
|
return True
|
|
|
|
|
|
self.shm = shared_memory.SharedMemory(name=self.shared_memory_name)
|
|
|
|
|
|
self.is_connected = True
|
|
|
|
|
|
logger.info(f"已连接X2共享内存: {self.shared_memory_name}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"连接X2共享内存失败: {e}")
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def read_data(self) -> Dict[str, Any]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.shm is None and not self.connect():
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
if self.shm is None:
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
expected_bytes = self.header_bytes + self.rows * self.cols * np.dtype(self.dtype).itemsize
|
|
|
|
|
|
buffer = np.frombuffer(self.shm.buf, dtype=np.uint8)
|
|
|
|
|
|
if buffer.nbytes < expected_bytes:
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
if now - self._last_warn_ts > 2.0:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"X2共享内存数据长度不足: got={buffer.nbytes}, expect>={expected_bytes}, name={self.shared_memory_name}"
|
|
|
|
|
|
)
|
|
|
|
|
|
self._last_warn_ts = now
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
raw_data = self._read_consistent_matrix()
|
|
|
|
|
|
if raw_data is None:
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
if self.crop_rows > 0:
|
|
|
|
|
|
raw_data = raw_data[: self.crop_rows, :]
|
|
|
|
|
|
|
|
|
|
|
|
raw_data = np.asarray(raw_data, dtype=np.float64)
|
|
|
|
|
|
self._check_frame_fresh(raw_data)
|
|
|
|
|
|
|
|
|
|
|
|
# 与厂家示例保持一致的可视化预处理:
|
|
|
|
|
|
# 1) 优先取前 N 行(示例为 64 行)
|
|
|
|
|
|
# 2) 再转置,得到更符合足底朝向与比例的显示矩阵
|
|
|
|
|
|
vis_data = raw_data
|
|
|
|
|
|
if self.crop_rows <= 0 and self.rows >= 128 and self.cols <= 128:
|
|
|
|
|
|
# 对 288x64 这类“长矩阵”默认取前 64 行,避免有效区域仅挤在顶部
|
|
|
|
|
|
vis_rows = min(self.cols, vis_data.shape[0])
|
|
|
|
|
|
vis_data = vis_data[:vis_rows, :]
|
|
|
|
|
|
vis_data = vis_data.T
|
|
|
|
|
|
# 旋转应作用在数据矩阵层,确保“压力分区计算”和“图片显示”使用同一坐标系
|
|
|
|
|
|
if self.rotate_90_cw:
|
|
|
|
|
|
vis_data = np.rot90(vis_data, k=3)
|
|
|
|
|
|
|
|
|
|
|
|
zones = self._calculate_foot_pressure_zones(vis_data)
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
should_emit_image = (
|
|
|
|
|
|
(now - self._last_image_emit_ts) >= self.image_emit_interval_s
|
|
|
|
|
|
)
|
|
|
|
|
|
image_base64 = ""
|
|
|
|
|
|
if should_emit_image:
|
|
|
|
|
|
image_base64 = self._generate_heatmap_image(vis_data)
|
|
|
|
|
|
self._last_image_emit_ts = now
|
|
|
|
|
|
|
|
|
|
|
|
self.is_connected = True
|
|
|
|
|
|
return {
|
|
|
|
|
|
"foot_pressure": {
|
|
|
|
|
|
"left_front": round(zones["left_front"], 2),
|
|
|
|
|
|
"left_rear": round(zones["left_rear"], 2),
|
|
|
|
|
|
"right_front": round(zones["right_front"], 2),
|
|
|
|
|
|
"right_rear": round(zones["right_rear"], 2),
|
|
|
|
|
|
"left_total": round(zones["left_total"], 2),
|
|
|
|
|
|
"right_total": round(zones["right_total"], 2),
|
|
|
|
|
|
},
|
|
|
|
|
|
"pressure_image": image_base64,
|
|
|
|
|
|
"timestamp": datetime.now().isoformat(),
|
|
|
|
|
|
}
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
self.close()
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"读取X2共享内存压力数据异常: {e}")
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
return self._get_empty_data()
|
|
|
|
|
|
|
|
|
|
|
|
def _read_consistent_matrix(self) -> Optional[np.ndarray]:
|
|
|
|
|
|
if self.shm is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
raw_data = None
|
|
|
|
|
|
for _ in range(self.sync_retry_times):
|
|
|
|
|
|
try:
|
|
|
|
|
|
pre_header = bytes(self.shm.buf[: self.header_bytes]) if self.header_bytes > 0 else b""
|
|
|
|
|
|
raw_data = np.ndarray(
|
|
|
|
|
|
shape=(self.rows, self.cols),
|
|
|
|
|
|
dtype=self.dtype,
|
|
|
|
|
|
buffer=self.shm.buf,
|
|
|
|
|
|
offset=self.header_bytes,
|
|
|
|
|
|
).copy()
|
|
|
|
|
|
post_header = bytes(self.shm.buf[: self.header_bytes]) if self.header_bytes > 0 else b""
|
|
|
|
|
|
if pre_header == post_header:
|
|
|
|
|
|
return raw_data
|
|
|
|
|
|
if self.sync_retry_sleep_s > 0:
|
|
|
|
|
|
time.sleep(self.sync_retry_sleep_s)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return raw_data
|
|
|
|
|
|
return raw_data
|
|
|
|
|
|
|
|
|
|
|
|
def _frame_fingerprint(self, raw_data: np.ndarray) -> int:
|
|
|
|
|
|
rows, cols = raw_data.shape
|
|
|
|
|
|
r_step = max(1, rows // 16)
|
|
|
|
|
|
c_step = max(1, cols // 16)
|
|
|
|
|
|
sample = np.ascontiguousarray(raw_data[::r_step, ::c_step])
|
|
|
|
|
|
return int(zlib.crc32(sample.tobytes()))
|
|
|
|
|
|
|
|
|
|
|
|
def _read_header_tick(self) -> Optional[int]:
|
|
|
|
|
|
"""尝试从共享内存头部读取一个可单调变化的tick(若协议支持)。"""
|
|
|
|
|
|
if self.shm is None or self.header_bytes < 8:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 约定优先按 little-endian uint64 读取前8字节;若厂商协议不同会自动退化到指纹法
|
|
|
|
|
|
return int.from_bytes(bytes(self.shm.buf[:8]), byteorder='little', signed=False)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _check_frame_fresh(self, raw_data: np.ndarray) -> bool:
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
header_tick = self._read_header_tick()
|
|
|
|
|
|
crc = self._frame_fingerprint(raw_data)
|
|
|
|
|
|
self._last_crc = crc
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
if header_tick is not None:
|
|
|
|
|
|
if self._last_header_tick is None or header_tick != self._last_header_tick:
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
self._last_header_tick = header_tick
|
|
|
|
|
|
else:
|
|
|
|
|
|
if self._last_frame_fingerprint is None or crc != self._last_frame_fingerprint:
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
self._last_frame_fingerprint = crc
|
|
|
|
|
|
|
|
|
|
|
|
if changed:
|
|
|
|
|
|
self._last_frame_change_ts = now
|
|
|
|
|
|
self._log_frame_diagnostics(now, header_tick, crc, changed=True, stale=False)
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if (now - self._last_frame_change_ts) <= self.stale_frame_timeout_s:
|
|
|
|
|
|
self._log_frame_diagnostics(now, header_tick, crc, changed=False, stale=False)
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if now - self._last_stale_warn_ts > 2.0:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"X2共享内存帧疑似陈旧,{now - self._last_frame_change_ts:.2f}s 无变化: {self.shared_memory_name}"
|
|
|
|
|
|
)
|
|
|
|
|
|
self._last_stale_warn_ts = now
|
|
|
|
|
|
|
|
|
|
|
|
# 帧长期不变化时尝试重连共享内存映射,避免读取端卡在旧映射/陈旧帧
|
|
|
|
|
|
if (now - self._last_stale_reconnect_ts) >= self.stale_reconnect_interval_s:
|
|
|
|
|
|
self._last_stale_reconnect_ts = now
|
|
|
|
|
|
self.close()
|
|
|
|
|
|
time.sleep(0.01)
|
|
|
|
|
|
self.connect()
|
|
|
|
|
|
self._log_frame_diagnostics(now, header_tick, crc, changed=False, stale=True)
|
|
|
|
|
|
# 注意:即使判定为陈旧帧,也不丢弃当前帧,避免前端出现“被动卡帧”体感
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _log_frame_diagnostics(
|
|
|
|
|
|
self,
|
|
|
|
|
|
now_ts: float,
|
|
|
|
|
|
header_tick: Optional[int],
|
|
|
|
|
|
crc: int,
|
|
|
|
|
|
changed: bool,
|
|
|
|
|
|
stale: bool
|
|
|
|
|
|
):
|
|
|
|
|
|
# 每秒打印一次,避免刷屏
|
|
|
|
|
|
if (now_ts - self._diag_last_log_ts) < 1.0:
|
|
|
|
|
|
return
|
|
|
|
|
|
self._diag_last_log_ts = now_ts
|
|
|
|
|
|
frame_age_ms = int(max(0.0, now_ts - self._last_frame_change_ts) * 1000.0)
|
|
|
|
|
|
tick_str = str(header_tick) if header_tick is not None else "None"
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"X2帧诊断 name=%s frame_age_ms=%d header_tick=%s crc=%s changed=%s stale=%s",
|
|
|
|
|
|
self.shared_memory_name,
|
|
|
|
|
|
frame_age_ms,
|
|
|
|
|
|
tick_str,
|
|
|
|
|
|
crc,
|
|
|
|
|
|
int(bool(changed)),
|
|
|
|
|
|
int(bool(stale)),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_foot_pressure_zones(self, raw_data: np.ndarray) -> Dict[str, Any]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
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"计算X2足部区域压力异常: {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_heatmap_image(self, raw_data: np.ndarray) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if raw_data.size == 0:
|
|
|
|
|
|
return self._get_empty_data()["pressure_image"]
|
|
|
|
|
|
|
|
|
|
|
|
# 1) 按分位数估计当帧动态范围,避免极值拉平色阶
|
|
|
|
|
|
p_low = float(np.percentile(raw_data, self.low_percentile))
|
|
|
|
|
|
p_high = float(np.percentile(raw_data, self.high_percentile))
|
|
|
|
|
|
if p_high <= p_low:
|
|
|
|
|
|
p_low, p_high = float(np.min(raw_data)), float(np.max(raw_data))
|
|
|
|
|
|
if p_high <= p_low:
|
|
|
|
|
|
p_high = p_low + 1e-9
|
|
|
|
|
|
|
|
|
|
|
|
# 2) 使用EMA平滑动态范围,减小闪烁
|
|
|
|
|
|
alpha = float(np.clip(self.ema_alpha, 0.0, 1.0))
|
|
|
|
|
|
if self._viz_vmin is None or self._viz_vmax is None:
|
|
|
|
|
|
self._viz_vmin, self._viz_vmax = p_low, p_high
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._viz_vmin = (1.0 - alpha) * self._viz_vmin + alpha * p_low
|
|
|
|
|
|
self._viz_vmax = (1.0 - alpha) * self._viz_vmax + alpha * p_high
|
|
|
|
|
|
if self._viz_vmax <= self._viz_vmin:
|
|
|
|
|
|
self._viz_vmax = self._viz_vmin + 1e-9
|
|
|
|
|
|
|
|
|
|
|
|
# 3) 归一化后做gamma增强(>1 高压更显著)
|
|
|
|
|
|
norm = np.clip((raw_data - self._viz_vmin) / (self._viz_vmax - self._viz_vmin), 0.0, 1.0)
|
|
|
|
|
|
gamma = max(0.05, float(self.gamma))
|
|
|
|
|
|
enhanced = np.power(norm, gamma)
|
|
|
|
|
|
norm_u8 = np.clip(enhanced * 255.0, 0, 255).astype(np.uint8)
|
|
|
|
|
|
|
|
|
|
|
|
heatmap = cv2.applyColorMap(norm_u8, cv2.COLORMAP_JET)
|
|
|
|
|
|
heatmap[norm_u8 <= 2] = (64, 48, 38)
|
|
|
|
|
|
rows, cols = raw_data.shape
|
|
|
|
|
|
target_w = max(1, int(cols * self.display_scale))
|
|
|
|
|
|
target_h = max(1, int(rows * self.display_scale))
|
|
|
|
|
|
# 对 288x64 这类强非方阵做显示补偿,避免前端看起来“压扁”
|
|
|
|
|
|
if self.display_equalize_aspect and rows > 0 and cols > 0:
|
|
|
|
|
|
if rows > cols:
|
|
|
|
|
|
target_w = max(target_w, int(target_w * (rows / cols)))
|
|
|
|
|
|
elif cols > rows:
|
|
|
|
|
|
target_h = max(target_h, int(target_h * (cols / rows)))
|
|
|
|
|
|
# 限制最长边,防止base64过大导致SocketIO排队和前端渲染延迟
|
|
|
|
|
|
max_side = max(target_w, target_h)
|
|
|
|
|
|
if max_side > self.display_max_side:
|
|
|
|
|
|
shrink = self.display_max_side / float(max_side)
|
|
|
|
|
|
target_w = max(1, int(target_w * shrink))
|
|
|
|
|
|
target_h = max(1, int(target_h * shrink))
|
|
|
|
|
|
heatmap = cv2.resize(heatmap, (target_w, target_h), interpolation=cv2.INTER_NEAREST)
|
|
|
|
|
|
heatmap_rgb = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
|
|
|
|
buffer = BytesIO()
|
|
|
|
|
|
Image.fromarray(heatmap_rgb).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:
|
|
|
|
|
|
return self._get_empty_data()["pressure_image"]
|
|
|
|
|
|
|
|
|
|
|
|
def _get_empty_data(self) -> Dict[str, Any]:
|
|
|
|
|
|
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.shm is not None:
|
|
|
|
|
|
self.shm.close()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self.shm = None
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
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
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.use_mock = bool(self.config.get('use_mock', False))
|
2026-08-11 18:17:13 +08:00
|
|
|
|
self.pressure_source = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_source', fallback='smitsense')
|
|
|
|
|
|
).lower()
|
|
|
|
|
|
self.shared_memory_name = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_name', fallback='x2_pressure')
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_rows = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_rows', fallback=288)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_cols = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_cols', fallback=64)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_header_bytes = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_header_bytes', fallback=72)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_dtype = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_dtype', fallback='float32')
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_crop_rows = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_crop_rows', fallback=0)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_low_percentile = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_low_percentile', fallback=5.0)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_high_percentile = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_high_percentile', fallback=98.0)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_gamma = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_gamma', fallback=1.0)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_ema_alpha = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_ema_alpha', fallback=0.1)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_display_equalize_aspect = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_display_equalize_aspect', fallback='True')
|
|
|
|
|
|
).lower() in ('1', 'true', 'yes', 'on')
|
|
|
|
|
|
self.shared_memory_display_scale = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_display_scale', fallback=4)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_display_max_side = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_display_max_side', fallback=384)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_image_emit_interval_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_image_emit_interval_s', fallback=0.2)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_rotate_90_cw = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_rotate_90_cw', fallback='True')
|
|
|
|
|
|
).lower() in ('1', 'true', 'yes', 'on')
|
|
|
|
|
|
self.shared_memory_stale_frame_timeout_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_stale_frame_timeout_s', fallback=1.2)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_stale_reconnect_interval_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_stale_reconnect_interval_s', fallback=2.0)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_sync_retry_times = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_sync_retry_times', fallback=2)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_sync_retry_sleep_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_sync_retry_sleep_s', fallback=0.001)
|
|
|
|
|
|
)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 数据流相关
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2025-09-11 17:40:03 +08:00
|
|
|
|
# 重连相关配置(与camera_manager保持一致的键名和默认值)
|
|
|
|
|
|
self.max_reconnect_attempts = int(self.config.get('max_reconnect_attempts', -1)) # -1 表示无限重连
|
|
|
|
|
|
self.reconnect_delay = float(self.config.get('reconnect_delay', 2.0))
|
|
|
|
|
|
self.read_fail_threshold = int(self.config.get('read_fail_threshold', 30))
|
|
|
|
|
|
self._last_connected_state = None # 去抖动状态广播
|
|
|
|
|
|
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.logger.info(f"压力板管理器初始化完成 - use_mock: {self.use_mock}")
|
2026-08-11 18:17:13 +08:00
|
|
|
|
|
|
|
|
|
|
def _create_pressure_device(self):
|
|
|
|
|
|
if self.use_mock:
|
|
|
|
|
|
return MockPressureDevice()
|
|
|
|
|
|
if self.pressure_source == 'shared_memory':
|
|
|
|
|
|
return SharedMemoryPressureDevice(
|
|
|
|
|
|
shared_memory_name=self.shared_memory_name,
|
|
|
|
|
|
rows=self.shared_memory_rows,
|
|
|
|
|
|
cols=self.shared_memory_cols,
|
|
|
|
|
|
header_bytes=self.shared_memory_header_bytes,
|
|
|
|
|
|
dtype_name=self.shared_memory_dtype,
|
|
|
|
|
|
crop_rows=self.shared_memory_crop_rows,
|
|
|
|
|
|
low_percentile=self.shared_memory_low_percentile,
|
|
|
|
|
|
high_percentile=self.shared_memory_high_percentile,
|
|
|
|
|
|
gamma=self.shared_memory_gamma,
|
|
|
|
|
|
ema_alpha=self.shared_memory_ema_alpha,
|
|
|
|
|
|
display_equalize_aspect=self.shared_memory_display_equalize_aspect,
|
|
|
|
|
|
display_scale=self.shared_memory_display_scale,
|
|
|
|
|
|
display_max_side=self.shared_memory_display_max_side,
|
|
|
|
|
|
image_emit_interval_s=self.shared_memory_image_emit_interval_s,
|
|
|
|
|
|
rotate_90_cw=self.shared_memory_rotate_90_cw,
|
|
|
|
|
|
stale_frame_timeout_s=self.shared_memory_stale_frame_timeout_s,
|
|
|
|
|
|
stale_reconnect_interval_s=self.shared_memory_stale_reconnect_interval_s,
|
|
|
|
|
|
sync_retry_times=self.shared_memory_sync_retry_times,
|
|
|
|
|
|
sync_retry_sleep_s=self.shared_memory_sync_retry_sleep_s,
|
|
|
|
|
|
)
|
|
|
|
|
|
return RealPressureDevice()
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
def initialize(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化压力板设备
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 初始化是否成功
|
|
|
|
|
|
"""
|
2026-02-07 13:41:31 +08:00
|
|
|
|
self._initializing = True
|
2025-08-17 12:48:10 +08:00
|
|
|
|
try:
|
2025-09-01 15:14:42 +08:00
|
|
|
|
self.logger.info(f"正在初始化压力板设备...")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用构造函数中已加载的配置,避免并发读取配置文件
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.logger.info(f"使用已加载配置: use_mock={self.use_mock}, stream_interval={self.stream_interval}")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 根据设备类型创建设备实例
|
2026-08-11 18:17:13 +08:00
|
|
|
|
self.device = self._create_pressure_device()
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
2026-02-07 13:41:31 +08:00
|
|
|
|
connected = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.use_mock:
|
|
|
|
|
|
connected = True
|
|
|
|
|
|
elif hasattr(self.device, 'is_connected'):
|
|
|
|
|
|
connected = bool(self.device.is_connected)
|
|
|
|
|
|
else:
|
|
|
|
|
|
connected = bool(self.check_hardware_connection())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
connected = False
|
|
|
|
|
|
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 使用set_connected方法启动连接监控线程
|
2026-02-07 13:41:31 +08:00
|
|
|
|
self.set_connected(bool(connected))
|
2025-08-17 12:48:10 +08:00
|
|
|
|
self._device_info.update({
|
2026-08-11 18:17:13 +08:00
|
|
|
|
'device_type': 'mock' if self.use_mock else self.pressure_source,
|
2025-08-17 12:48:10 +08:00
|
|
|
|
'matrix_size': '4x4' if hasattr(self.device, 'rows') else 'unknown'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-07 13:41:31 +08:00
|
|
|
|
if not connected:
|
|
|
|
|
|
self.logger.warning("压力板初始化完成但硬件未连接")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.logger.info(f"压力板初始化成功 - use_mock: {self.use_mock}")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"压力板初始化失败: {e}")
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 使用set_connected方法停止连接监控线程
|
|
|
|
|
|
self.set_connected(False)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
self.device = None
|
|
|
|
|
|
return False
|
2026-02-07 13:41:31 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
self._initializing = False
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
2025-09-11 17:40:03 +08:00
|
|
|
|
while self.is_streaming:
|
2025-08-17 12:48:10 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 从设备读取数据
|
2025-09-11 17:40:03 +08:00
|
|
|
|
pressure_data = None
|
2025-08-17 12:48:10 +08:00
|
|
|
|
if self.device:
|
|
|
|
|
|
pressure_data = self.device.read_data()
|
2025-09-11 17:40:03 +08:00
|
|
|
|
# 如果底层设备在读取时标记了断开,则在此处进入下一轮以触发重连
|
|
|
|
|
|
if hasattr(self.device, 'is_connected') and not self.device.is_connected:
|
|
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
time.sleep(self.reconnect_delay)
|
2025-09-27 12:14:19 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 读数成功,立即更新心跳和连接状态
|
|
|
|
|
|
self.is_connected = True
|
|
|
|
|
|
self.update_heartbeat()
|
2025-09-18 09:07:09 +08:00
|
|
|
|
|
2025-09-11 17:40:03 +08:00
|
|
|
|
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实例为空,无法发送压力数据")
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
time.sleep(self.stream_interval)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.error_count += 1
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# self.logger.error(f"压力数据流处理异常: {e}")
|
2025-08-17 12:48:10 +08:00
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"压力数据流线程异常: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
self.logger.info("压力数据流线程结束")
|
|
|
|
|
|
|
2025-09-18 09:07:09 +08:00
|
|
|
|
|
2025-09-11 17:40:03 +08:00
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取设备状态
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict[str, Any]: 设备状态信息
|
|
|
|
|
|
"""
|
|
|
|
|
|
return {
|
2026-01-12 15:21:44 +08:00
|
|
|
|
'device_type': 'mock' if self.use_mock else 'real',
|
2025-08-17 12:48:10 +08:00
|
|
|
|
'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
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 使用set_connected方法停止连接监控线程
|
|
|
|
|
|
self.set_connected(False)
|
2025-08-17 12:48:10 +08:00
|
|
|
|
|
|
|
|
|
|
self.logger.info("压力板设备连接已断开")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"断开压力板设备连接失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2025-09-01 15:14:42 +08:00
|
|
|
|
def reload_config(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
重新加载压力板配置
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 配置重新加载是否成功
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.logger.info("正在重新加载压力板配置...")
|
|
|
|
|
|
|
|
|
|
|
|
# 重新获取配置
|
|
|
|
|
|
new_config = self.config_manager.get_device_config('pressure')
|
|
|
|
|
|
|
|
|
|
|
|
# 更新配置属性
|
|
|
|
|
|
self.config = new_config
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.use_mock = bool(new_config.get('use_mock', False))
|
2025-09-01 15:14:42 +08:00
|
|
|
|
self.stream_interval = new_config.get('stream_interval', 0.1)
|
2026-08-11 18:17:13 +08:00
|
|
|
|
self.pressure_source = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_source', fallback=self.pressure_source)
|
|
|
|
|
|
).lower()
|
|
|
|
|
|
self.shared_memory_name = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_name', fallback=self.shared_memory_name)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_rows = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_rows', fallback=self.shared_memory_rows)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_cols = int(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_cols', fallback=self.shared_memory_cols)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_header_bytes = int(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_header_bytes', fallback=self.shared_memory_header_bytes
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_dtype = str(
|
|
|
|
|
|
self.config_manager.get_config_value('DEVICES', 'pressure_shared_memory_dtype', fallback=self.shared_memory_dtype)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_crop_rows = int(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_crop_rows', fallback=self.shared_memory_crop_rows
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_low_percentile = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_low_percentile', fallback=self.shared_memory_low_percentile
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_high_percentile = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_high_percentile', fallback=self.shared_memory_high_percentile
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_gamma = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_gamma', fallback=self.shared_memory_gamma
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_ema_alpha = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_ema_alpha', fallback=self.shared_memory_ema_alpha
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_display_equalize_aspect = str(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_display_equalize_aspect',
|
|
|
|
|
|
fallback=self.shared_memory_display_equalize_aspect
|
|
|
|
|
|
)
|
|
|
|
|
|
).lower() in ('1', 'true', 'yes', 'on')
|
|
|
|
|
|
self.shared_memory_display_scale = int(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_display_scale', fallback=self.shared_memory_display_scale
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_display_max_side = int(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_display_max_side', fallback=self.shared_memory_display_max_side
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_image_emit_interval_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_image_emit_interval_s',
|
|
|
|
|
|
fallback=self.shared_memory_image_emit_interval_s
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_rotate_90_cw = str(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES', 'pressure_shared_memory_rotate_90_cw', fallback=self.shared_memory_rotate_90_cw
|
|
|
|
|
|
)
|
|
|
|
|
|
).lower() in ('1', 'true', 'yes', 'on')
|
|
|
|
|
|
self.shared_memory_stale_frame_timeout_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_stale_frame_timeout_s',
|
|
|
|
|
|
fallback=self.shared_memory_stale_frame_timeout_s
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_stale_reconnect_interval_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_stale_reconnect_interval_s',
|
|
|
|
|
|
fallback=self.shared_memory_stale_reconnect_interval_s
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_sync_retry_times = int(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_sync_retry_times',
|
|
|
|
|
|
fallback=self.shared_memory_sync_retry_times
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
self.shared_memory_sync_retry_sleep_s = float(
|
|
|
|
|
|
self.config_manager.get_config_value(
|
|
|
|
|
|
'DEVICES',
|
|
|
|
|
|
'pressure_shared_memory_sync_retry_sleep_s',
|
|
|
|
|
|
fallback=self.shared_memory_sync_retry_sleep_s
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2025-09-11 17:40:03 +08:00
|
|
|
|
# 动态更新重连参数
|
|
|
|
|
|
self.max_reconnect_attempts = int(new_config.get('max_reconnect_attempts', self.max_reconnect_attempts))
|
|
|
|
|
|
self.reconnect_delay = float(new_config.get('reconnect_delay', self.reconnect_delay))
|
|
|
|
|
|
self.read_fail_threshold = int(new_config.get('read_fail_threshold', self.read_fail_threshold))
|
2025-09-01 15:14:42 +08:00
|
|
|
|
|
2026-01-12 15:21:44 +08:00
|
|
|
|
self.logger.info(f"压力板配置重新加载成功 - use_mock: {self.use_mock}, 流间隔: {self.stream_interval}")
|
2025-09-01 15:14:42 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"重新加载压力板配置失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2025-09-10 09:13:21 +08:00
|
|
|
|
def check_hardware_connection(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
检查压力板硬件连接状态
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 硬件连接是否正常
|
|
|
|
|
|
"""
|
2025-09-18 09:07:09 +08:00
|
|
|
|
try:
|
2025-09-10 09:13:21 +08:00
|
|
|
|
if not self.device:
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 如果设备实例不存在,返回False表示硬件未连接
|
2025-09-18 09:07:09 +08:00
|
|
|
|
return self._attempt_device_reconnection()
|
2026-08-11 18:17:13 +08:00
|
|
|
|
|
|
|
|
|
|
# 对于共享内存设备,依据连接状态判断
|
|
|
|
|
|
if hasattr(self.device, 'shared_memory_name'):
|
|
|
|
|
|
if bool(getattr(self.device, 'is_connected', False)):
|
|
|
|
|
|
return True
|
|
|
|
|
|
return self._attempt_device_reconnection()
|
2025-09-10 09:13:21 +08:00
|
|
|
|
|
|
|
|
|
|
# 对于真实设备,检查DLL和设备句柄状态
|
|
|
|
|
|
if hasattr(self.device, 'dll') and hasattr(self.device, 'device_handle'):
|
|
|
|
|
|
if not self.device.dll or not self.device.device_handle:
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# DLL或句柄无效,返回False表示硬件未连接
|
2025-09-18 09:07:09 +08:00
|
|
|
|
return self._attempt_device_reconnection()
|
2025-09-10 09:13:21 +08:00
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 直接检查设备句柄的有效性
|
2025-09-10 09:13:21 +08:00
|
|
|
|
try:
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 检查设备句柄是否有效
|
|
|
|
|
|
if not self.device.device_handle or not hasattr(self.device.device_handle, 'value'):
|
|
|
|
|
|
return self._attempt_device_reconnection()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查句柄值是否为0(无效句柄)
|
|
|
|
|
|
if self.device.device_handle.value == 0:
|
|
|
|
|
|
return self._attempt_device_reconnection()
|
|
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 尝试实际的设备通信来验证硬件连接
|
|
|
|
|
|
# 使用DLL函数检查设备列表,验证设备是否真实存在
|
|
|
|
|
|
count = ctypes.c_int()
|
|
|
|
|
|
devs = (FPMS_DEVICE_INFO * 10)()
|
|
|
|
|
|
r = self.device.dll.fpms_usb_get_device_list_wrap(devs, 10, ctypes.byref(count))
|
|
|
|
|
|
|
|
|
|
|
|
# 如果获取设备列表失败或设备数量为0,说明硬件已断开
|
|
|
|
|
|
if r != 0 or count.value == 0:
|
|
|
|
|
|
self.logger.debug(f"设备列表检查失败: r={r}, count={count.value}")
|
|
|
|
|
|
return self._attempt_device_reconnection()
|
|
|
|
|
|
|
|
|
|
|
|
# 设备列表正常,硬件连接正常
|
2025-09-18 09:07:09 +08:00
|
|
|
|
return True
|
2025-09-27 12:14:19 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.debug(f"硬件连接检查异常: {e}")
|
2025-09-18 09:07:09 +08:00
|
|
|
|
return self._attempt_device_reconnection()
|
2025-09-10 09:13:21 +08:00
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 对于Mock设备,直接返回True
|
2025-09-10 09:13:21 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.debug(f"检查压力板硬件连接时出错: {e}")
|
|
|
|
|
|
return False
|
2025-09-18 09:07:09 +08:00
|
|
|
|
|
|
|
|
|
|
def _attempt_device_reconnection(self) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
尝试重新连接压力板设备
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 重连是否成功
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.logger.info("检测到压力板设备断开,尝试重新连接...")
|
|
|
|
|
|
|
|
|
|
|
|
# 清理旧的设备实例
|
|
|
|
|
|
if self.device and hasattr(self.device, 'close'):
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.device.close()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.debug(f"清理旧设备实例时出错: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
self.device = None
|
|
|
|
|
|
|
2025-09-27 12:14:19 +08:00
|
|
|
|
# 重置USB状态,为重新插入的设备做准备
|
2026-08-11 18:17:13 +08:00
|
|
|
|
if self.pressure_source != 'shared_memory':
|
|
|
|
|
|
RealPressureDevice.reset_usb_state()
|
2025-09-27 12:14:19 +08:00
|
|
|
|
|
2025-09-18 09:07:09 +08:00
|
|
|
|
# 根据设备类型重新创建设备实例
|
2026-08-11 18:17:13 +08:00
|
|
|
|
self.device = self._create_pressure_device()
|
2025-09-18 09:07:09 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查新设备是否连接成功
|
|
|
|
|
|
if hasattr(self.device, 'is_connected') and self.device.is_connected:
|
|
|
|
|
|
self._notify_status_change(True)
|
|
|
|
|
|
# 重连成功后,确保数据流正在运行
|
2025-09-27 12:14:19 +08:00
|
|
|
|
self.logger.info("重连成功,启动压力数据流")
|
|
|
|
|
|
self.start_streaming()
|
2025-09-18 09:07:09 +08:00
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.logger.warning("压力板设备重连失败")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.logger.error(f"压力板设备重连过程中出错: {e}")
|
|
|
|
|
|
self.device = None
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
def cleanup(self) -> None:
|
|
|
|
|
|
"""清理资源"""
|
|
|
|
|
|
try:
|
2025-09-10 09:13:21 +08:00
|
|
|
|
# 停止连接监控
|
|
|
|
|
|
self._cleanup_monitoring()
|
|
|
|
|
|
|
2025-08-17 12:48:10 +08:00
|
|
|
|
self.stop_streaming()
|
|
|
|
|
|
self.disconnect()
|
|
|
|
|
|
self.logger.info("压力板设备资源清理完成")
|
|
|
|
|
|
except Exception as e:
|
2025-12-02 08:53:04 +08:00
|
|
|
|
self.logger.error(f"压力板设备资源清理失败: {e}")
|