Keep the root README concise while documenting MotorService, the Modbus TCP PLC runtime, the motor stack, and AUBO cabinet IO next to their owning code.
14 KiB
Devices 模块开发指南
devices/ 屏蔽厂商 SDK、通信总线和硬件型号差异,对 manager、service、task 和算法层提供稳定的设备能力接口。
仓库真实目录名是 cmvr-es/devices/。设备层使用复数 devices,不是 cmvr_es/device。
返回项目总览。
创建链路
config/cmvr_es.pb.txt
-> config/manager/device_manager.pb.txt
-> DeviceManager
-> DeviceFactory
按 DeviceConfigEntry::DeviceType 选择设备大类
-> CameraFactory / AGVFactory / RobotArmFactory / ...
按设备配置 oneof backend 选择厂商实现
-> 具体设备类
-> AbstractDevice 和设备大类抽象接口
关键文件:
abstract_device.hdevice_types.h../manager/device_manager/include/device_manager.h../manager/device_manager/src/device_factory.cpp../../protos/cmvr/config/device_manager_config/device_manager_config.proto../config/manager/device_manager.pb.txt
协议层和业务任务不应直接依赖厂商 SDK 类型。厂商错误码、报文和连接细节由具体后端转换为抽象接口的通用语义。
当前可由配置创建的设备
| 大类 | 抽象接口 | 类别工厂 | 当前可选后端 |
|---|---|---|---|
| Camera | camera/abstract_camera.h |
camera/camera_factory.h |
UVC、RealSense、Hikvision |
| AGV | agv/abstract_agv.h |
agv/agv_factory.h |
MyAgv、SRC1100 |
| RobotArm | arm/robot_arm.h |
arm/robot_arm_factory.h |
MotorRobotArm、AUBO、Huayan、UME |
| DexHand | dexhand/abstract_dexhand.h |
dexhand/dexhand_factory.h |
RH56DFTP、PX6AXGen3 |
| Microphone | microphone/abstract_microphone.h |
microphone/microphone_factory.h |
FFmpeg |
| Speaker | speaker/abstract_speaker.h |
speaker/speaker_factory.h |
FFmpeg |
| BioHead | biohead/abstract_biohead.h |
DeviceFactory 直接创建 | BioHeadRobot |
| MotorSystem | motor/ |
DeviceFactory 直接创建 | CAN、MuJoCo、EtherCAT、Modbus TCP PLC |
代码目录存在不等于已经接入配置创建链:
- MechMind Proto 和实现仍存在,但当前 CameraFactory 明确拒绝创建;
- MujocoCamera 有实现并参与部分构建,但当前没有 CameraFactory 分支;
- Battery、Gripper、Robot、CanBus 等抽象或实现不一定已注册到 DeviceFactory;
- 所有已注册设备共用一个全局 ID 命名空间。
新增能力前先确认“已有代码”“可被 CMake 构建”“可被 Factory 创建”“可被 DeviceManager 配置启用”四个状态,不要混为一谈。
新增同类厂商后端
以新增 Camera 后端为例。
1. 扩展配置 Proto
修改:
protos/cmvr/config/camera_config/camera_config.proto
新增厂商 config,并加入 CameraDeviceConfig.oneof backend。只使用新的字段 tag,不复用 reserved 或已发布 tag。
2. 新建后端目录
camera/vendor_camera/
├── CMakeLists.txt
├── include/
│ └── vendor_camera.h
├── src/
│ └── vendor_camera.cpp
└── tests/
└── vendor_camera_test.cpp
3. 实现抽象接口
至少实现:
typeName();init();- 实际支持的
start()/stop(); - 状态查询和该类别核心能力;
- 若能提供运行时健康信息,实现无阻塞的
healthSnapshot(); - 若支持实时媒体,完整实现流接口和并发停止。
不要为了厂商特例向抽象类加入 SDK handle、私有报文或厂商专有结构。只有多个后端都需要的稳定语义才进入抽象接口或 common/types/。
4. 类别工厂注册
在 camera/camera_factory.h 的 backend_case() 增加创建分支。
AGV、DexHand、Microphone、Speaker 等遵循相同模式。新增同类后端通常不需要修改全局 DeviceFactory。
5. CMake 聚合
- 在
camera/CMakeLists.txt增加add_subdirectory(vendor_camera); - 让类别 target 链接新后端 target;
- 安装需要随应用分发的共享库;
- 厂商 SDK 路径使用
dependency/${ARCH}/third_party/...,不能硬编码 x86; - 需要特殊 RPATH 时参考 Hikvision、Huayan 等现有实现。
推荐 target 形式:
add_library(vendor_camera SHARED
src/vendor_camera.cpp
)
target_include_directories(vendor_camera
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(vendor_camera
PUBLIC cmvr_es::proto
PRIVATE vendor_sdk
)
add_library(cmvr_es::device::vendor_camera ALIAS vendor_camera)
install(TARGETS vendor_camera LIBRARY DESTINATION lib)
6. 默认配置
在类别集合配置增加实例,并在 DeviceManager 配置增加同 ID 条目:
devices {
id: "camera_front"
type: DEVICE_TYPE_CAMERA
config_file: "devices/camera/camera.pb.txt"
enable: false
}
新硬件默认 enable: false,确保无设备开发主机仍可启动。
新增全新设备大类
当现有抽象无法表达新设备能力时,还需要:
- 在
device_types.h增加DeviceKind和toString(); - 新增类别抽象接口;
- 将稳定公共数据放入
../common/types/<category>/; - 新增配置 Proto;
- 在
DeviceConfigEntry::DeviceType使用新的 enum 数值; - 在
DeviceFactory::DeviceFactory()注册 creator; - 更新 DeviceManager 的 type-to-string 日志映射;
- 为
getDevice<AbstractNewDevice>()增加显式模板实例化; - 更新
devices/CMakeLists.txt、类别 CMake 和 device_manager target 链接; - 若平台需要访问,独立增加 API Proto 和 gRPC service。
不得复用 device_manager_config.proto 中已 reserved 的 enum 数值或名称。新增设备类别不会自动生成平台 RPC。
配置和 ID
集合类型设备必须满足:
DeviceConfigEntry.id
==
CameraDeviceConfig / AGVDeviceConfig / ... 的外层 id
厂商 backend 内部 ID 可以为空,由类别工厂补齐;若填写,也必须与外层 ID 相同。
其他约束:
- 相对配置路径以根
cmvr_es.pb.txt所在目录解析; - 生产密码、token 和证书不得提交到样例配置;
- 单个设备 init 失败时不会进入可用对象表,进程仍可能继续启动;失败条目会保留 在 DeviceManager 状态快照中,其中已启用的失败设备会通过 QUIC heartbeat 上报,禁用设备不会上报;
- 有初始化依赖的设备按配置顺序排列,例如 MotorSystem 在依赖它的 RobotArm 前;
- DeviceManager stop 遍历 unordered_map,不能依赖跨设备停止顺序;
- DeviceManager 支持并发查询、状态快照和动态注册,但动态设备不会自动补执行
已经发生的
start(),当前也没有设备移除或完整热插拔生命周期。
配置细节见 ../config/README.md。
生命周期约定
| 接口 | 当前语义 |
|---|---|
| 构造函数 | 保存配置和轻量校验,不启动长期工作线程 |
init() |
DeviceManager 构造期间对启用设备调用 |
start() |
当前 main 不统一调用,由 RPC、媒体或其他 owner 显式触发 |
startStreaming() |
获取一个实时流生产租约 |
stopStreaming() |
释放租约,最后一份租约停止生产者 |
stop() |
完整停止设备,必须幂等 |
update() |
当前没有统一 scheduler 自动调用 |
| 析构函数 | 回收线程、SDK callback、socket、fd 和 handle |
实现要求:
- init/start/stop 的重复调用有明确结果;
- stop 和最后一次 stopStreaming 返回前停止所有发布;
- 不持有 worker 退出所需的锁执行 join;
- SDK callback 不获取停止路径长期持有的控制锁;
- getState 使用与状态写入相同的锁;
- 含
std::string、vector 等状态不能无锁复制; healthSnapshot()只能读取已经缓存的内存状态,必须线程安全,不能同步访问 SDK、网络、串口或设备总线;- 无法提供可信健康状态时返回
UNKNOWN,不能用“没有观察到错误”冒充健康; - 析构函数调用安全停止路径;
- callback 捕获对象前保证 owner 生命周期。
并发停止可参考:
当前 main 收到退出信号只停止 TaskManager,没有显式调用 DeviceManager stop,设备析构仍必须可靠。
摄像头与麦克风实时流
设备实现抽象流接口后,由 ../manager/media_source_hub/ 适配给 gRPC 和 QUIC,不应在设备后端实现两套协议代码。
当前 Hub 轨道:
<camera-id>/video/color
<microphone-id>/audio/main
当前 adapter 只把 StreamFrameData.rgbFrame 注册为彩色视频轨道;depthFrame 尚未注册为 Hub 深度轨道。
Camera 完整编码帧
每个 access unit 应正确填写:
rgbFramecodec:H.264 或 H.265width、height、fpsbKeystream_epochsequencecapture_monotonic_nscapture_utc_nssource_timestamp:设备 SDK 提供的原始时间戳;单位和时钟域由设备定义,未知 时保持 0,不能直接当成 Unix 时间;source_frame_number:设备 SDK 提供的原始帧号,未知时保持 0;pts、dtstime_base_num、time_base_dendurationdiscontinuitycodec_config_generationcodec_config
H.264/H.265 后端必须识别关键帧,并尽量实现 requestKeyFrame()。
Hikvision 后端优先采用 SDK 回调中的有效帧率,并保留 SDK 的 64 位原始时间戳和帧号;
SDK 帧率无效时才回退到配置的 fps。这些字段用于跨层诊断,设备层不应在不了解
SDK 时钟语义时擅自换算。
Microphone 完整音频包
应正确填写:
datasample_ratechannelsformat和codecnb_samples- sequence、时间戳、time base、duration
- stream epoch、discontinuity 和 codec generation
生产者重启
采集/编码生产者真正停止并重新启动时:
- 增加
stream_epoch; - 将 source sequence 重置为 0;
- 增加
codec_config_generation; - 确保后续帧携带完整的新编码元数据;
- 将第一帧标记为 discontinuity;
- 视频从关键帧恢复输出。
设备后端不直接发布 TrackDescriptor。device_media_source_adapter.cpp 会根据帧元数据生成或更新 descriptor。
运行期编码配置变化
编码器没有重启、只在同一 stream epoch 内改变分辨率、codec config 等参数时:
- 保持
stream_epoch不变; - 保持 sequence 连续递增;
- 增加
codec_config_generation,或让其他描述字段反映变化; - 在后续帧中携带新元数据;
- 标记 discontinuity;
- H.264/H.265 从新的关键帧恢复。
adapter 检测到描述变化后创建新 descriptor,设备后端不要自行维护协议侧 descriptor 状态。
设备侧环形队列安全
现有设备后端多使用 SPMCRingBuffer<T>:
- 只允许一个逻辑生产者;
- 每个消费者独立持有读游标;
- 同一读游标不能跨线程并发访问;
- 不对同一路读取混用无参
pop()和带游标pop(index); - 需要最新帧时使用
getLatest(index); - 不要先取 head 再分两步读取,避免检查/读取竞态;
- 满队列覆盖旧数据是实时媒体的预期行为;
waitEncodedFrame()必须有有限 timeout,不能永久阻塞。
MediaSourceHub Subscription 同样是单消费者对象,不同协议或客户端必须各自订阅。
发布后的 MediaFrame、TrackDescriptor 和 payload 不可再修改。
测试要求
至少覆盖:
- 配置缺失、非法参数和 ID 不一致;
- Factory 选择正确后端;
- init/start/stop 重复执行;
- init 失败后无残留线程和句柄;
- stop 与 SDK callback 并发;
- 最后一个流租约释放后不再发布;
- 多消费者使用独立游标;
- 环形队列覆盖和丢帧;
- epoch、sequence、关键帧和 codec generation;
- 设备断开、超时和重连。
无硬件参考测试:
camera/hikvision_camera/tests/hikvision_camera_callback_test.cpp../manager/media_source_hub/tests/media_source_hub_test.cpp
cmake -S . -B build \
-DCMVR_ARCH=x86 \
-DBUILD_TESTING=ON \
-DCMVR_MEDIA_SOURCE_HUB_BUILD_TESTS=ON
cmake --build build -j"$(nproc)"
ctest \
--test-dir build \
-R 'hikvision_camera_callback_test|media_source_hub_test' \
--output-on-failure
新测试必须在 BUILD_TESTING 下使用 add_test() 登记。只创建 executable 不会自动被 CTest 执行。
提交检查
- 抽象接口没有厂商 SDK 类型
- manager、外层配置和 backend ID 一致
- 新硬件配置默认关闭
- 生命周期支持重复调用
- stop 是同步发布屏障
- 状态读写使用同一把锁
- 每个实时流只有一个生产者
- 每个消费者使用独立游标
- 时间戳、sequence 和编码元数据完整
- Factory 和类别 CMake 均已接入
- 厂商运行库有安装规则
- 有无真实硬件的自动测试
- 平台能力变化已评估 Proto 和 service