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

356 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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