# CMVR-ES CMVR-ES(CMVR Edge System)是部署在机器人边缘主机上的 C++17 运行时。它负责统一加载设备与任务、通过 gRPC 提供稳定的设备控制接口,并通过 QUIC 主动连接平台,上报节点在线状态、IP 地址以及允许丢帧的实时音视频。 本项目的协议边界是: - 机械臂、AGV、灵巧手等控制与状态查询继续使用 gRPC; - 节点注册、心跳和 IP 主动上报使用 QUIC 可靠流; - 摄像头和麦克风实时数据使用 QUIC DATAGRAM; - 浏览器不直接连接 CMVR 自定义 QUIC 协议,而是连接平台侧网关提供的 WebTransport、WebRTC、MSE/WebSocket 或其他 Web 接口。 ## 当前状态 | 项目 | 当前状态 | | --- | --- | | 已验证构建平台 | Ubuntu 22.04 x86_64、GCC 11.4、CMake 3.30.3 | | CMake / C++ 最低要求 | CMake 3.22、C++17 | | ARM | 目录和部分依赖已准备,但当前依赖树不完整,尚未形成可复现的完整构建 | | gRPC | 默认启用,监听 `0.0.0.0:50052`,reflection 默认开启 | | QUIC | 默认关闭;找到 MsQuic 时构建真实后端,否则构建不可用占位后端 | | 物理设备 | 默认全部关闭,适合没有连接设备的开发主机 | | 平台网关 / Web 前端 | 不在本仓库中,需要平台项目按本文协议接入 | > [!IMPORTANT] > 当前可复现构建目标是 `x86`。请不要把 `arm` 目录存在理解为 ARM 全量构建已经通过。 ## 目录 - [1. 工程概览](#1-工程概览) - [2. 系统架构](#2-系统架构) - [3. 目录结构](#3-目录结构) - [4. 依赖与环境准备](#4-依赖与环境准备) - [5. 构建与安装](#5-构建与安装) - [6. 配置说明](#6-配置说明) - [7. 运行与测试](#7-运行与测试) - [8. 生产部署](#8-生产部署) - [9. 平台端接入](#9-平台端接入) - [10. 开发扩展指南](#10-开发扩展指南) - [11. 安全说明](#11-安全说明) - [12. 已知限制与排障](#12-已知限制与排障) ## 1. 工程概览 ### 1.1 核心能力 | 能力 | 说明 | | --- | --- | | 设备统一管理 | 通过设备抽象、设备工厂和 `DeviceManager` 加载不同厂商实现 | | 任务统一管理 | 通过 `TaskManager` 运行周期任务和阻塞式服务任务 | | gRPC 控制面 | 提供系统、相机、音频、机械臂、AGV、灵巧手和生物头等服务 | | QUIC 节点在线面 | 边缘端主动连接平台,完成注册、心跳、IP 和 gRPC 地址上报 | | QUIC 实时媒体面 | 通过 QUIC DATAGRAM 发送可丢弃的音视频帧,控制信息仍走可靠流 | | 协议无关媒体分发 | `MediaSourceHub` 已供 gRPC RGB/麦克风流和 QUIC 彩色/麦克风轨道共享采集源 | | 算法与仿真 | 包含运动学、轨迹规划、控制算法以及部分 MuJoCo 接入 | ### 1.2 代码中已包含的设备实现 下表表示仓库中存在对应代码,并不表示所有型号都已在当前主机和当前版本完成真机验证。 | 类别 | 已包含实现 | | --- | --- | | 摄像头 | UVC、RealSense、Hikvision、Mech-Eye、MuJoCo Camera | | 麦克风 / 音箱 | FFmpeg Microphone、FFmpeg Speaker | | 机械臂 | AUBO、Huayan、Motor Robot Arm | | AGV | SRC1100、My AGV | | 灵巧手 | PX-6AX Gen3、RH56DFTP | | 生物头 | ESP32 BioHead | | 电机与总线 | TI5 Motor、MuJoCo Motor、CAN / CANopen、PCAN | ### 1.3 协议职责 | 数据 | 推荐通道 | 原因 | | --- | --- | --- | | 机械臂、AGV 控制命令 | gRPC | 需要明确的请求结果、错误处理和稳定语义 | | 设备状态和管理接口 | gRPC | 便于平台生成强类型客户端并统一治理 | | 节点注册、心跳、IP 上报 | QUIC reliable stream | 边缘端主动连接,复用 QUIC TLS 会话和重连机制 | | 视频、音频实时帧 | QUIC DATAGRAM | 网络拥塞时允许丢弃过期帧,避免旧数据阻塞实时数据 | | 浏览器控制和播放 | 平台 Web API | 浏览器不能直接使用本项目的自定义 QUIC ALPN | ## 2. 系统架构 ```mermaid flowchart LR MAIN["cmvr_es / main.cpp"] --> CONFIG["Proto Text 配置"] MAIN --> DEVICE_MANAGER["DeviceManager"] MAIN --> TASK_MANAGER["TaskManager"] DEVICE_MANAGER --> DEVICE_FACTORY["DeviceFactory"] DEVICE_FACTORY --> DEVICES["设备抽象与厂商驱动"] DEVICES --> HARDWARE["串口 / CAN / 厂商 SDK / 仿真"] TASK_MANAGER --> GRPC_TASK["GrpcServerTask"] TASK_MANAGER --> QUIC_TASK["QuicEdgeTask"] TASK_MANAGER --> PERIODIC_TASK["周期控制任务"] GRPC_TASK --> GRPC_SERVICE["gRPC Services"] GRPC_SERVICE --> DEVICE_MANAGER GRPC_SERVICE -->|"RGB / Microphone stream"| MEDIA_HUB["MediaSourceHub"] QUIC_TASK --> QUIC_SERVICE["QuicEdgeService"] QUIC_SERVICE --> MEDIA_HUB MEDIA_HUB --> MEDIA_ADAPTER["DeviceMediaSourceAdapter"] MEDIA_ADAPTER --> DEVICES QUIC_SERVICE -->|"可靠流:注册 / 心跳 / 媒体描述"| GATEWAY["Java / 平台 QUIC Gateway"] QUIC_SERVICE -->|"DATAGRAM:音视频"| GATEWAY GATEWAY -->|"WebTransport / WebRTC / HTTP"| BROWSER["Browser"] GATEWAY -->|"根据节点上报地址调用"| GRPC_SERVICE ``` ### 2.1 启动流程 1. 程序从 `/proc/self/exe` 获取可执行文件目录。 2. 无参数启动时加载可执行文件旁的 `config/cmvr_es.pb.txt`;传入一个参数时加载指定根配置。 3. 以根配置文件所在目录作为其他业务配置和证书相对路径的配置根目录。 4. 加载日志配置并初始化日志。 5. `DeviceManager` 根据配置创建并初始化启用的设备。 6. 注册 gRPC 和 QUIC 任务工厂。 7. `TaskManager` 创建并启动启用的周期任务或阻塞式服务任务。 8. 主线程等待 `SIGINT` 或 `SIGTERM`。 9. 收到退出信号后停止任务并关闭日志。 当前主退出路径会停止 `TaskManager` 中的任务,但没有显式调用 `DeviceManager::stop()`。需要统一关闭全部设备时,可调用 `SystemService/StopAll`;后续也应考虑补齐进程退出时的设备级统一停止。 ### 2.2 MediaSourceHub `MediaSourceHub` 是设备层与传输协议之间的协议无关媒体层。当前 gRPC 相机 RGB 流、gRPC 麦克风流,以及 QUIC 相机彩色轨道和麦克风轨道使用该 Hub;gRPC Depth/RGBD 流仍直接读取设备编码帧,尚未迁移到 Hub。 - 已接入 Hub 的 gRPC 与 QUIC 流共享同一设备采集源,后续新增协议也应从 Hub 订阅; - 第一个订阅者启动设备流,最后一个订阅者释放设备流; - 每个消费线程必须使用独立的 `Subscription`,读取游标不能跨线程共享; - 描述信息、帧及其 payload 均为不可变对象,可以安全地跨协议共享; - 环形队列有界,慢消费者不会阻塞生产者; - 被覆盖的帧会形成精确的 dropped count,协议层据此标记 discontinuity; - H.264/H.265 出现丢帧或 generation 变化后,协议层可通过 Hub 请求新的关键帧; - source `start` 必须响应 cancellation,source `stop` 必须同步解除回调并回收生产线程。 关键文件: - [`media_source_hub.h`](cmvr-es/manager/media_source_hub/include/media_source_hub.h) - [`device_media_source_adapter.cpp`](cmvr-es/manager/media_source_hub/src/device_media_source_adapter.cpp) - [`media_frame.h`](cmvr-es/common/media/media_frame.h) - [`ring_buffer.h`](cmvr-es/common/base/ring_buffer.h) ## 3. 目录结构 ```text cmvr-es/ ├── CMakeLists.txt ├── README.md ├── LICENSE ├── request.txt # 三方依赖清单 ├── cmake/ │ ├── FindExternalLib.cmake # 仓库内依赖发现与安装 │ └── FindMsQuic.cmake # 可选 MsQuic 后端发现 ├── protos/ │ └── cmvr/ │ ├── api/ # 平台 gRPC API │ ├── common/ # 通用协议类型 │ ├── config/ # Proto Text 配置 Schema │ ├── msgs/ # 设备和状态消息 │ └── quic_edge/v1/ # QUIC 控制面、媒体协议与线协议文档 ├── cmvr-es/ │ ├── main.cpp # 进程入口与生命周期 │ ├── config/ # 默认运行配置 │ ├── common/ # 日志、配置、基础容器和媒体模型 │ ├── hardware/ # 串口等底层能力 │ ├── devices/ # 设备抽象、工厂和厂商实现 │ ├── manager/ │ │ ├── device_manager/ │ │ ├── task_manager/ │ │ └── media_source_hub/ │ ├── service/ │ │ ├── grpc/ │ │ └── quic_edge/ │ ├── task/ │ │ ├── grpc_server_task/ │ │ ├── quic_edge_task/ │ │ └── touch_screen_task/ │ ├── algorithms/ │ └── simulate/ ├── dependency/ │ ├── x86/third_party/ # x86_64 预置依赖 │ └── arm/third_party/ # ARM 部分依赖,当前尚未完整验证 ├── model/ # 运行模型和机器人资源 ├── assets/ # SDK 安装包、grpcurl 等辅助资产 ├── python/ # 标定和辅助脚本 ├── script/ # 历史辅助脚本 ├── build/ # 本机构建目录,不纳入 Git └── output/ # cmake --install 产物,不纳入 Git ``` ### 3.1 模块开发文档 核心目录提供独立的扩展指南。新增模块时应先阅读目标目录文档,再沿其中列出的 Proto、配置、工厂、CMake 和测试链路完成接入。 | 模块 | 开发文档 | 主要内容 | | --- | --- | --- | | 设备层 | [`cmvr-es/devices/README.md`](cmvr-es/devices/README.md) | 新设备后端、新设备类别、生命周期和实时流安全 | | 管理层 | [`cmvr-es/manager/README.md`](cmvr-es/manager/README.md) | DeviceManager、TaskManager、MediaSourceHub | | 服务层 | [`cmvr-es/service/README.md`](cmvr-es/service/README.md) | gRPC Service 注册和 QUIC 协议引擎扩展 | | 任务层 | [`cmvr-es/task/README.md`](cmvr-es/task/README.md) | Task 接口、运行模式、Factory 和配置接入 | | 公共层 | [`cmvr-es/common/README.md`](cmvr-es/common/README.md) | 公共类型、媒体模型、环形队列和配置工具 | | 算法层 | [`cmvr-es/algorithms/README.md`](cmvr-es/algorithms/README.md) | IK、规划、控制、感知和算法工厂 | | 硬件层 | [`cmvr-es/hardware/README.md`](cmvr-es/hardware/README.md) | 串口/总线抽象、并发事务和权限 | | 配置层 | [`cmvr-es/config/README.md`](cmvr-es/config/README.md) | Proto Text 配置树、ID 和生产配置 | | 仿真层 | [`cmvr-es/simulate/README.md`](cmvr-es/simulate/README.md) | MuJoCo、仿真引擎和模拟设备边界 | | 协议定义 | [`protos/README.md`](protos/README.md) | Proto 生成、兼容规则、Java 和 QUIC 版本 | ## 4. 依赖与环境准备 ### 4.1 基础环境 已验证环境: - Ubuntu 22.04.5 LTS x86_64; - GCC 11.4; - CMake 3.30.3; - C++17; - 建议至少预留 10 GB 可用磁盘空间用于依赖、构建和安装产物。 工程要求 CMake 3.22 或更高版本。若系统 CMake 版本过低,可使用 [`assets/`](assets/) 中随仓库提供的对应架构 CMake 安装包,或由系统管理员安装新版 CMake。 ### 4.2 系统依赖 下面的命令面向 Ubuntu 22.04。基础构建依赖: ```bash sudo apt-get update sudo apt-get install -y \ build-essential \ cmake \ git \ pkg-config \ patchelf \ file \ libboost-all-dev \ libssl-dev ``` 图像、仿真和设备接入常用依赖: ```bash sudo apt-get install -y \ libjpeg-dev \ libpng-dev \ libtiff-dev \ libv4l-dev \ libgtk-3-dev \ libgl1-mesa-dev \ libglu1-mesa-dev \ libglfw3-dev \ libassimp-dev \ libx11-dev \ liblapack-dev \ libblas-dev \ libzbar-dev \ libpthread-stubs0-dev \ libdc1394-dev \ nlohmann-json3-dev \ libxml2-dev \ libpulse-dev \ libasound2-dev \ libusb-1.0-0-dev \ libudev-dev ``` 按实际设备选择安装: ```bash # AUBO SDK 的 Qt 相关能力 sudo apt-get install -y qtbase5-dev # CAN 调试工具 sudo apt-get install -y can-utils # Matplot++ 交互绘图 sudo apt-get install -y gnuplot-qt ``` FFmpeg、OpenCV、gRPC、Protobuf、RealSense、Hikvision SDK、AUBO SDK、Pinocchio、MuJoCo、ViSP、OSQP 等主要 C/C++ 依赖优先从 `dependency//third_party/` 查找。完整清单和版本以 [`request.txt`](request.txt) 与 [`FindExternalLib.cmake`](cmake/FindExternalLib.cmake) 为准。 ### 4.3 仓库依赖注意事项 当前依赖管理方式有两个需要特别注意的历史问题: 1. 三方依赖的主要来源是仓库中的 `dependency//third_party/`,不是 Git submodule。 2. 当前 `.gitmodules` 与仓库实际 gitlink 不一致,执行 `git submodule update --init --recursive` 会因 `assets/toppra` 缺少映射而失败。 因此,不要把旧版 README 中的 submodule 命令作为安装步骤。`script/install.sh` 也包含旧目录和硬编码架构,当前不作为标准安装入口。 ## 5. 构建与安装 以下命令都从仓库根目录执行。 ### 5.1 首次构建工具引导 根 CMake 当前固定从 `output/bin/protoc` 和 `output/bin/grpc_cpp_plugin` 生成 C++ protobuf/gRPC 代码,而 `output/` 不纳入 Git。因此,干净检出后的第一次配置前,需要先从仓库依赖树引导这两个工具: ```bash cd /path/to/cmvr-es CMVR_GRPC_PREFIX="$PWD/dependency/x86/third_party/grpc/v1.76.0" CMVR_PROTOC="$CMVR_GRPC_PREFIX/bin/protoc" if [ ! -x "$CMVR_PROTOC" ]; then CMVR_PROTOC="$CMVR_GRPC_PREFIX/bin/protoc-31.1.0" fi test -x "$CMVR_PROTOC" test -x "$CMVR_GRPC_PREFIX/bin/grpc_cpp_plugin" install -Dm755 "$CMVR_PROTOC" output/bin/protoc install -Dm755 \ "$CMVR_GRPC_PREFIX/bin/grpc_cpp_plugin" \ output/bin/grpc_cpp_plugin export LD_LIBRARY_PATH="$CMVR_GRPC_PREFIX/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ``` 该 `LD_LIBRARY_PATH` 仅用于首次配置和编译阶段找到仓库内的 Protobuf/gRPC 工具库。完成安装后,应用及安装库使用相对 RPATH,标准部署不需要全局设置它。 ### 5.2 开发构建 ```bash cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DCMVR_ARCH=x86 \ -DBUILD_TESTING=ON \ -DCMVR_MEDIA_SOURCE_HUB_BUILD_TESTS=ON cmake --build build -j"$(nproc)" ctest --test-dir build --output-on-failure cmake --install build ``` 安装前缀由根 CMake 固定为仓库下的 `output/`。传入普通的 `-DCMAKE_INSTALL_PREFIX=...` 不会改变该目录。 安装结果的核心结构为: ```text output/ ├── bin/ │ ├── cmvr_es │ ├── config/ │ └── model/ ├── lib/ └── log/ # 首次产生日志时创建 ``` ### 5.3 生产构建 生产构建可关闭无硬件测试目标: ```bash cmake -S . -B build-release \ -DCMAKE_BUILD_TYPE=Release \ -DCMVR_ARCH=x86 \ -DBUILD_TESTING=OFF \ -DCMVR_MEDIA_SOURCE_HUB_BUILD_TESTS=OFF cmake --build build-release -j"$(nproc)" cmake --install build-release ldd -r output/bin/cmvr_es ``` > [!WARNING] > 每次 `cmake --install` 都会删除并重新复制 `output/bin/config/` 和 `output/bin/model/`。不要把唯一一份生产配置、证书或现场模型直接维护在这两个目录中。 ### 5.4 构建真实 QUIC 后端 MsQuic 是可选编译依赖。未找到 MsQuic 时,工程仍能构建和运行 gRPC,但启用 QUIC 任务会返回明确的“后端不可用”错误。 MsQuic 安装前缀需要至少包含: ```text / ├── include/msquic.h └── lib/libmsquic.so # 也可位于 lib64/ 或 bin/ ``` 配置真实后端: ```bash cmake -S . -B build-quic \ -DCMAKE_BUILD_TYPE=Release \ -DCMVR_ARCH=x86 \ -DCMVR_ENABLE_MSQUIC_BACKEND=ON \ -DCMVR_MSQUIC_ROOT=/absolute/path/to/msquic cmake --build build-quic -j"$(nproc)" cmake --install build-quic ``` 也可以将 MsQuic 放在 `dependency/x86/third_party/msquic//`,CMake 会自动搜索。配置阶段应看到 `MsQuic found`;若看到 `building QUIC edge in unavailable/stub mode`,则当前构建不能启动 QUIC 任务。 ## 6. 配置说明 ### 6.1 配置格式与加载规则 配置使用 Protobuf TextFormat,不是 JSON 或 YAML。配置 Schema 位于 [`protos/cmvr/config/`](protos/cmvr/config/)。 默认配置树: ```text cmvr_es.pb.txt ├── logger/logger.pb.txt ├── manager/device_manager.pb.txt │ └── devices//*.pb.txt └── manager/task_manager.pb.txt └── tasks//*.pb.txt ``` 关键入口: | 配置 | 作用 | | --- | --- | | [`cmvr_es.pb.txt`](cmvr-es/config/cmvr_es.pb.txt) | 根配置,引用日志、设备管理和任务管理配置 | | [`device_manager.pb.txt`](cmvr-es/config/manager/device_manager.pb.txt) | 设备实例、类别、配置文件和启用状态 | | [`task_manager.pb.txt`](cmvr-es/config/manager/task_manager.pb.txt) | gRPC、QUIC、触屏等任务及运行模式 | | [`grpc_server_task.pb.txt`](cmvr-es/config/tasks/grpc_server_task/grpc_server_task.pb.txt) | gRPC 地址、端口和 reflection | | [`quic_edge_task.pb.txt`](cmvr-es/config/tasks/quic_edge_task/quic_edge_task.pb.txt) | QUIC 网关、TLS、重连、心跳和媒体轨道 | | [`logger.pb.txt`](cmvr-es/config/logger/logger.pb.txt) | 日志级别、路由、目录和轮转参数 | 传入显式根配置时,业务配置、设备配置和 QUIC 证书的相对路径均以根配置文件所在目录解析: ```bash ./output/bin/cmvr_es /etc/cmvr-es/cmvr_es.pb.txt ``` 日志配置中的相对 `directory` 是相对于可执行文件目录解析,而不是配置根目录。默认值 `../log` 对应 `output/log/` 或部署后的 `/opt/cmvr-es/log/`。 ### 6.2 默认无硬件开发配置 仓库默认配置适用于当前主机没有连接实际设备的场景: - 所有物理设备条目默认 `enable: false`; - `touch_screen` 默认关闭; - `quic_edge` 默认关闭; - `grpc_server` 默认开启; - gRPC 默认监听 `0.0.0.0:50052`,reflection 默认开启。 因此,首次启动应先验证进程和 gRPC 服务,再逐个开启设备。不要一次性启用全部设备。 ### 6.3 启用设备 启用设备时需要同时满足: 1. 对应设备集合配置中存在该实例; 2. `DeviceManager` 条目的 `id` 与设备配置中的 `id` 完全一致; 3. `DeviceManager` 条目的 `type` 与设备类别一致; 4. `enable: true`; 5. 厂商 SDK、权限、串口/CAN/USB 和网络参数已经准备完成。 推荐按以下顺序验证: 1. 保持所有设备关闭,验证基础进程和 gRPC reflection; 2. 一次只开启一个设备; 3. 检查启动日志和对应设备的单次 RPC; 4. 再验证流式接口、并发访问和停止流程; 5. 最后启用 QUIC 媒体轨道。 ### 6.4 启用 QUIC 注册与心跳 QUIC 必须同时在两处开启: 1. 在 [`task_manager.pb.txt`](cmvr-es/config/manager/task_manager.pb.txt) 中设置 `quic_edge` 任务的 `enable: true`; 2. 在 [`quic_edge_task.pb.txt`](cmvr-es/config/tasks/quic_edge_task/quic_edge_task.pb.txt) 中设置 `quic_edge.enable: true`。 随后配置: - `server_host` 和 `server_port`; - ALPN,v1 默认且要求精确匹配 `cmvr-quic-edge/1`; - 唯一的 `node_id`; - 心跳、ACK 超时和重连参数; - 对外声明的 gRPC endpoint; - CA、服务端名称以及可选的客户端证书和私钥。 媒体轨道可以为空。零轨道模式仍会完成节点注册、IP 上报和心跳,适合先联调在线状态。 正式环境应使用: ```text tls.allow_insecure: false tls.ca_file: "certs/quic_gateway_ca.pem" tls.server_name: "<与 server_host 匹配的服务端名称>" ``` 客户端证书和私钥必须同时配置或同时省略。`allow_insecure: true` 只允许用于开发联调。 ### 6.5 启用 QUIC 媒体轨道 启用媒体前必须先启用对应设备,然后在 QUIC 配置中增加唯一的 `track_id`: ```protobuf tracks { track_id: 1 source_kind: SOURCE_KIND_CAMERA device_id: "right_hand_cam" source_track_id: "right_hand_cam/video/color" enable: true } tracks { track_id: 2 source_kind: SOURCE_KIND_MICROPHONE device_id: "mic1" source_track_id: "mic1/audio/main" enable: true } ``` 默认轨道命名规则: - 摄像头彩色流:`/video/color`; - 麦克风主音频流:`/audio/main`。 `maximum_frame_bytes`、`maximum_datagram_bytes` 和 `datagram_send_queue_depth` 需要联合配置。一个完整帧的全部 DATAGRAM 分片必须能够作为批次进入发送队列,否则该帧会整体丢弃。 > [!NOTE] > `grpc_endpoint_tls` 当前只描述“上报给平台的 gRPC 地址是否声称使用 TLS”,它不会自动让本项目 gRPC Server 启用 TLS。 ## 7. 运行与测试 ### 7.1 启动 使用安装目录内的默认配置: ```bash ./output/bin/cmvr_es ``` 使用外部配置根: ```bash ./output/bin/cmvr_es /etc/cmvr-es/cmvr_es.pb.txt ``` 程序不接受更多位置参数。正常停止使用 `Ctrl+C`、`SIGINT` 或 `SIGTERM`。 ### 7.2 gRPC 快速验证 仓库提供 x86_64 版 grpcurl 资产: ```bash tar -xzf assets/grpcurl_1.8.7_linux_x86_64.tar.gz -C /tmp /tmp/grpcurl \ -plaintext \ 127.0.0.1:50052 \ list /tmp/grpcurl \ -plaintext \ 127.0.0.1:50052 \ list cmvr.api.ArmService ``` 系统信息请求: ```bash /tmp/grpcurl \ -plaintext \ -d '{}' \ 127.0.0.1:50052 \ cmvr.api.SystemService/GetSystemInfo ``` reflection 正常只说明 gRPC Server 已启动。具体设备 RPC 仍可能因为设备未启用、初始化失败或硬件离线而失败。 ### 7.3 无硬件自动测试 ```bash ctest --test-dir build --output-on-failure ``` 当前登记到 CTest 的核心测试: | 测试 | 覆盖内容 | | --- | --- | | `hikvision_camera_callback_test` | Hikvision 回调生命周期和并发安全 | | `media_source_hub_test` | 多订阅者、环形队列、丢帧和停止语义 | | `quic_edge_protocol_test` | QUIC 控制帧、DATAGRAM 和 fake transport | | `quic_edge_task_test` | QUIC 任务配置和生命周期 | 仓库中其他以 `_test` 命名的程序可能需要真机、SDK、仿真环境或人工观察,不属于默认无设备验证。 ### 7.4 进程烟雾测试 默认 gRPC 端口没有被占用时: ```bash timeout \ --signal=TERM \ --kill-after=2s \ 5s \ ./output/bin/cmvr_es ``` 如果进程保持运行并在 5 秒后由 `timeout` 终止,退出码通常为 `124`,这是烟雾测试的预期结果。测试期间应同时检查日志中是否存在配置、设备初始化或端口绑定错误。 ## 8. 生产部署 ### 8.1 打包 部署时应分发整个 `output/`,不能只复制 `cmvr_es`: ```bash tar \ --exclude='./log/*' \ -C output \ -czf cmvr-es-linux-x86_64.tar.gz \ . ``` 目标主机需要使用相同 CPU 架构,并具有兼容的 Linux/glibc 环境。归档会排除开发主机上的历史日志。安装产物使用相对 RPATH: - `bin/cmvr_es` 从自身目录和 `../lib` 查找共享库; - `lib/` 中的共享库从自身目录查找依赖; - 正常部署不依赖系统全局 `LD_LIBRARY_PATH`。 `output/` 仍会动态依赖目标系统的 libstdc++、PulseAudio/ALSA、X11、libudev、libusb,以及按设备启用的驱动或厂商服务。目标机应安装对应运行时依赖,并在启动服务前执行: ```bash ldd -r /opt/cmvr-es/bin/cmvr_es ``` 命令输出中不应出现 `not found` 或未解析符号。 ### 8.2 推荐目录 ```text /opt/cmvr-es/ ├── bin/ ├── lib/ └── log/ /etc/cmvr-es/ ├── cmvr_es.pb.txt ├── logger/ ├── manager/ ├── devices/ ├── tasks/ └── certs/ ``` 示例: ```bash sudo install -d -m 0755 /opt/cmvr-es sudo tar -C /opt/cmvr-es -xzf cmvr-es-linux-x86_64.tar.gz sudo install -d -m 0750 /etc/cmvr-es sudo cp -a /opt/cmvr-es/bin/config/. /etc/cmvr-es/ sudo install -d -m 0750 /etc/cmvr-es/certs sudo install -d -m 0750 /opt/cmvr-es/log ``` 外部配置引用模型时,建议使用 `/opt/cmvr-es/bin/model/...` 的绝对路径,避免配置根切换后产生歧义。 ### 8.3 systemd 创建专用用户,并按实际硬件授予最小权限: ```bash sudo groupadd --system cmvr-es sudo useradd \ --system \ --gid cmvr-es \ --home /opt/cmvr-es \ --shell /usr/sbin/nologin \ cmvr-es sudo usermod -aG video,audio,dialout cmvr-es sudo chown -R cmvr-es:cmvr-es /opt/cmvr-es/log sudo chown -R root:cmvr-es /etc/cmvr-es sudo chmod -R g+rX /etc/cmvr-es # 按需安装 QUIC 信任链、客户端证书和私钥 sudo install -o root -g cmvr-es -m 0640 \ /secure/path/quic_gateway_ca.pem \ /etc/cmvr-es/certs/quic_gateway_ca.pem sudo install -o root -g cmvr-es -m 0640 \ /secure/path/cmvr_edge_cert.pem \ /etc/cmvr-es/certs/cmvr_edge_cert.pem # 私钥只允许实际运行进程的用户读取 sudo install -o cmvr-es -g cmvr-es -m 0600 \ /secure/path/cmvr_edge_key.pem \ /etc/cmvr-es/certs/cmvr_edge_key.pem ``` 根据设备情况还可能需要配置 udev、CAN 网卡、USB 权限和厂商 SDK 服务。不要为了方便直接让进程长期以 root 身份运行。 `/etc/systemd/system/cmvr-es.service` 示例: ```ini [Unit] Description=CMVR Edge System Wants=network-online.target After=network-online.target [Service] Type=simple User=cmvr-es Group=cmvr-es WorkingDirectory=/opt/cmvr-es/bin ExecStart=/opt/cmvr-es/bin/cmvr_es /etc/cmvr-es/cmvr_es.pb.txt Restart=on-failure RestartSec=3 KillSignal=SIGTERM LimitNOFILE=65536 [Install] WantedBy=multi-user.target ``` 启用服务: ```bash sudo systemctl daemon-reload sudo systemctl enable --now cmvr-es sudo systemctl status cmvr-es journalctl -u cmvr-es -f ``` 应用文件日志默认位于 `/opt/cmvr-es/log/cmvr_es.log`,终端路由的日志同时进入 journald。 ### 8.4 网络与防火墙 | 方向 | 默认端口 | 协议 | 用途 | | --- | ---: | --- | --- | | Edge -> QUIC Gateway | `4433` | UDP / QUIC | 节点注册、心跳、IP 和实时媒体 | | Platform -> Edge | `50052` | TCP / gRPC | 设备控制与状态查询 | | Browser -> Gateway | 平台定义,通常 `443` | HTTPS / WebTransport / WebRTC | Web 控制和媒体播放 | 如果平台根据心跳上报的 gRPC endpoint 回连边缘节点,平台网络必须能够访问该 TCP 地址。IP 上报成功不代表该地址一定具备路由、防火墙和 NAT 可达性。 ## 9. 平台端接入 ### 9.1 gRPC 接入 gRPC 契约位于 [`protos/cmvr/api/`](protos/cmvr/api/),通用消息位于 [`protos/cmvr/msgs/`](protos/cmvr/msgs/) 和 [`protos/cmvr/common/`](protos/cmvr/common/)。 当前实际注册的服务: | 服务 | 主要用途 | | --- | --- | | `cmvr.api.SystemService` | 系统信息、状态和 StopAll | | `cmvr.api.CameraService` | 相机状态、图片、录像和 RGB/Depth/RGBD 流 | | `cmvr.api.SpeakerService` | 播放、流式输入、暂停、音量 | | `cmvr.api.MicPhoneService` | 录音、音量和服务端音频流 | | `cmvr.api.DexHandService` | 灵巧手控制、传感器数据和双向流 | | `cmvr.api.BioHeadService` | 表情、状态、急停和双向流 | | `cmvr.api.ArmService` | 上使能、运动、伺服、停止和运动学 | | `cmvr.api.AgvService` | 导航、速度、地图、建图和地图流 | | `cmvr.api.HlcService` | 触屏 / HLC 交互 | `test_service.proto` 存在于协议目录,但当前没有注册到 gRPC Server,平台不能把它当作在线服务。 以下 RPC 虽然存在于 proto,但当前实现不可用: - `cmvr.api.SystemService/UpdateParams` 固定返回业务失败,提示该能力不再支持; - `cmvr.api.ArmService/getPoseMatrix` 返回 gRPC `UNIMPLEMENTED`; - `cmvr.api.ArmService/computeForwardKinematics` 返回 gRPC `UNIMPLEMENTED`。 平台生成客户端后仍应以服务端实现状态为准,不能把“proto 中存在”直接等同于“运行时已支持”。 流式方向: - Camera RGB、Depth、RGBD:双向流; - DexHand Sensor、BioHead Expression:双向流; - Microphone Audio、AGV Map:服务端流; - Speaker Audio:客户端流。 Java 平台建议通过 Gradle/Maven 的 Protobuf 插件生成消息类和 grpc-java stub,并在平台仓库中锁定 `protoc`、`protoc-gen-grpc-java`、`protobuf-java` 和 grpc-java 版本。生成时应保持 `protos/` 为 import 根目录。 当前 proto 没有统一声明 `java_package` 和 `java_multiple_files`。平台接入时不要自行修改包名后仍声称与线协议一致;如要补充 Java options,应由边缘端与平台端在同一协议变更中完成并重新生成两端代码。 平台调用流程建议: 1. 通过 QUIC 注册/心跳维护节点在线表; 2. 保存最新 `node_id`、`session_id`、`observed_source_ip`、本地接口和 advertised gRPC endpoint; 3. 按平台网络策略选择实际可达的 gRPC 地址; 4. 建立并复用 gRPC channel; 5. 为控制请求设置 deadline、幂等策略、错误映射和审计; 6. 不要因为 QUIC 在线就假设设备本身在线。 > [!WARNING] > 当前 gRPC Server 使用 `grpc::InsecureServerCredentials()`,没有 TLS、认证和授权。只能部署在可信内网、VPN、服务网格或受保护的网关后方。 ### 9.2 Java QUIC Gateway 接入 边缘端是 QUIC client,平台网关是 QUIC server。本仓库没有提供 Java 网关实现。 Java 网关只需要为 QUIC 控制面生成普通 Protobuf Java 类,不需要生成 gRPC stub: ```bash mkdir -p platform-gateway/src/main/java output/bin/protoc \ -I protos \ --java_out=platform-gateway/src/main/java \ protos/cmvr/quic_edge/v1/quic_edge.proto ``` 完整协议见 [`QUIC edge v1 wire format`](protos/cmvr/quic_edge/v1/README.md)。网关至少需要实现以下行为。 #### 可靠控制流 边缘端建立 QUIC 连接后打开一个双向可靠流。消息格式为: ```text uint32_be protobuf_length protobuf EdgeControlEnvelope ``` 接收端不能把一次 QUIC stream callback 当作一条完整消息,必须缓存并按长度前缀拆包。长度不包含 4 字节前缀。 会话顺序: 1. Edge 首先发送 `NodeRegisterRequest`; 2. Gateway 返回 `accepted=true` 且 `session_id` 非空的 `NodeRegisterResponse`; 3. Edge 开始发送 `NodeHeartbeat`; 4. Gateway 返回相同 `session_id` 和精确 `acknowledged_sequence` 的 `NodeHeartbeatAck`; 5. 有媒体轨道时,Edge 先调用可靠流发送 `MediaSessionOpen` 和 `MediaTrackDescriptor`,再调用 DATAGRAM 发送媒体; 6. 编码参数或 generation 变化时,Edge 同样先发送新的 descriptor,再发送对应 generation 的 DATAGRAM。 QUIC reliable stream 与 DATAGRAM 之间没有跨通道到达顺序保证。即使 Edge 按上述顺序调用发送,媒体 DATAGRAM 仍可能先于 session/descriptor 元数据到达 Gateway。Gateway 对未知 `session_epoch` 或 `codec_generation_token` 的 DATAGRAM 必须限量暂存或直接丢弃,不能无界等待,也不能假设可靠元数据一定先到。 实现细节: - `protocol_version` 当前为 `1`; - `message_sequence` 在每个方向独立严格递增,允许有间隔; - Edge 当前第一条控制消息的 sequence 是 `0`,网关不能假设从 `1` 开始; - 默认控制响应超时为 1 秒,注册或心跳 ACK 缺失/不匹配会触发重连; - 每次心跳包含最新接口地址和 advertised gRPC endpoint; - `observed_source_ip` 必须从已认证的 QUIC peer 地址推导,不能复制客户端上报字段; - 当前 Edge 接收方向实现支持 `NodeRegisterResponse`、`NodeHeartbeatAck` 和 `ProtocolError`; - 不要依赖 Gateway 下发 `MediaSessionClose` 来控制 Edge,本版本尚未实现该入站行为。 #### 媒体 DATAGRAM 每个媒体 DATAGRAM 以固定 64 字节、网络字节序的 `CMQD` header 开始。字段和偏移详见 [`QUIC edge v1 wire format`](protos/cmvr/quic_edge/v1/README.md#media-datagram-wire-format)。 网关需要: - 以 `(session_epoch, track_id, frame_sequence)` 作为重组键; - 校验 magic、版本、header size、fragment count、offset 和 frame size; - 在播放 deadline 到达时丢弃不完整帧,不等待旧帧阻塞新帧; - 新的 `session_epoch` 到来时清除旧连接的残留分片; - 根据 `MediaTrackDescriptor` 和 `codec_generation_token` 选择解码参数; - 容忍 DATAGRAM 先于可靠元数据到达,对未知 session/generation 有界暂存或丢弃; - 根据 keyframe 和 discontinuity 标志重置解码/转封装状态; - 为每个节点和轨道设置内存、帧大小、并发和超时上限。 当前媒体模型支持 H.264、H.265、Opus、AAC 和 PCM S16LE。实际可用编码取决于具体设备和配置。 ### 9.3 浏览器接入 浏览器不能直接连接 `cmvr-quic-edge/1` 自定义 ALPN,也不承担节点注册服务。推荐拓扑: ```text cmvr_es │ ├── QUIC reliable stream + DATAGRAM ──> Platform QUIC Gateway │ │ │ ├── WebTransport │ ├── WebRTC │ ├── WebSocket + MSE │ └── HTTP/HLS 等平台接口 │ └── gRPC control <──────────────────── Platform Service │ └── REST / WebSocket / gRPC-Web │ Browser ``` 平台网关负责: - 终止 CMVR QUIC 和维护节点在线状态; - 重组音视频帧; - 根据浏览器兼容性转封装或转码; - 对 Web 用户执行认证、授权、租户隔离和设备 ACL; - 处理慢浏览器、断线、关键帧请求和背压; - 将浏览器控制请求转换为平台到边缘端的 gRPC; - 向浏览器隐藏设备密码、证书和内部 gRPC 地址。 H.265、AAC 等格式的浏览器支持度并不统一。平台不能假设收到的原始编码能够在所有浏览器中直接播放,应根据目标浏览器选择 WebRTC codec、fMP4/MSE、转码或降级策略。 ### 9.4 平台联调清单 建议按以下顺序联调: 1. Edge 保持零媒体轨道,验证 UDP、TLS、ALPN 和注册; 2. 验证 `observed_source_ip`、本地接口列表和 gRPC endpoint 更新; 3. 验证精确 heartbeat ACK、超时和重连; 4. 使用 grpcurl 或 Java client 验证平台到 Edge 的 gRPC 可达性; 5. 启用一个 fake/真实摄像头轨道,验证 descriptor 和 DATAGRAM 重组; 6. 模拟丢包、乱序、重复、超时和 Edge 重连; 7. 再启用音频、多轨道和多节点; 8. 最后接入浏览器转封装、权限和运维监控。 平台应至少监控: - 节点在线数、session 变化和最后心跳时间; - QUIC 握手/注册失败原因; - 心跳 RTT、ACK 超时和重连次数; - 每轨道 DATAGRAM、完整帧、丢帧和重组超时; - descriptor/generation 变化; - gRPC 调用时延、错误码和设备离线状态; - 网关内存、队列深度和浏览器订阅数。 ## 10. 开发扩展指南 ### 10.1 新增同类设备后端 以新增摄像头后端为例: 1. 在对应 config proto 的 `oneof backend` 中增加后端配置; 2. 实现已有摄像头抽象接口,不在设备类中加入 gRPC/QUIC 连接状态; 3. 在类别工厂中增加具体后端创建分支; 4. 增加 CMake target 和 SDK 查找; 5. 在设备集合配置中增加实例; 6. 在 `DeviceManager` 配置中增加相同 ID 的条目; 7. 实时媒体设备补齐编码描述、时间戳、序列号、generation 和关键帧能力; 8. 增加不依赖真机的生命周期与并发测试,再进行真机测试。 设备抽象应保持薄且与具体控制器协议无关。控制器专有结构、错误码和报文字段应留在具体后端。 ### 10.2 新增设备类别 除上述工作外,还需要: - 增加新的抽象接口与类别工厂; - 扩展 `DeviceConfigEntry::DeviceType`; - 在全局 `DeviceFactory` 注册 creator; - 在 `DeviceManager` 中增加类型实例化和日志名称; - 如需平台调用,增加 API proto、gRPC service 实现,并在 `GrpcServerTask` 中注册; - 明确设备关闭、急停和进程退出时的生命周期语义。 ### 10.3 新增任务 1. 增加任务配置 proto 和默认配置; 2. 实现 `Task` 接口; 3. 明确任务是 `PERIODIC_STEP` 还是 `BLOCKING_SERVICE`; 4. 扩展 `TaskType` 并注册 TaskFactory creator; 5. 在 `TaskManager` 默认配置中增加任务; 6. 增加 CMake target 和 init/start/stop 测试。 ### 10.4 新增媒体传输协议 后续接入 WebRTC、SRT 或其他协议时: 1. 直接订阅 `MediaSourceHub`; 2. 每个消费线程创建独立 `Subscription`; 3. 将 `TrackDescriptor` 映射为目标协议的 codec metadata; 4. 根据 dropped count、generation 和 frame 标志传播 discontinuity; 5. H.264/H.265 丢帧后请求关键帧; 6. 协议断开或停止时及时 reset subscription; 7. 不要修改摄像头或麦克风接口来保存协议连接状态; 8. 增加慢消费者、环形队列覆盖、设备重启和并发停止测试。 这一约束保证 MediaSourceHub 能快速适配其他协议,同时设备层保持稳定。 ### 10.5 修改 Proto 的兼容性要求 - 不复用已经发布字段的 tag; - 删除字段时使用 `reserved`; - 优先增加可选字段,不随意改变现有语义; - gRPC API 和 QUIC v1 线协议分别维护版本兼容; - 修改 QUIC DATAGRAM 固定头时必须提升协议版本并同步平台网关; - 提交前重新生成并验证 C++、Java 代码; - 协议文档、示例配置和自动测试必须与代码同一提交更新。 ## 11. 安全说明 生产环境必须关注: - gRPC 当前是明文且无认证授权,不能直接暴露到公网; - QUIC 正式环境必须验证服务端证书,必要时使用 mTLS; - `allow_insecure` 仅用于开发; - QUIC schema 当前没有业务 token 或设备 ACL,应用级身份和授权需要平台设计; - 配置可以包含设备账号、密码、私钥和生产地址,不要提交真实凭据; - 私钥和生产配置建议权限为 `0600`,由专用运行用户读取; - 平台应限制允许控制的用户、节点、设备和 RPC; - 机械臂、AGV 等控制必须设置 deadline、速度/位置限制、急停和审计; - 设备控制安全不能只依赖网络协议成功,仍需设备侧限位和安全控制器。 ## 12. 已知限制与排障 ### 12.1 首次配置提示找不到 protoc 或 grpc_cpp_plugin 确认已执行[首次构建工具引导](#51-首次构建工具引导),并在同一 shell 中保留 gRPC 依赖库的 `LD_LIBRARY_PATH` 后重新配置和编译。 ### 12.2 CMake 显示 MsQuic not found 这不会影响 gRPC 和 fake transport 测试,但当前产物不能启动 QUIC 任务。检查: - `CMVR_ENABLE_MSQUIC_BACKEND=ON`; - `CMVR_MSQUIC_ROOT` 使用绝对路径; - `/include/msquic.h` 存在; - `/lib/libmsquic.so`、`lib64/` 或 `bin/` 中存在库; - 重新配置时清理了错误的 CMake cache。 ### 12.3 启动后 gRPC connection refused 检查: ```bash ss -lntp | grep 50052 journalctl -u cmvr-es -n 200 --no-pager ``` 常见原因: - `grpc_server` 任务被关闭; - 端口被其他进程占用; - 主进程因配置或设备初始化错误提前退出; - 平台访问的是不可路由的心跳上报地址; - 主机或容器防火墙未开放 TCP 50052。 ### 12.4 reflection 正常但设备 RPC 失败 reflection 只验证服务注册。继续检查: - 设备配置和 `DeviceManager` 条目是否都启用; - 两处设备 ID 是否一致; - 设备 SDK、串口、CAN、USB 或网络是否可用; - 运行用户是否具备权限; - 日志中设备 `init()` 是否成功。 ### 12.5 QUIC 持续重连 按顺序检查: 1. Edge 到 Gateway 的 UDP 端口是否可达; 2. ALPN 是否精确为 `cmvr-quic-edge/1`; 3. CA、服务端证书和 `server_name` 是否匹配; 4. Gateway 是否返回 accepted 且非空 session ID; 5. heartbeat ACK 的 session 和 sequence 是否精确匹配; 6. Gateway 是否在默认 1 秒控制响应超时内回复; 7. 是否错误地把 stream callback 边界当作消息边界。 ### 12.6 找不到共享库或出现未解析符号 检查: ```bash ldd output/bin/cmvr_es ldd -r output/bin/cmvr_es find output/lib -maxdepth 1 -type f -name '*.so*' -print ``` 部署时确认复制了完整 `output/`,并保持 `bin/` 与 `lib/` 的相对位置。不要混用不同构建、不同架构或不兼容 glibc 的产物。 ### 12.7 安装后现场配置被覆盖 `cmake --install` 会重建 `output/bin/config/` 和 `output/bin/model/`。生产配置应保存在 `/etc/cmvr-es/` 等外部目录,并通过命令行显式传入根配置。 ### 12.8 Git submodule 初始化失败 当前仓库不应执行旧文档中的 `git submodule update --init --recursive`。构建依赖来自已跟踪的 `dependency//third_party/`;`.gitmodules` 与 `assets/toppra` 的历史不一致需要单独整理。 ### 12.9 当前未提供的能力 本仓库当前不包含: - Java QUIC Gateway; - 浏览器播放器或 Web 控制前端; - Dockerfile / 容器镜像; - 可直接安装的 systemd unit; - 标准 gRPC health service; - gRPC TLS、认证和授权; - 完整可复现的 ARM 构建。 这些能力不能仅通过修改配置开启,需要在对应工程中实现和验证。 ## License 许可证内容见 [`LICENSE`](LICENSE)。同时请遵守 `dependency/` 中各三方组件和厂商 SDK 的独立许可要求。