- 录屏: 高DPI缩放导致奇数尺寸触发libx264编码失败,宽高取偶; ffmpeg stderr写入日志 - 压力板: 新增 pressure_manager_small.py 原厂家小压力板,与现有大压力板并存 - 打包: 新增 build_backend.ps1,更新 requirements_build.txt / config.ini - 前端: Electron 本地化配置
16 KiB
多厂家足底压力板适配开发方案
1. 需求背景与目标
当前系统使用足底压力板采集足底压力数据。项目经历了一次设备更换:
| 设备 | 程序文件 | 状态 |
|---|---|---|
| 原厂家(小一号压力板,SMiTSense USB) | backend/devices/pressure_manager_small.py |
已停用 |
| 新厂家(大一号压力板,X2 共享内存) | backend/devices/pressure_manager.py |
当前使用 |
需求目标:程序同时支持两个厂家的设备,通过配置文件参数切换使用哪个厂家,无需修改代码、重新打包即可在两种设备间切换。
具体拆解为三点:
- 新增配置文件参数,用
small代表原厂家设备(对应pressure_manager_small.py),用large代表新厂家设备(对应pressure_manager.py); - 修改配置与初始化程序,根据配置文件动态初始化对应厂家的设备管理器;
- 检测时,根据配置文件加载对应厂家的设备进行数据采集。
2. 现状分析
2.1 两个压力板管理器的异同
两个文件均定义了 class PressureManager(BaseDevice),对外接口完全一致:
- 生命周期:
initialize()/start_streaming()/stop_streaming()/disconnect()/cleanup()/restart相关 - 配置:
reload_config()/get_status()/calibrate() - 数据上报:均通过 Socket.IO 以
pressure_data事件推送到/devices命名空间,数据结构相同(foot_pressure+pressure_image+timestamp)
因此前端无需任何改动,只需要保证后端"按配置加载正确的管理器类"即可。
底层设备实现差异:
| 项目 | pressure_manager_small.py(原厂家) | pressure_manager.py(新厂家) |
|---|---|---|
| 真实设备 | RealPressureDevice(SMiTSense Wrapper.dll USB 读取) |
SharedMemoryPressureDevice(X2 共享内存)为主,同时保留 RealPressureDevice |
| 模拟设备 | MockPressureDevice |
MockPressureDevice |
| 数据源切换方式 | 仅 use_mock |
use_mock + pressure_source(smitsense / shared_memory) |
| 与硬件耦合 | DLL 文件 dll/smitsense/Wrapper.dll |
共享内存名 x2_pressure 等一批 pressure_shared_memory_* 参数 |
2.2 当前硬编码点(需要改造的位置)
现有代码在多处直接 import 并实例化 pressure_manager.PressureManager,这是需要改造的核心:
| 文件 | 行号 | 现状 |
|---|---|---|
backend/devices/device_coordinator.py |
L19 / L26 | 模块顶部 from .pressure_manager import PressureManager |
backend/devices/device_coordinator.py |
L421-436 | _init_pressure() 直接 PressureManager(self.socketio, self.config_manager) |
backend/devices/device_coordinator.py |
L837-842 | restart_device('pressure') 分支再次直接 import + 实例化 |
backend/main.py |
L33 | 模块顶部 from devices.pressure_manager import PressureManager |
backend/devices/__init__.py |
L11 / L19 | from .pressure_manager import PressureManager 并导出 |
2.3 配置现状
[DEVICES] 段已有压力板相关参数(backend/config.ini 与打包产物 backend/dist/BodyBalanceBackend/config.ini 内容一致):
[DEVICES]
; pressure_enable = True
; pressure_use_mock = True
pressure_enable = False
pressure_use_mock = True
pressure_source = shared_memory
pressure_shared_memory_name = x2_pressure
pressure_shared_memory_header_bytes = 72
pressure_shared_memory_rows = 288
pressure_shared_memory_cols = 64
pressure_shared_memory_dtype = float32
pressure_shared_memory_crop_rows = 0
pressure_shared_memory_low_percentile = 5
pressure_shared_memory_high_percentile = 98
pressure_shared_memory_gamma = 1.0
pressure_shared_memory_ema_alpha = 0.1
ConfigManager._get_pressure_config() 目前只返回 enable / use_mock / port / baudrate 四个字段;pressure_source 及 pressure_shared_memory_* 由 pressure_manager.py 内部通过 get_config_value('DEVICES', ...) 直接读取。
3. 设计方案
3.1 新增配置参数:pressure_vendor
在 [DEVICES] 段新增参数,作为厂家选择的总开关:
[DEVICES]
pressure_enable = True
pressure_vendor = large ; small = 原厂家(小压力板,pressure_manager_small.py)
; large = 新厂家(大压力板,pressure_manager.py)
pressure_use_mock = False
取值约定(严格按需求):
small→ 加载pressure_manager_small.py中的PressureManagerlarge→ 加载pressure_manager.py中的PressureManager- 未配置或非法值 → 默认回退为
large(与当前线上部署行为一致,保证向后兼容)
厂商专用参数共存策略:small 与 large 各自的参数(pressure_source、pressure_shared_memory_*、pressure_dll_path 等)全部保留在 [DEVICES] 段中,互不冲突。加载哪个管理器,就由哪个管理器读取自己关心的参数,其余参数被忽略,无需拆分配置段。
3.2 新增设备工厂(适配器):pressure_device_factory.py
新建 backend/devices/pressure_device_factory.py,作为唯一的压力板管理器入口:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""压力板设备工厂
根据配置 [DEVICES] pressure_vendor 动态选择加载对应厂家的压力板管理器。
small = 原厂家(pressure_manager_small.py);large = 新厂家(pressure_manager.py)。
"""
import logging
logger = logging.getLogger(__name__)
# 静态导入两个厂家的管理器(静态导入可保证 PyInstaller 打包时两个模块都被收集)
try:
from .pressure_manager_small import PressureManager as SmallPressureManager
from .pressure_manager import PressureManager as LargePressureManager
except ImportError:
from pressure_manager_small import PressureManager as SmallPressureManager
from pressure_manager import PressureManager as LargePressureManager
# 厂家注册表:配置值 -> 管理器类
VENDOR_MAP = {
'small': SmallPressureManager, # 原厂家(小压力板)
'large': LargePressureManager, # 新厂家(大压力板)
}
DEFAULT_VENDOR = 'large' # 向后兼容:默认新厂家
def resolve_pressure_vendor(config_manager) -> str:
"""读取配置中的厂家值,非法值回退为默认厂家"""
try:
vendor = str(
config_manager.get_config_value('DEVICES', 'pressure_vendor', fallback=DEFAULT_VENDOR)
).strip().lower()
except Exception as e:
logger.warning(f'读取 pressure_vendor 失败,使用默认厂家 {DEFAULT_VENDOR}: {e}')
vendor = DEFAULT_VENDOR
if vendor not in VENDOR_MAP:
logger.warning(f'未知 pressure_vendor: {vendor},回退为 {DEFAULT_VENDOR}')
vendor = DEFAULT_VENDOR
return vendor
def get_pressure_manager_class(config_manager):
"""根据配置返回对应的压力板管理器类"""
vendor = resolve_pressure_vendor(config_manager)
cls = VENDOR_MAP[vendor]
logger.info(f'压力板厂家选择: {vendor} -> {cls.__module__}.{cls.__name__}')
return cls
def create_pressure_manager(socketio, config_manager):
"""创建压力板管理器实例(对外统一入口)"""
cls = get_pressure_manager_class(config_manager)
return cls(socketio, config_manager)
要点:
- 采用静态 import 两个模块而非
importlib动态导入——因为 PyInstaller 的静态分析能识别from X import Y,确保打包时两个厂家模块都被打进 exe,避免打包后切厂家时报"模块不存在"。 - 对外只暴露
create_pressure_manager(socketio, config_manager)一个入口,调用方不关心具体厂家。
3.3 初始化流程改造(动态初始化)
(1) backend/devices/device_coordinator.py
-
顶部 import(L19/L26):不再直接导入
PressureManager,改为导入工厂:try: from .pressure_device_factory import create_pressure_manager except ImportError: from pressure_device_factory import create_pressure_manager -
_init_pressure()(L421-436)改为:def _init_pressure(self) -> bool: try: pressure = create_pressure_manager(self.socketio, self.config_manager) self.devices['pressure'] = pressure if pressure.initialize(): return True return False except Exception as e: self.logger.error(f"初始化压力传感器失败: {e}") return False -
restart_device()的pressure分支(L837-842)同步改为:elif device_name == 'pressure': new_device = create_pressure_manager(self.socketio, self.config_manager)这样在运行中修改
pressure_vendor后调用"设备重启"接口即可热切换厂家,无需重启整个后端。
(2) backend/main.py
- L33
from devices.pressure_manager import PressureManager:该导入在 main.py 中并未被实际调用(压力板实例统一由协调器持有),建议删除该行,避免与工厂逻辑冲突;若确需引用,改为从工厂获取类。
(3) backend/devices/__init__.py
-
L11 / L19:
PressureManager导出改为通过工厂间接暴露,建议调整为:from .pressure_device_factory import create_pressure_manager, get_pressure_manager_class对外不再直接导出某个厂家的
PressureManager类,统一走工厂。
3.4 检测时加载对应设备(需求点 3)
检测流程(main.py 的检测启动、数据采集)均通过 DeviceCoordinator 获取压力板实例(self.device_coordinator.get_device('pressure') / device_managers),而协调器在初始化时已经按 pressure_vendor 创建了正确厂家的实例,因此检测时天然加载的就是配置指定厂家的设备,无需改动检测主流程。
补充两个健壮性措施(建议纳入实现):
- 检测启动前校验:在检测开始接口中读取
pressure_vendor,与当前压力板实例的__class__.__module__对比,若不一致则记录告警日志(提示"配置已切换但设备未重启"),引导用户通过设备重启接口热切换。 - 配置变更热切换:前端"设备设置"保存
pressure_vendor后,后端提示"重启软件系统生效"(沿用现有set_all_device_configs的提示语);同时支持在设备管理页调用现有restart_device('pressure')立即切换。
3.5 配置管理 / API 支持
backend/devices/utils/config_manager.py:
-
_get_pressure_config()(L243-255)新增返回vendor字段:return { 'enable': self.config.getboolean('DEVICES', 'pressure_enable', fallback=False), 'use_mock': self.config.getboolean('DEVICES', 'pressure_use_mock', fallback=False), 'vendor': self.config.get('DEVICES', 'pressure_vendor', fallback='large'), 'port': self.config.get('DEVICES', 'pressure_port', fallback='COM8'), 'baudrate': self.config.getint('DEVICES', 'pressure_baudrate', fallback=115200), } -
_batch_update_device_configs()的pressure分支(L453-475)新增:if 'vendor' in config_data: self.set_config_value('DEVICES', 'pressure_vendor', str(config_data['vendor']))
这样前端设备设置页(如果暴露压力板配置)可读写 pressure_vendor,走既有的 set_all_device_configs 接口即可。
3.6 打包注意事项(PyInstaller)
-
工厂采用静态导入两个厂家模块,PyInstaller 会自动收集
pressure_manager_small.py与pressure_manager.py,一般无需额外配置。 -
稳妥起见,在
backend/build_app.py生成的app.spec的hiddenimports中追加:hiddenimports=['devices.pressure_manager_small', 'devices.pressure_manager', 'devices.pressure_device_factory'], -
若某厂家依赖额外 DLL(
dll/smitsense/Wrapper.dll),需确认打包时该 DLL 已被收集(build_app.py后处理逻辑中已包含 dll 目录拷贝,需核对 smitsense 子目录是否在内)。
4. 配置文件示例
4.1 使用新厂家(大压力板,当前部署)
[DEVICES]
pressure_enable = True
pressure_vendor = large
pressure_use_mock = False
pressure_source = shared_memory
pressure_shared_memory_name = x2_pressure
pressure_shared_memory_header_bytes = 72
pressure_shared_memory_rows = 288
pressure_shared_memory_cols = 64
pressure_shared_memory_dtype = float32
; ... 其余 pressure_shared_memory_* 参数保持不变 ...
4.2 使用原厂家(小压力板)
[DEVICES]
pressure_enable = True
pressure_vendor = small
pressure_use_mock = False
; pressure_source / pressure_shared_memory_* 无需改动,small 管理器会忽略它们
4.3 无硬件联调(模拟数据)
pressure_enable = True
pressure_vendor = small ; 或 large,均可
pressure_use_mock = True
5. 兼容性与迁移
| 场景 | 说明 |
|---|---|
旧配置无 pressure_vendor |
工厂回退为 large,与当前线上行为一致,无感知迁移 |
| 前端无改动 | 两个管理器数据上报协议一致(pressure_data / /devices),前端不用改 |
| 运行中切换厂家 | 通过既有 restart_device('pressure') 接口热切换;或改配置后重启后端 |
| 模拟/真实切换 | 沿用各管理器内部 pressure_use_mock 逻辑,不受厂家切换影响 |
6. 测试计划
- 工厂单元验证(venv 下直接调用):
pressure_vendor=small→get_pressure_manager_class()返回pressure_manager_small.PressureManagerpressure_vendor=large→ 返回pressure_manager.PressureManagerpressure_vendor=非法值/缺省→ 回退large,日志有告警
- 集成验证(
pressure_use_mock=True,不接硬件):large:协调器初始化 → 压力板连接成功 →start_streaming→ 收到pressure_data事件small:同上流程验证
- 热切换验证:
set_all_device_configs修改pressure_vendor→ 调用restart_device('pressure')→ 日志确认切换为另一厂家、数据流恢复 - 检测回归:登录 → 创建检测 → 检测过程压力数据正常采集与保存
- 打包回归:
build_backend.ps1打包后,分别在两种配置下运行 exe 验证初始化与数据流
7. 文件修改清单
| 文件 | 操作 | 说明 |
|---|---|---|
backend/devices/pressure_device_factory.py |
新增 | 厂家注册表 + create_pressure_manager 工厂入口 |
backend/devices/device_coordinator.py |
修改 | 顶部 import、_init_pressure()、restart_device() pressure 分支改用工厂 |
backend/main.py |
修改 | 移除/替换 L33 直接导入 |
backend/devices/__init__.py |
修改 | 导出改为工厂接口 |
backend/devices/utils/config_manager.py |
修改 | _get_pressure_config() 增加 vendor;批量更新支持 vendor |
backend/config.ini |
修改 | [DEVICES] 增加 pressure_vendor = large |
backend/dist/BodyBalanceBackend/config.ini |
修改 | 同步增加 pressure_vendor = large |
backend/build_app.py |
修改 | app.spec 增加 hiddenimports(稳妥项) |
document/多厂家足底压力板适配开发方案.md |
新增 | 本文档 |
8. 风险与注意事项
- DLL 与硬件耦合:
small厂家依赖dll/smitsense/Wrapper.dll,切换回small时需确保该 DLL 存在于打包产物中;large厂家依赖 X2 上位机软件创建的共享内存x2_pressure,需先启动厂家采集程序。 - 打包收集:若日后将工厂改为
importlib动态导入,必须同步在app.spec的hiddenimports中显式声明两个厂家模块,否则打包后切换厂家会 ImportError。 - 两个模块内的
RealPressureDevice重名:small与large模块内部都有同名类,工厂按模块隔离引用,不产生冲突,但禁止在外部直接from xxx import RealPressureDevice混用。 - 检测中切换厂家:检测进行中不建议切换(会导致数据源中断),热切换应在非检测时段执行;检测启动前的"配置校验告警"可辅助提醒。
pressure_source语义:large管理器内部仍以pressure_source(smitsense/shared_memory)选择底层设备实现;本次方案中的pressure_vendor是"选哪个管理器文件"的更上层开关,两者不冲突、各司其职。