cmvr-es/cmvr-es/manager
2026-08-18 15:03:17 +08:00
..
control_authority_manager refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00
device_manager add system operational state recovery 2026-08-18 15:03:17 +08:00
media_source_manager refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00
safety_manager add system operational state recovery 2026-08-18 15:03:17 +08:00
task_manager refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00
README.md refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00

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 的顺序是:

  1. 加载根配置并设置全局配置根;
  2. 构造 DeviceManager;
  3. 创建启用的设备并调用 device->init();
  4. 注册 gRPC、QUIC TaskFactory creator;
  5. 构造 TaskManager 并调用启用任务的 init();
  6. startRunTask() 启动任务和周期调度线程;
  7. 收到 SIGINT/SIGTERM 后调用 TaskManager::stopRunTask()。

重要限制:

  • DeviceManager 构造不会自动调用全部设备的 start();
  • 当前主退出路径没有调用 DeviceManager::stop();
  • SystemService/StopAll 只停止当前运动、控制和媒体活动,不调用 DeviceManager::stop(),成功返回后可继续接受新命令;
  • DeviceManager::destroyInstance() 不调用设备 stop,销毁前必须先显式停止;
  • TaskManager::destroyInstance() 会调用 stopRunTask(),但 manager 未处于 running 状态时该调用会直接返回;
  • DeviceManager 和 TaskManager 都是首次配置生效的单例,不支持热加载。

DeviceManager

关键文件:

增加现有类别的新后端

例如增加一种摄像头:

  1. 扩展类别配置 Proto 的 oneof backend;
  2. 实现对应抽象设备;
  3. 修改 CameraFactory::create();
  4. 增加 CMake target;
  5. 在摄像头集合配置中增加实例;
  6. 在 DeviceManager 配置中增加相同 ID 的条目。

这种扩展通常不修改全局 DeviceFactory,因为 DEVICE_TYPE_CAMERA 已经路由到 CameraFactory。

增加全新设备类别

还需要:

  1. 扩展 DeviceConfigEntry::DeviceType;
  2. 扩展 DeviceKind 和字符串映射;
  3. 在 DeviceFactory::DeviceFactory() 注册 creator;
  4. 扩展 DeviceManager 的设备类型日志映射;
  5. 为 getDevice<NewAbstractType>() 增加显式模板实例化;
  6. 在 device_manager target 链接新设备 target;
  7. 如需平台访问,增加 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

关键文件:

当前默认轨道:

来源 Track ID 默认 ring capacity
摄像头彩色流 <device_id>/video/color 64
麦克风主流 <device_id>/audio/main 256

当前 gRPC RGB/麦克风流和 QUIC 彩色/麦克风轨道使用 MediaSourceManager;gRPC Depth/RGBD 仍直接读取设备帧。

注册新媒体源

  1. 创建不可变 TrackDescriptor;
  2. 使用唯一且稳定的 track_id;
  3. 提供 start(sink, cancelled);
  4. 提供同步 stop();
  5. 视频源按需提供 request_key_frame();
  6. 调用 registerSource();
  7. 生产不可变 MediaFramePtr;
  8. 保证 frame descriptor ID 与注册轨道一致;
  9. 在重启、编码变化和中断时更新 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

  1. 先确认能力不是 DeviceManager、TaskManager 或 MediaSourceManager 的子职责;
  2. 定义所有权、初始化、start/stop 和线程模型;
  3. 避免新增无必要的全局单例;
  4. 新建独立目录、头文件、实现和 CMake target;
  5. 在 ../CMakeLists.txt 增加子目录;
  6. 只在 main.cpp 或明确的上层 owner 组装;
  7. 增加无设备生命周期、失败回滚和并发测试。

测试

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
  • 无设备失败路径有测试