cmvr-es/cmvr-es/service
2026-08-18 16:17:46 +08:00
..
grpc chore: remove branch-only tests and generated artifacts 2026-08-18 16:17:46 +08:00
quic_edge refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00
CMakeLists.txt refactor: reorganize service and manager modules 2026-08-17 09:48:42 +08:00
README.md chore: remove branch-only tests and generated artifacts 2026-08-18 16:17:46 +08:00

Service 模块开发指南

service/ 实现边缘端对外协议和设备抽象之间的适配。Service 负责解析请求、查找设备、转换 DTO 和返回结果,不负责创建具体设备后端。

返回项目总览。

当前结构

service/ 顶层只按传输协议保留两个子目录:grpc/ 和 quic_edge/。gRPC 内部再按运行角色分层,避免把队列、停止控制、客户端和服务端实现混在同一层。

service/
├── grpc/
│   ├── action/                    # ActionQueue 校验、账本和 FIFO 执行器
│   ├── client/                    # 边缘端使用的 gRPC client
│   ├── server/                    # gRPC service、协调器和安全扩展
│   │   ├── include/
│   │   ├── src/
│   │   └── tests/
│   └── stop_all/                  # 高优先级 StopAll 通道
└── quic_edge/                     # QUIC client、控制状态机和媒体 packetizer
目录 职责
grpc/action/ SystemService ActionQueue 的校验、幂等账本和边缘端 FIFO 执行器
grpc/client/ 面向边缘端内部调用的 gRPC client
grpc/server/ 入站设备控制、状态查询、兼容流式接口和安全控制面
grpc/stop_all/ 不进入普通命令队列的高优先级停止通道
quic_edge/ 边缘端主动连接平台的 QUIC client、控制状态机和媒体 packetizer
quic_edge/tests/ 已登记到 CTest 的 QUIC 协议测试

两个遗留 gRPC client test 位于 grpc/server/tests/*_client_test.cpp,当前没有通过 add_test() 登记;它们是历史可执行文件,不代表默认自动覆盖。

gRPC 和 QUIC 的职责边界:

  • 机械臂、AGV 等可靠控制继续使用 gRPC;
  • 节点注册、心跳和 IP 上报使用 QUIC reliable stream;
  • 实时音视频使用 QUIC DATAGRAM;
  • quic_edge/ 不是平台 Gateway,也不是浏览器服务器。

SystemService 设备清单

SystemService/GetDeviceList 返回 DeviceManager 的当前只读快照,只包含 enabled=true 的设备。启用但创建、初始化、启动或健康检查失败的设备仍会返回, 并通过 manager_state、health、has_error 和 error_message 描述异常。 接口同时返回稳定的 device_type 和仅用于展示/诊断的具体 type_name;调用方 不得使用 type_name 做设备类别判断。

该 RPC 不修改配置、不动态注册设备,也不触发设备生命周期操作。启用 reflection 后可直接查询:

grpcurl -plaintext \
  -d '{}' \
  127.0.0.1:50052 \
  cmvr.api.SystemService/GetDeviceList

SystemService ActionQueue

SystemService/ExecuteActionQueue 接收一个完整的有限动作序列,在边缘端排队并 逐步串行执行,所有步骤结束后返回最终结果。平台只需要提交一次请求,因此连续机械臂 动作不会再受到每个单独 gRPC 往返和 Wi-Fi 抖动的影响。

当前 v1 仅允许以下 ActionStep.command:

  • 机械臂同步 MoveJ、MoveL;
  • AGV 同步 navigateToPose、navigateToStation、followPath;
  • 边缘端本地 delay。

机械臂和 AGV 请求复用各自已有的类型化 Request,目标设备仍由每一步的 header.device_id 指定。所有运动步骤必须设置 asynchronous=false;MoveL v1 仅接受 Base frame;AGV 后端还必须明确支持同步导航终态确认。speedJ、speedL、servoJ、 AGV translate、速度控制、查询和流式 RPC 都不属于 ActionQueue v1。

ActionQueue 遵循以下执行语义:

  • 平台先调用 GetSystemInfo 读取 action_service_instance_id,并在每次提交和重试中填入 expected_service_instance_id。ActionQueue 账本随服务实例重建;若断线期间边缘服务重启, 旧实例 ID 会被拒绝,平台必须先对账,不能用新 ID 自动重放不确定的动作;
  • action_id 是必填的全局唯一幂等键;同一服务实例内,相同内容的已受理请求不会重复下发 设备命令,相同 ID 但内容不同的请求必须拒绝。服务端缓存最近 4096 个完整结果,更早的 已执行 ID 由精确 retired-ID 账本 fail-closed 拒绝、不会重跑;单实例最多记录 262144 个已受理 ID,达到容量后仅拒绝新 ID,已有 ID 仍可查询;
  • 入队前校验全部步骤、设备、参数和同步能力,校验失败时不会执行任何步骤;
  • v1 每个请求最多 256 步、序列化大小最多 512 KiB、排队或执行中的 Action 最多 64 个、 同时提交或等待结果的 RPC 最多 256 个;Action 与单步超时上限均为 24 小时, total_timeout_ms=0 使用 30 分钟默认值,AGV 路径最多 4096 段;
  • total_timeout_ms 包含排队与执行时间,单步 timeout_ms=0 时继承 Action 剩余时间 或服务端默认值;所有超时值均由服务端施加上限;
  • 任一步失败、取消或超时后立即停止序列,不再执行后续步骤;completed_steps 表示此前 成功完成的步骤数,failed_step_index 仅在存在对应失败步骤时出现;
  • Action 一旦受理,不因平台连接中断而自动取消;断线只结束该 RPC waiter,边缘动作继续。 平台可用相同 action_id 重试并取得仍在缓存中的同一次执行结果;
  • StopAll、机械臂 stopMotion、AGV cancelNavigation 和软件急停不进入 FIFO,必须 作为高优先级安全/抢占路径执行。它们仍不具备功能安全等级。
  • 机械臂步骤超时会立即走 typed stopMotion 并等待停车确认;若无法确认停车,设备控制权 保持隔离,不会继续后续步骤或接受新的普通控制命令;需先按设备安全流程确认状态,再 重启边缘服务恢复控制。
  • AGV 的取消 ACK、零速度 ACK 均不等于停稳;ActionQueue 和安全停止 RPC 只有在导航任务 终态且底盘连续零速度采样确认后才释放控制权,否则同样保留隔离。

已知的执行完成、业务失败、取消、超时和预校验拒绝由 ActionResultCode 与 CommandHeader.Feedback 表达。ActionDeduplicationStatus 结构化区分新受理、合并等待、 缓存结果、已淘汰结果、账本耗尽、ID 冲突和服务实例不匹配;平台不得通过解析错误字符串 判断动作是否执行过。 ACTION_RESULT_CODE_UNSPECIFIED 不得作为服务端最终结果。

MotorService

MotorService 将 gRPC 电机命令适配到已经由 DeviceManager 创建的 MotorManager 和 AbstractMotor,不直接持有现场总线或厂商驱动。

关键文件:

服务按单电机仲裁。同步 Profile 命令、Cyclic Position/Velocity 双向流、 setEnabled、状态读取和软件 emergencyStop 共用同一控制权状态:

  • 同一电机已有 owner 时拒绝新的控制调用;
  • cyclic 流首帧必须是 open,后续 setpoint sequence 必须严格递增;
  • reader 使用 latest-wins 邮箱,客户端必须持续并发读取反馈;
  • 取消、deadline、watchdog、非法帧、后端拒绝或写失败都会触发 Quick Stop;
  • 任何清理 Quick Stop 未确认时,服务进入 fail-closed 锁存;
  • 只有成功执行 setEnabled(true) 才解除服务内软件急停锁存;
  • 服务层 Quick Stop 和 emergencyStop 都不具备功能安全等级。

AUBO 控制柜 IO 不经过 MotorService,由 ArmService/ExecuteJsonCommand 转发到目标 RobotArm。厂商命令和安全约束见 AUBO 控制柜 IO。 旧的 SystemService/ExecuteJsonCommand 已移除;相机 PTZ 应使用类型化的 CameraService/ControlPtz。

新增 gRPC Service

当前没有动态 service registry,必须完成以下全部步骤。

1. 定义 Proto

在 ../../protos/cmvr/api/ 增加或扩展:

  • <domain>_command.proto
  • <domain>_service.proto

import 路径必须相对于 protos/。兼容规则见 ../../protos/README.md。

2. 实现 Service

目录约定:

service/grpc/
└── server/
    ├── include/grpc_example_service.h
    └── src/grpc_example_service.cpp

实现类继承生成的:

cmvr::api::ExampleService::Service

通过已初始化的 DeviceManager 获取抽象设备。不要在 service 中创建厂商 SDK 对象,不要绕过设备 factory。

3. 加入 service target

将实现 .cpp 加入 CMakeLists.txt 的 service library,并声明最小依赖。

4. 注册到 GrpcServerTask

还必须修改:

完成:

  1. 增加 service owner;
  2. 在 start 中构造;
  3. 调用 builder.RegisterService(...);
  4. 在 clearServices() 中 reset。

漏掉该步骤时项目可能编译成功,但服务不会出现在 reflection 或运行时。

5. 测试

  • 直接测试 service handler 或启动临时 gRPC server;
  • 覆盖设备不存在、类型不匹配、设备错误和取消;
  • 使用 grpcurl/reflection 验证服务全名;
  • 流式 RPC 覆盖客户端断开和慢消费者;
  • 在 CMake 中使用 if(BUILD_TESTING) 包裹测试目标,并通过 add_test() 登记。

grpc_arm_client_test 和 grpc_hlc_client_test 是未登记到 CTest 的历史可执行文件,不能代表默认自动覆盖。

gRPC 实现约束

错误语义

当前历史服务存在两种风格:

  • gRPC status 返回 OK,业务失败写入 Feedback header;
  • 使用非 OK gRPC status 表达 transport/API 失败。

扩展已有服务时保持其兼容语义。新增服务必须在设计时明确:

  • 哪些错误使用 gRPC status;
  • 哪些错误使用业务 Feedback;
  • 是否允许部分成功;
  • deadline/cancellation 如何映射;
  • 不得同时返回互相矛盾的 transport 和业务状态。

流式 RPC

  • 检查 context->IsCancelled();
  • 检查 Read() / Write() 返回;
  • 使用 RAII 或 MediaSourceManager Subscription 释放 producer lease;
  • 不持有设备状态锁进行网络写;
  • 为 wait/read 使用有限 timeout;
  • 慢客户端不能阻塞设备生产线程;
  • H.264/H.265 丢帧后等待关键帧恢复;
  • gRPC RGB 流在积压超过 camera_stream_max_pending_frames 或帧龄超过 camera_stream_max_frame_age_ms 时主动丢弃旧帧,请求 IDR,并从下一个关键帧恢复。

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

gRPC 相机实时流默认最多保留 2 帧积压、最大允许 250 ms 帧龄。两个配置项填 0 时使用上述默认值。该策略以低延迟为目标,不保证每个视频帧都到达客户端;控制命令 仍由普通 gRPC RPC 承担。

FrameData 附带 capture_utc_ns、source_sequence、pts、dts、 source_fps、source_timestamp 和 source_frame_number。平台端可用采集时间 与接收时间的差值区分设备、网络、服务端写阻塞和客户端解码/渲染队列延迟。新增字段 保持 protobuf wire compatibility,旧客户端可以继续连接,但需要重新生成代码后才能 读取这些诊断字段。

当前安全状态

GrpcServerTask 使用同步 grpc::ServerBuilder 和 grpc::InsecureServerCredentials()。reflection 由配置控制。当前没有 gRPC TLS、认证、授权或标准 health service。

QUIC 配置中的 grpc_endpoint_tls 只是上报字段,不会启用 gRPC TLS。

扩展 QUIC Edge

关键层次:

层 主要文件
控制流 framing quic_edge/src/control_framing.cpp
DATAGRAM 固定头和分片 quic_edge/src/datagram_packetizer.cpp
会话和状态机 quic_edge/src/quic_edge_service.cpp
传输抽象 quic_edge/include/quic_transport.h
MsQuic 后端 quic_edge/src/msquic_transport.cpp
设备媒体适配 quic_edge/src/quic_edge_device_adapter.cpp
DeviceManager 心跳适配 quic_edge/src/quic_edge_device_adapter.cpp

线协议见 ../../protos/cmvr/quic_edge/v1/README.md。

新增 Transport 后端

  1. 实现 QuicTransport 完整接口;
  2. 明确 callback 所在线程;
  3. stop/close 后不得再访问已销毁 service;
  4. QUEUED 表示 transport 接管待发送数据;
  5. WOULD_BLOCK 或 ERROR 不得接管任何字节;
  6. DATAGRAM batch 本地准入必须原子;
  7. native send 部分失败时关闭连接并清理 session;
  8. 添加 fake transport 故障注入测试;
  9. 在默认 transport factory 中显式选择后端。

新增控制消息

不能只修改 Proto,还要同步:

  • envelope 构造与发送;
  • 入站 dispatch;
  • 合法状态和消息时序;
  • message sequence 校验;
  • session ID 和 heartbeat sequence 校验;
  • reconnect 后状态清理;
  • Java Gateway 对端;
  • framing、状态机和 fake transport 测试。

当前 Edge 入站只接受:

  • NodeRegisterResponse
  • NodeHeartbeatAck
  • ProtocolError

虽然 Proto 定义了 MediaSessionClose,本版本 Edge 收到它仍会判为 unexpected,不应将其描述为已实现的双向控制能力。

DeviceManager 心跳快照

QuicEdgeService 通过可注入的 DeviceSnapshotProvider 获取协议无关的纯值 快照。生产构造绑定已经初始化的 DeviceManager,fake transport 测试则注入 合成快照,因此协议状态机不需要创建硬件对象或依赖 DeviceManager 单例。 DeviceManager 的本地快照继续保留禁用设备;QUIC wire 映射层仅序列化 enabled=true 的设备。已启用但创建、初始化或启动失败的设备不会被过滤。

心跳线程只读取 Manager 维护的内存状态,不能在这里同步访问厂商 SDK、网络或 设备总线。新增设备健康探针必须实现 AbstractDevice::healthSnapshot() 的 线程安全、无阻塞 I/O 契约;未实现时上报 UNSPECIFIED,不得伪造为健康。 设备异常字符串会限长,整条消息仍受 maximum_control_frame_bytes 约束。

跨 QUIC 通道顺序

Edge 会先调用可靠流发送 session/descriptor,再调用 DATAGRAM 发送媒体,但 QUIC stream 与 DATAGRAM 没有跨通道到达顺序保证。

Gateway 必须容忍 DATAGRAM 先到,对未知 session epoch 或 codec generation 的数据有界暂存或丢弃。

QUIC 测试要求

参考 quic_edge/CMakeLists.txt 和 quic_edge_protocol_test,至少覆盖:

  • 控制消息拆包、粘包和超限;
  • 重复或倒退 sequence;
  • 注册、ACK 超时和重连;
  • DeviceManager 已启用设备过滤、类型/状态映射和 provider 失败隔离;
  • Gateway 返回零心跳周期时采用本地 heartbeat_interval_ms;
  • session epoch 清理;
  • DATAGRAM header 字节序;
  • 分片边界和超大帧;
  • 原子队列准入和背压;
  • 丢帧、generation 和关键帧恢复;
  • stop 与 callback 并发。
cmake --build build --target quic_edge_protocol_test
ctest \
  --test-dir build \
  -R '^quic_edge_protocol_test$' \
  --output-on-failure

提交检查

  • service 不创建具体硬件后端
  • Proto、实现、CMake 和 GrpcServerTask 注册均已更新
  • 错误语义与已有服务兼容
  • deadline、cancel、Read/Write 失败均处理
  • 流式资源通过 RAII 释放
  • gRPC 安全能力没有被配置字段误描述
  • QUIC 状态机与 Gateway 同步更新
  • 协议测试已登记到 CTest