工厂模式是 roboto_motors SDK 实现多品牌电机统一接入的核心机制。由于达妙(DM)、EVO、LeadRobot(LRO)等不同品牌电机在通信协议、寄存器定义和物理参数上存在本质差异,上层控制代码若直接依赖具体实现,将不可避免地陷入品牌耦合。本页文档将解析 SDK 如何通过 静态工厂方法(Static Factory Method) 将品牌相关的实例化细节封装在抽象基类内部,使调用者只需通过一条字符串标识即可获取符合统一接口的电机实例。理解这一层抽象,是后续阅读各品牌具体驱动实现与 Python 绑定的必要前提。
Sources: motor_driver.hpp
设计动机:为何需要工厂模式
在机器人系统中,硬件迭代往往快于软件架构。如果控制代码直接 new DmMotorDriver(...) 或 new EvoMotorDriver(...),则每一次更换电机品牌都需要逐行修改业务逻辑,这与面向对象设计的开闭原则相违背。roboto_motors 选择在 MotorDriver 抽象基类 中内聚实例化职责,主要解决以下三个问题:
第一,消除编译期品牌依赖。上层模块只需 #include "motor_driver.hpp" 并链接统一的 motors 静态库,无需感知 dm_motor_driver.hpp 等品牌头文件的存在。第二,统一参数入口。不同品牌电机的构造参数顺序、默认值、模型枚举各不相同,工厂方法通过标准化参数列表(motor_id, interface_type, interface, motor_type, motor_model 等)屏蔽了这些差异。第三,支持运行时动态选择。基于字符串 motor_type 的派发机制允许从配置文件或命令行参数中动态决定实例化哪种电机,这对于多关节异构机器人尤为重要。
Sources: motor_driver.hpp, motor_driver.cpp
工厂方法架构与类关系
整个工厂模式由两层抽象构成:外层是 电机品牌工厂(MotorDriver::create_motor),负责根据品牌字符串创建对应的电机驱动对象;内层是 通信后端工厂(MotorsCAN::get / MotorsCANFD::get),负责根据接口名获取 SocketCAN 或 EtherCAT 桥接的单例实例。两者的解耦使得“选择什么品牌的电机”与“通过什么总线通信”这两个决策完全独立。
下面的类交互图展示了工厂方法在 SDK 中的定位。注意所有具体驱动类(DmMotorDriver、EvoMotorDriver、LroMotorDriver)都继承自 MotorDriver,而工厂方法返回的是抽象基类的智能指针 std::shared_ptr<MotorDriver>,这确保了多态调用的安全性。
classDiagram
class MotorDriver {
+create_motor(motor_id, interface_type, interface, motor_type, motor_model, master_id_offset, motor_zero_offset)$ shared_ptr~MotorDriver~
+lock_motor()$ void
+unlock_motor()$ void
+init_motor()$ uint8_t
+motor_mit_cmd(f_p, f_v, f_kp, f_kd, f_t)$ void
#motor_id_ uint16_t
#motor_pos_ atomic~float~
#comm_type_ CommType
#can_interface_ string
}
class DmMotorDriver {
+DmMotorDriver(...)
+lock_motor() override
+init_motor() override
-can_rx_cbk(frame) void
-canfd_rx_cbk(frame) void
-can_ shared_ptr~MotorsCAN~
-canfd_ shared_ptr~MotorsCANFD~
}
class EvoMotorDriver {
+EvoMotorDriver(...)
+lock_motor() override
+init_motor() override
-can_rx_cbk(frame) void
-canfd_rx_cbk(frame) void
-bus_registry_ static unordered_map
}
class LroMotorDriver {
+LroMotorDriver(...)
+lock_motor() override
+init_motor() override
-canfd_rx_cbk(frame) void
-canfd_ shared_ptr~MotorsCANFD~
}
class MotorsCAN {
+get(interface, backend)$ shared_ptr~MotorsCAN~
+transmit(frame) void
+add_can_callback(cb, id) void
}
class MotorsCANFD {
+get(interface, backend)$ shared_ptr~MotorsCANFD~
+transmit(frame) void
+add_canfd_callback(cb, id) void
}
MotorDriver <|-- DmMotorDriver
MotorDriver <|-- EvoMotorDriver
MotorDriver <|-- LroMotorDriver
DmMotorDriver --> MotorsCAN : uses singleton
DmMotorDriver --> MotorsCANFD : uses singleton
EvoMotorDriver --> MotorsCAN : uses singleton
EvoMotorDriver --> MotorsCANFD : uses singleton
LroMotorDriver --> MotorsCANFD : uses singleton
Sources: motor_driver.hpp, motor_driver.cpp, can_iso.hpp, canfd_iso.hpp
create_motor 接口详解
工厂方法 create_motor 被声明为 MotorDriver 的静态成员函数,其完整签名如下。所有参数均使用值语义或常量引用,避免了生命周期管理的复杂性;返回值采用 std::shared_ptr<MotorDriver>,既支持 C++ 侧的资源共享,也便于与 pybind11 的 std::shared_ptr holder type 自然对接。
| 参数 | 类型 | 说明 |
|---|---|---|
motor_id |
uint16_t |
电机在总线上的通信标识(CAN ID) |
interface_type |
const std::string& |
通信类型,取值为 "can"、"canfd" 或 "ethercanfd" |
interface |
const std::string& |
物理接口名,例如 "can0"、"can1" |
motor_type |
const std::string& |
品牌标识,当前支持 "DM"、"EVO"、"LRO" |
motor_model |
int |
具体型号枚举的整数值,由工厂内部 static_cast 为品牌专属枚举 |
master_id_offset |
uint16_t |
主节点 ID 偏移量,默认 0,主要用于 DM 系列 |
motor_zero_offset |
double |
机械零点的角度偏移(弧度),默认 0.0 |
需要特别关注的是 motor_model 参数。由于 C++ 枚举在编译期相互独立,工厂方法统一接收 int 类型,在函数体内根据 motor_type 将其分别转型为 DM_Motor_Model、EVO_Motor_Model 或 LRO_Motor_Model。这种设计避免了在公共头文件中引入各品牌枚举定义,保持了抽象层的纯净性。
Sources: motor_driver.hpp, motor_driver.cpp
多品牌实例化的派发逻辑
create_motor 的实现采用简单的字符串条件分支完成派发。虽然看起来直接,但在当前仅支持三个品牌、且品牌名稳定的场景下,这种显式分支比注册表反射机制更易读、调试更直观,同时零运行时开销。当 motor_type 无法匹配任何已知品牌时,工厂会立即抛出 std::runtime_error("Motor type not supported"),确保失败快速(fail-fast)。
std::shared_ptr<MotorDriver> MotorDriver::create_motor(...) {
if (motor_type == "DM") {
return std::make_shared<DmMotorDriver>(
motor_id, interface_type, interface, master_id_offset,
static_cast<DM_Motor_Model>(motor_model), motor_zero_offset);
} else if (motor_type == "EVO") {
return std::make_shared<EvoMotorDriver>(
motor_id, interface_type, interface,
static_cast<EVO_Motor_Model>(motor_model), motor_zero_offset);
} else if (motor_type == "LRO") {
return std::make_shared<LroMotorDriver>(
motor_id, interface_type, interface,
static_cast<LRO_Motor_Model>(motor_model), motor_zero_offset);
} else {
throw std::runtime_error("Motor type not supported");
}
}
下表列出了当前 SDK 支持的品牌、型号枚举及对应的总线能力。这些枚举定义分散在各品牌驱动头文件中,工厂方法通过 static_cast 桥接了整型参数与类型安全枚举之间的鸿沟。
| 品牌 | 枚举类型 | 已定义型号 | 支持的总线类型 |
|---|---|---|---|
| DM | DM_Motor_Model |
DM4340P_48V, DM10010L_48V |
CAN, CAN-FD |
| EVO | EVO_Motor_Model |
EVO431040, EVO811825, EVO811832 |
CAN, CAN-FD |
| LRO | LRO_Motor_Model |
LRO_5550, LRO_6562, LRO_8462, LRO_10062 |
CAN-FD |
Sources: motor_driver.cpp, dm_motor_driver.hpp, evo_motor_driver.hpp, lro_motor_driver.hpp
协议层单例与构造时绑定
工厂方法返回的并非一个“裸”的电机对象,而是一个已经完成了通信后端绑定的完整驱动实例。每个具体驱动类的构造函数内部会根据 interface_type 调用 MotorsCAN::get(interface) 或 MotorsCANFD::get(interface) 获取协议层单例,随后将自身的接收回调函数注册到该单例中。这意味着调用者从工厂拿到指针后,无需再手动配置 SocketCAN 或管理回调生命周期。
以 DmMotorDriver 为例,其构造函数内部分支如下:当 interface_type 为 "can" 时,使用经典 CAN 协议并注册 can_rx_cbk;当为 "canfd" 或 "ethercanfd" 时,则使用 CAN-FD 协议并注册 canfd_rx_cbk。析构函数中会自动执行对应的 remove_can_callback 或 remove_canfd_callback,防止悬空回调。这种在构造期完成的自组装(self-assembly)模式,使得工厂的使用者完全不需要了解底层协议对象的获取细节。
Sources: dm_motor_driver.cpp, can_iso.hpp, canfd_iso.hpp
Python 绑定中的工厂暴露
通过 pybind11,MotorDriver::create_motor 被直接导出为 Python 侧的静态方法,Python 用户因此能够以与 C++ 几乎一致的参数语义创建电机对象。pybind11 的 std::shared_ptr<MotorDriver> holder type 确保了 Python 垃圾回收与 C++ 引用计数之间的自动同步,当 Python 中最后一个引用被释放时,驱动对象及其析构逻辑(包括回调注销)会自动触发。
py::class_<MotorDriver, std::shared_ptr<MotorDriver>>(m, "MotorDriver")
.def_static("create_motor", &MotorDriver::create_motor,
py::arg("motor_id"),
py::arg("interface_type"),
py::arg("interface"),
py::arg("motor_type"),
py::arg("motor_model"),
py::arg("master_id_offset") = 0,
py::arg("motor_zero_offset") = 0.0)
由于工厂返回的是抽象基类指针,Python 侧看到的 MotorDriver 对象拥有完整的统一接口:上锁/解锁、MIT 控制、位置速度指令、状态刷新等。Python 开发者无需也无法直接实例化 DmMotorDriver 或 EvoMotorDriver,这从语言绑定层面再次强化了品牌无关的编程模型。
Sources: pybind_module.cpp
扩展新品牌电机的标准步骤
工厂模式的最大优势在于可扩展性。当需要引入新的电机品牌(例如注释中预留的 XYN)时,只需遵循以下步骤即可完成无痛扩展,而无需修改任何上层业务代码:
- 创建品牌子目录与实现:在
src/drivers/下新建品牌目录(如xyn/),提供xyn_motor_driver.hpp和xyn_motor_driver.cpp,并实现MotorDriver的所有纯虚接口。 - 定义型号枚举:在品牌头文件中定义
XYN_Motor_Model枚举,枚举末尾以XYN_Num_Of_Motor作为哨兵值,便于数组索引。 - 注册 CMake 构建目标:在
src/drivers/CMakeLists.txt中add_subdirectory(xyn),并在顶层CMakeLists.txt的target_link_libraries中加入xyn_motors。 - 纳入工厂派发:在
src/motor_driver.cpp中#include新品牌头文件,并在create_motor的if-else链中新增motor_type == "XYN"分支,完成std::make_shared<XynMotorDriver>(...)的调用。 - 暴露 Python 绑定(可选):在
src/pybind_module.cpp中#include新品牌头文件,若需要访问品牌专属 API,可进一步绑定具体子类;若仅使用统一接口,则无需额外操作,因为工厂返回的基类指针已具备全部公共方法。
上述流程体现了工厂模式在物理硬件 SDK 中的典型落地方式:通过编译期的模块化和运行时的字符串派发,兼顾了类型安全、代码隔离与部署灵活性。
Sources: motor_driver.cpp, CMakeLists.txt, drivers/CMakeLists.txt, pybind_module.cpp
设计权衡与最佳实践
roboto_motors 的工厂实现并没有采用更复杂的抽象工厂(Abstract Factory)或原型模式(Prototype),而是选择了最简单的静态工厂方法。这一决策背后有明确的工程考量:
| 方案 | 优点 | 缺点 | SDK 的取舍 |
|---|---|---|---|
| 静态工厂方法(当前) | 实现简单、零额外内存开销、编译期链接清晰 | 新增品牌需修改 create_motor 源码 |
品牌数量少且稳定,显式分支更可靠 |
| 注册表/反射工厂 | 新增品牌无需改动工厂代码,插件化 | 需要运行时注册机制,增加复杂度和调试难度 | 当前不需要动态加载 .so 插件 |
| 抽象工厂族 | 可以同时创建电机 + 配套编码器等系列对象 | 过度设计,当前每个电机驱动自包含所有功能 | 不满足 YAGNI 原则 |
在实际使用中,建议调用者将 motor_type、motor_model、interface_type 等参数抽取到 JSON/YAML 配置文件中,并在程序启动时通过工厂统一批量实例化。这样,当硬件 BOM(物料清单)发生变更时,只需修改配置文件中的字符串和整型值,而无需重新编译控制逻辑。此外,由于 create_motor 在失败时抛出异常,调用侧应当做好 try-catch 包裹,尤其是在从外部配置读取参数的边界场景中。
Sources: motor_driver.cpp
下一步阅读建议
如果您希望从使用者角度快速上手,可以阅读 C++ SDK快速上手 或 Python SDK快速上手 中的工厂调用示例。如果您想深入理解某个具体品牌电机的协议实现与寄存器映射,推荐继续阅读 达妙DM电机驱动详解、EVO电机驱动详解 或 LeadRobot电机驱动详解。对通信协议层感兴趣的用户,则可以转向 CAN/CANFD协议抽象接口 了解 MotorsCAN::get 这一内层工厂的实现细节。