| .. | ||
| control_authority_manager | ||
| device_manager | ||
| media_source_manager | ||
| safety_manager | ||
| task_manager | ||
| README.md | ||
Manager 模块开发指南
manager/ 负责组织设备、任务和协议无关媒体源。Manager 管理对象生命周期和调度,不实现厂商协议,也不实现平台 wire protocol。
返回项目总览。
当前管理器
管理模块目录统一使用 *_manager 后缀,主管理类使用 *Manager 后缀。工厂、适配器、
账本、快照和结果结构体属于管理器内部的支撑类型,保留其职责名称,不强行改成
*Manager。
| 目录 | CMake target | 职责 |
|---|---|---|
control_authority_manager/ |
cmvr_es::control_authority_manager |
控制权租约、代际、dispatch fence 和 quarantine |
device_manager/ |
cmvr_es::device_manager |
按配置创建、初始化、查询和批量启停设备 |
safety_manager/ |
cmvr_es::safety_manager |
Sensor/Control 安全准入、StopAll、恢复和命令账本 |
task_manager/ |
cmvr_es::task_manager |
创建任务、校验运行模式、统一启停和调度周期任务 |
media_source_manager/ |
cmvr_es::media_source_manager、cmvr_es::device_media_source_adapter |
实时媒体源注册、按需启停和多消费者分发 |
manager/ 当前没有聚合 CMakeLists.txt,所有模块由 ../CMakeLists.txt
按依赖顺序加入。新增 manager 时必须同时更新目录、target、依赖顺序和本 README。
进程生命周期
当前 ../main.cpp 的顺序是:
- 加载根配置并设置全局配置根;
- 构造
DeviceManager; - 创建启用的设备并调用
device->init(); - 注册 gRPC、QUIC TaskFactory creator;
- 构造
TaskManager并调用启用任务的init(); startRunTask()启动任务和周期调度线程;- 收到 SIGINT/SIGTERM 后调用
TaskManager::stopRunTask()。
重要限制:
- DeviceManager 构造不会自动调用全部设备的
start(); - 当前主退出路径没有调用
DeviceManager::stop(); SystemService/StopAll只停止当前运动、控制和媒体活动,不调用DeviceManager::stop(),成功返回后可继续接受新命令;DeviceManager::destroyInstance()不调用设备 stop,销毁前必须先显式停止;TaskManager::destroyInstance()会调用stopRunTask(),但 manager 未处于 running 状态时该调用会直接返回;- DeviceManager 和 TaskManager 都是首次配置生效的单例,不支持热加载。
DeviceManager
关键文件:
device_manager/include/device_manager.hdevice_manager/include/device_factory.hdevice_manager/src/device_factory.cpp../devices/README.md
增加现有类别的新后端
例如增加一种摄像头:
- 扩展类别配置 Proto 的
oneof backend; - 实现对应抽象设备;
- 修改
CameraFactory::create(); - 增加 CMake target;
- 在摄像头集合配置中增加实例;
- 在 DeviceManager 配置中增加相同 ID 的条目。
这种扩展通常不修改全局 DeviceFactory,因为 DEVICE_TYPE_CAMERA 已经路由到 CameraFactory。
增加全新设备类别
还需要:
- 扩展
DeviceConfigEntry::DeviceType; - 扩展
DeviceKind和字符串映射; - 在
DeviceFactory::DeviceFactory()注册 creator; - 扩展 DeviceManager 的设备类型日志映射;
- 为
getDevice<NewAbstractType>()增加显式模板实例化; - 在 device_manager target 链接新设备 target;
- 如需平台访问,增加 API Proto、gRPC service 和系统设备类型映射。
DeviceFactory::registerCreator() 虽然是 public,但 factory 是 DeviceManager 的私有成员,当前不是运行时插件入口。新增全局设备类别仍需修改 device_factory.cpp。
容器和顺序约束
- 配置启用条目按配置顺序创建并
init(); - 有初始化依赖的设备应把依赖项写在使用方之前;
start()、stop()遍历unordered_map,不能依赖启停顺序;- 某个设备 start 返回 false 时,当前实现会继续启动其他设备且不会回滚;
- DeviceManager 会把 create/init/start/stop 和健康探针异常转换成设备状态错误, 但后端仍应把预期失败转换为返回值;
- collection 配置要求 manager entry ID 能找到同 ID 子配置;
- ID 重复、不匹配或配置路径为空都会拒绝创建;
registerDevice()不会替调用方调用init();devices_和状态表由读写锁保护,运行期动态注册不会与 heartbeat/query 形成数据竞争;但动态设备不会自动补执行已经发生的start(),也暂不支持移除;getDevice<T>()类型不匹配或 ID 不存在时返回空指针。
状态快照与 QUIC Heartbeat
DeviceManager::snapshot() 返回协议无关的纯值快照,包含 Manager
名称、版本、描述以及按设备 ID 排序的完整设备表。状态表与可用设备对象表分开:
- 禁用、创建失败、初始化失败的配置项仍会出现在快照中;
devices_仍只保存可供业务查询的已初始化对象,不改变现有 service 语义;- 动态注册设备初始为
REGISTERED,不会冒充已经由 Manager 初始化或启动; - create/init/start/stop 的已知错误会设置 Manager
ERROR和has_error;设备 health 与生命周期独立,仍由探针报告HEALTHY/DEGRADED/FAULT/UNKNOWN; healthSnapshot()未实现时为UNKNOWN,不能解释为健康;- heartbeat 只消费该内存快照,并在 QUIC 映射边界过滤
enabled=false的设备; 禁止从发送线程同步访问厂商 SDK 或设备网络。
快照复制设备元数据和临时 shared_ptr 后立即释放容器锁,再调用设备的轻量健康
探针,避免持锁进入设备代码。start/stop 同样在锁外调用设备,并由单独的生命周期
锁防止同一 Manager 并发启停。
不要在仍有 service/task 持有 manager 引用时调用 destroyInstance()。
TaskManager
完整任务实现指南见 ../task/README.md。
PERIODIC_STEP
control_period_s必须是有限正数;- 所有周期任务的
step()串行运行在同一个 scheduler 线程; - 慢 I/O 或长计算会延迟其他周期任务;
- 只有 state 为
RUNNING的任务会执行step(); step()内不能同步调用stopRunTask()等待当前 scheduler 自身。
BLOCKING_SERVICE
- TaskManager 只调用
init/start/stop,不调用step(); start()仍必须快速返回,由任务自己持有服务线程或事件循环;stop()必须唤醒阻塞操作并 join 自己创建的线程。
启停语义
- manager 构造阶段只调用 task
init(); - 任一 task
start()失败,会停止此前已启动的任务且不启动 scheduler; - 返回 false 的 task 必须自行清理本次 start 已经创建的部分资源,TaskManager 不会再调用该失败 task 的 stop;
- TaskManager 处于 running 状态时,停止流程先停止并 join scheduler,再调用每个 task 的
stop(); - 未启动或已经停止时,
stopRunTask()会直接返回,不会再次逐个调用 task stop; - 任务存储在
unordered_map,启动和停止顺序不确定; - 有顺序依赖的工作应放入同一协调任务或显式建模;
- task 返回后,其内部状态并发安全由具体实现负责。
MediaSourceManager
关键文件:
media_source_manager/include/media_source_manager.hmedia_source_manager/src/device_media_source_adapter.cpp../common/media/media_frame.h../common/base/ring_buffer.h
当前默认轨道:
| 来源 | Track ID | 默认 ring capacity |
|---|---|---|
| 摄像头彩色流 | <device_id>/video/color |
64 |
| 麦克风主流 | <device_id>/audio/main |
256 |
当前 gRPC RGB/麦克风流和 QUIC 彩色/麦克风轨道使用 MediaSourceManager;gRPC Depth/RGBD 仍直接读取设备帧。
注册新媒体源
- 创建不可变
TrackDescriptor; - 使用唯一且稳定的
track_id; - 提供
start(sink, cancelled); - 提供同步
stop(); - 视频源按需提供
request_key_frame(); - 调用
registerSource(); - 生产不可变
MediaFramePtr; - 保证 frame descriptor ID 与注册轨道一致;
- 在重启、编码变化和中断时更新 producer 元数据和 discontinuity,由 adapter 更新 descriptor。
Descriptor 最低要求:
id、source_id非空;- kind 不能是
UNKNOWN; - time base 分子和分母均大于零;
- generation 大于零。
Source callback 生命周期
- 第一位 subscriber 触发一次 source start;
- 多位 subscriber 共享同一个采集生产者;
- 最后一份 Subscription reset/析构时同步调用 source stop;
- Hub 会先 close ring 并唤醒消费者;stop callback 必须解除生产端阻塞、停止采集并 join producer,形成同步发布屏障;
- start 必须在阻塞阶段检查 cancellation;
- cancellation predicate 必须快速、非阻塞,不能回调同一个 Hub;
- active source 不能 unregister,应先销毁全部 subscriptions;
- request-key-frame 与 stop 串行化,只在 source 运行时调用;
- adapter 的最后 lease 只停止媒体 streaming,不等于设备级 stop。
永久不响应 cancellation 的 start 虽会被隔离以避免 use-after-free,仍可能泄漏线程和外部资源,不能依赖该隔离替代正确实现。
Subscription 与广播环形队列
- 每个消费线程单独调用一次
subscribe(); - Subscription move-only 且为单消费者对象;
- 同一 Subscription 不能跨线程并发 read/reset;
- 满队列覆盖最旧帧,不阻塞生产者;
dropped_since_last_read是该消费者实际错过的帧数;- ring 全局 dropped count 与 cursor dropped count 含义不同;
NEXT_PUBLISHED忽略已有帧;OLDEST_AVAILABLE从最旧保留帧开始;LATEST_AVAILABLE读取当前最新帧;discardPendingIfExceeds(limit)在积压超过阈值时原子地把该消费者游标推进到 当前发布末尾,并把主动丢弃数量计入下一次读取的dropped_since_last_read;discardPendingIfExceeds()与read()一样只能由该 Subscription 的单一消费 线程调用,不能用它替代 Subscription 的线程所有权约束;- source 重启会 reset ring、提升
BroadcastFrameRing内部 generation,并把 ring 的ReadResult.sequence从 0 重新计数; - close 唤醒等待者并拒绝新 publish。
ring generation 不等于 TrackDescriptor::generation,ring 的 ReadResult.sequence 也不等于 MediaFrame::sequence。Descriptor generation 和媒体帧 sequence 仍由 producer/adapter 维护。
协议消费者需要分别处理 ring generation_changed、descriptor generation、消费者 drop 和 frame discontinuity;任一不连续发生时都应传播状态,帧间编码还应请求关键帧。主动丢弃 H.264/H.265 积压后不得直接发送 P/B 帧,必须等新的关键帧恢复。
新增第四种 Manager
- 先确认能力不是 DeviceManager、TaskManager 或 MediaSourceManager 的子职责;
- 定义所有权、初始化、start/stop 和线程模型;
- 避免新增无必要的全局单例;
- 新建独立目录、头文件、实现和 CMake target;
- 在
../CMakeLists.txt增加子目录; - 只在
main.cpp或明确的上层 owner 组装; - 增加无设备生命周期、失败回滚和并发测试。
测试
MediaSourceManager:
cmake --build build --target media_source_manager_test
ctest \
--test-dir build \
-R '^media_source_manager_test$' \
--output-on-failure
DeviceManager 已有 device_manager_snapshot_test,覆盖全量状态表、生命周期失败、
异常限长、排序和值快照并发读取。TaskManager 仍缺少独立 CTest。修改其行为时
至少补充:
- fake device 创建、ID 冲突和 init/start/stop 失败;
- 设备依赖顺序;
- 周期任务调度和慢 step;
- service task 启停、重复 stop 和启动失败回滚;
- 并发查询、取消和 shutdown。
提交检查
- manager 没有包含厂商 wire protocol
- 初始化、start、stop 和 destroy 语义明确
- 不依赖 unordered_map 的遍历顺序
- 动态集合修改不会与查询并发
- Media Subscription 每线程独立
- stop 能唤醒并 join 所有工作线程
- 新 manager 已加入
cmvr-es/CMakeLists.txt - 无设备失败路径有测试