cmvr-es/cmvr-es/devices/README.md
xtkuang edb01463ff docs: move hardware guides into component directories
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.
2026-07-31 09:39:44 +08:00

14 KiB
Raw Blame History

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 和设备大类抽象接口

关键文件:

协议层和业务任务不应直接依赖厂商 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.hbackend_case() 增加创建分支。

AGV、DexHand、Microphone、Speaker 等遵循相同模式。新增同类后端通常不需要修改全局 DeviceFactory。

5. CMake 聚合

  1. camera/CMakeLists.txt 增加 add_subdirectory(vendor_camera)
  2. 让类别 target 链接新后端 target
  3. 安装需要随应用分发的共享库;
  4. 厂商 SDK 路径使用 dependency/${ARCH}/third_party/...,不能硬编码 x86
  5. 需要特殊 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,确保无设备开发主机仍可启动。

新增全新设备大类

当现有抽象无法表达新设备能力时,还需要:

  1. device_types.h 增加 DeviceKindtoString()
  2. 新增类别抽象接口;
  3. 将稳定公共数据放入 ../common/types/<category>/
  4. 新增配置 Proto
  5. DeviceConfigEntry::DeviceType 使用新的 enum 数值;
  6. DeviceFactory::DeviceFactory() 注册 creator
  7. 更新 DeviceManager 的 type-to-string 日志映射;
  8. getDevice<AbstractNewDevice>() 增加显式模板实例化;
  9. 更新 devices/CMakeLists.txt、类别 CMake 和 device_manager target 链接;
  10. 若平台需要访问,独立增加 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 应正确填写:

  • rgbFrame
  • codecH.264 或 H.265
  • widthheightfps
  • bKey
  • stream_epoch
  • sequence
  • capture_monotonic_ns
  • capture_utc_ns
  • source_timestamp:设备 SDK 提供的原始时间戳;单位和时钟域由设备定义,未知 时保持 0不能直接当成 Unix 时间;
  • source_frame_number:设备 SDK 提供的原始帧号,未知时保持 0
  • ptsdts
  • time_base_numtime_base_den
  • duration
  • discontinuity
  • codec_config_generation
  • codec_config

H.264/H.265 后端必须识别关键帧,并尽量实现 requestKeyFrame()

Hikvision 后端优先采用 SDK 回调中的有效帧率,并保留 SDK 的 64 位原始时间戳和帧号; SDK 帧率无效时才回退到配置的 fps。这些字段用于跨层诊断,设备层不应在不了解 SDK 时钟语义时擅自换算。

Microphone 完整音频包

应正确填写:

  • data
  • sample_rate
  • channels
  • formatcodec
  • nb_samples
  • sequence、时间戳、time base、duration
  • stream epoch、discontinuity 和 codec generation

生产者重启

采集/编码生产者真正停止并重新启动时:

  1. 增加 stream_epoch
  2. 将 source sequence 重置为 0
  3. 增加 codec_config_generation
  4. 确保后续帧携带完整的新编码元数据;
  5. 将第一帧标记为 discontinuity
  6. 视频从关键帧恢复输出。

设备后端不直接发布 TrackDescriptordevice_media_source_adapter.cpp 会根据帧元数据生成或更新 descriptor。

运行期编码配置变化

编码器没有重启、只在同一 stream epoch 内改变分辨率、codec config 等参数时:

  1. 保持 stream_epoch 不变;
  2. 保持 sequence 连续递增;
  3. 增加 codec_config_generation,或让其他描述字段反映变化;
  4. 在后续帧中携带新元数据;
  5. 标记 discontinuity
  6. H.264/H.265 从新的关键帧恢复。

adapter 检测到描述变化后创建新 descriptor设备后端不要自行维护协议侧 descriptor 状态。

设备侧环形队列安全

现有设备后端多使用 SPMCRingBuffer<T>

  • 只允许一个逻辑生产者;
  • 每个消费者独立持有读游标;
  • 同一读游标不能跨线程并发访问;
  • 不对同一路读取混用无参 pop() 和带游标 pop(index)
  • 需要最新帧时使用 getLatest(index)
  • 不要先取 head 再分两步读取,避免检查/读取竞态;
  • 满队列覆盖旧数据是实时媒体的预期行为;
  • waitEncodedFrame() 必须有有限 timeout不能永久阻塞。

MediaSourceHub Subscription 同样是单消费者对象,不同协议或客户端必须各自订阅。

发布后的 MediaFrameTrackDescriptor 和 payload 不可再修改。

测试要求

至少覆盖:

  • 配置缺失、非法参数和 ID 不一致;
  • Factory 选择正确后端;
  • init/start/stop 重复执行;
  • init 失败后无残留线程和句柄;
  • stop 与 SDK callback 并发;
  • 最后一个流租约释放后不再发布;
  • 多消费者使用独立游标;
  • 环形队列覆盖和丢帧;
  • epoch、sequence、关键帧和 codec generation
  • 设备断开、超时和重连。

无硬件参考测试:

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