project init

This commit is contained in:
xtkuang 2026-07-20 16:59:37 +08:00
commit 515eb3f09c
604 changed files with 29171 additions and 0 deletions

421
README.md Normal file
View File

@ -0,0 +1,421 @@
# 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
- 可直接执行的 smoke、PPE 检测和对话占位配置。
`detect_server/pipeline.yaml` 当前只启用 Construction PPE 模型,并经过时间窗口规则向
8081 上报告警。六类 PPE 模型及第二 HTTP 平台的实现仍保留在注册表和连接器中,但不在
当前 Pipeline 图中实例化,因此不会加载第二份权重、执行第二次推理或访问 8082。
VAD/ASR/LLM/TTS 尚未内置;`talk_server/pipeline.yaml` 仍使用模拟音频数据,等待
cmvr-es 音频双向流 proto 落地。
## 架构概览
```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/
│ └── smoke.yaml # 不依赖外部服务的最小运行验证
├── detect_server/
│ └── pipeline.yaml # cmvr-es 相机 -> PPE 告警 -> HTTP 平台
├── talk_server/
│ ├── nodes/ # 对话插件预留目录
│ └── pipeline.yaml # 模拟音频 -> 对话占位 -> 日志
├── 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/
```
## 快速开始
项目使用 `uv` 管理 Python、`.venv` 和锁定依赖。`.python-version` 默认选择
Python 3.10,支持范围是 3.103.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` 使用已经安装好的环境:
```bash
uv run --no-sync cmvr-edge-ai validate --config configs/smoke.yaml
uv run --no-sync cmvr-edge-ai plugins
uv run --no-sync cmvr-edge-ai run \
--config configs/smoke.yaml \
--log-level INFO \
--log-format text
```
预期会看到两条日志,内容分别包含 `framework-ready``bounded-dag-running`。该 Source 是有限数据源,数据处理完后进程会自然退出。
bootstrap 支持以下环境:
| Profile | 安装内容 | 命令 |
|---|---|---|
| `core` | 框架核心和模拟 smoke/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
bash scripts/bootstrap.sh --help
```
不要使用 `uv sync --all-extras``yolo` 与 `yolo-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`
## 运行 PPE 检测链路
默认 bootstrap 就是当前 YAML 使用的 CPU 检测环境。它会从相邻的
`../cmvr-es` 读取 proto、用锁定的 `grpcio-tools` 生成 bindings然后安装完整
检测依赖并校验 smoke 和 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
已经按这个顺序处理。
`detect_server/pipeline.yaml` 中配置部署参数:
```yaml
endpoints:
cmvr_es:
target: 127.0.0.1:50052
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: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
device: cpu
repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
platform:
with:
failure_mode: log_and_drop
```
然后验证并运行检测链路,不再要求预先导出环境变量:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
--config detect_server/pipeline.yaml \
--pipeline detection
uv run --no-sync cmvr-edge-ai run --config detect_server/pipeline.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` 秒,生产环境可设为 `30``60` 秒,省略则关闭
周期推理日志。这里使用标准日志而不是裸 `print`,因此与 `--log-format json` 兼容。
相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后
才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded`
`camera stream opening`、`camera stream first frame` 和周期性的 `camera stream progress`
如果只有 opening 而没有 first frameprogress 中会持续显示
`first_frame_received=false window_frames=0`用于区分“RPC 已建立但相机没有出帧”。
运行前需要确认 Construction PPE 权重存在、cmvr-es 已启用 `right_hand_cam`8081 平台的
`/v1/detection-alerts` 可访问。默认 `yolo-cpu` profile 将 PyTorch 2.7.0 和
torchvision 0.22.0 绑定到官方 CPU wheel并固定 checkpoint 记录的 Ultralytics
8.4.31。`model_options.device` 在该环境中应保持 `cpu``half` 应保持 `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` 根据 `model``DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前内置 `construction-ppe-yolov8@1` 的 19 个标签和 `ppe-6classes-yolov8n@1` 的 6 个标签都可以通过 `cmvr-edge-ai models` 查看。`attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。
六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe`
语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person``No-*`
类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅
注册、未被 `detect_server/pipeline.yaml` 引用;需要恢复第二分支时,应同时配置 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: track` 按 `track_id` 统计,但当前 YOLO adapter 只做逐帧检测,不产生 `track_id`,因此使用 track 规则前必须增加跟踪/关联节点。
启用 `alert_image`repeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG不会给每一帧都生成图片。告警的 `detections` 和图片中的 bounding boxes 都来自达到 `min_hits` 的阈值帧;窗口内更早帧只参与 `hit_count`、时间范围和最大置信度统计。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 和内存开销。
对话占位链路不依赖音频 proto
```bash
uv run --no-sync cmvr-edge-ai validate --config talk_server/pipeline.yaml
uv run --no-sync cmvr-edge-ai run --config talk_server/pipeline.yaml
```
## 配置最小示例
```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。
模型实际输出的标签仍会在通用 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` 都会在编译期被拒绝。健康状态和队列统计目前只能通过 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-当前限制与演进顺序) 中有更详细说明。

41
configs/smoke.yaml Normal file
View File

@ -0,0 +1,41 @@
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: 2
shutdown_timeout_s: 5
pipelines:
smoke:
enabled: true
nodes:
source:
uses: core.sequence_source@1
with:
items:
- framework-ready
- bounded-dag-running
schema_name: TextEvent
schema_version: 1
operator:
uses: core.passthrough@1
sink:
uses: core.log_sink@1
with:
logger: cmvr_edge_ai.smoke
edges:
- from: source.output
to: operator.input
qos:
profile: request
capacity: 4
overflow: block
- from: operator.output
to: sink.input
qos:
profile: telemetry
capacity: 4
overflow: block

293
detect_server/README.md Normal file
View File

@ -0,0 +1,293 @@
# PPE 检测流水线
`pipeline.yaml` 是当前可运行的园区施工安全装备检测链路。当前只启用 Construction
PPE 模型和 8081 告警平台;六类模型与 8082 模拟平台的实现保留但不实例化:
```text
cmvr-es CameraService
-> H264/H265 ImageFrame/v1
-> media.video_decoder.pyav@1
-> BGR8 ImageFrame/v1
-> construction-ppe-yolov8@1 -> DetectionResult/v1
-> detection.repeat_gate@1 -> DetectionAlert/v1
-> POST 127.0.0.1:8081/v1/detection-alerts
```
## 安装与启动
从仓库根目录执行一键安装。默认 profile 安装锁定的 CPU 检测环境、生成 cmvr-es
bindings并验证 smoke 与本检测配置:
```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh
```
cmvr-es 不在相邻的 `../cmvr-es` 时指定实际路径:
```bash
bash scripts/bootstrap.sh \
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
```
环境固定 Python 3.10、Ultralytics 8.4.31、PyTorch 2.7.0 CPU 和 torchvision
0.22.0,并通过 `image` extra 安装 Pillow 以生成告警图片;所有具体包版本记录在
仓库的 `uv.lock` 中。GPU/Jetson 需要单独适配与驱动或 JetPack 匹配的 PyTorch
wheel不能直接复用 `detection-cpu` profile。手动组合依赖时必须显式增加
`--extra image`,不能只依赖 YOLO 间接安装 Pillow。
直接编辑 `detect_server/pipeline.yaml` 中的部署参数:
```yaml
endpoints:
cmvr_es:
target: 127.0.0.1:50052
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: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
device: cpu
repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
platform:
with:
failure_mode: log_and_drop
```
完成配置后启动,不需要再通过 shell `export` 传入这些值:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
-c detect_server/pipeline.yaml \
--pipeline detection
uv run --no-sync cmvr-edge-ai run -c detect_server/pipeline.yaml \
--pipeline detection \
--log-level INFO \
--log-format json
```
运行时会看到类似下面两类 JSON 日志:
```json
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection model loaded node=detector model=construction-ppe-yolov8@1 ..."}
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection inference node=detector model=construction-ppe-yolov8@1 total_frames=1 window_frames=1 window_detections=2 hit_labels=No-Helmet:2 ..."}
```
- `detection model loaded`checkpoint 已成功加载并完成标签顺序校验;当前 detector
应出现一条;
- `detection inference`:模型确实收到解码帧并执行了 `predict`;第一帧立即输出,
后续按配置周期聚合;
- `window_detections=0 hit_labels=none`:模型在工作,但本周期没有高于配置阈值的命中;
- 只有 loaded、长期没有 inference优先检查 cmvr-es 相机流、decoder 和关键帧;
- 短时观察可把 `inference_log_interval_s` 改为 `1` 秒,长期运行建议 `30``60` 秒,
省略该字段会关闭周期推理日志。
运行前确认 cmvr-es 已启用 `right_hand_cam`、Construction PPE 权重存在,并且 8081
平台接受 `POST /v1/detection-alerts`。默认 profile 下 `model_options.device` 应设为
`cpu`;只有完成设备专用的 CUDA/Jetson PyTorch 环境适配后,才能改为 `cuda:0` 等值。
## 模型与标签
`detection.model@1` 不绑定某一个框架;它通过 `DetectionModelRegistry` 查找配置中的
`model`。每个 `DetectionModelSpec` 注册版本化模型 ID、模型名称、backend 和有序
`supported_labels`。注册表当前包含两个内置模型,但 Pipeline 只引用第一个:
- `construction-ppe-yolov8@1`19 类,包含原分支用于违规告警的 `No-*` 标签;
- `ppe-6classes-yolov8n@1``Gloves`、`Vest`、`goggles`、`helmet`、`mask`、
`safety_shoe` 六个正向装备标签。
可用 `cmvr-edge-ai models` 核对 ID、名称、backend 和标签顺序。六类模型只表达
“检测到某件装备”,不包含 `Person``No-*` 类,也不执行人员/PPE 关联;所以它
不能直接判断某个人缺少装备。需要这种语义时,仍应增加人员检测、空间关联和缺失
判定节点,不能把“没有检测到 helmet”直接当成“人员未戴安全帽”。
检测节点的关键参数:
| 参数 | 语义 |
|---|---|
| `model` | 必须是已注册的精确模型 ID |
| `detect_labels` | 本次部署需要的标签;省略表示模型的全部注册标签 |
| `confidence` | 所有选中标签的默认置信度阈值 |
| `label_confidence` | 可选的逐标签阈值覆盖 |
| `max_fps` | 推理启动频率上限;过密的解码帧会被跳过 |
| `inference_log_interval_s` | 可选推理摘要周期(秒);首帧立即输出,省略表示关闭周期日志 |
| `attach_frame` | 是否让 `DetectionResult` 临时附带对应解码帧;生成告警图片时必须为 `true` |
| `model_options` | backend 私有选项;当前 YOLO 支持 weights、device、imgsz、iou、half、max_det、agnostic_nms |
配置阶段会拒绝未知模型、模型不支持的 `detect_labels`、重复标签和非法阈值。YOLO 加载 checkpoint 时还会严格比较 checkpoint 的实际标签及顺序与注册信息,避免类别编号静默错位。
## 六类逐推理结果分支
这一分支当前默认关闭。恢复后,六类 detector 不连接 repeat gate而是把每次完成推理产生的
`DetectionResult/v1` 直接交给第二个 HTTP Sink。其 POST Envelope 的
`schema``DetectionResult/v1``payload` 示例为:
```json
{
"detections": [
{
"label": "helmet",
"confidence": 0.91,
"box": {"x_min": 120.0, "y_min": 60.0, "x_max": 250.0, "y_max": 220.0},
"track_id": null
}
],
"model_id": "ppe-6classes-yolov8n@1",
"inference_ms": 34.2,
"model_name": "PPE Detection YOLOv8n (6 Classes)"
}
```
该结构刻意不同于原分支的 `DetectionAlert/v1`:它没有 `rule_id`、`hit_count`、
`event_id``image`,空检测帧也会以空 `detections` 数组上报。`cooldown_ms` 是
repeat gate 的内部规则配置,不属于 `DetectionAlert` payload。由于
payload 没有 event IDHTTP Sink 的 `Idempotency-Key` 回退使用 Envelope
`trace_id`。这只是模拟的逐结果接口;若平台要求另一套字段命名或嵌套,需要增加
平台专用转换节点/Sink而不是仅修改 endpoint。
## 重复触发规则
`detection.repeat_gate@1` 按规则维护滑动时间窗口。规则字段如下:
| 参数 | 语义 |
|---|---|
| `id` | 唯一规则 ID写入告警 |
| `labels` | 任一标签匹配即视为该规则在当前帧命中 |
| `min_confidence` | 规则侧最低置信度,可高于 detector 阈值 |
| `min_hits` | 触发所需的不同帧数 |
| `window_ms` | 上述命中必须落入的时间窗口 |
| `cooldown_ms` | 告警后的静默期;期间不累计,结束后重新计数 |
| `scope` | `source` 按相机统计;`track` 按相机内的 `track_id` 分别统计 |
一次命中按 `(source_id, sequence)` 的不同视频帧计算:同一帧出现多个相同标签框只算一次,重复投递同一帧也不会增加计数。一个规则即使配置多个标签,同一 scope 在一帧内仍只增加一次。`time_source` 可选 `captured`、`received` 或优先采集时间的 `auto`;当前 cmvr-es 成功帧通常没有 `header.timestamp`,示例显式使用 `received`
`alert_image.enabled` 默认为 `false`;启用后要求 detector 同时配置
`attach_frame: true`。`jpeg_quality` 是 `1..95` 的整数,默认 85。repeat gate
只在规则达到 `min_hits`、准备输出告警时,才用 Pillow 在当前阈值帧上绘制匹配的
bounding boxes、标签和置信度并编码 JPEG不会为每个推理结果都渲染。告警里的
`detections` 和图片框均来自阈值帧;窗口内更早的命中只参与次数、时间范围和
`max_confidence` 统计。
同一阈值帧同时触发多条规则或多个 track 时repeat gate 对这些告警相关框取并集,
只画框并编码一次,然后让本帧产生的告警共享该 JPEG避免在边缘端重复编码。
非有限坐标、反向/退化框、非法置信度等 detection 会在规则计数前被忽略,并记录在
`invalid_detections` 健康指标中,避免 `NaN/Inf` 令整个 HTTP JSON 告警无法发送。
当前 YOLO adapter 是逐帧检测,不执行人员跟踪,输出的 `track_id``None`。因此示例使用 `scope: source`;如果业务要求“同一个人连续多次违规”,必须在 detector 和 repeat gate 之间加入 tracker 及人员/PPE 关联节点,之后才能使用 `scope: track`
## 相机启动与视频流日志
cmvr-es 的 `StartCamera``GetRGBImageStream` 是两个不同阶段:前者打开物理相机,
后者只启动编码和流式传输。连接器在每个首次连接或重连 session 中严格执行:
```text
StartCamera -> 检查 feedback.header.success -> GetRGBImageStream -> 等待首帧
```
`StartCamera` 使用 `cmvr_es.timeout_s`RPC 异常或 `header.success=false` 都进入同一套
指数退避重连。正常关闭只取消当前 stream不自动调用设备级 `StopCamera`,避免影响
同一相机的其他客户端。
使用 `--log-level INFO --log-format json` 时,按顺序关注:
- `camera source configured`endpoint、device 和超时配置完成;
- `camera start requested/succeeded`StartCamera 已被调用并成功;
- `camera stream opening`:流请求已创建,正在等待第一帧;
- `camera stream first frame`:已经收到真实编码帧,包含 codec、尺寸、字节数和远程序号
- `camera stream progress`:每 `stream_log_interval_s` 秒输出帧数、FPS、bitrate、关键帧、
最后序号和 `last_frame_age_s`;即使完全没帧也会输出 `first_frame_received=false`
- `camera stream disconnected`:包含错误类型、重连次数和退避时间;
- `camera stream closed`、`camera source stopped`:当前 session 和本地 Source 已清理。
INFO 日志不会打印帧二进制。示例设置 `stream_log_interval_s: 5` 便于联调,正式部署
可改为 3060 秒。
## 视频连续性和背压
H264/H265 是有参考关系的编码流,解码前不能任意丢包:
- `camera -> decoder` 使用 `video_contiguous + block`,保持 edge-ai 内部已经接受的数据连续;
- decoder 在新 session、编码参数变化、sequence gap 或解码错误后释放上下文,等待下一个关键帧;
- `decoder -> detector` 已经是完整 BGR 图像,使用 `realtime_latest + drop_oldest` 和容量 1以有限内存换取较低实时延迟
- `detector -> repeat_gate``attach_frame: true` 时携带未压缩图像,示例把队列容量限制为 2避免排队的 `DetectionResult` 长时间占用大量内存。
该边使用 `drop_oldest` 偏向低延迟;持续过载时,被丢弃的推理结果不会计入
`min_hits`。如果业务更重视每次推理结果都参与计数,可改为 `overflow: block`,但要
接受延迟向上游传播,并继续保持很小的队列容量。
恢复第二模型时,应从 decoder 输出端口 fan-out以避免第二次相机订阅和第二次
H264/H265 解码;这不会复用模型计算,两个 YOLO 实例仍会分别加载权重并共享应用的
有界线程池。
这个保证只覆盖 edge-ai 内部。当前 cmvr-es 服务端通过 `getLatestEncodedFrame` 读取最新编码数据;如果它在负载或时序竞争下已经跳过参考包,`video_contiguous` 无法恢复丢失内容,而且当前 edge-ai 的本地 sequence 不能可靠暴露这种上游跳包。此时 PyAV 可能报错decoder 会重置并等待关键帧。上线前必须用真实摄像头长时间验证连续性;更稳妥的方案是让 cmvr-es 提供连续 access unit 流,或额外提供原始/JPEG/带显式 discontinuity 的 AI 接口。
相机 Source 默认启用指数退避重连;每次重新订阅生成新的 `session_id`,使 decoder 主动重置。`reconnect_initial_s`、`reconnect_max_s` 和可选 `max_reconnect_attempts` 可配置;省略最大次数表示持续重连直到进程关闭。
## HTTP 输出可靠性
每次规则触发会产生带唯一 `event_id``DetectionAlert/v1`。HTTP Sink 优先把该
`event_id` 放入 `Idempotency-Key`,没有事件 ID 时才回退到 `trace_id`。它对连接/超时
错误和配置的临时 HTTP 状态执行有限指数退避,示例最多尝试 3 次。
`failure_mode` 支持两种明确语义:默认 `raise` 在最终投递失败时让 Pipeline 失败;当前
检测配置使用 `log_and_drop`,最终连接失败、超时或非 2xx 时输出
`HTTP report dropped ... action=drop` WARNING丢弃当前告警并继续处理后续视频帧。
`CancelledError`、序列化错误和未知本地程序异常不会被吞掉。
当前唯一 HTTP 输出将告警发送到
`http://127.0.0.1:8081/v1/detection-alerts`。8082 六类逐推理结果 endpoint 当前未配置。
启用告警图片后POST JSON 的 `payload.image` 结构如下:
```json
{
"media_type": "image/jpeg",
"width": 1280,
"height": 720,
"encoding": "base64",
"data": "/9j/4AAQSk..."
}
```
`width/height` 是 JPEG 的像素尺寸HTTP JSON Sink 将内部 JPEG `bytes` 特判为
上述扁平对象,`encoding` 固定为 `base64`。关闭 `alert_image`,或运行时因第三方
结果未附带帧、坏帧等原因渲染失败时,告警仍会发送且该字段为 `null`。Base64 会
额外增加约三分之一的体积,平台和反向代理需要配置足够的请求体上限。
这不是持久化 outbox`log_and_drop` WARNING 表示该告警已经永久丢失,不会自动补发;
每次告警在放弃前仍会完成有限重试,因此平台离线时 Sink 队列会短暂阻塞。进程崩溃、
断电或告警仍在内存队列中时也可能丢失。平台必须按幂等键去重;对“不可丢告警”的
部署,还需要后续增加有界磁盘 outbox、投递确认和恢复发送。
## 部署风险
- 模型 README 报告某些小样本类别存在漏检,实际园区需要按相机角度、光照、遮挡和距离重新标定置信度与 `min_hits`;规则降噪不能补偿模型系统性漏检。
- 权重目录对权重许可的描述与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业部署前核对;同时确认训练数据来源和权重再分发权利。
- `.pt` 使用 PyTorch checkpoint loader只加载可信构建和受控分发的权重并固定、验证实际 Ultralytics 8.x 版本。
如果检测结果还要驱动机器人,控制路径必须保持:
```text
DetectionResult -> policy -> RobotCommand/v1
-> safety.robot_command_gate@1
-> ApprovedRobotCommand/v1
-> cmvr.grpc.agv_command_sink@1
```
编译器要求 actuator 的每个直接前驱都是安全门,安全门和执行器之间不能插入普通变换节点,也不能存在绕过路径。

180
detect_server/pipeline.yaml Normal file
View File

@ -0,0 +1,180 @@
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: 3
shutdown_timeout_s: 8
endpoints:
cmvr_es:
transport: grpc
# cmvr-es gRPC address. Change this value for each deployed robot.
target: 192.168.0.102:50052
tls: false
timeout_s: 5
options:
max_receive_mb: 32
platform:
transport: http
# Violation-alert platform HTTP base URL.
base_url: http://127.0.0.1:8081
timeout_s: 3
pipelines:
detection:
enabled: true
nodes:
camera:
uses: cmvr.grpc.camera_rgb_stream@1
with:
endpoint: cmvr_es
device_id: wrist_cam
pixel_format: BGR8
reconnect: true
reconnect_initial_s: 0.5
reconnect_max_s: 10
# Log stream state immediately on first frame and emit a periodic
# progress/stall heartbeat without printing every encoded frame.
stream_log_interval_s: 5
decoder:
uses: media.video_decoder.pyav@1
detector:
uses: detection.model@1
with:
model: construction-ppe-yolov8@1
# Omitting detect_labels means all registered labels. This example
# asks the backend to return only PPE violations used by the rules.
detect_labels:
- No-Boots
- No-Ear-Protection
- No-Glass
- No-Glove
- No-Helmet
- No-Mask
- No-Vest
confidence: 0.50
max_fps: 10
# Log the first completed inference immediately, then aggregate one
# heartbeat every 5 seconds so model activity is visible without
# printing every frame. Set to 1 for one-second debugging, or omit to disable.
inference_log_interval_s: 5
# Keep the decoded threshold frame available to repeat_gate so an
# annotated alert image can be rendered only when a rule triggers.
attach_frame: true
model_options:
# Model artifact and inference device are deployment configuration,
# not process environment requirements.
weights: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
device: cpu
imgsz: 640
iou: 0.70
half: false
max_det: 100
repeat_gate:
uses: detection.repeat_gate@1
with:
# cmvr-es currently omits capture timestamps on successful stream
# frames, so received time is the deterministic deployment default.
time_source: received
alert_image:
enabled: true
jpeg_quality: 85
rules:
- id: no-boots
labels: [No-Boots]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-ear-protection
labels: [No-Ear-Protection]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-glass
labels: [No-Glass]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-glove
labels: [No-Glove]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-helmet
labels: [No-Helmet]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-mask
labels: [No-Mask]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
- id: no-vest
labels: [No-Vest]
min_hits: 3
window_ms: 2000
cooldown_ms: 30000
min_confidence: 0.50
scope: source
platform:
uses: platform.http_json_sink@1
with:
endpoint: platform
path: /v1/detection-alerts
# Platform outages must not stop camera capture or inference. After
# bounded retries, log a WARNING and drop only this report.
failure_mode: log_and_drop
max_attempts: 3
retry_initial_s: 0.25
retry_max_s: 2
edges:
# Encoded H264/H265 packets must remain contiguous before decode.
- from: camera.frames
to: decoder.frames
qos:
profile: video_contiguous
capacity: 8
overflow: block
# Once frames are decoded, keeping only the newest frame bounds latency.
- from: decoder.frames
to: detector.frames
qos:
profile: realtime_latest
capacity: 1
overflow: drop_oldest
- from: detector.detections
to: repeat_gate.detections
qos:
profile: telemetry
# DetectionResult carries a decoded frame when attach_frame is enabled;
# keep this queue short so raw image buffers cannot accumulate. Under
# sustained overload drop_oldest also means dropped results do not count.
capacity: 2
overflow: drop_oldest
- from: repeat_gate.alerts
to: platform.input
qos:
profile: telemetry
capacity: 64
overflow: block

812
docs/architecture.md Normal file
View File

@ -0,0 +1,812 @@
# cmvr-edge-ai 架构与开发指南
本文描述当前代码已经实现的架构、配置语义和扩展边界。文中的“当前”指运行时 v1规划能力会明确标注避免把配置字段误认为已经具备的调度能力。
## 1. 目标与边界
`cmvr-edge-ai` 负责在单台机器人边缘计算设备上,把三类模块可靠地连接起来:
1. 上游数据cmvr-es 的相机、未来的麦克风音频流,以及平台下发的数据;
2. 中间 AI视频解码、检测、VAD、ASR、LLM、TTS、决策和策略
3. 下游输出cmvr-es 的 AGV/机械臂/扬声器控制,以及平台 HTTP/gRPC 接口。
框架刻意保持轻量:默认不依赖 Kafka、Redis、Celery 或 Kubernetes。一个进程内使用 `asyncio` 管理 I/O 和 DAG需要 CPU/GPU 隔离的插件自行接入有限线程池或常驻 Worker。这样可以在算力、内存和显存有限的边缘端按需增加复杂度。
核心设计约束:
- **配置驱动**YAML 只选择已注册插件、连接端口并声明 QoS/资源,不执行任意 Python 导入;
- **协议隔离**protobuf、HTTP JSON、未来 QUIC 包只在连接器中出现;
- **内部契约稳定**:算法通过 `Envelope` 和版本化 schema 交流;
- **有界资源**:所有边都有固定容量,满载行为必须显式配置;
- **故障显式**:节点异常默认终止 Pipeline只有连接器显式配置的有界重试和
`log_and_drop` 等降级策略可以改变该行为;
- **控制失效安全**:执行器只接收已批准命令,且其每个直接前驱都必须是安全门;持续运动还依赖 cmvr-es 服务端 lease/deadman。
## 2. 分层架构
```mermaid
flowchart TB
subgraph External["外部系统"]
CE["cmvr-es<br/>gRPC / future QUIC"]
PLAT["业务平台<br/>HTTP / gRPC"]
end
subgraph Boundary["协议边界"]
SRC["Source Connectors"]
SNK["Sink Connectors"]
POOL["共享 gRPC Channel / HTTP Client"]
end
subgraph Runtime["单进程 asyncio 运行时"]
ENV["Envelope + 内部契约"]
QUEUE["每条 Edge 的有界队列"]
OPS["Operator DAG"]
SAFE["Safety Gate"]
LIFE["生命周期 / 健康状态 / 优雅退出"]
end
CFG["严格 YAML + 插件注册表 + DAG 编译器"] --> Runtime
CE --> SRC
PLAT --> SRC
SRC --> ENV
ENV --> QUEUE --> OPS
OPS --> SAFE --> SNK
OPS --> SNK
SNK --> CE
SNK --> PLAT
POOL --- SRC
POOL --- SNK
LIFE --- SRC
LIFE --- OPS
LIFE --- SNK
```
### 2.1 配置层
`config/loader.py` 使用 `yaml.safe_load` 读取文件并递归展开环境变量,随后由 Pydantic 严格模型校验。所有模型均设置 `extra="forbid"`,拼错字段会立即失败。
`compiler.py` 继续完成无法只靠字段类型判断的检查:
- `api_version` 必须等于 `cmvr.edge.ai/v1`
- 插件必须存在,工厂创建的对象必须与声明的 Source/Operator/Sink 种类一致;
- Source 不能有入边Sink 不能有出边Operator/Sink 至少有一条入边;
- 端口必须存在schema 必须相同或其中一端为通配符 `*`
- 图必须无环,不能存在重复边;
- 带 `transport:<name>` 标签的连接器只能引用相同 transport 的 endpoint
- 活跃 Pipeline 中不能包含 `enabled: false` 的节点;
- 每个带 `actuator` 标签 Sink 的直接前驱都必须是带 `safety_gate` 标签的节点,安全门和执行器之间不能插入其他节点;
- v1 只接受 `execution.mode: async|inline` 和默认单并发语义;保留的 mode/并发字段会在编译期被拒绝;
- 显式保留 runtime 字段、非 `normal` priority 和非空 resources 会被拒绝,不会被静默忽略;
- QoS profile 必须是 v1 支持的五种之一,且 overflow 必须符合该 profile 的语义;
- 直接连接的 `detection.model@1 -> detection.repeat_gate@1` 会检查规则标签确实包含在 detector 本次选择的标签中;
- 当前未实现的 `max_age_ms``put_timeout_ms` 不能设置。
`validate` 会实际调用插件工厂,因此也会检查构造函数所需参数、已注册模型 ID 和标签选择;但不会执行组件的 `setup()`。YOLO 权重是否存在、网络连通性、cmvr protobuf 是否存在、PyAV/Ultralytics/Pillow 可选依赖是否可导入,以及 checkpoint 内部标签是否与注册信息一致,要到 `run` 的 setup 或首个编码帧时才检查;启用 `alert_image` 后 Pillow 会在 repeat gate 的 setup 阶段提前检查。
### 2.2 内部契约层
协议边界进入运行时后统一变为:
```text
Envelope[T]
├── payload: T
├── schema_name + schema_version
├── source_id + sequence
├── captured_at_ns + received_at_ns
├── deadline_ns
├── trace_id + session_id
└── attributes
```
`Envelope` 是冻结 dataclass`attributes` 被复制为只读 mapping。它只保证浅层不可变如果 payload 是 NumPy 数组等可变对象,发布后必须把它当作只读数据,才能安全地 fan-out 并为以后零拷贝/共享内存保留空间。
当前稳定内部 payload
| Schema | Python 类型 | 用途 |
|---|---|---|
| `ImageFrame/v1` | `ImageFrame` | 编码视频包或解码后的图像 buffer |
| `AudioChunk/v1` | `AudioChunk` | 连续音频块,包含格式、采样率和 discontinuity 标记 |
| `DetectionResult/v1` | `DetectionResult` | 检测框、标签、置信度和推理时间;可临时附带对应解码帧供告警节点使用 |
| `DetectionAlert/v1` | `DetectionAlert` | 规则 ID、模型、scope、时间窗口、命中数、置信度、阈值帧检测框、可选 JPEG 告警图和唯一 event ID |
| `TextEvent/v1` | `TextEvent` | ASR/LLM/TTS 链路中的文本事件 |
| `ChatTurn/v1` | `ChatTurn` | 带 session 和历史的对话输入 |
| `RobotCommand/v1` | `RobotCommand` | 带 TTL、序号和 command ID 的协议无关控制命令 |
| `ApprovedRobotCommand/v1` | `ApprovedRobotCommand` | 由可信安全门批准后交给执行器的控制命令 |
`PluginSpec` 中的 schema 是编译期契约字符串。当前运行时不会反射检查 payload 的 Python 类型;连接器和算法插件仍应使用 `isinstance` 或自己的严格模型在边界处失败。
### 2.3 组件层
运行时只有三类组件:
| 组件 | 输入/输出 | 核心方法 |
|---|---|---|
| `Source` | 无输入,一个或多个输出端口 | `messages()` 返回 async iterator |
| `Operator` | 一个或多个输入/输出端口 | `process(envelope, input_port)` |
| `Sink` | 一个或多个输入端口,无输出 | `consume(envelope, input_port)` |
所有组件共享以下生命周期:
```text
setup(context) -> start() -> 执行 -> health() -> stop()
```
建议在 `__init__` 只解析轻量参数,在 `setup` 建立网络连接或加载模型,在 `stop` 释放资源并保证幂等。`ComponentContext` 提供 `pipeline_id`、`node_id`、全局 shutdown event以及 endpoint、共享连接池和节点配置等 metadata。
Operator 可以返回:
- `None`:过滤该消息;
- `Envelope`:从默认 `output` 端口发送;
- `Emission(port, envelope)`:从指定端口发送;
- 上述对象的同步或异步 iterable一进多出。
Source 也可以直接 yield `Envelope``Emission`。返回原始 bytes、模型对象或其他未包装值会在产生该值的节点附近报错。
### 2.4 DAG 运行时
当前每个 Pipeline 在一个 asyncio event loop 中运行,每个节点一个 task。`execution.mode: async` 和 `inline` 在 v1 中都由该 task await 组件方法Operator 和 Sink 每次只处理一个消息。v1 不会复制节点 task也不提供配置驱动的并行调度。
每条有向边拥有独立队列。一个输出端口 fan-out 到多条边时,同一个只读 Envelope 会送入每个队列;队列之间的容量和丢弃计数独立。注意:路由会等待所有边的 `put()`,因此任意 fan-out 分支使用 `overflow: block` 且消费变慢时,仍会对共同生产者产生背压。希望“平台慢但控制链不停”时,平台分支必须选择合适的丢弃策略,或在后续加入独立 spool。
多个入边到同一节点时,运行时处理最先就绪的边,不保证不同边之间的全局顺序。单条边内部保持 FIFO`drop_*` 导致的丢弃除外)。
节点自然结束后,其全部出边会关闭;消费者会读完已接受的数据后结束。收到 SIGINT/SIGTERM 时:
1. 设置全局 shutdown event
2. 取消 Source task并先调用 Source 的 `stop()`
3. 让 Operator/Sink 在 `shutdown_timeout_s` 内排空队列;
4. 超时后取消剩余 task、丢弃未处理消息
5. 按 setup 的反序释放组件和共享网络客户端。
任何未被组件显式处理的节点执行异常都会包装为带 node ID 的 `NodeExecutionError`,停止
Pipeline并由 CLI 以运行错误退出。相机 Source 和平台 HTTP Sink 内部实现了各自明确、
有界的重连/重试语义HTTP Sink 还可显式选择 `log_and_drop`,但框架仍不提供通用节点级
supervisor 或 retry policy。
## 3. 配置参考
### 3.1 根字段
| 字段 | 必填 | 说明 |
|---|---:|---|
| `api_version` | 是 | 当前只能是 `cmvr.edge.ai/v1` |
| `runtime` | 否 | 进程级运行参数 |
| `endpoints` | 否 | 命名的外部服务连接信息 |
| `pipelines` | 是 | 至少一个命名 Pipeline |
endpoint、pipeline 和 node 名称只能包含字母、数字、`_`、`-`,且不能以数字开头。端口引用固定使用 `node.port`
### 3.2 `runtime`
| 字段 | 默认值 | 当前语义 |
|---|---:|---|
| `thread_workers` | `4` | 创建共享 `ThreadPoolExecutor`,并通过 ComponentContext 提供给插件 |
| `max_processes` | `1` | 保留v1 配置中必须省略,显式设置(即使写默认值)也会被拒绝 |
| `process_start_method` | `spawn` | 保留v1 配置中必须省略 |
| `shutdown_timeout_s` | `10.0` | 优雅排空的最长等待时间 |
| `health_bind` | `null` | 保留v1 配置中必须省略,当前不监听健康检查端口 |
| `reserved_memory_mb` | `0` | 保留v1 配置中必须省略,当前不执行内存仲裁 |
v1 可配置的 runtime 字段只有 `thread_workers``shutdown_timeout_s`。边缘端应保持较小的 `thread_workers`;第三方推理库本身还可能创建线程,应同时限制 OpenMP/MKL/模型运行时线程数,避免过度订阅 CPU。
### 3.3 `endpoints.<id>`
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `transport` | 无 | 必填,转为小写;当前连接器支持 `grpc`、`http` |
| `target` | `null` | gRPC 地址,例如 `127.0.0.1:50052` |
| `base_url` | `null` | HTTP 基础 URL |
| `bind` | `null` | 为未来 ingress/server 连接器预留 |
| `tls` | `false` | 仅用于 gRPC channel 是否使用 TLSHTTP endpoint 禁止设置该字段 |
| `timeout_s` | `5.0` | 连接器请求超时;流式相机订阅不使用该 unary timeout |
| `metadata` | `{}` | gRPC metadata 预留;当前 HTTP 连接器把它作为默认 header |
| `options` | `{}` | 传输私有参数 |
当前 gRPC options 支持:
```yaml
options:
root_certificates: /path/to/ca.pem
max_receive_mb: 32
max_send_mb: 8
channel_options:
grpc.keepalive_time_ms: 20000
```
HTTP 是否使用 TLS 由 `base_url``https://` scheme 决定,证书验证默认开启;只有隔离测试环境才可显式设置 `options.verify_tls: false`。当前 gRPC `metadata` 尚未自动附加到 RPC若需要鉴权应在连接器中显式实现。
### 3.4 `pipelines.<id>`
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `enabled` | `true` | 未指定 `--pipeline` 时是否启动 |
| `priority` | `normal` | v1 必须为 `normal``low/high` 或数值优先级会因无调度器而被拒绝 |
| `nodes` | 无 | 至少一个节点 |
| `edges` | `[]` | 有向连接列表 |
不要在活跃 Pipeline 中保留 `enabled: false` 节点;当前编译器会直接拒绝。要暂时关闭逻辑,请禁用整个 Pipeline 或从图和配置中移除该节点。
### 3.5 `nodes.<id>`
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `uses` | 无 | 版本化插件 ID例如 `cmvr.grpc.camera_rgb_stream@1` |
| `with` | `{}` | 原样传给插件工厂的参数Python 模型内字段名为 `params` |
| `execution` | 见下表 | 调度意图声明 |
| `resources` | `{}` | 预留资源需求字段v1 必须保持为空/默认值 |
| `enabled` | `true` | 当前只允许活跃 Pipeline 中为 `true` |
`execution` 字段:
| 字段 | 默认值 | 允许值/限制 |
|---|---:|---|
| `mode` | `async` | v1 支持 `async`、`inline``thread/process/model_worker` 为保留值并被编译器拒绝 |
| `concurrency` | `1` | v1 必须为 `1`,其他值被编译器拒绝 |
| `max_in_flight` | `null` | 预留;非空值被编译器拒绝 |
| `timeout_s` | `null` | 预留;非空值被编译器拒绝 |
| `ordered` | `true` | v1 必须为 `true``false` 被编译器拒绝 |
`async``inline` 当前没有调度差异,都是 event loop 中的单 task、单 in-flight 调用。配置模型保留其他枚举值是为了后续版本演进,不代表 v1 能执行它们;`validate` 会 fail closed而不是静默退化到 async。
`resources` 模型预留了 `cpu_cores`、`memory_mb`、`gpu_device`、`gpu_memory_mb`、`exclusive_gpu`,但 v1 无法强制执行资源隔离,因此任何非空/非默认声明都会被编译器拒绝。不要依赖“只写说明但不生效”的资源配置;当前应在部署层和插件实现中限制 CPU/GPU。
### 3.6 `edges[]` 与 QoS
```yaml
- from: camera.frames
to: decoder.frames
qos:
profile: video_contiguous
capacity: 8
overflow: block
- from: decoder.frames
to: detector.frames
qos:
profile: realtime_latest
capacity: 1
overflow: drop_oldest
```
| 字段 | 默认值 | 当前语义 |
|---|---:|---|
| `profile` | `request` | v1 支持 `realtime_latest`、`video_contiguous`、`audio_contiguous`、`request`、`telemetry` |
| `capacity` | `1` | 队列最大消息数 |
| `overflow` | `block` | `drop_oldest`、`drop_newest`、`block`、`reject``error` 是 `reject` 的兼容别名 |
| `max_age_ms` | `null` | 已预留;设置后编译失败 |
| `put_timeout_ms` | `null` | 已预留;设置后编译失败 |
溢出行为:
| 策略 | 队列满时行为 | 适合场景 |
|---|---|---|
| `drop_oldest` | 丢掉最旧消息,再接受新消息 | 实时视频,优先处理最新画面 |
| `drop_newest` | 保留已有消息,拒收这次新消息但不抛异常 | 需要保留已排队批次的低优先级数据 |
| `block` | 生产者等待空位 | 必须连续的视频编码包、音频或请求;会传播背压 |
| `reject` / `error` | 抛出队列满异常Pipeline 失败 | 不能静默丢失且希望监督器介入的控制链 |
v1 强制执行的 profile/overflow 矩阵:
| profile | 允许的 overflow | 典型用途与注意事项 |
|---|---|---|
| `realtime_latest` | `drop_oldest`、`drop_newest` | 已解码的完整视频帧;常用容量 `1..2`,优先限制推理延迟 |
| `video_contiguous` | `block`、`reject`、`error` | H264/H265 等 inter-frame 编码流在解码前不能静默丢包;此保证不覆盖上游在发送前已经丢失的数据 |
| `audio_contiguous` | `block`、`reject`、`error` | 连续音频不能静默丢弃;阻塞预算必须明确,拒绝时应重建连续性 |
| `request` | `block`、`reject`、`error` | 请求/控制;使用有限容量和消息 TTL禁止执行积压旧动作 |
| `telemetry` | 所有五种策略 | 平台上报;当前无磁盘 spool按数据重要性选择阻塞、丢弃或失败 |
队列记录入队、出队、水位、丢弃、拒绝和强制关闭丢弃计数,当前可通过 Python `PipelineRuntime.edge_stats()` 获取,尚未暴露为服务指标。
## 4. 内置插件与连接器
运行 `cmvr-edge-ai plugins` 可查看当前实际注册结果。
### 4.1 基础插件
| 插件 ID | 类型 | 端口 | 用途 |
|---|---|---|---|
| `core.sequence_source@1` | Source | `output: *` | 有限模拟数据源,用于 smoke/replay |
| `core.passthrough@1` | Operator | `input: * -> output: *` | 占位和协议无关透传 |
| `core.log_sink@1` | Sink | `input: *` | 把 Envelope 摘要和 payload 写日志 |
| `safety.robot_command_gate@1` | Operator | `RobotCommand/v1 -> ApprovedRobotCommand/v1` | TTL、白名单、参数限值、序号与限频并建立批准边界 |
安全门参数:
```yaml
with:
allowed_actions: [set_velocity, emergency_stop]
allowed_devices: [src1100]
min_interval_ms: 100
max_ttl_ms: 5000
enforce_monotonic_sequence: true
argument_limits:
set_velocity:
vx: {min: -0.3, max: 0.3}
vy: {min: -0.2, max: 0.2}
wz: {min: -0.5, max: 0.5}
```
`allowed_actions` 必须非空;`allowed_devices` 为空表示不额外限定设备。`max_ttl_ms` 默认 5000拒绝有效期异常长的命令。`require_limits_for` 可以列出还必须配置参数范围的 action`set_velocity` 无条件要求 `argument_limits.set_velocity` 同时定义 `vx/vy/wz`。范围边界必须是有限数且 `min <= max`。过期、过快、重放、乱序、未授权、缺少参数或越界命令都会 fail-closed 丢弃并写审计 warning不会因一条业务拒绝停止整条控制 Pipeline。通过检查后安全门把原始 `RobotCommand` 包装为带 `approved_by/approved_at_ns``ApprovedRobotCommand`
### 4.2 检测模型、解码与触发规则
检测流水线内置三个模型无关插件:
| 插件 ID | 输入 -> 输出 | 语义 |
|---|---|---|
| `media.video_decoder.pyav@1` | `ImageFrame/v1 -> ImageFrame/v1` | 用可选 PyAV 保持 H264/H265 decoder context输出 packed `BGR8`stream/session/codec/尺寸变化、sequence gap 或解码错误后重置并等待关键帧 |
| `detection.model@1` | `ImageFrame/v1 -> DetectionResult/v1` | 加载一个已注册模型,在线程池中推理,按部署标签和阈值二次过滤;可附带对应解码帧 |
| `detection.repeat_gate@1` | `DetectionResult/v1 -> DetectionAlert/v1` | 按不同帧、时间窗口、scope 和 cooldown 把逐帧检测转换为平台告警,仅在规则触发时按需画框并编码 JPEG |
核心包不会强制安装大型视觉运行时。使用解码器安装 `video` extra告警图的画框和 JPEG 编码由 Pillow 提供,安装独立的 `image` extra内置 YOLO adapter 提供通用 `yolo` 和固定 CPU wheel 的 `yolo-cpu` 两个互斥 extra。当前 PPE CPU 链路通过 `bash scripts/bootstrap.sh` 一键生成 cmvr-es bindings并按 `uv.lock` 安装 `grpc + http + video + image + yolo-cpu`。开发环境使用 `bash scripts/bootstrap.sh --profile dev`。PyAV、Pillow 和 Ultralytics 都延迟导入,因此不运行对应检测能力的对话服务不会加载它们。不要依赖 Ultralytics 间接带入 Pillow手动组合告警图片环境时必须显式选择 `--extra image`。CUDA/Jetson 必须按目标驱动或 JetPack 建立独立依赖源和锁文件。
具体模型不再各自注册一套 DAG 插件。`DetectionModelRegistry` 保存 `DetectionModelSpec`
| 字段 | 说明 |
|---|---|
| `model_id` | 版本化稳定 ID例如 `construction-ppe-yolov8@1` |
| `name` | 日志、CLI 和告警中的可读模型名称 |
| `supported_labels` | 有序、非空、无重复的标签;顺序必须与 backend 类别编号一致 |
| `backend` | backend 标识,例如 `ultralytics-yolo` |
| `factory` | 接收 `model_options` 并返回 `DetectionModel` 的工厂 |
`cmvr-edge-ai models` 输出当前 model registry 的 ID、name、backend 和 labels。第三方包可在 `cmvr_edge_ai.detection_models` entry point 中暴露一个 `DetectionModelSpec` 或注册回调。当前内置 `construction-ppe-yolov8@1`19 类)和 `ppe-6classes-yolov8n@1``Gloves/Vest/goggles/helmet/mask/safety_shoe`YOLO adapter 在加载时严格比较 checkpoint `model.names` 与各自注册的标签及顺序,避免错误的类别编号继续运行。
`detection.model@1` 参数:
```yaml
with:
model: construction-ppe-yolov8@1
detect_labels: [No-Helmet, No-Vest]
confidence: 0.5
label_confidence:
No-Helmet: 0.6
max_fps: 10
inference_log_interval_s: 5
attach_frame: true
model_options:
weights: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
device: cpu
imgsz: 640
```
- `detect_labels` 省略时选择模型注册的全部标签;显式空列表、重复或未知标签会失败;
- `confidence` 是全局阈值,`label_confidence` 可逐标签覆盖backend 接收所有选中标签中的最低阈值,通用 Operator 再逐框做严格后过滤;
- `max_fps` 是推理启动频率上限,跳过的帧不会产生 `DetectionResult`
- `inference_log_interval_s` 省略时不输出周期推理日志;配置后第一帧立即输出,之后按
周期聚合 `window_frames/window_detections/hit_labels/avg_inference_ms`。模型加载成功
始终输出一次 INFO。这样可以区分“权重已加载但没有输入帧”“持续推理但没有命中”与
“检测到目标”,同时避免逐帧日志拖慢边缘端;
- `attach_frame` 默认 `false`;启用时 `DetectionResult` 临时引用对应的解码帧,供 repeat gate 在阈值帧上生成告警图片。由于它携带未压缩图像detector 到 repeat gate 的队列容量必须保持较小;
- `model_options` 属于 backend内置 YOLO 支持 `weights/device/imgsz/iou/half/max_det/agnostic_nms`
- YOLO 只接受解码后的 `BGR8/RGB8` packed buffer并把 `RGB8` 转为 backend 使用的 BGR 顺序;它不运行 tracker返回的 `track_id``None`
示例 detector 到 repeat gate 使用小容量 `drop_oldest` 队列以控制延迟和原始帧内存;
持续过载时丢弃的推理结果不会参与 `min_hits`。需要每个已完成推理都参与计数时可改
`block`,代价是背压和延迟向上游传播。
如果恢复六类模型,可把同一个 decoder 输出 fan-out 到两个 detector而不是创建两个
独立 Pipeline
```text
camera -> decoder -> construction detector -> repeat gate -> 8081 alert API
`-> six-class detector -----------------> 8082 result API
```
该可选双分支方案的两条 `decoder.frames` 出边使用独立的
`realtime_latest + drop_oldest` 容量 1 队列,
路由的是同一个只读 `ImageFrame` 引用。这样只建立一个相机 gRPC stream 和一个 PyAV
decoder如果把两个分支拆成两个 Pipeline即使 gRPC channel 可以复用,也会创建
两个订阅 RPC 和两个 decoder 实例。fan-out 只复用输入,两个 YOLO 模型仍分别加载并
执行推理,共享进程级有界线程池。示例把六类 YOLOv8n 限制为 5 FPS避免在尚未完成
目标硬件测量前让两个 CPU 模型都追赶相机帧率。
六类模型只包含正向装备类,没有 `Person``No-*` 标签;它的
`DetectionResult/v1` 表示当前推理帧中“检测到了哪些装备”,不能单独推断某个人
缺少装备。该分支保持 `attach_frame: false`,绕过 repeat gate把每个完成推理的
结果直接发送到 `/v1/ppe-detections`,所以不会生成 `DetectionAlert``rule_id`
`hit_count`、event ID 或告警图字段;`cooldown_ms` 只是 repeat gate 的内部规则配置,
不属于告警 payload。输出边使用容量 16 的 `telemetry + drop_oldest`,模拟
平台持续变慢时会优先保留较新的结果;要求逐条可靠送达时应改用可接受背压的策略或
增加持久 outbox。
`detection.repeat_gate@1``rules` 每项包含 `id/labels/min_hits/window_ms/cooldown_ms/min_confidence/scope`。一个规则/scope 在同一 `(source_id, sequence)` 帧最多增加一次 hit不按框数量累加`window_ms` 内达到 `min_hits` 才输出告警。触发后清空命中窗口,`cooldown_ms` 内的帧不累计,冷却结束后重新计数。`time_source` 支持 `captured/received/auto``captured` 缺失会失败,`auto` 优先 captured 后回退 received。
告警图片是 repeat gate 的全局配置,不需要在每条 rule 中重复:
```yaml
with:
time_source: received
alert_image:
enabled: true
jpeg_quality: 85
rules:
- id: no-helmet
labels: [No-Helmet]
min_hits: 3
window_ms: 2000
```
`alert_image.enabled` 默认 `false`;启用时上游 detector 必须设置
`attach_frame: true`。`jpeg_quality` 默认 85只接受 `1..95` 的整数。Pillow 只在
规则达到阈值并准备发出告警时绘制 bounding boxes、标签和置信度并编码 JPEG
不会为每个 `DetectionResult` 产生图片。
`scope: source` 按相机和规则隔离状态;`scope: track` 还按 `track_id` 隔离,同一 track ID 在不同相机之间不会混合。没有 track ID 的检测会被 track 规则忽略。当前 Construction PPE 告警分支必须使用 `source`;要判断同一个人,需要先增加 tracker 和人员/PPE 关联节点。
每次触发创建 `DetectionAlert.event_id`。告警的 `detections` 和图片中的 bounding boxes 只来自达到 `min_hits` 的阈值帧,以控制内存和 HTTP payload窗口内更早帧只参与 `labels/max_confidence/first_seen/last_seen/hit_count` 的聚合。内部可选图片为 JPEG bytesHTTP wire 中 `payload.image` 被序列化为扁平对象:
```json
{
"media_type": "image/jpeg",
"width": 1280,
"height": 720,
"encoding": "base64",
"data": "/9j/4AAQSk..."
}
```
关闭图片,或因第三方结果未附带帧、坏帧等原因渲染失败时,告警仍会发送且
`payload.image``null`证据图失败不能阻断结构化告警。Base64 比原始 JPEG
额外增加约三分之一体积,平台入口和反向代理必须设置相应的请求体上限。
同一阈值帧同时触发多条规则或多个 track 时,节点对所有相关告警框取并集并只编码
一次,再让这些告警共享同一个不可变 JPEG从而限制瞬时 CPU 与内存开销。
非有限坐标、反向/退化框、非法置信度或 track ID 的 detection 会在规则计数前被
忽略并增加 `invalid_detections` 健康计数,防止 `NaN/Inf` 破坏整个 HTTP JSON。
### 4.3 cmvr-es gRPC 连接器
`cmvr.grpc.camera_rgb_stream@1`
- 参数:`endpoint`、必填 `device_id`、可选 `pixel_format`(默认 `BGR8`)、
`stream_log_interval_s`(默认 30 秒),以及
`reconnect/reconnect_initial_s/reconnect_max_s/max_reconnect_attempts`
- 每个首次连接和重连 session 都先调用 unary `CameraService.StartCamera`,复用 endpoint
`timeout_s`,并检查业务反馈 `header.success/error_message`;只有成功后才调用
`CameraService.GetRGBImageStream`。这两步不能省略:前者打开物理相机,后者在服务端
启动编码流;
- 发送一次订阅请求并保持 request side兼容当前 cmvr-es 行为和未来真正双向协议;
- 把 `color_frame` 转为 `ImageFrame/v1`,保留 codec、关键帧、内参、序号和时间戳
- 默认在 Start RPC、断流、流 RPC 或业务失败后指数退避重连;每次尝试使用新的
`session_id`,收到一帧后重置连续失败计数;`max_reconnect_attempts` 省略表示持续尝试;
- INFO 日志覆盖 source configured、StartCamera 请求/成功、stream opening、首帧、周期
progress、stream close 和 source stopWARNING/ERROR 覆盖业务反馈错误、断流、重连与
重试耗尽。progress 即使尚未收到首帧也会定时输出,并报告帧数、吞吐、关键帧、
codec、尺寸和最后一帧年龄但不会输出图像 bytes
- stop 时主动 cancel stream并且等待重连期间也能被 shutdown event 立即打断。默认
不调用设备级 `StopCamera`,避免中断同一相机的其他客户端;
- 不做 H264 解码,编码包必须交给有状态解码器插件。
重连不能修复 cmvr-es 生产端已经丢失的编码包。当前 cmvr-es 服务端使用 `getLatestEncodedFrame` 取得最新数据,负载或时序竞争可能在 edge-ai 看到数据之前跳过 H264/H265 参考包;本地 `video_contiguous` 只保护已经入队的数据,而且 camera Source 的本地 sequence 不能可靠表示这种上游跳包。decoder 遇到 FFmpeg 错误会重置并等待关键帧,但上线前仍应验证服务端提供连续 access unit或增加原始/JPEG/显式 discontinuity 的 AI 接口。
`cmvr.grpc.agv_command_sink@1`
- 参数:`endpoint` 和固定、非空的 `device_id`;一个 Sink 只绑定一台设备;
- 输入必须为 `ApprovedRobotCommand/v1`,直接传入原始 `RobotCommand` 会被拒绝;
- 支持 `emergency_stop`、`clear_fault`、`pause_navigation`、`resume_navigation`、`cancel_navigation`、`stop_velocity_control`、`stop_mapping` 和 `set_velocity`
- `set_velocity` 默认禁用;只有显式设置 `unsafe_allow_unleased_velocity: true`,并在 Sink 再配置完整 `velocity_limits.vx/vy/wz` 后才会发出;
- `set_velocity``vx/vy/wz` 必须是有限数,并同时通过安全门和 Sink 两层范围校验;
- 同时检查 gRPC 调用异常与响应 `header.success`,并把 RPC timeout 限制在命令剩余 TTL 内;
- 不自动重试运动命令;正常关闭时若曾成功发送速度,会尽力调用 `stopVelocityControl`
下面只展示 actuator 节点的危险参数;该节点仍必须直接连接前述安全门。此开关只应用于架空轮、受控台架等隔离环境:
```yaml
actuator:
uses: cmvr.grpc.agv_command_sink@1
with:
endpoint: cmvr_es
device_id: src1100
unsafe_allow_unleased_velocity: true
velocity_limits:
vx: {min: -0.3, max: 0.3}
vy: {min: -0.2, max: 0.2}
wz: {min: -0.5, max: 0.5}
```
`unsafe_allow_unleased_velocity` 必须是 YAML 布尔字面量,字符串 `"true"/"false"` 会被拒绝。该 override 不是 crash-safetyedge-ai 被 `SIGKILL`、崩溃、断电或与 cmvr-es 网络分区时Python `stop()` 没有机会可靠执行。真实机器人必须由 cmvr-es 服务端持有速度 lease并在 lease/heartbeat 超时后独立执行清零和停车。
### 4.4 平台 HTTP 连接器
`platform.http_json_sink@1` 接收任意 schema
- 参数:`endpoint`、`path`(默认 `/`)、`max_attempts`(默认 3、`retry_initial_s`、`retry_max_s` 和 `retry_statuses`
- POST Envelope 元数据、输入端口、attributes 和 payload
- bytes 转换为 `{encoding: base64, data: ...}`
- `EncodedImage` 特判为 `media_type/width/height/encoding/data` 同层的扁平对象,避免 `payload.image.data` 再嵌套一层;
- payload 有非空 `event_id` 时以它作为 `Idempotency-Key`,否则使用 `trace_id`;平台仍必须真正实现按键去重;
- 对连接/超时类错误以及默认 `408/425/429/500/502/503/504` 执行有限指数退避;不在
allowlist 的状态不会重试;最终失败由 `failure_mode` 决定:`raise` 抛异常,
`log_and_drop` 输出 WARNING、丢弃当前报告并继续
- 没有持久化 outbox/spool。有限重试只存在于当前进程内崩溃、断电或重启不会恢复尚未投递的告警。
同一个通用 Sink 可以按节点配置不同的 endpoint 和 path。当前检测配置只把
`DetectionAlert/v1` 发到 `http://127.0.0.1:8081/v1/detection-alerts`。若恢复六类模型,
可把每次推理的 `DetectionResult/v1` 发到独立 endpoint它与告警复用 Envelope JSON
外层,但 payload 契约不同,且没有 event ID幂等键会回退为 `trace_id`。Sink 不支持
通过 YAML 重命名或重排 payload 字段,如果目标平台要求自定义 wire contract应增加
平台专用转换节点或 Sink。
运行时对未处理异常采用 fail-fast。HTTP Sink 默认 `failure_mode: raise`,最终发送失败
会终止它所在的 Pipeline当前 8081 告警 Sink 显式使用 `log_and_drop`,所以平台离线只会
产生 WARNING 并丢弃对应告警,不会停止相机和检测。该模式不是可靠投递机制。
## 5. 插件开发约定
### 5.1 创建组件
插件工厂固定接收 `node_id: str``params: Mapping[str, Any]`,应在构造阶段检查无需 I/O 的必填参数。下面展示一个最小 Operator
```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
class UppercaseOperator(Operator):
def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
self._node_id = node_id
self._prefix = str(params.get("prefix", ""))
async def process(
self,
envelope: Envelope[Any],
input_port: str = "input",
) -> Emission:
del input_port
event = envelope.payload
if not isinstance(event, TextEvent):
raise TypeError(
f"{self._node_id} expected TextEvent, got {type(event).__name__}"
)
result = TextEvent(
text=self._prefix + event.text.upper(),
role=event.role,
final=event.final,
)
return Emission(
"output",
envelope.with_payload(
result,
schema_name="TextEvent",
schema_version=1,
),
)
```
使用 `with_payload()` 可以保留 trace、session、sequence、采集时间和 deadline只更新载荷与接收时间。除非开始了一次新的独立请求不要随意生成新的 `trace_id`
### 5.2 注册 PluginSpec
```python
from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec
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 a text event",
tags=frozenset({"domain:dialogue"}),
)
)
```
规则:
- ID 必须包含版本,例如 `@1`;不兼容的端口或参数变更发布新版本;
- Source 不能声明 inputSink 不能声明 output
- 端口名必须与组件实际发送/接收的端口一致;
- 使用精确 schema只在真正协议无关的调试/路由节点使用 `*`
- 执行器 Sink 加 `actuator` 标签,并只声明/接受 `ApprovedRobotCommand/v1`;安全节点加 `safety_gate` 标签并完成 `RobotCommand -> ApprovedRobotCommand` 转换;不要为了通过编译给普通变换节点冒充安全标签;连接器加 `transport:grpc` 等标签;
- 同一个 ID 重复注册会失败。
### 5.3 通过 entry point 发布
第三方插件包的 `pyproject.toml`
```toml
[project]
name = "cmvr-edge-ai-example-plugin"
version = "0.1.0"
dependencies = ["cmvr-edge-ai>=0.1,<0.2"]
[project.entry-points."cmvr_edge_ai.plugins"]
example = "my_cmvr_plugin.plugins:register_plugins"
```
entry point 可以直接导出一个 `PluginSpec`,也可以像示例一样导出接收 registry 的回调。安装包后,默认 registry 会在 CLI 启动时发现它。只安装可信插件entry point 是 Python 可执行代码,虽然 YAML 本身不能任意导入模块,已安装插件仍拥有当前进程权限。
### 5.4 阻塞计算与模型
禁止直接在 `process()` 中执行长时间阻塞 I/O 或 Python/模型推理,否则整个 event loop 的 gRPC、HTTP、队列和其他 Pipeline 都会停顿。
框架提供 `workers.run_blocking()`。插件在 `setup()` 中取得应用共享的有界线程池,再显式 offload
```python
from cmvr_edge_ai.workers import run_blocking
async def setup(self, context):
self._thread_executor = context.metadata["thread_executor"]
async def process(self, envelope, input_port="input"):
result = await run_blocking(
self._blocking_infer,
envelope.payload,
executor=self._thread_executor,
)
return envelope.with_payload(
result,
schema_name="DetectionResult",
schema_version=1,
)
```
线程 offload 适合释放 GIL 的推理库或阻塞 SDK。不要省略 `executor` 后假定 `runtime.thread_workers` 仍然生效;未指定 executor 时会落到宿主 event loop 的默认 executor。
纯 Python CPU 密集任务、大模型或需要故障隔离的模型应由插件维护常驻进程/模型 Worker不要每帧创建进程或重复加载模型。框架提供 `PersistentProcessWorker` 作为小型基础设施:它使用一个常驻子进程、有界请求/结果队列、串行关联和可选超时,并把子进程异常还原为 `RemoteWorkerError`。请求一旦超时或在途取消Worker 会标记为 `poisoned` 并拒绝后续 submit避免迟到结果被误配插件必须 stop 后新建 Worker。插件仍负责在 `setup/start` 创建和启动它、在 `stop` 回收它,并确保 `spawn` 模式下 factory/payload 可序列化。
在 v1 配置中,节点仍必须使用 `execution.mode: async`(默认)或 `inline`,且保持默认单并发字段。`thread|process|model_worker` 以及非默认并发字段会在编译期被拒绝。上面的线程/进程 helper 是组件内部显式调用的实现细节,框架不会根据 YAML 自动 offload。插件需要自行限制 in-flight 数量并在 `stop()` 回收 Worker。若跨进程发送大图像优先传编码数据确认复制成为瓶颈后再实现固定大小共享内存池并只通过 IPC 传 slot/shape/dtype/时间戳。
### 5.5 开发新协议连接器
新增 gRPC、HTTP、UDP 或 QUIC 支持时,应把 wire format 的解析和错误映射放在 `connectors/`/`transports/`,对 DAG 只暴露内部契约:
```text
wire message -> 校验 -> 内部 payload -> Envelope -> AI Operators
AI result -> typed command/event -> wire message -> external service
```
不要让算法插件导入 `*_pb2`。连接器应:
- 在 `setup` 获取/建立连接,在 `stop` 取消 stream 并释放资源;
- 为每条消息保留设备 ID、序号、采集时间、trace/session
- 区分 transport failure 与业务响应失败;
- 明确超时、重连、幂等和背压策略;
- 对流式编码数据保持状态,不能把任意 H264 packet 当作独立图片;
- 为音频丢包/重连产生 `discontinuity`,不能静默拼接不连续 PCM
- 对控制接口使用显式 action 到 RPC 的白名单映射,禁止从配置反射任意方法名。
- 新增控制 Sink 时只接受 `ApprovedRobotCommand`,并要求安全门成为图上的直接前驱;不要兼容接收原始 `RobotCommand`
gRPC transport 已提供有界的 `BidiRequestStream` 请求迭代器,可供未来真正的双向音频 RPC 使用。它限制为单消费者,正常 close 会排空已接受请求并原子唤醒被背压阻塞的 producer若 gRPC 放弃迭代器则丢弃无法再发送的请求。它只解决 request side 的背压与关闭,不包含任何音频 proto、重连、响应解析或扬声器策略这些仍属于具体连接器。
## 6. 并发和资源模型
当前真实执行关系如下:
```mermaid
flowchart LR
MAIN["一个 Python 进程"] --> LOOP["一个 asyncio event loop"]
LOOP --> P1["Pipeline A: 每节点一个 task"]
LOOP --> P2["Pipeline B: 每节点一个 task"]
LOOP --> TP["共享有界 ThreadPoolExecutor"]
P1 -. "插件显式 workers.run_blocking" .-> TP
P2 -. "插件显式 workers.run_blocking" .-> TP
P1 -. "插件自行实现" .-> MW["可选常驻进程/Model Worker"]
```
多个 Pipeline 共用同一 event loop、gRPC ChannelPool、HttpClientPool 和应用线程池。同名 endpoint 会复用客户端;同一 endpoint ID 如果请求了冲突的 settings 会报错。线程池只通过 `ComponentContext.metadata["thread_executor"]` 提供;`execution.mode: thread` 在 v1 是非法保留值,不会自动调用线程池。
共享 gRPC channel 不等于共享流式 RPC 或 Source 实例。两个 Pipeline 各自配置同一
camera node 时会建立两个订阅并重复解码;同一物理输入需要供多个模型使用时,应优先
在一个 Pipeline 内从 decoder 的输出端口 fan-out。每个 fan-out 分支仍应使用独立的
小容量队列,避免慢模型把实时图像积压在内存中。
边缘端容量规划建议:
- 优先减少输入帧率、分辨率和队列容量,不用无界缓存换吞吐;
- 同一 GPU 上尽量复用一个常驻模型实例,避免每 Pipeline 重复加载;
- 编码视频在 decoder 前使用 `video_contiguous`,不能用 latest-frame 丢包;只有 decoder 输出完整图像后,才能在推理前使用 `realtime_latest` 主动丢旧帧;当前运行时不会根据 deadline 自动丢弃;
- 音频需要连续性,不能照搬视频的 `drop_oldest`
- 平台上报与机器人控制使用不同边,并为非关键遥测选择非阻塞溢出策略;
- 通过实际端到端延迟、queue high watermark、drop counter 和显存峰值决定是否引入进程/共享内存。
## 7. 控制安全模型
机器人控制链必须保持以下形态:
```text
AI 结果 -> Policy -> RobotCommand/v1
-> Safety Gate
-> ApprovedRobotCommand/v1
-> 类型化执行器 Sink
```
这里有四层进程内防线:
1. 编译器要求 `actuator` 的每个直接前驱都带 `safety_gate` 标签;安全门与 Sink 之间不能插入转换/透传节点,也不能增加旁路输入;
2. 安全门只接收 `RobotCommand/v1`,检查通过后才输出 `ApprovedRobotCommand/v1`
3. AGV Sink 的端口 schema 和运行时类型检查都只接受 `ApprovedRobotCommand`
4. AGV Sink 只支持明确列出的 action不会根据配置字符串反射调用任意 gRPC 方法,也不会自动重试运动命令。
`ApprovedRobotCommand` 是进程内类型边界,不是加密签名。只有可信插件可以被安装并赋予 `safety_gate` 标签;恶意或被篡改的 Python 插件仍拥有当前进程权限,因此插件供应链、包版本锁定和文件完整性属于安全模型的一部分。
当前安全门已经检查:
- payload 必须是 `RobotCommand`
- Envelope deadline 和 `RobotCommand.valid_until_ns`
- action 白名单;
- 可选 device 白名单;
- 为 action 配置的每个参数必须存在、是有限数并位于 `[min, max]`
- `set_velocity` 必须为 `vx/vy/wz` 配置完整范围;`require_limits_for` 可把同一规则扩展到其他 action
- 默认按 `(device, action)` 拒绝 sequence 重放和乱序;
- 同一 `(device, action)` 的最小发送间隔。
投入真实机器人前仍必须由机器人型号相关 Policy/Safety 插件补齐:
- 除已配置参数范围外的速度、位置、加速度和 jerk 联合约束;
- 当前模式、故障、急停、定位质量、电量及障碍物状态;
- command ID 去重和多来源优先级仲裁;
- 急停独立高优先级路径和权限控制;
- 审计日志、认证、TLS 和密钥管理。
最重要的边界位于进程外:持续速度命令必须由 cmvr-es 服务端发放短期 lease并由独立 deadman 在续租/heartbeat 超时后清零速度。当前 Sink 会在调用前检查 TTL并把客户端 RPC timeout 截断到剩余 TTL但当前 cmvr-es 请求没有承载 `valid_until/command_id/sequence`,服务端无法据此拒绝网络中迟到的命令。`unsafe_allow_unleased_velocity`、客户端 TTL、安全门以及 Sink `stop()` 都不能覆盖进程崩溃、`SIGKILL`、断电或网络分区。cmvr-es 未实现 server-side lease/deadman 前,`set_velocity` 只允许在隔离测试环境使用,禁止用于真实机器人持续运动。
运动命令默认不重试。只有外部 API 提供明确幂等契约,并完成 command ID 去重后,才能为特定 action 增加有界重试。
## 8. 运维流程
推荐发布/启动顺序:
1. 安装固定版本的核心包和可信插件包;
2. 如果使用 cmvr-es gRPC 连接器,安装匹配 cmvr-es proto 版本的 `cmvr-api` wheel或运行 stub 生成脚本;
3. 在部署 YAML 中配置 endpoint、证书引用、模型路径和推理设备凭据应通过部署平台的 secret 机制注入;
4. 执行 `cmvr-edge-ai plugins``cmvr-edge-ai models`,保存实际插件、模型及标签清单;
5. 执行 `cmvr-edge-ai validate -c ...`
6. 确认 cmvr-es 设备启用、设备 ID 正确、平台接口可达;
7. 先运行回放/smoke再连接真实传感器
8. 控制链先使用空载/限速/人工急停条件验证;
9. 使用 SIGTERM 停止并给 `shutdown_timeout_s` 留出排空时间。
示例命令:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate -c detect_server/pipeline.yaml --pipeline detection
uv run --no-sync cmvr-edge-ai run -c detect_server/pipeline.yaml \
--pipeline detection \
--log-level INFO \
--log-format json
```
当前日志可输出文本或单行 JSON。日志中不要写入音频原始数据、图像 base64、认证 metadata 或用户隐私内容;生产插件应只记录 trace ID、schema、耗时、尺寸、丢弃计数和经过脱敏的错误信息。
## 9. 当前限制与演进顺序
### 9.1 当前限制
- cmvr-es 音频双向流 proto 尚未实现;框架只有 `AudioChunk` 契约和对话占位配置;
- 检测链路已经提供 PyAV H264/H265 解码、双 PPE YOLO 注册/推理、单次解码 fan-out 和重复触发规则VAD、ASR、LLM、TTS 仍需插件提供;
- UDP/QUIC 只有扩展目录,无 transport/connector
- execution v1 只支持 `async/inline` 默认单并发;其他 mode 和非默认并发字段会在编译期拒绝,线程/进程/模型 offload 必须由插件显式实现;
- 非空 resources、非 `normal` priority 和显式保留 runtime 字段都会被拒绝;`health_bind` 未启动服务,队列指标也未导出;
- 无热更新、overlay、通用节点级 supervisor/restart、持久化 outbox/spool相机和 HTTP 的重连/重试是 connector 内部的局部策略;
- 无自动 deadline 丢弃、`max_age_ms`、`put_timeout_ms`
- 无共享内存池;跨进程 Worker 由插件负责;
- 配置只支持单个 YAML 加环境变量,不支持 include/merge
- 当前 cmvr-es 相机成功响应通常未填写 `header.timestamp`,因此连接器的 `captured_at_ns` 可能为 `null`;精确采集时延需要后续在帧协议中增加设备采集时钟,不能用客户端接收时间冒充;
- cmvr-es 当前通过 `getLatestEncodedFrame` 取得最新编码数据,可能在 edge-ai 收到之前跳过 inter-frame 参考包;本地连续队列和重连无法补回上游丢失内容,必须验证/改造服务端流语义;
- HTTP Sink 对告警使用 `event_id`、对普通逐帧结果回退使用 `trace_id` 作为幂等键,并
提供有限退避和 `raise/log_and_drop` 终态策略,但没有持久投递;`log_and_drop` 的
WARNING 即表示该报告已丢失,进程崩溃或断电也可能丢失内存中的输出;
- 内置 YOLO adapter 不产生 `track_id`,所以 `scope: track` 需要外部 tracker 及人员/PPE 关联节点;
- PPE `.pt` 只能来自可信制品源商业部署还需核对权重说明、Ultralytics runtime/checkpoint 的 AGPL/商业许可,以及训练数据和权重分发权利;
- AGV `set_velocity` 默认禁用unsafe override 和正常 shutdown stop 都不具备崩溃安全性,不能替代 cmvr-es server-side lease/deadman、机器人本体限位、急停和功能安全系统。
### 9.2 推荐演进顺序
1. **真实流与模型验收**:用录制数据和目标边缘设备验证 cmvr-es 编码连续性、PyAV 长时间恢复、PPE 精度/FPS/内存/显存和端到端告警;
2. **可靠告警投递**:在现有 event ID 和有限重试之上增加有界持久 outbox、确认、恢复发送和容量/保留策略;
3. **同人违规语义**:增加 tracker 与 Worker/PPE 空间关联,验证 ID switch 后再启用 `scope: track`
4. **补齐可观测性**:导出 health、队列水位/丢弃、重连/重试、节点延迟和模型资源指标;
5. **控制安全闭环**:先在 cmvr-es 实现速度 lease/server-side deadman再补齐设备状态输入、机器人型号限值、优先级与审计然后才启用真实 AGV/机械臂动作;
6. **接入音频双向流**proto 落地后实现麦克风 Source/扬声器 Sink严格处理 chunk 顺序、背压和 discontinuity
7. **按测量结果优化并发**:先使用现有显式 thread offload再按测量结果增加常驻 model/process worker确认复制瓶颈后才加入共享内存
8. **扩展协议**:用相同内部契约实现 QUIC/UDP connector不修改算法插件。
每一步都应先通过 `validate`、smoke、录制数据 replay 和资源峰值检查,再接入真实设备。

91
pyproject.toml Normal file
View File

@ -0,0 +1,91 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "cmvr-edge-ai"
version = "0.1.0"
description = "Configuration-driven AI pipeline runtime for CMVR robot edge devices"
readme = "README.md"
requires-python = ">=3.10,<3.13"
dependencies = [
"pydantic>=2.8,<3",
"PyYAML>=6,<7",
]
[project.optional-dependencies]
grpc = [
"grpcio>=1.76,<2",
"protobuf>=6.31.1,<7",
]
http = [
"httpx>=0.27,<1",
]
video = [
"av>=12,<17",
]
image = [
"Pillow>=10,<13",
]
yolo = [
"numpy>=1.24,<3",
"ultralytics==8.4.31",
]
yolo-cpu = [
"numpy>=1.24,<3",
"torch==2.7.0",
"torchvision==0.22.0",
"ultralytics==8.4.31",
]
[dependency-groups]
test = [
"pytest>=7.4,<9",
"pytest-asyncio>=0.23,<1",
]
codegen = [
"grpcio-tools>=1.76,<2",
]
dev = [
{ include-group = "test" },
{ include-group = "codegen" },
]
[project.scripts]
cmvr-edge-ai = "cmvr_edge_ai.cli:main"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.uv]
required-version = ">=0.11.16"
default-groups = []
conflicts = [
[
{ extra = "yolo" },
{ extra = "yolo-cpu" },
],
]
required-environments = [
"sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'",
]
[tool.uv.sources]
torch = [
{ index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
]
torchvision = [
{ index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"

161
scripts/bootstrap.sh Executable file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
profile="detection-cpu"
cmvr_es_root="${PROJECT_ROOT}/../cmvr-es"
skip_codegen=false
skip_check=false
usage() {
cat <<'EOF'
Usage: bash scripts/bootstrap.sh [options]
Create the locked uv environment, optionally generate cmvr-es Python bindings,
and run basic health checks.
Options:
--profile PROFILE core, detection-cpu (default), or dev
--cmvr-es-root PATH cmvr-es checkout (default: sibling ../cmvr-es)
--skip-codegen do not generate cmvr-es protobuf/gRPC bindings
--skip-check install only; skip validation and tests
-h, --help show this help
Profiles:
core framework and simulated talk/smoke pipeline only
detection-cpu gRPC + HTTP + PyAV + locked CPU YOLO runtime
dev detection-cpu plus tests and portable protobuf codegen tools
EOF
}
while (($#)); do
case "$1" in
--profile)
if (($# < 2)); then
echo "error: --profile requires a value" >&2
exit 2
fi
profile="$2"
shift 2
;;
--cmvr-es-root)
if (($# < 2)); then
echo "error: --cmvr-es-root requires a path" >&2
exit 2
fi
cmvr_es_root="$2"
shift 2
;;
--skip-codegen)
skip_codegen=true
shift
;;
--skip-check)
skip_check=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
case "${profile}" in
core)
sync_args=(--locked --no-default-groups)
needs_cmvr_bindings=false
;;
detection-cpu)
sync_args=(
--locked
--no-default-groups
--extra grpc
--extra http
--extra video
--extra image
--extra yolo-cpu
)
needs_cmvr_bindings=true
;;
dev)
sync_args=(
--locked
--no-default-groups
--group dev
--extra grpc
--extra http
--extra video
--extra image
--extra yolo-cpu
)
needs_cmvr_bindings=true
;;
*)
echo "error: unsupported profile '${profile}'" >&2
echo "expected one of: core, detection-cpu, dev" >&2
exit 2
;;
esac
if ! command -v uv >/dev/null 2>&1; then
echo "error: uv is not installed or not available on PATH" >&2
echo "see https://docs.astral.sh/uv/getting-started/installation/" >&2
exit 1
fi
cd "${PROJECT_ROOT}"
echo "==> project: ${PROJECT_ROOT}"
echo "==> profile: ${profile}"
if [[ "${needs_cmvr_bindings}" == true && "${skip_codegen}" == false ]]; then
if [[ ! -d "${cmvr_es_root}/protos/cmvr" ]]; then
echo "error: cmvr-es protos were not found under: ${cmvr_es_root}" >&2
echo "pass --cmvr-es-root PATH or use --skip-codegen with existing bindings" >&2
exit 1
fi
echo "==> installing the locked protobuf codegen environment"
uv sync --locked --only-group codegen
echo "==> generating cmvr-es Python bindings from ${cmvr_es_root}"
.venv/bin/python scripts/generate_cmvr_stubs.py \
--cmvr-es-root "${cmvr_es_root}" \
--portable
fi
echo "==> installing the locked ${profile} environment"
uv sync "${sync_args[@]}"
if [[ "${skip_check}" == true ]]; then
echo "==> installation complete (checks skipped)"
exit 0
fi
echo "==> validating the smoke pipeline"
.venv/bin/cmvr-edge-ai validate --config configs/smoke.yaml
if [[ "${needs_cmvr_bindings}" == true ]]; then
echo "==> checking detection runtime imports"
.venv/bin/python -c \
"import av, grpc, httpx, PIL, torch, ultralytics; import cmvr.api.camera_service_pb2_grpc; print(f'torch={torch.__version__} cuda={torch.cuda.is_available()} ultralytics={ultralytics.__version__} pillow={PIL.__version__}')"
echo "==> validating the PPE detection pipeline"
.venv/bin/cmvr-edge-ai validate \
--config detect_server/pipeline.yaml \
--pipeline detection
.venv/bin/cmvr-edge-ai models
fi
if [[ "${profile}" == dev ]]; then
echo "==> running tests"
.venv/bin/pytest
fi
echo "==> bootstrap complete"
echo "run commands with: uv run --no-sync cmvr-edge-ai ..."

View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Generate Python protobuf/gRPC bindings from the cmvr-es source tree."""
from __future__ import annotations
import argparse
import platform
import subprocess
import sys
from pathlib import Path
DEFAULT_CMVR_ES_ROOT = Path("/home/xtkuang/Projects/cmvr/cmvr-es")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--cmvr-es-root",
type=Path,
default=DEFAULT_CMVR_ES_ROOT,
help="cmvr-es checkout containing protos/ and output/bin/",
)
parser.add_argument(
"--output",
type=Path,
default=Path("src"),
help="output root; generated imports require a top-level cmvr package",
)
parser.add_argument(
"--portable",
action="store_true",
help="use python -m grpc_tools.protoc instead of cmvr-es bundled tools",
)
return parser.parse_args()
def build_command(args: argparse.Namespace, proto_files: list[Path]) -> list[str]:
proto_root = args.cmvr_es_root.resolve() / "protos"
output = args.output.resolve()
relative_files = [str(path.relative_to(proto_root)) for path in proto_files]
if args.portable:
try:
import grpc_tools
except ImportError as exc:
raise RuntimeError(
"portable generation requires grpcio-tools; install the dev extra"
) from exc
well_known = Path(grpc_tools.__file__).resolve().parent / "_proto"
return [
sys.executable,
"-m",
"grpc_tools.protoc",
f"-I{proto_root}",
f"-I{well_known}",
f"--python_out={output}",
f"--pyi_out={output}",
f"--grpc_python_out={output}",
*relative_files,
]
protoc = args.cmvr_es_root.resolve() / "output/bin/protoc"
grpc_plugin = args.cmvr_es_root.resolve() / "output/bin/grpc_python_plugin"
if not protoc.is_file() or not grpc_plugin.is_file():
raise FileNotFoundError(
"cmvr-es protobuf tools are missing; build cmvr-es first or pass --portable"
)
well_known = _bundled_well_known_proto_root(args.cmvr_es_root.resolve())
return [
str(protoc),
f"-I{proto_root}",
f"-I{well_known}",
f"--python_out={output}",
f"--pyi_out={output}",
f"--grpc_python_out={output}",
f"--plugin=protoc-gen-grpc_python={grpc_plugin}",
*relative_files,
]
def _bundled_well_known_proto_root(cmvr_es_root: Path) -> Path:
architecture = (
"arm" if platform.machine().lower() in {"aarch64", "arm64"} else "x86"
)
candidates = (
cmvr_es_root / "dependency" / architecture / "third_party/grpc/v1.76.0/include",
cmvr_es_root / "dependency/x86/third_party/grpc/v1.76.0/include",
cmvr_es_root / "dependency/arm/third_party/grpc/v1.76.0/include",
)
for candidate in candidates:
if (candidate / "google/protobuf/timestamp.proto").is_file():
return candidate
raise FileNotFoundError(
"google/protobuf/timestamp.proto was not found in the cmvr-es toolchain"
)
def main() -> int:
args = parse_args()
proto_root = args.cmvr_es_root.resolve() / "protos"
if not proto_root.is_dir():
raise FileNotFoundError(f"proto root does not exist: {proto_root}")
proto_files = sorted((proto_root / "cmvr").rglob("*.proto"))
if not proto_files:
raise FileNotFoundError(f"no cmvr proto files found under {proto_root}")
args.output.mkdir(parents=True, exist_ok=True)
command = build_command(args, proto_files)
subprocess.run(command, cwd=proto_root, check=True)
print(f"generated {len(proto_files)} proto files into {args.output.resolve()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: cmvr/api/agv_command.proto
# Protobuf Python Version: 6.33.5
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
'',
'cmvr/api/agv_command.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
from cmvr.msgs import agv_pb2 as cmvr_dot_msgs_dot_agv__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/agv_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\x1a\x13\x63mvr/msgs/agv.proto\"\xbd\x01\n\x16\x41gvRuntimeStateCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1ag\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12)\n\x05state\x18\x02 \x01(\x0b\x32\x1a.cmvr.msgs.AgvRuntimeState\"\xc6\x01\n\x1a\x41gvNavigationStatusCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1al\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12.\n\x06status\x18\x02 \x01(\x0b\x32\x1e.cmvr.msgs.AgvNavigationStatus\"\x9c\x02\n\x18\x41gvNavigateToPoseCommand\x1a\xc1\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\"\n\x04pose\x18\x02 \x01(\x0b\x32\x14.cmvr.msgs.AgvPose2d\x12,\n\x07options\x18\x03 \x01(\x0b\x32\x1b.cmvr.msgs.AgvMotionOptions\x12\x33\n\x0e\x61\x64\x61pter_params\x18\x04 \x01(\x0b\x32\x1b.cmvr.msgs.AgvAdapterParams\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8f\x02\n\x1b\x41gvNavigateToStationCommand\x1a\xb1\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x12\n\nstation_id\x18\x02 \x01(\t\x12,\n\x07options\x18\x03 \x01(\x0b\x32\x1b.cmvr.msgs.AgvMotionOptions\x12\x33\n\x0e\x61\x64\x61pter_params\x18\x04 \x01(\x0b\x32\x1b.cmvr.msgs.AgvAdapterParams\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xb9\x01\n\x14\x41gvFollowPathCommand\x1a\x63\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\'\n\x04path\x18\x02 \x03(\x0b\x32\x19.cmvr.msgs.AgvPathSegment\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xbb\x01\n\x15\x41gvSetVelocityCommand\x1a\x64\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12(\n\x08velocity\x18\x02 \x01(\x0b\x32\x16.cmvr.msgs.AgvVelocity\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x9c\x01\n\x12\x41gvListMapsCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1aJ\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x0c\n\x04maps\x18\x02 \x03(\t\"\xbb\x01\n\x16\x41gvListStationsCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x65\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\'\n\x08stations\x18\x02 \x03(\x0b\x32\x15.cmvr.msgs.AgvStation\"\xbd\x01\n\rAgvMapCommand\x1a]\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x10\n\x08map_name\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x1aM\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"\xfb\x01\n\x16\x41gvStartMappingCommand\x1a\x8e\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12-\n\tdimension\x18\x02 \x01(\x0e\x32\x1a.cmvr.msgs.AgvMapDimension\x12\x10\n\x08map_name\x18\x03 \x01(\t\x12\x11\n\treal_time\x18\x04 \x01(\x08\x1aP\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x12\n\nsession_id\x18\x02 \x01(\t\"\xd7\x02\n\x13\x41gvMapStreamCommand\x1a\xd1\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12-\n\tdimension\x18\x02 \x01(\x0e\x32\x1a.cmvr.msgs.AgvMapDimension\x12\x10\n\x08map_name\x18\x03 \x01(\t\x12\x14\n\x0cresume_token\x18\x04 \x01(\t\x12\x10\n\x08snapshot\x18\x05 \x01(\x08\x12\x13\n\x0bincremental\x18\x06 \x01(\x08\x12\x17\n\x0fmax_chunk_bytes\x18\x07 \x01(\x05\x1al\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12.\n\x06update\x18\x02 \x01(\x0b\x32\x1e.cmvr.msgs.AgvUnifiedMapUpdateb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.agv_command_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_AGVRUNTIMESTATECOMMAND']._serialized_start=85
_globals['_AGVRUNTIMESTATECOMMAND']._serialized_end=274
_globals['_AGVRUNTIMESTATECOMMAND_REQUEST']._serialized_start=111
_globals['_AGVRUNTIMESTATECOMMAND_REQUEST']._serialized_end=169
_globals['_AGVRUNTIMESTATECOMMAND_FEEDBACK']._serialized_start=171
_globals['_AGVRUNTIMESTATECOMMAND_FEEDBACK']._serialized_end=274
_globals['_AGVNAVIGATIONSTATUSCOMMAND']._serialized_start=277
_globals['_AGVNAVIGATIONSTATUSCOMMAND']._serialized_end=475
_globals['_AGVNAVIGATIONSTATUSCOMMAND_REQUEST']._serialized_start=111
_globals['_AGVNAVIGATIONSTATUSCOMMAND_REQUEST']._serialized_end=169
_globals['_AGVNAVIGATIONSTATUSCOMMAND_FEEDBACK']._serialized_start=367
_globals['_AGVNAVIGATIONSTATUSCOMMAND_FEEDBACK']._serialized_end=475
_globals['_AGVNAVIGATETOPOSECOMMAND']._serialized_start=478
_globals['_AGVNAVIGATETOPOSECOMMAND']._serialized_end=762
_globals['_AGVNAVIGATETOPOSECOMMAND_REQUEST']._serialized_start=507
_globals['_AGVNAVIGATETOPOSECOMMAND_REQUEST']._serialized_end=700
_globals['_AGVNAVIGATETOPOSECOMMAND_FEEDBACK']._serialized_start=171
_globals['_AGVNAVIGATETOPOSECOMMAND_FEEDBACK']._serialized_end=231
_globals['_AGVNAVIGATETOSTATIONCOMMAND']._serialized_start=765
_globals['_AGVNAVIGATETOSTATIONCOMMAND']._serialized_end=1036
_globals['_AGVNAVIGATETOSTATIONCOMMAND_REQUEST']._serialized_start=797
_globals['_AGVNAVIGATETOSTATIONCOMMAND_REQUEST']._serialized_end=974
_globals['_AGVNAVIGATETOSTATIONCOMMAND_FEEDBACK']._serialized_start=171
_globals['_AGVNAVIGATETOSTATIONCOMMAND_FEEDBACK']._serialized_end=231
_globals['_AGVFOLLOWPATHCOMMAND']._serialized_start=1039
_globals['_AGVFOLLOWPATHCOMMAND']._serialized_end=1224
_globals['_AGVFOLLOWPATHCOMMAND_REQUEST']._serialized_start=1063
_globals['_AGVFOLLOWPATHCOMMAND_REQUEST']._serialized_end=1162
_globals['_AGVFOLLOWPATHCOMMAND_FEEDBACK']._serialized_start=171
_globals['_AGVFOLLOWPATHCOMMAND_FEEDBACK']._serialized_end=231
_globals['_AGVSETVELOCITYCOMMAND']._serialized_start=1227
_globals['_AGVSETVELOCITYCOMMAND']._serialized_end=1414
_globals['_AGVSETVELOCITYCOMMAND_REQUEST']._serialized_start=1252
_globals['_AGVSETVELOCITYCOMMAND_REQUEST']._serialized_end=1352
_globals['_AGVSETVELOCITYCOMMAND_FEEDBACK']._serialized_start=171
_globals['_AGVSETVELOCITYCOMMAND_FEEDBACK']._serialized_end=231
_globals['_AGVLISTMAPSCOMMAND']._serialized_start=1417
_globals['_AGVLISTMAPSCOMMAND']._serialized_end=1573
_globals['_AGVLISTMAPSCOMMAND_REQUEST']._serialized_start=111
_globals['_AGVLISTMAPSCOMMAND_REQUEST']._serialized_end=169
_globals['_AGVLISTMAPSCOMMAND_FEEDBACK']._serialized_start=1499
_globals['_AGVLISTMAPSCOMMAND_FEEDBACK']._serialized_end=1573
_globals['_AGVLISTSTATIONSCOMMAND']._serialized_start=1576
_globals['_AGVLISTSTATIONSCOMMAND']._serialized_end=1763
_globals['_AGVLISTSTATIONSCOMMAND_REQUEST']._serialized_start=111
_globals['_AGVLISTSTATIONSCOMMAND_REQUEST']._serialized_end=169
_globals['_AGVLISTSTATIONSCOMMAND_FEEDBACK']._serialized_start=1662
_globals['_AGVLISTSTATIONSCOMMAND_FEEDBACK']._serialized_end=1763
_globals['_AGVMAPCOMMAND']._serialized_start=1766
_globals['_AGVMAPCOMMAND']._serialized_end=1955
_globals['_AGVMAPCOMMAND_REQUEST']._serialized_start=1783
_globals['_AGVMAPCOMMAND_REQUEST']._serialized_end=1876
_globals['_AGVMAPCOMMAND_FEEDBACK']._serialized_start=1878
_globals['_AGVMAPCOMMAND_FEEDBACK']._serialized_end=1955
_globals['_AGVSTARTMAPPINGCOMMAND']._serialized_start=1958
_globals['_AGVSTARTMAPPINGCOMMAND']._serialized_end=2209
_globals['_AGVSTARTMAPPINGCOMMAND_REQUEST']._serialized_start=1985
_globals['_AGVSTARTMAPPINGCOMMAND_REQUEST']._serialized_end=2127
_globals['_AGVSTARTMAPPINGCOMMAND_FEEDBACK']._serialized_start=2129
_globals['_AGVSTARTMAPPINGCOMMAND_FEEDBACK']._serialized_end=2209
_globals['_AGVMAPSTREAMCOMMAND']._serialized_start=2212
_globals['_AGVMAPSTREAMCOMMAND']._serialized_end=2555
_globals['_AGVMAPSTREAMCOMMAND_REQUEST']._serialized_start=2236
_globals['_AGVMAPSTREAMCOMMAND_REQUEST']._serialized_end=2445
_globals['_AGVMAPSTREAMCOMMAND_FEEDBACK']._serialized_start=2447
_globals['_AGVMAPSTREAMCOMMAND_FEEDBACK']._serialized_end=2555
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,215 @@
from cmvr.api import common_pb2 as _common_pb2
from cmvr.msgs import agv_pb2 as _agv_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class AgvRuntimeStateCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "state")
HEADER_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
state: _agv_pb2.AgvRuntimeState
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[_agv_pb2.AgvRuntimeState, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvNavigationStatusCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "status")
HEADER_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
status: _agv_pb2.AgvNavigationStatus
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., status: _Optional[_Union[_agv_pb2.AgvNavigationStatus, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvNavigateToPoseCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "pose", "options", "adapter_params")
HEADER_FIELD_NUMBER: _ClassVar[int]
POSE_FIELD_NUMBER: _ClassVar[int]
OPTIONS_FIELD_NUMBER: _ClassVar[int]
ADAPTER_PARAMS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
pose: _agv_pb2.AgvPose2d
options: _agv_pb2.AgvMotionOptions
adapter_params: _agv_pb2.AgvAdapterParams
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., pose: _Optional[_Union[_agv_pb2.AgvPose2d, _Mapping]] = ..., options: _Optional[_Union[_agv_pb2.AgvMotionOptions, _Mapping]] = ..., adapter_params: _Optional[_Union[_agv_pb2.AgvAdapterParams, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvNavigateToStationCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "station_id", "options", "adapter_params")
HEADER_FIELD_NUMBER: _ClassVar[int]
STATION_ID_FIELD_NUMBER: _ClassVar[int]
OPTIONS_FIELD_NUMBER: _ClassVar[int]
ADAPTER_PARAMS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
station_id: str
options: _agv_pb2.AgvMotionOptions
adapter_params: _agv_pb2.AgvAdapterParams
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., station_id: _Optional[str] = ..., options: _Optional[_Union[_agv_pb2.AgvMotionOptions, _Mapping]] = ..., adapter_params: _Optional[_Union[_agv_pb2.AgvAdapterParams, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvFollowPathCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "path")
HEADER_FIELD_NUMBER: _ClassVar[int]
PATH_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
path: _containers.RepeatedCompositeFieldContainer[_agv_pb2.AgvPathSegment]
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., path: _Optional[_Iterable[_Union[_agv_pb2.AgvPathSegment, _Mapping]]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvSetVelocityCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "velocity")
HEADER_FIELD_NUMBER: _ClassVar[int]
VELOCITY_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
velocity: _agv_pb2.AgvVelocity
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., velocity: _Optional[_Union[_agv_pb2.AgvVelocity, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvListMapsCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "maps")
HEADER_FIELD_NUMBER: _ClassVar[int]
MAPS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
maps: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., maps: _Optional[_Iterable[str]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvListStationsCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "stations")
HEADER_FIELD_NUMBER: _ClassVar[int]
STATIONS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
stations: _containers.RepeatedCompositeFieldContainer[_agv_pb2.AgvStation]
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., stations: _Optional[_Iterable[_Union[_agv_pb2.AgvStation, _Mapping]]] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvMapCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "map_name", "content")
HEADER_FIELD_NUMBER: _ClassVar[int]
MAP_NAME_FIELD_NUMBER: _ClassVar[int]
CONTENT_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
map_name: str
content: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., map_name: _Optional[str] = ..., content: _Optional[str] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "content")
HEADER_FIELD_NUMBER: _ClassVar[int]
CONTENT_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
content: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., content: _Optional[str] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvStartMappingCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "dimension", "map_name", "real_time")
HEADER_FIELD_NUMBER: _ClassVar[int]
DIMENSION_FIELD_NUMBER: _ClassVar[int]
MAP_NAME_FIELD_NUMBER: _ClassVar[int]
REAL_TIME_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
dimension: _agv_pb2.AgvMapDimension
map_name: str
real_time: bool
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., dimension: _Optional[_Union[_agv_pb2.AgvMapDimension, str]] = ..., map_name: _Optional[str] = ..., real_time: _Optional[bool] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "session_id")
HEADER_FIELD_NUMBER: _ClassVar[int]
SESSION_ID_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
session_id: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., session_id: _Optional[str] = ...) -> None: ...
def __init__(self) -> None: ...
class AgvMapStreamCommand(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "dimension", "map_name", "resume_token", "snapshot", "incremental", "max_chunk_bytes")
HEADER_FIELD_NUMBER: _ClassVar[int]
DIMENSION_FIELD_NUMBER: _ClassVar[int]
MAP_NAME_FIELD_NUMBER: _ClassVar[int]
RESUME_TOKEN_FIELD_NUMBER: _ClassVar[int]
SNAPSHOT_FIELD_NUMBER: _ClassVar[int]
INCREMENTAL_FIELD_NUMBER: _ClassVar[int]
MAX_CHUNK_BYTES_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
dimension: _agv_pb2.AgvMapDimension
map_name: str
resume_token: str
snapshot: bool
incremental: bool
max_chunk_bytes: int
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., dimension: _Optional[_Union[_agv_pb2.AgvMapDimension, str]] = ..., map_name: _Optional[str] = ..., resume_token: _Optional[str] = ..., snapshot: _Optional[bool] = ..., incremental: _Optional[bool] = ..., max_chunk_bytes: _Optional[int] = ...) -> None: ...
class Feedback(_message.Message):
__slots__ = ("header", "update")
HEADER_FIELD_NUMBER: _ClassVar[int]
UPDATE_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
update: _agv_pb2.AgvUnifiedMapUpdate
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., update: _Optional[_Union[_agv_pb2.AgvUnifiedMapUpdate, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...

View File

@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in cmvr/api/agv_command_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: cmvr/api/agv_service.proto
# Protobuf Python Version: 6.33.5
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
'',
'cmvr/api/agv_service.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
from cmvr.api import agv_command_pb2 as cmvr_dot_api_dot_agv__command__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/agv_service.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\x1a\x1a\x63mvr/api/agv_command.proto2\xd0\x0e\n\nAgvService\x12\x66\n\x0fgetRuntimeState\x12(.cmvr.api.AgvRuntimeStateCommand.Request\x1a).cmvr.api.AgvRuntimeStateCommand.Feedback\x12r\n\x13getNavigationStatus\x12,.cmvr.api.AgvNavigationStatusCommand.Request\x1a-.cmvr.api.AgvNavigationStatusCommand.Feedback\x12R\n\remergencyStop\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12O\n\nclearFault\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12i\n\x0enavigateToPose\x12*.cmvr.api.AgvNavigateToPoseCommand.Request\x1a+.cmvr.api.AgvNavigateToPoseCommand.Feedback\x12r\n\x11navigateToStation\x12-.cmvr.api.AgvNavigateToStationCommand.Request\x1a..cmvr.api.AgvNavigateToStationCommand.Feedback\x12]\n\nfollowPath\x12&.cmvr.api.AgvFollowPathCommand.Request\x1a\'.cmvr.api.AgvFollowPathCommand.Feedback\x12T\n\x0fpauseNavigation\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12U\n\x10resumeNavigation\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12U\n\x10\x63\x61ncelNavigation\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12`\n\x0bsetVelocity\x12\'.cmvr.api.AgvSetVelocityCommand.Request\x1a(.cmvr.api.AgvSetVelocityCommand.Feedback\x12X\n\x13stopVelocityControl\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12W\n\x08listMaps\x12$.cmvr.api.AgvListMapsCommand.Request\x1a%.cmvr.api.AgvListMapsCommand.Feedback\x12\x63\n\x0clistStations\x12(.cmvr.api.AgvListStationsCommand.Request\x1a).cmvr.api.AgvListStationsCommand.Feedback\x12N\n\tswitchMap\x12\x1f.cmvr.api.AgvMapCommand.Request\x1a .cmvr.api.AgvMapCommand.Feedback\x12N\n\tuploadMap\x12\x1f.cmvr.api.AgvMapCommand.Request\x1a .cmvr.api.AgvMapCommand.Feedback\x12P\n\x0b\x64ownloadMap\x12\x1f.cmvr.api.AgvMapCommand.Request\x1a .cmvr.api.AgvMapCommand.Feedback\x12\x63\n\x0cstartMapping\x12(.cmvr.api.AgvStartMappingCommand.Request\x1a).cmvr.api.AgvStartMappingCommand.Feedback\x12\\\n\tstreamMap\x12%.cmvr.api.AgvMapStreamCommand.Request\x1a&.cmvr.api.AgvMapStreamCommand.Feedback0\x01\x12P\n\x0bstopMapping\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedbackb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.agv_service_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_AGVSERVICE']._serialized_start=92
_globals['_AGVSERVICE']._serialized_end=1964
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,6 @@
from cmvr.api import common_pb2 as _common_pb2
from cmvr.api import agv_command_pb2 as _agv_command_pb2
from google.protobuf import descriptor as _descriptor
from typing import ClassVar as _ClassVar
DESCRIPTOR: _descriptor.FileDescriptor

View File

@ -0,0 +1,943 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
from cmvr.api import agv_command_pb2 as cmvr_dot_api_dot_agv__command__pb2
from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in cmvr/api/agv_service_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class AgvServiceStub:
"""AGV 通用服务。该服务只暴露控制器无关的能力,
厂商协议地图文件格式和控制器特有参数由具体 AGV 适配器内部处理
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.getRuntimeState = channel.unary_unary(
'/cmvr.api.AgvService/getRuntimeState',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Feedback.FromString,
_registered_method=True)
self.getNavigationStatus = channel.unary_unary(
'/cmvr.api.AgvService/getNavigationStatus',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Feedback.FromString,
_registered_method=True)
self.emergencyStop = channel.unary_unary(
'/cmvr.api.AgvService/emergencyStop',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.clearFault = channel.unary_unary(
'/cmvr.api.AgvService/clearFault',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.navigateToPose = channel.unary_unary(
'/cmvr.api.AgvService/navigateToPose',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Feedback.FromString,
_registered_method=True)
self.navigateToStation = channel.unary_unary(
'/cmvr.api.AgvService/navigateToStation',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Feedback.FromString,
_registered_method=True)
self.followPath = channel.unary_unary(
'/cmvr.api.AgvService/followPath',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Feedback.FromString,
_registered_method=True)
self.pauseNavigation = channel.unary_unary(
'/cmvr.api.AgvService/pauseNavigation',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.resumeNavigation = channel.unary_unary(
'/cmvr.api.AgvService/resumeNavigation',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.cancelNavigation = channel.unary_unary(
'/cmvr.api.AgvService/cancelNavigation',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.setVelocity = channel.unary_unary(
'/cmvr.api.AgvService/setVelocity',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Feedback.FromString,
_registered_method=True)
self.stopVelocityControl = channel.unary_unary(
'/cmvr.api.AgvService/stopVelocityControl',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
self.listMaps = channel.unary_unary(
'/cmvr.api.AgvService/listMaps',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Feedback.FromString,
_registered_method=True)
self.listStations = channel.unary_unary(
'/cmvr.api.AgvService/listStations',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Feedback.FromString,
_registered_method=True)
self.switchMap = channel.unary_unary(
'/cmvr.api.AgvService/switchMap',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
_registered_method=True)
self.uploadMap = channel.unary_unary(
'/cmvr.api.AgvService/uploadMap',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
_registered_method=True)
self.downloadMap = channel.unary_unary(
'/cmvr.api.AgvService/downloadMap',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
_registered_method=True)
self.startMapping = channel.unary_unary(
'/cmvr.api.AgvService/startMapping',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Feedback.FromString,
_registered_method=True)
self.streamMap = channel.unary_stream(
'/cmvr.api.AgvService/streamMap',
request_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Feedback.FromString,
_registered_method=True)
self.stopMapping = channel.unary_unary(
'/cmvr.api.AgvService/stopMapping',
request_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
response_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
_registered_method=True)
class AgvServiceServicer:
"""AGV 通用服务。该服务只暴露控制器无关的能力,
厂商协议地图文件格式和控制器特有参数由具体 AGV 适配器内部处理
"""
def getRuntimeState(self, request, context):
"""获取 AGV 当前运行状态快照。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def getNavigationStatus(self, request, context):
"""获取当前导航任务状态。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def emergencyStop(self, request, context):
"""执行急停或等效安全停止动作。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def clearFault(self, request, context):
"""清除可恢复故障或告警。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def navigateToPose(self, request, context):
"""导航到指定地图位姿。目标位姿 x/y 单位为米theta 单位为弧度。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def navigateToStation(self, request, context):
"""导航到指定地图站点。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def followPath(self, request, context):
"""按显式站点路径执行导航。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def pauseNavigation(self, request, context):
"""暂停当前导航任务。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def resumeNavigation(self, request, context):
"""恢复已暂停的导航任务。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def cancelNavigation(self, request, context):
"""取消当前导航任务。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def setVelocity(self, request, context):
"""下发底盘速度控制指令。vx/vy 单位为米/秒wz 单位为弧度/秒。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def stopVelocityControl(self, request, context):
"""停止底盘速度控制。该接口不等价于取消导航任务。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def listMaps(self, request, context):
"""查询 AGV 控制器可用地图名称列表。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def listStations(self, request, context):
"""查询当前地图中的站点列表。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def switchMap(self, request, context):
"""切换当前使用地图。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def uploadMap(self, request, context):
"""上传地图内容到 AGV 控制器。地图内容字段由适配器解释。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def downloadMap(self, request, context):
"""下载指定地图内容。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def startMapping(self, request, context):
"""开始建图/扫图会话。请求只选择 2D、3D 或二者都要,
控制器特有地图格式由 AGV 适配器内部转换
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def streamMap(self, request, context):
"""以服务端流方式发送统一地图。resume_token 为空时应先发送全量快照;
后续是否发送增量由 AGV 适配器能力决定并通过 update_type 标记
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def stopMapping(self, request, context):
"""停止当前建图/扫图会话。
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_AgvServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'getRuntimeState': grpc.unary_unary_rpc_method_handler(
servicer.getRuntimeState,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Feedback.SerializeToString,
),
'getNavigationStatus': grpc.unary_unary_rpc_method_handler(
servicer.getNavigationStatus,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Feedback.SerializeToString,
),
'emergencyStop': grpc.unary_unary_rpc_method_handler(
servicer.emergencyStop,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'clearFault': grpc.unary_unary_rpc_method_handler(
servicer.clearFault,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'navigateToPose': grpc.unary_unary_rpc_method_handler(
servicer.navigateToPose,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Feedback.SerializeToString,
),
'navigateToStation': grpc.unary_unary_rpc_method_handler(
servicer.navigateToStation,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Feedback.SerializeToString,
),
'followPath': grpc.unary_unary_rpc_method_handler(
servicer.followPath,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Feedback.SerializeToString,
),
'pauseNavigation': grpc.unary_unary_rpc_method_handler(
servicer.pauseNavigation,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'resumeNavigation': grpc.unary_unary_rpc_method_handler(
servicer.resumeNavigation,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'cancelNavigation': grpc.unary_unary_rpc_method_handler(
servicer.cancelNavigation,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'setVelocity': grpc.unary_unary_rpc_method_handler(
servicer.setVelocity,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Feedback.SerializeToString,
),
'stopVelocityControl': grpc.unary_unary_rpc_method_handler(
servicer.stopVelocityControl,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
'listMaps': grpc.unary_unary_rpc_method_handler(
servicer.listMaps,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Feedback.SerializeToString,
),
'listStations': grpc.unary_unary_rpc_method_handler(
servicer.listStations,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Feedback.SerializeToString,
),
'switchMap': grpc.unary_unary_rpc_method_handler(
servicer.switchMap,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.SerializeToString,
),
'uploadMap': grpc.unary_unary_rpc_method_handler(
servicer.uploadMap,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.SerializeToString,
),
'downloadMap': grpc.unary_unary_rpc_method_handler(
servicer.downloadMap,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.SerializeToString,
),
'startMapping': grpc.unary_unary_rpc_method_handler(
servicer.startMapping,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Feedback.SerializeToString,
),
'streamMap': grpc.unary_stream_rpc_method_handler(
servicer.streamMap,
request_deserializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Request.FromString,
response_serializer=cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Feedback.SerializeToString,
),
'stopMapping': grpc.unary_unary_rpc_method_handler(
servicer.stopMapping,
request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'cmvr.api.AgvService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('cmvr.api.AgvService', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class AgvService:
"""AGV 通用服务。该服务只暴露控制器无关的能力,
厂商协议地图文件格式和控制器特有参数由具体 AGV 适配器内部处理
"""
@staticmethod
def getRuntimeState(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/getRuntimeState',
cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvRuntimeStateCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def getNavigationStatus(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/getNavigationStatus',
cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvNavigationStatusCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def emergencyStop(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/emergencyStop',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def clearFault(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/clearFault',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def navigateToPose(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/navigateToPose',
cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToPoseCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def navigateToStation(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/navigateToStation',
cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvNavigateToStationCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def followPath(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/followPath',
cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvFollowPathCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def pauseNavigation(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/pauseNavigation',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def resumeNavigation(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/resumeNavigation',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def cancelNavigation(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/cancelNavigation',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def setVelocity(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/setVelocity',
cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvSetVelocityCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def stopVelocityControl(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/stopVelocityControl',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def listMaps(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/listMaps',
cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvListMapsCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def listStations(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/listStations',
cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvListStationsCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def switchMap(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/switchMap',
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def uploadMap(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/uploadMap',
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def downloadMap(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/downloadMap',
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvMapCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def startMapping(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/startMapping',
cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvStartMappingCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def streamMap(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/cmvr.api.AgvService/streamMap',
cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Request.SerializeToString,
cmvr_dot_api_dot_agv__command__pb2.AgvMapStreamCommand.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def stopMapping(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/cmvr.api.AgvService/stopMapping',
cmvr_dot_api_dot_common__pb2.CommandHeader.Request.SerializeToString,
cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,318 @@
from cmvr.api import common_pb2 as _common_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class ArmFrameType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
ARM_FRAME_BASE: _ClassVar[ArmFrameType]
ARM_FRAME_TOOL: _ClassVar[ArmFrameType]
ARM_FRAME_WORLD: _ClassVar[ArmFrameType]
ARM_FRAME_USER: _ClassVar[ArmFrameType]
ARM_FRAME_BASE: ArmFrameType
ARM_FRAME_TOOL: ArmFrameType
ARM_FRAME_WORLD: ArmFrameType
ARM_FRAME_USER: ArmFrameType
class JointPositionCommand(_message.Message):
__slots__ = ("position",)
POSITION_FIELD_NUMBER: _ClassVar[int]
position: _containers.RepeatedScalarFieldContainer[float]
def __init__(self, position: _Optional[_Iterable[float]] = ...) -> None: ...
class JointVelocityCommand(_message.Message):
__slots__ = ("velocity",)
VELOCITY_FIELD_NUMBER: _ClassVar[int]
velocity: _containers.RepeatedScalarFieldContainer[float]
def __init__(self, velocity: _Optional[_Iterable[float]] = ...) -> None: ...
class MotionOptions(_message.Message):
__slots__ = ("velocity", "acceleration", "blend_radius", "jerk", "joint_velocity_limits", "asynchronous")
VELOCITY_FIELD_NUMBER: _ClassVar[int]
ACCELERATION_FIELD_NUMBER: _ClassVar[int]
BLEND_RADIUS_FIELD_NUMBER: _ClassVar[int]
JERK_FIELD_NUMBER: _ClassVar[int]
JOINT_VELOCITY_LIMITS_FIELD_NUMBER: _ClassVar[int]
ASYNCHRONOUS_FIELD_NUMBER: _ClassVar[int]
velocity: float
acceleration: float
blend_radius: float
jerk: float
joint_velocity_limits: _containers.RepeatedScalarFieldContainer[float]
asynchronous: bool
def __init__(self, velocity: _Optional[float] = ..., acceleration: _Optional[float] = ..., blend_radius: _Optional[float] = ..., jerk: _Optional[float] = ..., joint_velocity_limits: _Optional[_Iterable[float]] = ..., asynchronous: _Optional[bool] = ...) -> None: ...
class CartesianPose(_message.Message):
__slots__ = ("x", "y", "z", "rx", "ry", "rz")
X_FIELD_NUMBER: _ClassVar[int]
Y_FIELD_NUMBER: _ClassVar[int]
Z_FIELD_NUMBER: _ClassVar[int]
RX_FIELD_NUMBER: _ClassVar[int]
RY_FIELD_NUMBER: _ClassVar[int]
RZ_FIELD_NUMBER: _ClassVar[int]
x: float
y: float
z: float
rx: float
ry: float
rz: float
def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ..., rx: _Optional[float] = ..., ry: _Optional[float] = ..., rz: _Optional[float] = ...) -> None: ...
class CartesianVelocity(_message.Message):
__slots__ = ("vx", "vy", "vz", "wx", "wy", "wz")
VX_FIELD_NUMBER: _ClassVar[int]
VY_FIELD_NUMBER: _ClassVar[int]
VZ_FIELD_NUMBER: _ClassVar[int]
WX_FIELD_NUMBER: _ClassVar[int]
WY_FIELD_NUMBER: _ClassVar[int]
WZ_FIELD_NUMBER: _ClassVar[int]
vx: float
vy: float
vz: float
wx: float
wy: float
wz: float
def __init__(self, vx: _Optional[float] = ..., vy: _Optional[float] = ..., vz: _Optional[float] = ..., wx: _Optional[float] = ..., wy: _Optional[float] = ..., wz: _Optional[float] = ...) -> None: ...
class TransformMatrix4x4(_message.Message):
__slots__ = ("m00", "m01", "m02", "m03", "m10", "m11", "m12", "m13", "m20", "m21", "m22", "m23", "m30", "m31", "m32", "m33")
M00_FIELD_NUMBER: _ClassVar[int]
M01_FIELD_NUMBER: _ClassVar[int]
M02_FIELD_NUMBER: _ClassVar[int]
M03_FIELD_NUMBER: _ClassVar[int]
M10_FIELD_NUMBER: _ClassVar[int]
M11_FIELD_NUMBER: _ClassVar[int]
M12_FIELD_NUMBER: _ClassVar[int]
M13_FIELD_NUMBER: _ClassVar[int]
M20_FIELD_NUMBER: _ClassVar[int]
M21_FIELD_NUMBER: _ClassVar[int]
M22_FIELD_NUMBER: _ClassVar[int]
M23_FIELD_NUMBER: _ClassVar[int]
M30_FIELD_NUMBER: _ClassVar[int]
M31_FIELD_NUMBER: _ClassVar[int]
M32_FIELD_NUMBER: _ClassVar[int]
M33_FIELD_NUMBER: _ClassVar[int]
m00: float
m01: float
m02: float
m03: float
m10: float
m11: float
m12: float
m13: float
m20: float
m21: float
m22: float
m23: float
m30: float
m31: float
m32: float
m33: float
def __init__(self, m00: _Optional[float] = ..., m01: _Optional[float] = ..., m02: _Optional[float] = ..., m03: _Optional[float] = ..., m10: _Optional[float] = ..., m11: _Optional[float] = ..., m12: _Optional[float] = ..., m13: _Optional[float] = ..., m20: _Optional[float] = ..., m21: _Optional[float] = ..., m22: _Optional[float] = ..., m23: _Optional[float] = ..., m30: _Optional[float] = ..., m31: _Optional[float] = ..., m32: _Optional[float] = ..., m33: _Optional[float] = ...) -> None: ...
class MoveJ(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "target", "options")
HEADER_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
OPTIONS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
target: JointPositionCommand
options: MotionOptions
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., target: _Optional[_Union[JointPositionCommand, _Mapping]] = ..., options: _Optional[_Union[MotionOptions, _Mapping]] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class MoveL(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "target", "options", "frame")
HEADER_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
OPTIONS_FIELD_NUMBER: _ClassVar[int]
FRAME_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
target: CartesianPose
options: MotionOptions
frame: ArmFrameType
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., target: _Optional[_Union[CartesianPose, _Mapping]] = ..., options: _Optional[_Union[MotionOptions, _Mapping]] = ..., frame: _Optional[_Union[ArmFrameType, str]] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class SpeedJ(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "velocity", "acceleration", "duration")
HEADER_FIELD_NUMBER: _ClassVar[int]
VELOCITY_FIELD_NUMBER: _ClassVar[int]
ACCELERATION_FIELD_NUMBER: _ClassVar[int]
DURATION_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
velocity: JointVelocityCommand
acceleration: float
duration: float
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., velocity: _Optional[_Union[JointVelocityCommand, _Mapping]] = ..., acceleration: _Optional[float] = ..., duration: _Optional[float] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class SpeedL(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "velocity", "acceleration", "duration", "frame")
HEADER_FIELD_NUMBER: _ClassVar[int]
VELOCITY_FIELD_NUMBER: _ClassVar[int]
ACCELERATION_FIELD_NUMBER: _ClassVar[int]
DURATION_FIELD_NUMBER: _ClassVar[int]
FRAME_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
velocity: CartesianVelocity
acceleration: float
duration: float
frame: ArmFrameType
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., velocity: _Optional[_Union[CartesianVelocity, _Mapping]] = ..., acceleration: _Optional[float] = ..., duration: _Optional[float] = ..., frame: _Optional[_Union[ArmFrameType, str]] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class ServoJ(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "target")
HEADER_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
target: JointPositionCommand
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., target: _Optional[_Union[JointPositionCommand, _Mapping]] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class JointState(_message.Message):
__slots__ = ("name", "position", "velocity", "effort", "timestamp")
NAME_FIELD_NUMBER: _ClassVar[int]
POSITION_FIELD_NUMBER: _ClassVar[int]
VELOCITY_FIELD_NUMBER: _ClassVar[int]
EFFORT_FIELD_NUMBER: _ClassVar[int]
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
name: _containers.RepeatedScalarFieldContainer[str]
position: _containers.RepeatedScalarFieldContainer[float]
velocity: _containers.RepeatedScalarFieldContainer[float]
effort: _containers.RepeatedScalarFieldContainer[float]
timestamp: float
def __init__(self, name: _Optional[_Iterable[str]] = ..., position: _Optional[_Iterable[float]] = ..., velocity: _Optional[_Iterable[float]] = ..., effort: _Optional[_Iterable[float]] = ..., timestamp: _Optional[float] = ...) -> None: ...
class JointRequest(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ...) -> None: ...
class JointResponse(_message.Message):
__slots__ = ("header", "state")
HEADER_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
state: JointState
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[JointState, _Mapping]] = ...) -> None: ...
class GetPose(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "base_link", "ee_link")
HEADER_FIELD_NUMBER: _ClassVar[int]
BASE_LINK_FIELD_NUMBER: _ClassVar[int]
EE_LINK_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
base_link: str
ee_link: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., base_link: _Optional[str] = ..., ee_link: _Optional[str] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header", "pose")
HEADER_FIELD_NUMBER: _ClassVar[int]
POSE_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
pose: CartesianPose
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., pose: _Optional[_Union[CartesianPose, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class CalibrateZeroQ(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "joint_name")
HEADER_FIELD_NUMBER: _ClassVar[int]
JOINT_NAME_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
joint_name: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., joint_name: _Optional[str] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header",)
HEADER_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class GetPoseMatrix(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "base_link", "ee_link")
HEADER_FIELD_NUMBER: _ClassVar[int]
BASE_LINK_FIELD_NUMBER: _ClassVar[int]
EE_LINK_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
base_link: str
ee_link: str
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., base_link: _Optional[str] = ..., ee_link: _Optional[str] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header", "matrix")
HEADER_FIELD_NUMBER: _ClassVar[int]
MATRIX_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
matrix: TransformMatrix4x4
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., matrix: _Optional[_Union[TransformMatrix4x4, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...
class ComputeForwardKinematics(_message.Message):
__slots__ = ()
class Request(_message.Message):
__slots__ = ("header", "base_link", "ee_link", "joints")
HEADER_FIELD_NUMBER: _ClassVar[int]
BASE_LINK_FIELD_NUMBER: _ClassVar[int]
EE_LINK_FIELD_NUMBER: _ClassVar[int]
JOINTS_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Request
base_link: str
ee_link: str
joints: JointPositionCommand
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., base_link: _Optional[str] = ..., ee_link: _Optional[str] = ..., joints: _Optional[_Union[JointPositionCommand, _Mapping]] = ...) -> None: ...
class Response(_message.Message):
__slots__ = ("header", "matrix")
HEADER_FIELD_NUMBER: _ClassVar[int]
MATRIX_FIELD_NUMBER: _ClassVar[int]
header: _common_pb2.CommandHeader.Feedback
matrix: TransformMatrix4x4
def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., matrix: _Optional[_Union[TransformMatrix4x4, _Mapping]] = ...) -> None: ...
def __init__(self) -> None: ...

View File

@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in cmvr/api/arm_command_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: cmvr/api/arm_service.proto
# Protobuf Python Version: 6.33.5
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
'',
'cmvr/api/arm_service.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
from cmvr.api import arm_command_pb2 as cmvr_dot_api_dot_arm__command__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/arm_service.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\x1a\x1a\x63mvr/api/arm_command.proto2\xd5\x07\n\nArmService\x12N\n\ttorqueOff\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12M\n\x08torqueOn\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12:\n\x05moveJ\x12\x17.cmvr.api.MoveJ.Request\x1a\x18.cmvr.api.MoveJ.Response\x12:\n\x05moveL\x12\x17.cmvr.api.MoveL.Request\x1a\x18.cmvr.api.MoveL.Response\x12=\n\x06speedJ\x12\x18.cmvr.api.SpeedJ.Request\x1a\x19.cmvr.api.SpeedJ.Response\x12=\n\x06speedL\x12\x18.cmvr.api.SpeedL.Request\x1a\x19.cmvr.api.SpeedL.Response\x12=\n\x06servoJ\x12\x18.cmvr.api.ServoJ.Request\x1a\x19.cmvr.api.ServoJ.Response\x12O\n\nstopMotion\x12\x1f.cmvr.api.CommandHeader.Request\x1a .cmvr.api.CommandHeader.Feedback\x12@\n\rgetJointState\x12\x16.cmvr.api.JointRequest\x1a\x17.cmvr.api.JointResponse\x12@\n\x07getPose\x12\x19.cmvr.api.GetPose.Request\x1a\x1a.cmvr.api.GetPose.Response\x12U\n\x0e\x63\x61librateZeroQ\x12 .cmvr.api.CalibrateZeroQ.Request\x1a!.cmvr.api.CalibrateZeroQ.Response\x12R\n\rgetPoseMatrix\x12\x1f.cmvr.api.GetPoseMatrix.Request\x1a .cmvr.api.GetPoseMatrix.Response\x12s\n\x18\x63omputeForwardKinematics\x12*.cmvr.api.ComputeForwardKinematics.Request\x1a+.cmvr.api.ComputeForwardKinematics.Responseb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.arm_service_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_ARMSERVICE']._serialized_start=92
_globals['_ARMSERVICE']._serialized_end=1073
# @@protoc_insertion_point(module_scope)

View File

@ -0,0 +1,6 @@
from cmvr.api import common_pb2 as _common_pb2
from cmvr.api import arm_command_pb2 as _arm_command_pb2
from google.protobuf import descriptor as _descriptor
from typing import ClassVar as _ClassVar
DESCRIPTOR: _descriptor.FileDescriptor

Some files were not shown because too many files have changed in this diff Show More