cmvr-es/cmvr-es/devices/README.md

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、SEER Robokit
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

代码目录存在不等于已经接入配置创建链:

  • 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