BodyBalanceEvaluation/document/多厂家足底压力板适配开发方案.md
zhengsl d91af11b7d 修复录屏0字节问题,支持压力板多厂家切换
- 录屏: 高DPI缩放导致奇数尺寸触发libx264编码失败,宽高取偶; ffmpeg stderr写入日志

- 压力板: 新增 pressure_manager_small.py 原厂家小压力板,与现有大压力板并存

- 打包: 新增 build_backend.ps1,更新 requirements_build.txt / config.ini

- 前端: Electron 本地化配置
2026-08-11 18:17:13 +08:00

351 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 多厂家足底压力板适配开发方案
## 1. 需求背景与目标
当前系统使用足底压力板采集足底压力数据。项目经历了一次设备更换:
| 设备 | 程序文件 | 状态 |
| --- | --- | --- |
| 原厂家小一号压力板SMiTSense USB | `backend/devices/pressure_manager_small.py` | 已停用 |
| 新厂家大一号压力板X2 共享内存) | `backend/devices/pressure_manager.py` | 当前使用 |
**需求目标**:程序同时支持两个厂家的设备,通过**配置文件参数**切换使用哪个厂家,无需修改代码、重新打包即可在两种设备间切换。
具体拆解为三点:
1. 新增配置文件参数,用 `small` 代表原厂家设备(对应 `pressure_manager_small.py`),用 `large` 代表新厂家设备(对应 `pressure_manager.py`
2. 修改配置与初始化程序,根据配置文件**动态初始化**对应厂家的设备管理器;
3. 检测时,根据配置文件**加载对应厂家**的设备进行数据采集。
---
## 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` 内容一致):
```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]` 段新增参数,作为**厂家选择的总开关**
```ini
[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` 中的 `PressureManager`
- `large` → 加载 `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`,作为**唯一的压力板管理器入口**
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""压力板设备工厂
根据配置 [DEVICES] pressure_vendor 动态选择加载对应厂家的压力板管理器。
small = 原厂家pressure_manager_small.pylarge = 新厂家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`
- 顶部 importL19/L26不再直接导入 `PressureManager`,改为导入工厂:
```python
try:
from .pressure_device_factory import create_pressure_manager
except ImportError:
from pressure_device_factory import create_pressure_manager
```
- `_init_pressure()`L421-436改为
```python
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同步改为
```python
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` 导出改为通过工厂间接暴露,建议调整为:
```python
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` 创建了正确厂家的实例,因此检测时天然加载的就是配置指定厂家的设备,**无需改动检测主流程**。
补充两个健壮性措施(建议纳入实现):
1. **检测启动前校验**:在检测开始接口中读取 `pressure_vendor`,与当前压力板实例的 `__class__.__module__` 对比,若不一致则记录告警日志(提示"配置已切换但设备未重启"),引导用户通过设备重启接口热切换。
2. **配置变更热切换**:前端"设备设置"保存 `pressure_vendor` 后,后端提示"重启软件系统生效"(沿用现有 `set_all_device_configs` 的提示语);同时支持在设备管理页调用现有 `restart_device('pressure')` 立即切换。
### 3.5 配置管理 / API 支持
`backend/devices/utils/config_manager.py`
- `_get_pressure_config()`L243-255新增返回 `vendor` 字段:
```python
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新增
```python
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` 中追加:
```python
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 使用新厂家(大压力板,当前部署)
```ini
[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 使用原厂家(小压力板)
```ini
[DEVICES]
pressure_enable = True
pressure_vendor = small
pressure_use_mock = False
; pressure_source / pressure_shared_memory_* 无需改动small 管理器会忽略它们
```
### 4.3 无硬件联调(模拟数据)
```ini
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. 测试计划
1. **工厂单元验证**venv 下直接调用):
- `pressure_vendor=small``get_pressure_manager_class()` 返回 `pressure_manager_small.PressureManager`
- `pressure_vendor=large` → 返回 `pressure_manager.PressureManager`
- `pressure_vendor=非法值/缺省` → 回退 `large`,日志有告警
2. **集成验证**`pressure_use_mock=True`,不接硬件):
- `large`:协调器初始化 → 压力板连接成功 → `start_streaming` → 收到 `pressure_data` 事件
- `small`:同上流程验证
3. **热切换验证**`set_all_device_configs` 修改 `pressure_vendor` → 调用 `restart_device('pressure')` → 日志确认切换为另一厂家、数据流恢复
4. **检测回归**:登录 → 创建检测 → 检测过程压力数据正常采集与保存
5. **打包回归**`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. 风险与注意事项
1. **DLL 与硬件耦合**`small` 厂家依赖 `dll/smitsense/Wrapper.dll`,切换回 `small` 时需确保该 DLL 存在于打包产物中;`large` 厂家依赖 X2 上位机软件创建的共享内存 `x2_pressure`,需先启动厂家采集程序。
2. **打包收集**:若日后将工厂改为 `importlib` 动态导入,必须同步在 `app.spec``hiddenimports` 中显式声明两个厂家模块,否则打包后切换厂家会 ImportError。
3. **两个模块内的 `RealPressureDevice` 重名**`small` 与 `large` 模块内部都有同名类,工厂按模块隔离引用,不产生冲突,但**禁止**在外部直接 `from xxx import RealPressureDevice` 混用。
4. **检测中切换厂家**:检测进行中不建议切换(会导致数据源中断),热切换应在非检测时段执行;检测启动前的"配置校验告警"可辅助提醒。
5. **`pressure_source` 语义**`large` 管理器内部仍以 `pressure_source``smitsense`/`shared_memory`)选择底层设备实现;本次方案中的 `pressure_vendor` 是"选哪个管理器文件"的更上层开关,两者不冲突、各司其职。