cmvr_edge_ai/README.md
2026-07-21 12:17:13 +08:00

25 KiB
Raw Blame History

cmvr-edge-ai

cmvr-edge-ai 是部署在机器人边缘端的、配置驱动的 AI 流水线运行时。它把上游传感器、AI 算法和下游机器人/平台接口拆成可组合节点,通过 YAML 把节点连接成有向无环图DAG。协议只出现在边界连接器中算法节点使用统一的内部消息因此以后增加 HTTP、gRPC、UDP 或 QUIC 时不需要改动算法本身。

当前版本已经包含可运行的检测链路和框架基础设施:

  • 严格 YAML 配置校验、环境变量展开和 DAG 编译检查;
  • Source -> Operator -> Sink 异步运行时;
  • 每条边独立的有界队列和明确的溢出策略;
  • 版本化插件注册表、检测模型注册表和 Python entry point 扩展机制;
  • cmvr-es RGB 相机 gRPC Source带退避上限的指数重连、AGV gRPC Sink
  • PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点;
  • Construction PPE YOLOv8 与六类 PPE YOLOv8n 模型注册,以及按标签、置信度和最大 FPS 的部署配置;
  • 平台 HTTP JSON Sink支持有限重试、raise/log_and_drop 失败策略,并使用告警 event_id 作为幂等键;
  • RobotCommand -> ApprovedRobotCommand 安全门和无重试的类型化 AGV 命令映射;
  • 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client
  • 可直接执行的最小测试 fixture以及包含 PPE 检测和对话占位链路的统一部署配置。

configs/edge_ai.yaml 是统一部署配置,其中同时定义 detectiontalk 两个 Pipeline。detection 当前只启用 Construction PPE 模型,并经过时间窗口规则向 8081 上报告警。六类 PPE 模型及第二 HTTP 平台的实现仍保留在注册表和连接器中,但不在当前 Pipeline 图中实例化,因此不会加载第二份权重、执行第二次推理或访问 8082。 VAD/ASR/LLM/TTS 尚未内置;talk 仍使用模拟音频数据,等待 cmvr-es 音频双向流 proto 落地。

架构概览

flowchart LR
    A["cmvr-es / 平台<br/>gRPC、HTTP、未来 UDP/QUIC"] --> B["Source 连接器"]
    B --> C["每条边独立的有界队列"]
    C --> D["Operator DAG<br/>解码、检测、VAD、ASR、LLM、TTS、策略"]
    D --> E["Safety Gate"]
    D --> F["平台 Sink"]
    E --> G["机器人执行器 Sink"]

内部节点传递 Envelope[T],其中包含载荷、schema/version、采集时间、序号、截止时间、trace_idsession_id。连接器负责 protobuf/HTTP JSON 与内部契约之间的转换AI 插件不应直接依赖 cmvr-es protobuf。

完整设计和配置字段见 docs/architecture.md

目录

cmvr_edge_ai/
├── .python-version                # uv 默认 Python 3.10
├── uv.lock                        # 所有 profile 的可复现依赖锁
├── configs/
│   ├── edge_ai.yaml               # detection + talk 统一部署配置
│   └── debug/
│       └── detection_viewer.yaml  # 远端相机 -> PPE 检测 -> 本地画框窗口
├── detect_server/
│   ├── README.md                  # PPE 检测链路与 Viewer 使用说明
│   └── show_detections.py         # OpenCV 实时检测结果 Demo
├── models/
│   └── detection/                 # 按模型 ID/版本组织的检测模型制品库
│       ├── construction-ppe-yolov8/v1/ # best.pt + 独立 model card
│       └── ppe-6classes-yolov8n/v1/    # best.pt + 独立 model card
├── talk_server/
│   └── nodes/                     # 对话插件预留目录
├── scripts/
│   ├── bootstrap.sh               # 一键创建 uv 环境、生成 bindings 并自检
│   └── generate_cmvr_stubs.py     # 从 cmvr-es proto 生成 Python bindings
├── src/cmvr_edge_ai/
│   ├── config/                    # 配置模型、加载与环境变量展开
│   ├── core/                      # Envelope、组件接口、队列和 DAG 运行时
│   ├── contracts/                 # 协议无关的图像、音频、AI 和控制契约
│   ├── detection/                 # 模型注册、视频解码、推理与时间窗口规则
│   ├── plugins/                   # 插件注册、发现和内置基础插件
│   ├── connectors/                # cmvr-es 与平台边界连接器
│   ├── transports/                # gRPC/HTTP 连接池UDP/QUIC 扩展位置
│   ├── workers/                   # 显式线程 offload 与常驻进程 Worker 工具
│   ├── observability/             # 低开销文本/JSON 日志
│   ├── application.py             # 多 Pipeline 与共享网络客户端的所有者
│   ├── compiler.py                # 配置到可执行 DAG 的编译器
│   └── cli.py                     # validate/run/plugins/models
└── tests/
    └── fixtures/
        └── minimal_pipeline.yaml  # 不依赖外部服务的框架/CLI 自检配置

快速开始

项目使用 uv 管理 Python、.venv 和锁定依赖。.python-version 默认选择 Python 3.10,支持范围是 3.103.12首次执行时uv 会在本机没有合适解释器时 自动安装。先确认已经安装 uv

uv --version

未安装时可使用 uv 官方安装器:

curl -LsSf https://astral.sh/uv/install.sh | sh

只验证框架和模拟对话链路时,一条命令创建最小环境并自检:

cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh --profile core

uv.lock 是安装的唯一版本来源bootstrap 使用 uv sync --locked,不会在用户 机器上重新选择依赖版本。无需 source .venv/bin/activate,统一通过 uv run --no-sync 使用已经安装好的环境:

配置中的相对文件路径按进程启动时的当前工作目录(cwd)解析,不是按 YAML 文件所在目录解析。因此本文的 bootstrap、validate、run 和 Viewer 命令都应从仓库根目录 /home/xtkuang/Projects/cmvr/cmvr_edge_ai 执行;从其他目录启动时,必须把 配置中的模型等文件路径改为正确的绝对路径。

uv run --no-sync cmvr-edge-ai validate \
  --config tests/fixtures/minimal_pipeline.yaml
uv run --no-sync cmvr-edge-ai plugins
uv run --no-sync cmvr-edge-ai run \
  --config tests/fixtures/minimal_pipeline.yaml \
  --log-level INFO \
  --log-format text

预期会看到两条日志,内容分别包含 framework-readybounded-dag-running。该 Source 是有限数据源,数据处理完后进程会自然退出。

bootstrap 支持以下环境:

Profile 安装内容 命令
core 框架核心、最小测试 fixture 和模拟 talk 链路 bash scripts/bootstrap.sh --profile core
detection-cpu gRPC、HTTP、PyAV、Pillow 告警图片和固定版本 CPU YOLO默认值 bash scripts/bootstrap.sh
dev detection-cpu 加测试和 protobuf codegen 工具,并运行完整测试 bash scripts/bootstrap.sh --profile dev

如果只希望安装、不执行自检,可加 --skip-check。完整参数通过以下命令查看:

bash scripts/bootstrap.sh --help

不要使用 uv sync --all-extrasyoloyolo-cpu 是为不同 PyTorch 来源准备的 互斥环境。请使用 bootstrap profile或显式只选择其中一个 extra。 告警图片的画框和 JPEG 编码由独立的 image extra 提供;默认的 detection-cpu/dev profile 已安装它,手动组合检测环境时也必须选择 --extra image

CLI 的四个子命令如下:

命令 用途
validate -c FILE [--pipeline ID] 加载配置、展开环境变量、构造插件并校验 DAG不启动 Pipeline
run -c FILE [--pipeline ID] 启动选中的 Pipeline未指定时启动所有 enabled: true 的 Pipeline
plugins 列出内置插件和已安装 entry point 插件
models 列出检测模型 ID、名称、backend 和注册的全部标签

--pipeline 可以重复传入。run 还支持 --log-level--log-format text|json。配置错误退出码为 2,运行错误为 1,键盘中断为 130

生产配置 configs/edge_ai.yaml 在一个 YAML 中同时定义 detectiontalk。部署时 建议显式传 --pipeline detection--pipeline talk,这样进程只加载并运行选中的 链路;需要同进程运行两条链路时,可以重复传两个 --pipeline。如果完全省略 --pipeline,运行时会启动配置中所有 enabled: true 的 Pipeline。

运行 PPE 检测链路

默认 bootstrap 就是当前 YAML 使用的 CPU 检测环境。它会从相邻的 ../cmvr-es 读取 proto、用锁定的 grpcio-tools 生成 bindings然后安装完整 检测依赖并校验最小测试 fixture 和统一配置中的 PPE 链路:

cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh

如果 cmvr-es 不在相邻目录,显式指定路径:

bash scripts/bootstrap.sh \
  --cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es

bootstrap 默认使用 portable codegen所以不要求 cmvr-es 已经编译出 protoc。 要跳过生成(例如部署包已经包含匹配版本的 bindings使用 --skip-codegen。手动生成时可以执行:

uv sync --locked --only-group codegen
.venv/bin/python scripts/generate_cmvr_stubs.py \
  --cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es \
  --portable

生成文件默认写入 src/cmvr/...,使 cmvr.api.*_pb2 可以被连接器导入。 生成后应再次执行目标 profile 的 uv sync --locked让可编辑安装识别新包bootstrap 已经按这个顺序处理。

configs/edge_ai.yamldetection Pipeline 中配置部署参数:

endpoints:
  cmvr_es:
    target: 127.0.0.1:50052
  ppe_alert_platform:
    base_url: http://127.0.0.1:8081

pipelines:
  detection:
    nodes:
      camera:
        with:
          device_id: right_hand_cam
          stream_log_interval_s: 5
      detector:
        with:
          attach_frame: true
          inference_log_interval_s: 5
          model_options:
            weights: models/detection/construction-ppe-yolov8/v1/best.pt
            device: cpu
      repeat_gate:
        with:
          alert_image:
            enabled: true
            jpeg_quality: 85
      alert_platform:
        with:
          endpoint: ppe_alert_platform
          failure_mode: log_and_drop

然后验证并运行检测链路,不再要求预先导出环境变量:

uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
  --config configs/edge_ai.yaml \
  --pipeline detection
uv run --no-sync cmvr-edge-ai run --config configs/edge_ai.yaml \
  --pipeline detection \
  --log-level INFO \
  --log-format json

启动后detector 会先输出一条 detection model loaded,表示对应权重已经成功 加载。收到解码帧并完成真实 predict 后,会立即输出第一条 detection inference,之后 按 inference_log_interval_s 聚合输出一次;其中 window_frames 是本周期推理帧数, window_detections 是检测框总数,hit_labels 是各标签的检测框累计数。持续只有 loaded 而没有 inference说明相机或 decoder 尚未把帧送到模型inference 中 detection 为 0 只表示 当前阈值下没有命中。短时调试可设为 1 秒,生产环境可设为 3060 秒,省略则关闭 周期推理日志。这里使用标准日志而不是裸 print,因此与 --log-format json 兼容。

如果需要直接观察每次推理对应的画框图像,使用独立的 OpenCV Demo。它连接同一个 cmvr-es gRPC CameraService但不经过重复触发规则也不会访问 HTTP 平台:

uv run --no-sync python detect_server/show_detections.py \
  --config configs/debug/detection_viewer.yaml \
  --pipeline detection_show \
  --log-level INFO

运行前在 configs/debug/detection_viewer.yaml 中配置远端地址、device_id 和模型权重;按 qEsc 退出。详细说明见 detect_server/README.md

相机连接器会在每次首次连接或重连时先发 CameraService.StartCamera,收到成功反馈后 才建立 GetRGBImageStream。终端会依次出现 camera start requested/succeededcamera stream openingcamera stream first frame 和周期性的 camera stream progress。 如果只有 opening 而没有 first frameprogress 中会持续显示 first_frame_received=false window_frames=0用于区分“RPC 已建立但相机没有出帧”。

运行前需要确认 Construction PPE 权重存在、cmvr-es 已启用 right_hand_cam8081 平台的 /v1/detection-alerts 可访问。默认 yolo-cpu profile 将 PyTorch 2.7.0 和 torchvision 0.22.0 绑定到官方 CPU wheel并固定 checkpoint 记录的 Ultralytics 8.4.31。model_options.device 在该环境中应保持 cpuhalf 应保持 false

x86 CUDA 和 Jetson/JetPack 的 PyTorch wheel 与驱动强绑定,不能复用这个 CPU profile。项目保留了不绑定 CPU index 的 yolo extra 作为设备专用环境的基础,但 GPU 部署前应为目标设备建立单独的 uv source/lock或使用 NVIDIA 容器),再把 YAML 中的 device 改为 cuda:0;不要只改 YAML 就认为 CUDA 环境已经就绪。

detection.model@1 根据 modelDetectionModelRegistry 解析模型。detect_labels 只选择需要检测的标签,省略时检测注册模型的全部标签;confidence 是全局阈值,也可以用 label_confidence 为个别标签覆盖。当前内置 construction-ppe-yolov8@1 的 19 个标签和 ppe-6classes-yolov8n@1 的 6 个标签都可以通过 cmvr-edge-ai models 查看。两个模型制品和独立 model card 位于仓库内:

注册 ID 中的 @1 与制品目录的 v1 对应;这是项目的版本组织约定,实际 weights 路径仍由部署 YAML 显式指定。标签顺序、训练指标、局限和许可声明以上述 model card 为准。attach_frame: true 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。

六类模型的标签是 GlovesVestgoggleshelmetmasksafety_shoe 语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 PersonNo-* 类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅 注册、未被 configs/edge_ai.yamldetection Pipeline 引用;需要恢复第二分支时,应同时配置 detector、 8082 endpoint、HTTP Sink 和两条关联 edge。

若以后恢复双模型配置,应从 decoder 输出端口 fan-out让两个 detector 共享同一个相机 订阅和 PyAV decoder两个模型仍会分别加载和推理并共享应用的有界线程池。

detection.repeat_gate@1 只在一个规则的 window_ms 内看到至少 min_hits 个不同帧后生成 DetectionAlert/v1。同一帧有多个同类框仍只算一次;触发后进入 cooldown_ms,冷却期间不累计,结束后必须重新满足次数。scope: source 按相机统计;scope: tracktrack_id 统计,但当前 YOLO adapter 只做逐帧检测,不产生 track_id,因此使用 track 规则前必须增加跟踪/关联节点。

启用 alert_imagerepeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG不会给每一帧都生成图片。告警的 detections 和图片中的 bounding boxes 都来自达到 min_hits 的阈值帧;窗口内更早帧只参与 hit_count、时间范围和最大置信度统计。HTTP JSON 中图片位于 payload.image

{
  "image": {
    "media_type": "image/jpeg",
    "width": 1280,
    "height": 720,
    "encoding": "base64",
    "data": "..."
  }
}

这里的外层对象是 DetectionAlert payload 的片段HTTP JSON Sink 会把内部 JPEG bytes 转成上述扁平 Base64 图片对象。未启用图片,或运行时因第三方结果未附带 帧、坏帧等原因渲染失败时,告警仍会发送且 payload.imagenull。 同一阈值帧若同时触发多条规则或多个 track只编码一次相关框的并集并让这些 告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。

对话占位链路不依赖音频 proto

uv run --no-sync cmvr-edge-ai validate \
  --config configs/edge_ai.yaml \
  --pipeline talk
uv run --no-sync cmvr-edge-ai run \
  --config configs/edge_ai.yaml \
  --pipeline talk

配置最小示例

api_version: cmvr.edge.ai/v1

runtime:
  thread_workers: 2
  shutdown_timeout_s: 5

pipelines:
  example:
    enabled: true
    nodes:
      source:
        uses: core.sequence_source@1
        with:
          items: [hello]
          schema_name: TextEvent
          schema_version: 1
      sink:
        uses: core.log_sink@1
    edges:
      - from: source.output
        to: sink.input
        qos:
          profile: request
          capacity: 4
          overflow: block

配置模型是严格的,多余字段会报错。字符串支持以下环境变量形式:

  • ${NAME}:变量必须存在;
  • ${NAME:-default}:未设置或为空时使用默认值;
  • env://NAME:整个字符串取自必填环境变量。

v1 支持五个 qos.profile,并在编译期约束其溢出策略:编码 H264/H265 在解码前必须使用 video_contiguous,只能阻塞或拒绝;解码后的完整图像可使用 realtime_latest 丢旧帧控制延迟;audio_contiguousrequest 只能阻塞或拒绝;telemetry 支持全部策略。未填写 QoS 时使用无损的 request + block + capacity=1reject 会抛异常,error 是它的兼容别名。max_age_msput_timeout_ms 已预留,设置后 validate 会拒绝配置。完整矩阵见架构文档。

注册新的检测模型

检测模型和 DAG 插件是两层注册:流水线固定使用通用的 detection.model@1,具体模型通过 DetectionModelRegistry 注册 DetectionModelSpec。每个 spec 必须给出版本化 model_id、面向运维的 name、有序且唯一的 supported_labelsbackend 和 factory。factory 返回实现 load/predict/closeDetectionModel;部署 YAML 中的 model_options 原样交给它。第三方模型包可以使用 cmvr_edge_ai.detection_models entry point 发布 spec 或注册回调。安装后先执行 cmvr-edge-ai models,再让配置引用其中的精确模型 ID。 仓库自带制品统一放在 models/detection/<model-name>/vN/,并在每个版本目录保存 README.md model card。新的 model_id 尾部 @N 应与制品目录 vN 保持一致。

模型实际输出的标签仍会在通用 Operator 边界二次校验和过滤;模型返回未注册标签会让节点失败。直接相连的重复规则若引用了 detector 没有选择的标签,也会在 validate 阶段被编译器拒绝。

开发插件

插件必须使用带版本的稳定 ID例如 example.text_upper@1,并声明节点种类、输入/输出端口及 schema。工厂签名固定为 (node_id, params);组件分别继承 SourceOperatorSink

from collections.abc import Mapping
from typing import Any

from cmvr_edge_ai.contracts import TextEvent
from cmvr_edge_ai.core import Emission, Envelope, Operator
from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec


class UppercaseOperator(Operator):
    def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
        self.node_id = node_id

    async def process(
        self, envelope: Envelope[Any], input_port: str = "input"
    ) -> Emission:
        event = envelope.payload
        if not isinstance(event, TextEvent):
            raise TypeError("expected TextEvent")
        result = TextEvent(text=event.text.upper(), role=event.role, final=event.final)
        return Emission(
            "output",
            envelope.with_payload(result, schema_name="TextEvent", schema_version=1),
        )


def register_plugins(registry: PluginRegistry) -> None:
    registry.register(
        PluginSpec(
            plugin_id="example.text_upper@1",
            kind=PluginKind.OPERATOR,
            factory=UppercaseOperator,
            inputs={"input": "TextEvent/v1"},
            outputs={"output": "TextEvent/v1"},
            description="Uppercase text events",
        )
    )

在插件包的 pyproject.toml 中注册:

[project.entry-points."cmvr_edge_ai.plugins"]
example = "my_cmvr_plugin.plugins:register_plugins"

安装插件包后,用下面的命令确认发现成功:

uv run --no-sync cmvr-edge-ai plugins

配置文件不能通过 module:Class 任意导入代码;只会使用内置或已安装 entry point 注册的插件。插件完整生命周期、返回值规范和连接器开发约定见 docs/architecture.md

机器人控制安全边界

控制链固定为:

Policy -> RobotCommand/v1
       -> safety.robot_command_gate@1
       -> ApprovedRobotCommand/v1
       -> cmvr.grpc.agv_command_sink@1

AGV Sink 只接受 ApprovedRobotCommand,原始 RobotCommand 会在运行时被拒绝。配置编译器还要求安全门是执行器的直接前驱;安全门与 actuator 之间不能插入其他节点,也不能增加绕过安全门的输入边。内置安全门负责过期检查和默认 5 秒的最大 TTL、动作/设备白名单、参数范围、最小发送间隔及默认开启的单调序号检查;不合格命令会被丢弃并记录 warning不会因为一条业务拒绝停止整条 Pipeline。

AGV Sink 必须绑定固定的非空 device_idset_velocity 默认禁用;unsafe_allow_unleased_velocity: true 只是面向隔离测试场景的显式逃生开关,并且仍要求 Sink 配置 vx/vy/wzvelocity_limits。它只能限制当前客户端发出的数值,并在正常关闭时尽力调用 stopVelocityControl;进程崩溃、SIGKILL、断电或网络分区时无法保证停车。

真实机器人要启用持续速度控制,必须先在 cmvr-es 服务端实现带过期时间的 lease/deadman客户端停止续租后由 cmvr-es 在独立于 edge-ai 进程的安全路径中自动清零速度并停车。客户端的安全门、TTL 和 shutdown hook 不能替代这项服务端保护。

当前实现边界

  • 音频内部契约 AudioChunk/v1 已定义,但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。
  • UDP 与 QUIC 目录目前是扩展占位,没有可用传输实现。
  • 运行时 v1 只支持 execution.mode: async|inline,两者当前都是单 task、单并发执行。threadprocessmodel_worker 是保留值;concurrency != 1、非空 max_in_flight/timeout_sordered: false 也是保留配置,都会在编译期被拒绝。插件仍可在组件内部显式使用 workers.run_blocking()PersistentProcessWorker 或自有模型 Worker但不能把 execution 声明误当作自动调度。
  • v1 对未实现的声明采取 fail-closed显式设置 runtime.max_processes/process_start_method/health_bind/reserved_memory_mb、非默认 pipeline priority 或任何非空 resources 都会在编译期被拒绝。健康状态和队列统计目前只能通过 Python API 获取。
  • 当前没有配置热更新、配置 overlay、持久化 outbox/spool 或共享内存图像池。HTTP Sink 只有当前进程内的有限重试;默认 failure_mode: raise 会终止 Pipeline当前检测配置的 log_and_drop 则在重试耗尽后记录 WARNING 并丢弃该告警。两种模式在进程退出或断电时 都可能丢失未上报数据。
  • 相机 Source 可以重连,但 cmvr-es 当前服务端通过 getLatestEncodedFrame 获取最新编码数据;如果上游在 edge-ai 收到之前已跳过 H264/H265 参考包,video_contiguous 无法补回数据,解码器只能在错误后重置并等待关键帧。正式部署应验证 cmvr-es 输出的是连续 access unit 流,或改为对 AI 友好的原始/JPEG/可检测不连续性的接口。
  • 内置 YOLO adapter 不运行 tracker所有检测的 track_id 都为空;当前示例因此使用 scope: source
  • PPE 权重仓库对权重的许可说明与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业交付前核对,并同时确认训练数据和权重分发许可。
  • 端口 schema 在编译期按字符串匹配;运行时不会自动验证 Python payload 类型,插件必须在边界处主动检查。
  • AGV 执行器不会自动重试。安全门已提供动作/设备白名单、TTL、参数范围、最小间隔和单调序号检查但设备状态、command ID 去重、来源仲裁、服务端 lease/deadman 和急停优先级仍需按机器人型号补齐。服务端 deadman 未完成前,禁止把 set_velocity 用于真实机器人持续运动。

这些限制及推荐演进顺序在 docs/architecture.md 中有更详细说明。