356 lines
13 KiB
Markdown
356 lines
13 KiB
Markdown
# Devices 模块开发指南
|
||
|
||
`devices/` 屏蔽厂商 SDK、通信总线和硬件型号差异,对 manager、service、task 和算法层提供稳定的设备能力接口。
|
||
|
||
仓库真实目录名是 `cmvr-es/devices/`。设备层使用复数 `devices`,不是 `cmvr_es/device`。
|
||
|
||
返回[项目总览](../../README.md)。
|
||
|
||
## 创建链路
|
||
|
||
```text
|
||
config/cmvr_es.pb.txt
|
||
-> config/manager/device_manager.pb.txt
|
||
-> DeviceManager
|
||
-> DeviceFactory
|
||
按 DeviceConfigEntry::DeviceType 选择设备大类
|
||
-> CameraFactory / AGVFactory / RobotArmFactory / ...
|
||
按设备配置 oneof backend 选择厂商实现
|
||
-> 具体设备类
|
||
-> AbstractDevice 和设备大类抽象接口
|
||
```
|
||
|
||
关键文件:
|
||
|
||
- [`abstract_device.h`](abstract_device.h)
|
||
- [`device_types.h`](device_types.h)
|
||
- [`../manager/device_manager/include/device_manager.h`](../manager/device_manager/include/device_manager.h)
|
||
- [`../manager/device_manager/src/device_factory.cpp`](../manager/device_manager/src/device_factory.cpp)
|
||
- [`../../protos/cmvr/config/device_manager_config/device_manager_config.proto`](../../protos/cmvr/config/device_manager_config/device_manager_config.proto)
|
||
- [`../config/manager/device_manager.pb.txt`](../config/manager/device_manager.pb.txt)
|
||
|
||
协议层和业务任务不应直接依赖厂商 SDK 类型。厂商错误码、报文和连接细节由具体后端转换为抽象接口的通用语义。
|
||
|
||
## 当前可由配置创建的设备
|
||
|
||
| 大类 | 抽象接口 | 类别工厂 | 当前可选后端 |
|
||
| --- | --- | --- | --- |
|
||
| Camera | [`camera/abstract_camera.h`](camera/abstract_camera.h) | [`camera/camera_factory.h`](camera/camera_factory.h) | UVC、RealSense、Hikvision |
|
||
| AGV | [`agv/abstract_agv.h`](agv/abstract_agv.h) | [`agv/agv_factory.h`](agv/agv_factory.h) | MyAgv、SRC1100 |
|
||
| RobotArm | [`arm/robot_arm.h`](arm/robot_arm.h) | [`arm/robot_arm_factory.h`](arm/robot_arm_factory.h) | MotorRobotArm、AUBO、Huayan |
|
||
| DexHand | [`dexhand/abstract_dexhand.h`](dexhand/abstract_dexhand.h) | [`dexhand/dexhand_factory.h`](dexhand/dexhand_factory.h) | RH56DFTP、PX6AXGen3 |
|
||
| Microphone | [`microphone/abstract_microphone.h`](microphone/abstract_microphone.h) | [`microphone/microphone_factory.h`](microphone/microphone_factory.h) | FFmpeg |
|
||
| Speaker | [`speaker/abstract_speaker.h`](speaker/abstract_speaker.h) | [`speaker/speaker_factory.h`](speaker/speaker_factory.h) | FFmpeg |
|
||
| BioHead | [`biohead/abstract_biohead.h`](biohead/abstract_biohead.h) | DeviceFactory 直接创建 | BioHeadRobot |
|
||
| MotorSystem | `motor/motor_system/` | DeviceFactory 直接创建 | CAN/MuJoCo motor group |
|
||
|
||
代码目录存在不等于已经接入配置创建链:
|
||
|
||
- MechMind Proto 和实现仍存在,但当前 CameraFactory 明确拒绝创建;
|
||
- MujocoCamera 有实现并参与部分构建,但当前没有 CameraFactory 分支;
|
||
- Battery、Gripper、Robot、CanBus 等抽象或实现不一定已注册到 DeviceFactory;
|
||
- 所有已注册设备共用一个全局 ID 命名空间。
|
||
|
||
新增能力前先确认“已有代码”“可被 CMake 构建”“可被 Factory 创建”“可被 DeviceManager 配置启用”四个状态,不要混为一谈。
|
||
|
||
## 新增同类厂商后端
|
||
|
||
以新增 Camera 后端为例。
|
||
|
||
### 1. 扩展配置 Proto
|
||
|
||
修改:
|
||
|
||
```text
|
||
protos/cmvr/config/camera_config/camera_config.proto
|
||
```
|
||
|
||
新增厂商 config,并加入 `CameraDeviceConfig.oneof backend`。只使用新的字段 tag,不复用 reserved 或已发布 tag。
|
||
|
||
### 2. 新建后端目录
|
||
|
||
```text
|
||
camera/vendor_camera/
|
||
├── CMakeLists.txt
|
||
├── include/
|
||
│ └── vendor_camera.h
|
||
├── src/
|
||
│ └── vendor_camera.cpp
|
||
└── tests/
|
||
└── vendor_camera_test.cpp
|
||
```
|
||
|
||
### 3. 实现抽象接口
|
||
|
||
至少实现:
|
||
|
||
- `typeName()`;
|
||
- `init()`;
|
||
- 实际支持的 `start()` / `stop()`;
|
||
- 状态查询和该类别核心能力;
|
||
- 若支持实时媒体,完整实现流接口和并发停止。
|
||
|
||
不要为了厂商特例向抽象类加入 SDK handle、私有报文或厂商专有结构。只有多个后端都需要的稳定语义才进入抽象接口或 `common/types/`。
|
||
|
||
### 4. 类别工厂注册
|
||
|
||
在 [`camera/camera_factory.h`](camera/camera_factory.h) 的 `backend_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 形式:
|
||
|
||
```cmake
|
||
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 条目:
|
||
|
||
```protobuf
|
||
devices {
|
||
id: "camera_front"
|
||
type: DEVICE_TYPE_CAMERA
|
||
config_file: "devices/camera/camera.pb.txt"
|
||
enable: false
|
||
}
|
||
```
|
||
|
||
新硬件默认 `enable: false`,确保无设备开发主机仍可启动。
|
||
|
||
## 新增全新设备大类
|
||
|
||
当现有抽象无法表达新设备能力时,还需要:
|
||
|
||
1. 在 [`device_types.h`](device_types.h) 增加 `DeviceKind` 和 `toString()`;
|
||
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
|
||
|
||
集合类型设备必须满足:
|
||
|
||
```text
|
||
DeviceConfigEntry.id
|
||
==
|
||
CameraDeviceConfig / AGVDeviceConfig / ... 的外层 id
|
||
```
|
||
|
||
厂商 backend 内部 ID 可以为空,由类别工厂补齐;若填写,也必须与外层 ID 相同。
|
||
|
||
其他约束:
|
||
|
||
- 相对配置路径以根 `cmvr_es.pb.txt` 所在目录解析;
|
||
- 生产密码、token 和证书不得提交到样例配置;
|
||
- 单个设备 init 失败时会被跳过,进程仍可能继续启动;
|
||
- 有初始化依赖的设备按配置顺序排列,例如 MotorSystem 在依赖它的 RobotArm 前;
|
||
- DeviceManager stop 遍历 unordered_map,不能依赖跨设备停止顺序;
|
||
- 当前不支持 service 运行期间并发热插拔设备集合。
|
||
|
||
配置细节见 [`../config/README.md`](../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 等状态不能无锁复制;
|
||
- 析构函数调用安全停止路径;
|
||
- callback 捕获对象前保证 owner 生命周期。
|
||
|
||
并发停止可参考:
|
||
|
||
- [`camera/uvc_camera/`](camera/uvc_camera/)
|
||
- [`camera/hikvision_camera/`](camera/hikvision_camera/)
|
||
- [`camera/hikvision_camera/tests/`](camera/hikvision_camera/tests/)
|
||
|
||
当前 main 收到退出信号只停止 TaskManager,没有显式调用 DeviceManager stop,设备析构仍必须可靠。
|
||
|
||
## 摄像头与麦克风实时流
|
||
|
||
设备实现抽象流接口后,由 [`../manager/media_source_hub/`](../manager/media_source_hub/) 适配给 gRPC 和 QUIC,不应在设备后端实现两套协议代码。
|
||
|
||
当前 Hub 轨道:
|
||
|
||
```text
|
||
<camera-id>/video/color
|
||
<microphone-id>/audio/main
|
||
```
|
||
|
||
当前 adapter 只把 `StreamFrameData.rgbFrame` 注册为彩色视频轨道;depthFrame 尚未注册为 Hub 深度轨道。
|
||
|
||
### Camera 完整编码帧
|
||
|
||
每个 access unit 应正确填写:
|
||
|
||
- `rgbFrame`
|
||
- `codec`:H.264 或 H.265
|
||
- `width`、`height`、`fps`
|
||
- `bKey`
|
||
- `stream_epoch`
|
||
- `sequence`
|
||
- `capture_monotonic_ns`
|
||
- `capture_utc_ns`
|
||
- `pts`、`dts`
|
||
- `time_base_num`、`time_base_den`
|
||
- `duration`
|
||
- `discontinuity`
|
||
- `codec_config_generation`
|
||
- `codec_config`
|
||
|
||
H.264/H.265 后端必须识别关键帧,并尽量实现 `requestKeyFrame()`。
|
||
|
||
### Microphone 完整音频包
|
||
|
||
应正确填写:
|
||
|
||
- `data`
|
||
- `sample_rate`
|
||
- `channels`
|
||
- `format` 和 `codec`
|
||
- `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. 视频从关键帧恢复输出。
|
||
|
||
设备后端不直接发布 `TrackDescriptor`。`device_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 同样是单消费者对象,不同协议或客户端必须各自订阅。
|
||
|
||
发布后的 `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`](camera/hikvision_camera/tests/hikvision_camera_callback_test.cpp)
|
||
- [`../manager/media_source_hub/tests/media_source_hub_test.cpp`](../manager/media_source_hub/tests/media_source_hub_test.cpp)
|
||
|
||
```bash
|
||
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
|