1、Abstract Class 和 Interface
C++里面没有 Java 那种严格意义上的interface关键字。
通常我们用:
class MotionInput这种纯虚函数抽象类来实现接口。
例如:
class MotionInput { public: virtual ~MotionInput() = default; virtual bool start() = 0; virtual bool getFrame(MotionFrame&) = 0; virtual void stop() = 0; };它基本就是接口。
而“抽象类”可以比接口更丰富:
class MotionInput { public: virtual bool start() = 0; void printStatus() { // 公共实现 } protected: int timeout_ms; };所以简单记:
Interface:主要规定规范。
Abstract Class:既可以规定规范,也可以提供公共实现。
你现在做工程时,先把这个区别理解到这里就够了。
2、Strategy:策略模式
这是你项目里非常重要的一个。你的问题是:
不同动捕源,都是“获取人体运动数据”,但具体获取方式不同。
这天然适合 Strategy。
例如:
MotionInput │ ┌────────────┼────────────┐ ↓ ↓ ↓ Xsens策略 Axis策略 Noitom策略上层:
MotionInput* input;运行时可以:
配置文件: source_type = xsens就使用:
XsensUdpSource改成:
source_type = axis_bvh就使用:
AxisBvhSource上层 GMR 不需要修改。
Strategy 到底解决什么问题?一句话:
把“可替换的算法/行为”独立出来。
例如你的系统:
人体数据输入 ↓ GMR ↓ 机器人关节输入策略可以替换:
MotionInput │ ┌──────────┼──────────┐ ↓ ↓ ↓ Xsens Axis NoitomGMR 永远不变。
所以:
换数据源 ↓ 换 Strategy ↓ 不用改 GMR这就是 Strategy。
3、Adapter:适配器模式
这个和 Strategy 很容易混。
假设 Noitom SDK 给你的数据是:
NoitomPoseAxis 给你:
AxisPoseXsens:
XsensPose但是你的系统统一需要:
MotionFrame怎么办?
使用 Adapter。
Noitom SDK ↓ NoitomAdapter ↓ MotionFrame例如:
class NoitomAdapter : public MotionInput { public: bool getFrame(MotionFrame& frame) override { NoitomPose pose = sdk.read(); // 转换成统一格式 frame = convert(pose); return true; } };所以:
Adapter解决“接口不兼容”。
Strategy
解决:
“我有多个可替换的实现。”
例如:
Xsens Axis Noitom都是不同的数据输入策略。
Adapter
解决:
“这个东西已经存在,但它的接口和我的系统不一样。”
例如:
Noitom SDK ↓ Adapter ↓ MotionInput可以这样记:
Strategy = 换一种做法 Adapter = 把不一样的东西接进来4、Factory:工厂模式
现在又出现一个问题:
source_type = xsens系统怎么创建:
XsensUdpSource而不是:
AxisBvhSource这时候就可以使用 Factory。
例如:
std::unique_ptr<MotionInput> createMotionInput(const Config& config) { if (config.source_type == "xsens") return std::make_unique<XsensUdpSource>(); if (config.source_type == "axis_bvh") return std::make_unique<AxisBvhSource>(); if (config.source_type == "noitom") return std::make_unique<NoitomSource>(); return nullptr; }于是主程序只需要:
auto input = createMotionInput(config);而不需要自己判断。
5、Observer:观察者模式
这个在你的项目里可以简单理解成:
一个模块产生状态变化,其他模块希望知道。
比如:
MotionInput │ ├── 新帧到达 ├── 数据源断开 ├── 延迟异常 └── 丢帧 │ ↓ Observer / \ ↓ ↓ statistics diagnostics例如:
MotionInput ↓ 统计模块 ↓ P50/P95/P99 latency lost frames queue waterline或者:
MotionInput ↓ Diagnostics ↓ 设备状态 连接状态 错误信息你现在不需要把 Observer 写得很复杂。
先理解:
一个对象发生变化 → 通知关注它的其他对象。
MotionInput (抽象接口) ↑ ┌───────────────┼───────────────┐ │ │ │ Xsens Axis Noitom Strategy Strategy Strategy │ │ │ └───────────────┼───────────────┘ ↓ MotionFrame ↑ Adapter (数据格式转换) ↑ Factory (根据配置创建对象) ↓ GMR ↓ Robot Target ↓ Observer ┌─────────┴─────────┐ ↓ ↓ Statistics Diagnostics| 概念 | 解决的问题 |
|---|---|
| OOP | 把复杂系统拆成对象 |
| Interface | 规定统一能力 |
| Strategy | 多种实现可以替换 |
| Adapter | 不兼容接口转换 |
| Factory | 负责对象创建 |
| Observer | 状态变化通知其他模块 |
| RAII/智能指针 | 管理资源和生命周期 |
测试人员 │ ↓ Web 浏览器 │ HTTP / SocketIO │ ↓ Flask │ ┌────────────┼────────────┐ ↓ ↓ ↓ 测试控制 状态查询 实时通知 │ │ │ └────────────┼────────────┘ ↓ Python测试程序 │ ┌────────┼────────┐ ↓ ↓ ↓ ROS2 CAN EtherCAT │ ↓ 机器人 │ ↓ 测试结果 │ ↓ SQLite │ ↓ 测试报告/PDF所以这里其实是:
Web → 测试程序 → 机器人 → 数据库 → 报告
6、为什么还需要 SocketIO?
这里是一个很重要的区别。
普通 HTTP:
浏览器 ──请求──→ Flask 浏览器 ←─响应─── Flask例如:
GET /status浏览器主动问:
现在怎么样?
但是机器人测试经常需要:
服务器主动告诉浏览器发生了什么。
例如:
机器人测试开始 ↓ 10% ↓ 20% ↓ 30% ↓ 发现异常 ↓ 测试停止如果全部用 HTTP:
浏览器不断 GET /status 浏览器不断 GET /status 浏览器不断 GET /status比较麻烦。
SocketIO 可以建立实时事件通信:
浏览器 ↕ SocketIO ↕ Python服务器可以主动发送:
test_started progress test_error test_finished浏览器 │ SocketIO连接 │ ↓ Flask │ ↓ Python测试框架 │ ┌───────────┼───────────┐ ↓ ↓ ↓ ROS2 CAN EtherCAT │ │ │ └───────────┼───────────┘ ↓ DUT ↓ Result例如测试开始:
浏览器 │ │ start_test ↓ Python │ ↓ 执行测试 │ ├── 读取设备 ├── 发送命令 ├── 检查反馈 ├── 统计数据 └── 判断Pass/Fail │ ↓ SocketIO │ ↓ 浏览器显示测试任务 ↓ 执行机器人测试 ↓ 采集数据 ↓ 判断结果 ↓ 保存 SQLite ↓ 生成日志 ↓ 生成报告这就比单纯:
python test.py完整很多。
7、pytest
1. assert
assert result == expected2. fixture
给测试准备环境:
测试开始 ↓ 准备设备/数据 ↓ 执行测试 ↓ 清理3. parametrize
同一个测试跑多组参数:
速度 0.1 速度 0.5 速度 1.0 速度 2.0不用写四个测试函数。
4. mock
当真实硬件不存在时:
真实 EtherCAT ↓ Mock ↓ 模拟设备这样可以先测试软件逻辑。
现在把这一块全部串起来:
Web浏览器 │ HTTP / SocketIO │ ↓ Flask │ ↓ Python测试框架 │ ┌──────────┼──────────┐ ↓ ↓ ↓ ROS2 CAN EtherCAT │ │ │ └──────────┼──────────┘ ↓ 机器人 │ ↓ 测试数据 │ ┌──────────┴──────────┐ ↓ ↓ SQLite 日志 │ │ └──────────┬──────────┘ ↓ 测试报告而 pytest 负责:
Python代码 ↓ pytest ↓ ┌────────┼────────┐ ↓ ↓ ↓ 单元测试 集成测试 回归测试PyInstaller 则负责:
Python测试工具 ↓ PyInstaller ↓ 可部署程序Flask → 提供 Web/API SocketIO → 实时双向通信 SQLite → 保存测试结果 pytest → 自动化测试 PyInstaller → 打包部署再记一条完整工程链:
Web控制 → Python测试 → 机器人执行 → 数据采集 → SQLite保存 → 日志 → 报告 → PyInstaller部署
8、日志最基本的几个等级
通常会看到:
DEBUG INFO WARN ERROR可以简单理解成:
DEBUG
调试细节:
received frame id=123 queue size=20INFO
正常运行信息:
EtherCAT master started Controller activatedWARN
出现异常趋势,但系统还能继续:
packet delay high queue almost fullERROR
明确出现错误:
device disconnected controller activation failed你不需要死记日志库。
先记:
不同等级帮助你快速筛选问题。
测试需求 ↓ 测试用例 ↓ 自动化测试程序 ↓ ┌───────────┼───────────┐ ↓ ↓ ↓ ROS2 CAN EtherCAT └───────────┼───────────┘ ↓ 机器人 ↓ 数据 ↓ ┌──────┴──────┐ ↓ ↓ 日志 测试结果 ↓ ↓ └──────┬──────┘ ↓ SQLite ↓ 统计分析 ↓ 测试报告 ↓ 回归测试这才是完整的:
机器人测试工程。