629 lines
34 KiB
Markdown
629 lines
34 KiB
Markdown
# 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、六类 PPE YOLOv8n、People-Talking YOLOv8x 与 YOLOv8n Mobile Phone 模型注册,以及按标签、置信度和最大 FPS 的部署配置;
|
||
- 平台 HTTP JSON Sink,支持有限重试、`raise/log_and_drop` 失败策略,使用告警
|
||
`event_id` 作为幂等键,并可把已配置的 gRPC endpoint IP 作为 `grpc_ip` 上报;
|
||
- `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射;
|
||
- 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client;
|
||
- `serve` 被动调用入口、版本化推理契约和能力目录,以及只按稳定业务 `category`
|
||
选择服务端路由的 Detect Client SDK;
|
||
- `gauge.analog` 被动调用适配:主服务保持 Python 3.10,ETHZ Analog Gauge Reader
|
||
通过独立的 Python 3.8 uv 环境和常驻二进制协议 worker 运行;
|
||
- 纯 ONNX Runtime 的 YOLOv8 推理 backend 与受控导出工具;原 `.pt` `@1` 版本作为
|
||
导出/回滚源保留,三个部署配置已使用带 manifest/SHA256 校验的 `@2` ONNX 制品;
|
||
- 可直接执行的最小测试 fixture,以及包含 PPE/电话使用检测的主动推送部署配置。
|
||
|
||
`configs/active_detection.yaml` 是主动检测部署配置。它从同一个相机 decoder 并行运行 Construction PPE 与
|
||
People-Talking 两个 detector,各自经过独立时间窗口规则后向同一个告警平台上报;
|
||
`model_id` 和 `rule_id` 用于区分事件。六类 PPE 模型仍保留在注册表中,但不在当前
|
||
Pipeline 图中实例化。
|
||
VAD/ASR/LLM/TTS 尚未内置,因此仓库不再保留不能提供真实能力的 Talk 占位 Pipeline。
|
||
|
||
主动推送与被动调用可以在同一份配置、同一进程中并存:原有相机 Pipeline 持续向平台
|
||
推送事件;被动 Detect Server 则接收单次 HTTP 请求,经服务端拥有的 Pipeline 完成推理并
|
||
返回响应。两者共用版本化契约和能力注册表,但不会把模型 ID、Pipeline ID 或权重路径
|
||
暴露为客户端可选参数。
|
||
|
||
## 架构概览
|
||
|
||
```mermaid
|
||
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_id` 和 `session_id`。连接器负责 protobuf/HTTP JSON 与内部契约之间的转换,AI 插件不应直接依赖 cmvr-es protobuf。
|
||
|
||
完整设计和配置字段见 [docs/architecture.md](docs/architecture.md)。
|
||
|
||
## 目录
|
||
|
||
```text
|
||
cmvr_edge_ai/
|
||
├── .python-version # uv 默认 Python 3.10
|
||
├── uv.lock # 所有 profile 的可复现依赖锁
|
||
├── configs/
|
||
│ ├── README.md # 配置命名、归属与路径规则
|
||
│ ├── active_detection.yaml # 主动检测与平台告警
|
||
│ ├── server_detect.yaml # 被动 PPE/手机检测 HTTP 服务
|
||
│ ├── server_gauge.yaml # 被动 Analog Gauge HTTP 服务
|
||
│ └── detection_viewer.yaml # 远端相机 -> 双模型 -> OpenCV 窗口
|
||
├── server/
|
||
│ ├── detect/ # 被动检测服务说明与 OpenCV Viewer
|
||
│ ├── gauge/ # 仪表读数服务及独立 Python 3.8 uv worker
|
||
│ └── talk/ # 对话能力边界说明(当前未实现)
|
||
├── client/
|
||
│ └── detect/ # 不依赖 Pipeline 的 HTTP Detect Client 示例
|
||
├── models/
|
||
│ └── detection/ # 按模型 ID/版本组织的检测模型制品库
|
||
│ ├── construction-ppe-yolov8/v1,v2/ # PT 源 + ONNX/manifest/model card
|
||
│ ├── ppe-6classes-yolov8n/v1,v2/ # PT 源 + ONNX/manifest/model card
|
||
│ ├── people-talking-yolov8x/v1,v2/ # PT 源 + ONNX/manifest/model card
|
||
│ └── yolov8n-mobile-phone/v2/ # ONNX;v1 PT 保留在父目录
|
||
├── scripts/
|
||
│ ├── bootstrap.sh # 一键创建 uv 环境、生成 bindings 并自检
|
||
│ ├── export_detection_onnx.py # 受控导出检测模型 @2 ONNX 制品与 manifest
|
||
│ └── generate_cmvr_stubs.py # 从 cmvr-es proto 生成 Python bindings
|
||
├── src/cmvr_edge_ai/
|
||
│ ├── config/ # 配置模型、加载与环境变量展开
|
||
│ ├── core/ # Envelope、组件接口、队列和 DAG 运行时
|
||
│ ├── contracts/ # 内部消息及 inference/catalog 公共 wire 契约
|
||
│ ├── capabilities/ # 稳定 category、服务模式与模型能力注册表
|
||
│ ├── client/ # 远程调用 SDK;当前包含 Detect HTTP Client
|
||
│ ├── server/ # 请求 broker、边界节点、能力目录与 HTTP API
|
||
│ ├── detection/ # 模型注册、视频解码、推理与时间窗口规则
|
||
│ ├── gauge/ # Analog Gauge 主进程 adapter 与 Python 3.8 worker
|
||
│ ├── plugins/ # 插件注册、发现和内置基础插件
|
||
│ ├── connectors/ # cmvr-es 与平台边界连接器
|
||
│ ├── transports/ # 已实现的 gRPC/HTTP 连接池
|
||
│ ├── workers/ # 显式线程 offload 与常驻进程 Worker 工具
|
||
│ ├── observability/ # 低开销文本/JSON 日志
|
||
│ ├── application.py # 多 Pipeline 与共享网络客户端的所有者
|
||
│ ├── compiler.py # 配置到可执行 DAG 的编译器
|
||
│ └── cli.py # validate/run/serve/plugins/models
|
||
└── tests/
|
||
└── fixtures/
|
||
└── minimal_pipeline.yaml # 不依赖外部服务的框架/CLI 自检配置
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
项目使用 `uv` 管理 Python、`.venv` 和锁定依赖。`.python-version` 默认选择
|
||
Python 3.10,支持范围是 3.10~3.12;首次执行时,uv 会在本机没有合适解释器时
|
||
自动安装。先确认已经安装 uv:
|
||
|
||
```bash
|
||
uv --version
|
||
```
|
||
|
||
未安装时可使用 uv 官方安装器:
|
||
|
||
```bash
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||
```
|
||
|
||
只验证框架核心时,一条命令创建最小环境并自检:
|
||
|
||
```bash
|
||
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` 执行;从其他目录启动时,必须把
|
||
配置中的模型等文件路径改为正确的绝对路径。
|
||
|
||
```bash
|
||
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-ready` 和 `bounded-dag-running`。该 Source 是有限数据源,数据处理完后进程会自然退出。
|
||
|
||
bootstrap 支持以下环境:
|
||
|
||
| Profile | 安装内容 | 命令 |
|
||
|---|---|---|
|
||
| `core` | 框架核心与最小测试 fixture | `bash scripts/bootstrap.sh --profile core` |
|
||
| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 和 CPU ONNX Runtime;默认值 | `bash scripts/bootstrap.sh` |
|
||
| `dev` | `detection-cpu` 加锁定的 ONNX exporter、测试和 protobuf codegen | `bash scripts/bootstrap.sh --profile dev` |
|
||
|
||
如果只希望安装、不执行自检,可加 `--skip-check`。完整参数通过以下命令查看:
|
||
|
||
```bash
|
||
bash scripts/bootstrap.sh --help
|
||
```
|
||
|
||
不要使用 `uv sync --all-extras`:旧 PT adapter 的 `yolo`/`yolo-cpu` 与构建期
|
||
`onnx-export-cpu` 具有互斥依赖来源。请使用 bootstrap profile 或只选择目标 extra。
|
||
告警图片的画框和 JPEG 编码由独立的 `image` extra 提供;默认的
|
||
`detection-cpu`/`dev` profile 已安装它,手动组合检测环境时也必须选择
|
||
`--extra image`。
|
||
|
||
启动被动 HTTP 服务需要 `server` extra,使用仓库提供的 Detect Client 还需要 `http`
|
||
extra。CPU ONNX 检测服务可显式安装以下组合:
|
||
|
||
```bash
|
||
uv sync --locked --extra server --extra http --extra onnx-cpu
|
||
```
|
||
|
||
CLI 的五个子命令如下:
|
||
|
||
| 命令 | 用途 |
|
||
|---|---|
|
||
| `validate -c FILE [--pipeline ID]` | 加载配置、展开环境变量、构造插件并校验 DAG;不启动 Pipeline |
|
||
| `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline;未指定时启动所有 `enabled: true` 的 Pipeline |
|
||
| `serve -c FILE` | 启动所有已启用 Pipeline 和远程推理 HTTP API;要求 `server.enabled: true` |
|
||
| `plugins` | 列出内置插件和已安装 entry point 插件 |
|
||
| `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 |
|
||
|
||
`--pipeline` 可以重复传给 `validate` 和 `run`。`run`、`serve` 还支持 `--log-level` 和
|
||
`--log-format text|json`;`serve` 按配置启动全部已启用 Pipeline,不接受客户端或命令行
|
||
覆盖单条 route 的模型选择。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130`。
|
||
|
||
主动推送配置为 `configs/active_detection.yaml`,当前只包含 `detection` Pipeline。
|
||
部署时可以显式传 `--pipeline detection`;省略时会启动配置中所有已启用 Pipeline。
|
||
|
||
## 被动检测 HTTP 服务
|
||
|
||
被动调用使用统一的 `InferenceRequest/v1`、`InferenceResponse/v1` 和能力目录契约。
|
||
阶段一提供两个入口:
|
||
|
||
| HTTP 入口 | 用途 |
|
||
|---|---|
|
||
| `GET /v1/models` | 返回 `cmvr.model-catalog/v1`,分别列出 `active_push` 与 `passive_invoke` 能力及部署状态 |
|
||
| `POST /v1/inference` | 接收 `cmvr.inference-request/v1` JSON,并返回 `cmvr.inference-response/v1` 或稳定的错误响应 |
|
||
|
||
客户端只提交稳定业务 category,例如 `detect.ppe` 或 `detect.mobile_phone`。客户端不允许
|
||
指定 `model_id`、Pipeline ID 或权重路径;`server.routes.<category>` 与对应 Pipeline 配置
|
||
决定实际模型、制品、provider 和队列容量。这样服务端可以升级实现而不改变调用方协议。
|
||
|
||
从仓库根目录校验并启动示例服务:
|
||
|
||
```bash
|
||
uv run --no-sync cmvr-edge-ai validate \
|
||
--config configs/server_detect.yaml
|
||
uv run --no-sync cmvr-edge-ai serve \
|
||
--config configs/server_detect.yaml \
|
||
--log-level INFO \
|
||
--log-format text
|
||
```
|
||
|
||
服务启动后,可在另一终端查询能力并通过 Detect Client 调用 `POST /v1/inference`:
|
||
|
||
```bash
|
||
curl -sS http://127.0.0.1:8081/v1/models
|
||
CMVR_DETECT_BASE_URL=http://127.0.0.1:8081 \
|
||
uv run --no-sync python client/detect/example.py \
|
||
/path/to/image.jpg detect.ppe
|
||
```
|
||
|
||
存活与就绪探针分别是 `GET /health/live` 和 `GET /health/ready`。完整服务端配置和链路见
|
||
[server/detect/README.md](server/detect/README.md),SDK 契约与错误分类见
|
||
[client/detect/README.md](client/detect/README.md)。
|
||
|
||
默认示例仅监听 loopback。监听局域网或其他非 loopback 地址时,配置必须包含
|
||
`server.http.bearer_token`,并默认要求 `tls_certfile` 与 `tls_keyfile`;只有受信隔离网络
|
||
才能显式设置 `allow_insecure_remote: true` 使用明文 HTTP。除 `/health/live` 和
|
||
`/health/ready` 外,所有 HTTP 入口都要求 `Authorization: Bearer <token>`。
|
||
|
||
被动调用链采用 `request/block` 单线性 DAG。每个 Operator 必须声明并在运行时满足
|
||
`exactly_one` 输出;单条请求的解码/模型异常会返回 500,但不会终止可复用 Pipeline。
|
||
broker 使用每次提交生成的 invocation token 防止迟到结果串到复用的 `request_id`;已经
|
||
进入模型的超时任务会继续占用该 route 容量,直到真实执行结束,避免超时请求堆积。
|
||
|
||
## Analog Gauge 被动服务
|
||
|
||
[ethz-asl/analog_gauge_reader](https://github.com/ethz-asl/analog_gauge_reader) 使用同一个
|
||
`POST /v1/inference` URL,客户端只需选择 `category: gauge.analog`。它返回通用
|
||
`scalar` 输出,并可按请求返回 `original`、`annotated` 和 `diagnostics` artifact。
|
||
|
||
该上游项目依赖 Python 3.8、Torch 2.0、MMCV/MMOCR,与主项目 Python 3.10 环境隔离。
|
||
首次部署从仓库根目录执行:
|
||
|
||
```bash
|
||
bash server/gauge/worker/bootstrap.sh
|
||
uv run --no-sync cmvr-edge-ai validate \
|
||
--config configs/server_gauge.yaml
|
||
uv run --no-sync cmvr-edge-ai serve \
|
||
--config configs/server_gauge.yaml \
|
||
--log-level INFO
|
||
```
|
||
|
||
worker 在进程内只串行处理一个请求,模型只加载一次;请求超时或协议失配时会终止被污染
|
||
的子进程,并在下一次请求时重新创建。项目路径和三份权重路径只存在于服务端配置,调用方
|
||
不能覆盖。完整准备步骤、请求 JSON 和当前 Linux x86_64 限制见
|
||
[server/gauge/README.md](server/gauge/README.md)。
|
||
|
||
## ONNX 检测迁移
|
||
|
||
四个检测模型都保留 `ultralytics-yolo` 的 `@1` 注册,同时提供
|
||
`onnxruntime-yolov8` 的 `@2` 注册。`@2` backend 直接执行静态 batch=1、NMS 外置的
|
||
YOLOv8 ONNX 图,运行环境只需 NumPy、Pillow 和 ONNX Runtime,不导入 Torch 或
|
||
Ultralytics:
|
||
|
||
```bash
|
||
uv sync --locked --extra onnx-cpu
|
||
```
|
||
|
||
导出是单独的构建步骤,必须使用锁定的 exporter 环境;以下命令以 Mobile Phone 模型为例:
|
||
|
||
```bash
|
||
uv sync --locked --extra onnx-export-cpu
|
||
uv run --no-sync python scripts/export_detection_onnx.py \
|
||
--model-id yolov8n-mobile-phone@2
|
||
```
|
||
|
||
脚本会校验来源标签、静态输入输出和 ONNX metadata,清除训练机路径/时间戳,并在
|
||
`v2/` 写入 `model.onnx` 与 SHA256 `manifest.json`。四份制品已经生成,三个部署 YAML
|
||
已切换到 `@2`;运行时会在 session 创建前验证 manifest、模型身份与制品哈希。当前已
|
||
完成真实 ORT smoke,但正式现场使用仍要用授权图片完成 `.pt(rect=False)`/ONNX 框与
|
||
置信度一致性、延迟、峰值内存和长期稳定性验收。YOLOv8x People-Talking 计算量较大,
|
||
格式转换本身不会把它变成轻量网络。
|
||
具体状态与普通 Git 制品规则见 [models/detection/README.md](models/detection/README.md)。
|
||
|
||
## 运行视觉检测链路
|
||
|
||
默认 bootstrap 就是当前 YAML 使用的 CPU 检测环境。它会从相邻的
|
||
`../cmvr-es` 读取 proto、用锁定的 `grpcio-tools` 生成 bindings,然后安装完整
|
||
检测依赖并校验最小测试 fixture 和统一配置中的 PPE 链路:
|
||
|
||
```bash
|
||
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
|
||
bash scripts/bootstrap.sh
|
||
```
|
||
|
||
如果 cmvr-es 不在相邻目录,显式指定路径:
|
||
|
||
```bash
|
||
bash scripts/bootstrap.sh \
|
||
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
|
||
```
|
||
|
||
bootstrap 默认使用 portable codegen,所以不要求 cmvr-es 已经编译出 `protoc`。
|
||
要跳过生成(例如部署包已经包含匹配版本的 bindings),使用
|
||
`--skip-codegen`。手动生成时可以执行:
|
||
|
||
```bash
|
||
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/active_detection.yaml` 的 `detection` Pipeline 中配置部署参数:
|
||
|
||
```yaml
|
||
endpoints:
|
||
cmvr_es:
|
||
target: 192.168.0.119:50052
|
||
ppe_alert_platform:
|
||
base_url: http://192.168.0.222:13080
|
||
|
||
pipelines:
|
||
detection:
|
||
nodes:
|
||
camera:
|
||
with:
|
||
device_id: wrist_cam
|
||
stream_log_interval_s: 5
|
||
detector:
|
||
with:
|
||
model: construction-ppe-yolov8@2
|
||
attach_frame: true
|
||
inference_log_interval_s: 5
|
||
model_options:
|
||
weights: models/detection/construction-ppe-yolov8/v2/model.onnx
|
||
providers: [CPUExecutionProvider]
|
||
intra_op_threads: 1
|
||
inter_op_threads: 1
|
||
repeat_gate:
|
||
with:
|
||
alert_image:
|
||
enabled: true
|
||
jpeg_quality: 85
|
||
phone_detector:
|
||
with:
|
||
model: people-talking-yolov8x@2
|
||
detect_labels: [talking on phone]
|
||
max_fps: 5
|
||
attach_frame: true
|
||
model_options:
|
||
weights: models/detection/people-talking-yolov8x/v2/model.onnx
|
||
providers: [CPUExecutionProvider]
|
||
intra_op_threads: 1
|
||
inter_op_threads: 1
|
||
phone_repeat_gate:
|
||
with:
|
||
alert_image:
|
||
enabled: true
|
||
jpeg_quality: 85
|
||
alert_platform:
|
||
with:
|
||
endpoint: ppe_alert_platform
|
||
grpc_endpoint: cmvr_es
|
||
failure_mode: log_and_drop
|
||
```
|
||
|
||
然后验证并运行检测链路,不再要求预先导出环境变量:
|
||
|
||
```bash
|
||
uv run --no-sync cmvr-edge-ai models
|
||
uv run --no-sync cmvr-edge-ai validate \
|
||
--config configs/active_detection.yaml \
|
||
--pipeline detection
|
||
uv run --no-sync cmvr-edge-ai run --config configs/active_detection.yaml \
|
||
--pipeline detection \
|
||
--log-level INFO \
|
||
--log-format json
|
||
```
|
||
|
||
启动后,`detector` 和 `phone_detector` 会分别输出一条 `detection model loaded`,表示
|
||
两份权重已经成功加载。收到解码帧并完成真实 `predict` 后,会分别输出
|
||
`detection inference`,之后
|
||
按 `inference_log_interval_s` 聚合输出一次;其中 `window_frames` 是本周期推理帧数,
|
||
`window_detections` 是检测框总数,`hit_labels` 是各标签的检测框累计数。持续只有 loaded 而没有
|
||
inference,说明相机或 decoder 尚未把帧送到模型;inference 中 detection 为 0 只表示
|
||
当前阈值下没有命中。短时调试可设为 `1` 秒,生产环境可设为 `30`~`60` 秒,省略则关闭
|
||
周期推理日志。这里使用标准日志而不是裸 `print`,因此与 `--log-format json` 兼容。
|
||
|
||
如果需要直接观察两个模型在同一帧上的画框结果,使用独立的 OpenCV Demo。它连接同一个
|
||
cmvr-es gRPC CameraService,共享解码帧并行执行 PPE 与 YOLOv8n Mobile Phone,再把两个结果
|
||
合并到一个窗口;它不经过重复触发规则,也不会访问 HTTP 平台:
|
||
|
||
```bash
|
||
uv run --no-sync python server/detect/show_detections.py \
|
||
--config configs/detection_viewer.yaml \
|
||
--pipeline detection_show \
|
||
--log-level INFO
|
||
```
|
||
|
||
运行前在 `configs/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型
|
||
权重;按 `q` 或 `Esc` 退出。框标签使用 `PPE:`/`PHONE:` 前缀,手机框标签为
|
||
`PHONE:mobile_phone`,窗口刷新率由较慢的模型分支决定。详细说明见
|
||
[server/detect/README.md](server/detect/README.md#opencv-实时画框-viewer)。
|
||
|
||
相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后
|
||
才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded`、
|
||
`camera stream opening`、`camera stream first frame` 和周期性的 `camera stream progress`。
|
||
如果只有 opening 而没有 first frame,progress 中会持续显示
|
||
`first_frame_received=false window_frames=0`,用于区分“RPC 已建立但相机没有出帧”。
|
||
|
||
运行前需要确认 Construction PPE 与 People-Talking 的 `model.onnx`、`manifest.json`
|
||
存在,cmvr-es 已启用配置相机,并且告警平台 `/v1/detection-alerts` 可访问。默认
|
||
`detection-cpu` profile 只安装 NumPy、Pillow 和 ONNX Runtime,不安装 Torch 或
|
||
Ultralytics。CUDA/TensorRT、Jetson 和其他 provider 必须建立与目标设备匹配的独立
|
||
依赖/锁文件并重新验收,不能只修改 `providers` 就认为硬件环境已经就绪。
|
||
|
||
`detection.model@1` 根据 `model` 从 `DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前四个内置模型都可以通过 `cmvr-edge-ai models` 查看,模型制品和独立 model card 位于仓库内:
|
||
|
||
- [Construction PPE ONNX v2](models/detection/construction-ppe-yolov8/v2/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类;
|
||
- [PPE YOLOv8n 6 Classes ONNX v2](models/detection/ppe-6classes-yolov8n/v2/README.md):六类正向装备检测模型,当前未实例化;
|
||
- [People Talking ONNX v2](models/detection/people-talking-yolov8x/v2/README.md):电话使用行为检测,只选择 `talking on phone`;
|
||
- [YOLOv8n Mobile Phone ONNX v2](models/detection/yolov8n-mobile-phone/v2/README.md):Viewer 和被动服务使用的单类手机检测。
|
||
|
||
注册 ID 中 `@N` 与版本目录 `vN` 对应;Mobile Phone 的 v1 PT 是历史扁平布局,v2 已
|
||
恢复版本目录。实际 `weights` 路径仍由部署 YAML 显式指定。标签顺序、训练指标、局限
|
||
和许可声明以各版本 model card 为准。
|
||
`attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。
|
||
|
||
六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe`,
|
||
语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person` 或 `No-*`
|
||
类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅
|
||
注册、未被 `configs/active_detection.yaml` 的 `detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、
|
||
8082 endpoint、HTTP Sink 和两条关联 edge。
|
||
|
||
当前 Construction PPE 和 People-Talking 分支从 decoder 输出端口 fan-out,共享同一个
|
||
相机订阅和 PyAV decoder;两个模型仍会分别加载和推理,并共享应用的有界线程池。
|
||
People-Talking 是视觉行为判断,不能证明电话已接通,也不能区分来电和去电。
|
||
|
||
`detection.repeat_gate@1` 只在一个规则的 `window_ms` 内看到至少 `min_hits` 个不同帧后生成 `DetectionAlert/v1`。同一帧有多个同类框仍只算一次;触发后进入 `cooldown_ms`,冷却期间不累计,结束后必须重新满足次数。`scope: source` 按相机统计;`scope: track` 按 `track_id` 统计,但当前 YOLO adapter 只做逐帧检测,不产生 `track_id`,因此使用 track 规则前必须增加跟踪/关联节点。
|
||
|
||
启用 `alert_image` 后,repeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG,不会给每一帧都生成图片。告警的 `detections` 和图片中的 bounding boxes 都来自达到 `min_hits` 的阈值帧;窗口内更早帧只参与 `hit_count`、时间范围和最大置信度统计。
|
||
|
||
`alert_platform.with.grpc_endpoint: cmvr_es` 会读取 `endpoints.cmvr_es.target`,从
|
||
`192.168.0.119:50052` 这样的字面 IP target 中去掉端口,并把结果放在 HTTP JSON
|
||
顶层。`source_id` 仍是相机 ID,二者不会互相覆盖:
|
||
|
||
```json
|
||
{
|
||
"schema": "DetectionAlert/v1",
|
||
"source_id": "wrist_cam",
|
||
"grpc_ip": "192.168.0.119",
|
||
"payload": {"event_id": "...", "image": {"encoding": "base64", "data": "..."}}
|
||
}
|
||
```
|
||
|
||
被引用 target 必须是单个字面 IPv4/IPv6 加端口;hostname、Unix socket 和多地址
|
||
target 会在 Sink setup 时被拒绝,避免 DNS 地址变化造成边缘端身份漂移。
|
||
|
||
HTTP JSON 中完整图片对象位于 `payload.image`:
|
||
|
||
```json
|
||
{
|
||
"image": {
|
||
"media_type": "image/jpeg",
|
||
"width": 1280,
|
||
"height": 720,
|
||
"encoding": "base64",
|
||
"data": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
这里的外层对象是 `DetectionAlert` payload 的片段;HTTP JSON Sink 会把内部 JPEG
|
||
`bytes` 转成上述扁平 Base64 图片对象。未启用图片,或运行时因第三方结果未附带
|
||
帧、坏帧等原因渲染失败时,告警仍会发送且 `payload.image` 为 `null`。
|
||
同一阈值帧若同时触发多条规则或多个 track,只编码一次相关框的并集,并让这些
|
||
告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。
|
||
|
||
Talk Server 当前只保留能力边界说明;在真实音频 proto、VAD/ASR/LLM/TTS 节点和远程
|
||
调用契约落地前,不提供容易被误认为可部署服务的占位配置。
|
||
|
||
## 配置最小示例
|
||
|
||
```yaml
|
||
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_contiguous` 和 `request` 只能阻塞或拒绝;`telemetry` 支持全部策略。未填写 QoS 时使用无损的 `request + block + capacity=1`。`reject` 会抛异常,`error` 是它的兼容别名。`max_age_ms`、`put_timeout_ms` 已预留,设置后 `validate` 会拒绝配置。完整矩阵见架构文档。
|
||
|
||
## 注册新的检测模型
|
||
|
||
检测模型和 DAG 插件是两层注册:流水线固定使用通用的 `detection.model@1`,具体模型通过 `DetectionModelRegistry` 注册 `DetectionModelSpec`。每个 spec 必须给出版本化 `model_id`、面向运维的 `name`、有序且唯一的 `supported_labels`、`backend` 和 factory。factory 返回实现 `load/predict/close` 的 `DetectionModel`;部署 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)`;组件分别继承 `Source`、`Operator` 或 `Sink`。
|
||
|
||
```python
|
||
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` 中注册:
|
||
|
||
```toml
|
||
[project.entry-points."cmvr_edge_ai.plugins"]
|
||
example = "my_cmvr_plugin.plugins:register_plugins"
|
||
```
|
||
|
||
安装插件包后,用下面的命令确认发现成功:
|
||
|
||
```bash
|
||
uv run --no-sync cmvr-edge-ai plugins
|
||
```
|
||
|
||
配置文件不能通过 `module:Class` 任意导入代码;只会使用内置或已安装 entry point 注册的插件。插件完整生命周期、返回值规范和连接器开发约定见 [docs/architecture.md](docs/architecture.md#5-插件开发约定)。
|
||
|
||
## 机器人控制安全边界
|
||
|
||
控制链固定为:
|
||
|
||
```text
|
||
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_id`。`set_velocity` 默认禁用;`unsafe_allow_unleased_velocity: true` 只是面向隔离测试场景的显式逃生开关,并且仍要求 Sink 配置 `vx/vy/wz` 的 `velocity_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、单并发执行。`thread`、`process`、`model_worker` 是保留值;`concurrency != 1`、非空 `max_in_flight/timeout_s`、`ordered: false` 也是保留配置,都会在编译期被拒绝。插件仍可在组件内部显式使用 `workers.run_blocking()`、`PersistentProcessWorker` 或自有模型 Worker,但不能把 `execution` 声明误当作自动调度。
|
||
- v1 对未实现的声明采取 fail-closed:显式设置 `runtime.max_processes/process_start_method/health_bind/reserved_memory_mb`、非默认 pipeline `priority` 或任何非空 `resources` 都会在编译期被拒绝。`serve` 已提供 `/health/live` 和 `/health/ready`;详细节点健康信息与队列统计仍只能通过 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](docs/architecture.md#9-当前限制与演进顺序) 中有更详细说明。
|