commit 515eb3f09cddd25a0b6f2ba0cb6a815426596b31
Author: xtkuang <87661715@qq.com>
Date: Mon Jul 20 16:59:37 2026 +0800
project init
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..43d148d
--- /dev/null
+++ b/README.md
@@ -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 / 平台
gRPC、HTTP、未来 UDP/QUIC"] --> B["Source 连接器"]
+ B --> C["每条边独立的有界队列"]
+ C --> D["Operator DAG
解码、检测、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.10~3.12;首次执行时,uv 会在本机没有合适解释器时
+自动安装。先确认已经安装 uv:
+
+```bash
+uv --version
+```
+
+未安装时可使用 uv 官方安装器:
+
+```bash
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
+
+只验证框架和模拟对话链路时,一条命令创建最小环境并自检:
+
+```bash
+cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
+bash scripts/bootstrap.sh --profile core
+```
+
+`uv.lock` 是安装的唯一版本来源;bootstrap 使用 `uv sync --locked`,不会在用户
+机器上重新选择依赖版本。无需 `source .venv/bin/activate`,统一通过
+`uv run --no-sync` 使用已经安装好的环境:
+
+```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 frame,progress 中会持续显示
+`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-当前限制与演进顺序) 中有更详细说明。
diff --git a/configs/smoke.yaml b/configs/smoke.yaml
new file mode 100644
index 0000000..d148214
--- /dev/null
+++ b/configs/smoke.yaml
@@ -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
diff --git a/detect_server/README.md b/detect_server/README.md
new file mode 100644
index 0000000..7c535fc
--- /dev/null
+++ b/detect_server/README.md
@@ -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 ID,HTTP 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` 便于联调,正式部署
+可改为 30~60 秒。
+
+## 视频连续性和背压
+
+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 的每个直接前驱都是安全门,安全门和执行器之间不能插入普通变换节点,也不能存在绕过路径。
diff --git a/detect_server/pipeline.yaml b/detect_server/pipeline.yaml
new file mode 100644
index 0000000..2ce7667
--- /dev/null
+++ b/detect_server/pipeline.yaml
@@ -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
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..73b5cb6
--- /dev/null
+++ b/docs/architecture.md
@@ -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
gRPC / future QUIC"]
+ PLAT["业务平台
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:` 标签的连接器只能引用相同 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.`
+
+| 字段 | 默认值 | 说明 |
+|---|---:|---|
+| `transport` | 无 | 必填,转为小写;当前连接器支持 `grpc`、`http` |
+| `target` | `null` | gRPC 地址,例如 `127.0.0.1:50052` |
+| `base_url` | `null` | HTTP 基础 URL |
+| `bind` | `null` | 为未来 ingress/server 连接器预留 |
+| `tls` | `false` | 仅用于 gRPC channel 是否使用 TLS;HTTP 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.`
+
+| 字段 | 默认值 | 说明 |
+|---|---:|---|
+| `enabled` | `true` | 未指定 `--pipeline` 时是否启动 |
+| `priority` | `normal` | v1 必须为 `normal`;`low/high` 或数值优先级会因无调度器而被拒绝 |
+| `nodes` | 无 | 至少一个节点 |
+| `edges` | `[]` | 有向连接列表 |
+
+不要在活跃 Pipeline 中保留 `enabled: false` 节点;当前编译器会直接拒绝。要暂时关闭逻辑,请禁用整个 Pipeline 或从图和配置中移除该节点。
+
+### 3.5 `nodes.`
+
+| 字段 | 默认值 | 说明 |
+|---|---:|---|
+| `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 bytes;HTTP 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 stop;WARNING/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-safety:edge-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 不能声明 input,Sink 不能声明 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 和资源峰值检查,再接入真实设备。
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7f2c28d
--- /dev/null
+++ b/pyproject.toml
@@ -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"
diff --git a/scripts/__pycache__/generate_cmvr_stubs.cpython-312.pyc b/scripts/__pycache__/generate_cmvr_stubs.cpython-312.pyc
new file mode 100644
index 0000000..79eabaa
Binary files /dev/null and b/scripts/__pycache__/generate_cmvr_stubs.cpython-312.pyc differ
diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh
new file mode 100755
index 0000000..960aee7
--- /dev/null
+++ b/scripts/bootstrap.sh
@@ -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 ..."
diff --git a/scripts/generate_cmvr_stubs.py b/scripts/generate_cmvr_stubs.py
new file mode 100644
index 0000000..927f609
--- /dev/null
+++ b/scripts/generate_cmvr_stubs.py
@@ -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())
diff --git a/src/cmvr/api/__pycache__/agv_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/agv_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..c143299
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/agv_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..1cc0461
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..c085bd9
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..b040e24
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/agv_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..ac621b4
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/agv_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..ecdfaef
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..c79d7f6
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..2a0e17c
Binary files /dev/null and b/src/cmvr/api/__pycache__/agv_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/arm_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..bd3acf9
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/arm_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..10f8649
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..d0522d2
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..a8cc432
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/arm_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b89e720
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/arm_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..ec0b7a7
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..67bbc9a
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..b194f8a
Binary files /dev/null and b/src/cmvr/api/__pycache__/arm_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..d2dd5e7
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..29d2a60
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..5e9fc2b
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..df3c30c
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..259d4d3
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..5b26640
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..42cc923
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..58302db
Binary files /dev/null and b/src/cmvr/api/__pycache__/biohead_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/camera_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..c53da81
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/camera_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..b2b22bc
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..a029067
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..9ad9602
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/camera_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..3deb11d
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/camera_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..03aec9d
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..3215599
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..6cd552b
Binary files /dev/null and b/src/cmvr/api/__pycache__/camera_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/common_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/common_pb2.cpython-310.pyc
new file mode 100644
index 0000000..98a58ab
Binary files /dev/null and b/src/cmvr/api/__pycache__/common_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/common_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/common_pb2.cpython-312.pyc
new file mode 100644
index 0000000..c515127
Binary files /dev/null and b/src/cmvr/api/__pycache__/common_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..279ee94
Binary files /dev/null and b/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..4acbbd1
Binary files /dev/null and b/src/cmvr/api/__pycache__/common_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..255cc73
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..18085db
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..778f4cc
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..4d21ba0
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..f07eeea
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..0cf115b
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..03d62c7
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..a07fe54
Binary files /dev/null and b/src/cmvr/api/__pycache__/dexhand_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..88d2571
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..a0161e1
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..9ceacee
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..e7fbb50
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b060da1
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..6312d2b
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1578e73
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..5013945
Binary files /dev/null and b/src/cmvr/api/__pycache__/hlc_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..ddc10a1
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..82612d6
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..33af068
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..4b9d77d
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..84dd7a7
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..5bc63ba
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..684f6cc
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..7e39778
Binary files /dev/null and b/src/cmvr/api/__pycache__/microphone_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..ab48a59
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..1673f4c
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..d9b4e34
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..c5fd106
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..c742f9a
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..ff7ceaa
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..8378014
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..fff7f40
Binary files /dev/null and b/src/cmvr/api/__pycache__/speaker_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_command_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/system_command_pb2.cpython-310.pyc
new file mode 100644
index 0000000..8e0a20e
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_command_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_command_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/system_command_pb2.cpython-312.pyc
new file mode 100644
index 0000000..0bd9c0b
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_command_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..6884518
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..dd69cb7
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_command_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/system_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..a4800f5
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/system_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..07abe3f
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..d9ed722
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..8416ecb
Binary files /dev/null and b/src/cmvr/api/__pycache__/system_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/test_service_pb2.cpython-310.pyc b/src/cmvr/api/__pycache__/test_service_pb2.cpython-310.pyc
new file mode 100644
index 0000000..ef9063b
Binary files /dev/null and b/src/cmvr/api/__pycache__/test_service_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/test_service_pb2.cpython-312.pyc b/src/cmvr/api/__pycache__/test_service_pb2.cpython-312.pyc
new file mode 100644
index 0000000..6a6acdd
Binary files /dev/null and b/src/cmvr/api/__pycache__/test_service_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-310.pyc b/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..b21212d
Binary files /dev/null and b/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-312.pyc b/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..e97eae9
Binary files /dev/null and b/src/cmvr/api/__pycache__/test_service_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/api/agv_command_pb2.py b/src/cmvr/api/agv_command_pb2.py
new file mode 100644
index 0000000..59a9584
--- /dev/null
+++ b/src/cmvr/api/agv_command_pb2.py
@@ -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)
diff --git a/src/cmvr/api/agv_command_pb2.pyi b/src/cmvr/api/agv_command_pb2.pyi
new file mode 100644
index 0000000..6e3bff9
--- /dev/null
+++ b/src/cmvr/api/agv_command_pb2.pyi
@@ -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: ...
diff --git a/src/cmvr/api/agv_command_pb2_grpc.py b/src/cmvr/api/agv_command_pb2_grpc.py
new file mode 100644
index 0000000..80bbe76
--- /dev/null
+++ b/src/cmvr/api/agv_command_pb2_grpc.py
@@ -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}.'
+ )
diff --git a/src/cmvr/api/agv_service_pb2.py b/src/cmvr/api/agv_service_pb2.py
new file mode 100644
index 0000000..91d1339
--- /dev/null
+++ b/src/cmvr/api/agv_service_pb2.py
@@ -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)
diff --git a/src/cmvr/api/agv_service_pb2.pyi b/src/cmvr/api/agv_service_pb2.pyi
new file mode 100644
index 0000000..9861922
--- /dev/null
+++ b/src/cmvr/api/agv_service_pb2.pyi
@@ -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
diff --git a/src/cmvr/api/agv_service_pb2_grpc.py b/src/cmvr/api/agv_service_pb2_grpc.py
new file mode 100644
index 0000000..0070d2f
--- /dev/null
+++ b/src/cmvr/api/agv_service_pb2_grpc.py
@@ -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)
diff --git a/src/cmvr/api/arm_command_pb2.py b/src/cmvr/api/arm_command_pb2.py
new file mode 100644
index 0000000..be8e45e
--- /dev/null
+++ b/src/cmvr/api/arm_command_pb2.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/arm_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/arm_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/arm_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"(\n\x14JointPositionCommand\x12\x10\n\x08position\x18\x01 \x03(\x01\"(\n\x14JointVelocityCommand\x12\x10\n\x08velocity\x18\x01 \x03(\x01\"\x90\x01\n\rMotionOptions\x12\x10\n\x08velocity\x18\x01 \x01(\x01\x12\x14\n\x0c\x61\x63\x63\x65leration\x18\x02 \x01(\x01\x12\x14\n\x0c\x62lend_radius\x18\x03 \x01(\x01\x12\x0c\n\x04jerk\x18\x04 \x01(\x01\x12\x1d\n\x15joint_velocity_limits\x18\x05 \x03(\x01\x12\x14\n\x0c\x61synchronous\x18\x06 \x01(\x08\"T\n\rCartesianPose\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\t\n\x01z\x18\x03 \x01(\x01\x12\n\n\x02rx\x18\x04 \x01(\x01\x12\n\n\x02ry\x18\x05 \x01(\x01\x12\n\n\x02rz\x18\x06 \x01(\x01\"[\n\x11\x43\x61rtesianVelocity\x12\n\n\x02vx\x18\x01 \x01(\x01\x12\n\n\x02vy\x18\x02 \x01(\x01\x12\n\n\x02vz\x18\x03 \x01(\x01\x12\n\n\x02wx\x18\x04 \x01(\x01\x12\n\n\x02wy\x18\x05 \x01(\x01\x12\n\n\x02wz\x18\x06 \x01(\x01\"\xe4\x01\n\x12TransformMatrix4x4\x12\x0b\n\x03m00\x18\x01 \x01(\x01\x12\x0b\n\x03m01\x18\x02 \x01(\x01\x12\x0b\n\x03m02\x18\x03 \x01(\x01\x12\x0b\n\x03m03\x18\x04 \x01(\x01\x12\x0b\n\x03m10\x18\x05 \x01(\x01\x12\x0b\n\x03m11\x18\x06 \x01(\x01\x12\x0b\n\x03m12\x18\x07 \x01(\x01\x12\x0b\n\x03m13\x18\x08 \x01(\x01\x12\x0b\n\x03m20\x18\t \x01(\x01\x12\x0b\n\x03m21\x18\n \x01(\x01\x12\x0b\n\x03m22\x18\x0b \x01(\x01\x12\x0b\n\x03m23\x18\x0c \x01(\x01\x12\x0b\n\x03m30\x18\r \x01(\x01\x12\x0b\n\x03m31\x18\x0e \x01(\x01\x12\x0b\n\x03m32\x18\x0f \x01(\x01\x12\x0b\n\x03m33\x18\x10 \x01(\x01\"\xdc\x01\n\x05MoveJ\x1a\x94\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12.\n\x06target\x18\x02 \x01(\x0b\x32\x1e.cmvr.api.JointPositionCommand\x12(\n\x07options\x18\x03 \x01(\x0b\x32\x17.cmvr.api.MotionOptions\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xfc\x01\n\x05MoveL\x1a\xb4\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\'\n\x06target\x18\x02 \x01(\x0b\x32\x17.cmvr.api.CartesianPose\x12(\n\x07options\x18\x03 \x01(\x0b\x32\x17.cmvr.api.MotionOptions\x12%\n\x05\x66rame\x18\x04 \x01(\x0e\x32\x16.cmvr.api.ArmFrameType\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xdd\x01\n\x06SpeedJ\x1a\x94\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x30\n\x08velocity\x18\x02 \x01(\x0b\x32\x1e.cmvr.api.JointVelocityCommand\x12\x14\n\x0c\x61\x63\x63\x65leration\x18\x03 \x01(\x01\x12\x10\n\x08\x64uration\x18\x04 \x01(\x01\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x81\x02\n\x06SpeedL\x1a\xb8\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12-\n\x08velocity\x18\x02 \x01(\x0b\x32\x1b.cmvr.api.CartesianVelocity\x12\x14\n\x0c\x61\x63\x63\x65leration\x18\x03 \x01(\x01\x12\x10\n\x08\x64uration\x18\x04 \x01(\x01\x12%\n\x05\x66rame\x18\x05 \x01(\x0e\x32\x16.cmvr.api.ArmFrameType\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xb2\x01\n\x06ServoJ\x1aj\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12.\n\x06target\x18\x02 \x01(\x0b\x32\x1e.cmvr.api.JointPositionCommand\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"a\n\nJointState\x12\x0c\n\x04name\x18\x01 \x03(\t\x12\x10\n\x08position\x18\x02 \x03(\x01\x12\x10\n\x08velocity\x18\x03 \x03(\x01\x12\x0e\n\x06\x65\x66\x66ort\x18\x04 \x03(\x01\x12\x11\n\ttimestamp\x18\x05 \x01(\x01\"?\n\x0cJointRequest\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\"f\n\rJointResponse\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12#\n\x05state\x18\x02 \x01(\x0b\x32\x14.cmvr.api.JointState\"\xce\x01\n\x07GetPose\x1a^\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x11\n\tbase_link\x18\x02 \x01(\t\x12\x0f\n\x07\x65\x65_link\x18\x03 \x01(\t\x1a\x63\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12%\n\x04pose\x18\x02 \x01(\x0b\x32\x17.cmvr.api.CartesianPose\"\x9e\x01\n\x0e\x43\x61librateZeroQ\x1aN\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x12\n\njoint_name\x18\x02 \x01(\t\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xdb\x01\n\rGetPoseMatrix\x1a^\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x11\n\tbase_link\x18\x02 \x01(\t\x12\x0f\n\x07\x65\x65_link\x18\x03 \x01(\t\x1aj\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12,\n\x06matrix\x18\x02 \x01(\x0b\x32\x1c.cmvr.api.TransformMatrix4x4\"\x97\x02\n\x18\x43omputeForwardKinematics\x1a\x8e\x01\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x11\n\tbase_link\x18\x02 \x01(\t\x12\x0f\n\x07\x65\x65_link\x18\x03 \x01(\t\x12.\n\x06joints\x18\x04 \x01(\x0b\x32\x1e.cmvr.api.JointPositionCommand\x1aj\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12,\n\x06matrix\x18\x02 \x01(\x0b\x32\x1c.cmvr.api.TransformMatrix4x4*_\n\x0c\x41rmFrameType\x12\x12\n\x0e\x41RM_FRAME_BASE\x10\x00\x12\x12\n\x0e\x41RM_FRAME_TOOL\x10\x01\x12\x13\n\x0f\x41RM_FRAME_WORLD\x10\x02\x12\x12\n\x0e\x41RM_FRAME_USER\x10\x03\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.arm_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_ARMFRAMETYPE']._serialized_start=2989
+ _globals['_ARMFRAMETYPE']._serialized_end=3084
+ _globals['_JOINTPOSITIONCOMMAND']._serialized_start=63
+ _globals['_JOINTPOSITIONCOMMAND']._serialized_end=103
+ _globals['_JOINTVELOCITYCOMMAND']._serialized_start=105
+ _globals['_JOINTVELOCITYCOMMAND']._serialized_end=145
+ _globals['_MOTIONOPTIONS']._serialized_start=148
+ _globals['_MOTIONOPTIONS']._serialized_end=292
+ _globals['_CARTESIANPOSE']._serialized_start=294
+ _globals['_CARTESIANPOSE']._serialized_end=378
+ _globals['_CARTESIANVELOCITY']._serialized_start=380
+ _globals['_CARTESIANVELOCITY']._serialized_end=471
+ _globals['_TRANSFORMMATRIX4X4']._serialized_start=474
+ _globals['_TRANSFORMMATRIX4X4']._serialized_end=702
+ _globals['_MOVEJ']._serialized_start=705
+ _globals['_MOVEJ']._serialized_end=925
+ _globals['_MOVEJ_REQUEST']._serialized_start=715
+ _globals['_MOVEJ_REQUEST']._serialized_end=863
+ _globals['_MOVEJ_RESPONSE']._serialized_start=865
+ _globals['_MOVEJ_RESPONSE']._serialized_end=925
+ _globals['_MOVEL']._serialized_start=928
+ _globals['_MOVEL']._serialized_end=1180
+ _globals['_MOVEL_REQUEST']._serialized_start=938
+ _globals['_MOVEL_REQUEST']._serialized_end=1118
+ _globals['_MOVEL_RESPONSE']._serialized_start=865
+ _globals['_MOVEL_RESPONSE']._serialized_end=925
+ _globals['_SPEEDJ']._serialized_start=1183
+ _globals['_SPEEDJ']._serialized_end=1404
+ _globals['_SPEEDJ_REQUEST']._serialized_start=1194
+ _globals['_SPEEDJ_REQUEST']._serialized_end=1342
+ _globals['_SPEEDJ_RESPONSE']._serialized_start=865
+ _globals['_SPEEDJ_RESPONSE']._serialized_end=925
+ _globals['_SPEEDL']._serialized_start=1407
+ _globals['_SPEEDL']._serialized_end=1664
+ _globals['_SPEEDL_REQUEST']._serialized_start=1418
+ _globals['_SPEEDL_REQUEST']._serialized_end=1602
+ _globals['_SPEEDL_RESPONSE']._serialized_start=865
+ _globals['_SPEEDL_RESPONSE']._serialized_end=925
+ _globals['_SERVOJ']._serialized_start=1667
+ _globals['_SERVOJ']._serialized_end=1845
+ _globals['_SERVOJ_REQUEST']._serialized_start=715
+ _globals['_SERVOJ_REQUEST']._serialized_end=821
+ _globals['_SERVOJ_RESPONSE']._serialized_start=865
+ _globals['_SERVOJ_RESPONSE']._serialized_end=925
+ _globals['_JOINTSTATE']._serialized_start=1847
+ _globals['_JOINTSTATE']._serialized_end=1944
+ _globals['_JOINTREQUEST']._serialized_start=1946
+ _globals['_JOINTREQUEST']._serialized_end=2009
+ _globals['_JOINTRESPONSE']._serialized_start=2011
+ _globals['_JOINTRESPONSE']._serialized_end=2113
+ _globals['_GETPOSE']._serialized_start=2116
+ _globals['_GETPOSE']._serialized_end=2322
+ _globals['_GETPOSE_REQUEST']._serialized_start=2127
+ _globals['_GETPOSE_REQUEST']._serialized_end=2221
+ _globals['_GETPOSE_RESPONSE']._serialized_start=2223
+ _globals['_GETPOSE_RESPONSE']._serialized_end=2322
+ _globals['_CALIBRATEZEROQ']._serialized_start=2325
+ _globals['_CALIBRATEZEROQ']._serialized_end=2483
+ _globals['_CALIBRATEZEROQ_REQUEST']._serialized_start=2343
+ _globals['_CALIBRATEZEROQ_REQUEST']._serialized_end=2421
+ _globals['_CALIBRATEZEROQ_RESPONSE']._serialized_start=865
+ _globals['_CALIBRATEZEROQ_RESPONSE']._serialized_end=925
+ _globals['_GETPOSEMATRIX']._serialized_start=2486
+ _globals['_GETPOSEMATRIX']._serialized_end=2705
+ _globals['_GETPOSEMATRIX_REQUEST']._serialized_start=2127
+ _globals['_GETPOSEMATRIX_REQUEST']._serialized_end=2221
+ _globals['_GETPOSEMATRIX_RESPONSE']._serialized_start=2599
+ _globals['_GETPOSEMATRIX_RESPONSE']._serialized_end=2705
+ _globals['_COMPUTEFORWARDKINEMATICS']._serialized_start=2708
+ _globals['_COMPUTEFORWARDKINEMATICS']._serialized_end=2987
+ _globals['_COMPUTEFORWARDKINEMATICS_REQUEST']._serialized_start=2737
+ _globals['_COMPUTEFORWARDKINEMATICS_REQUEST']._serialized_end=2879
+ _globals['_COMPUTEFORWARDKINEMATICS_RESPONSE']._serialized_start=2599
+ _globals['_COMPUTEFORWARDKINEMATICS_RESPONSE']._serialized_end=2705
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/arm_command_pb2.pyi b/src/cmvr/api/arm_command_pb2.pyi
new file mode 100644
index 0000000..aed0966
--- /dev/null
+++ b/src/cmvr/api/arm_command_pb2.pyi
@@ -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: ...
diff --git a/src/cmvr/api/arm_command_pb2_grpc.py b/src/cmvr/api/arm_command_pb2_grpc.py
new file mode 100644
index 0000000..3accff3
--- /dev/null
+++ b/src/cmvr/api/arm_command_pb2_grpc.py
@@ -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}.'
+ )
diff --git a/src/cmvr/api/arm_service_pb2.py b/src/cmvr/api/arm_service_pb2.py
new file mode 100644
index 0000000..2419ca9
--- /dev/null
+++ b/src/cmvr/api/arm_service_pb2.py
@@ -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)
diff --git a/src/cmvr/api/arm_service_pb2.pyi b/src/cmvr/api/arm_service_pb2.pyi
new file mode 100644
index 0000000..9d65316
--- /dev/null
+++ b/src/cmvr/api/arm_service_pb2.pyi
@@ -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
diff --git a/src/cmvr/api/arm_service_pb2_grpc.py b/src/cmvr/api/arm_service_pb2_grpc.py
new file mode 100644
index 0000000..7f1b320
--- /dev/null
+++ b/src/cmvr/api/arm_service_pb2_grpc.py
@@ -0,0 +1,614 @@
+# 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 arm_command_pb2 as cmvr_dot_api_dot_arm__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/arm_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 ArmServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.torqueOff = channel.unary_unary(
+ '/cmvr.api.ArmService/torqueOff',
+ 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.torqueOn = channel.unary_unary(
+ '/cmvr.api.ArmService/torqueOn',
+ 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.moveJ = channel.unary_unary(
+ '/cmvr.api.ArmService/moveJ',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.MoveJ.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.MoveJ.Response.FromString,
+ _registered_method=True)
+ self.moveL = channel.unary_unary(
+ '/cmvr.api.ArmService/moveL',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.MoveL.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.MoveL.Response.FromString,
+ _registered_method=True)
+ self.speedJ = channel.unary_unary(
+ '/cmvr.api.ArmService/speedJ',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Response.FromString,
+ _registered_method=True)
+ self.speedL = channel.unary_unary(
+ '/cmvr.api.ArmService/speedL',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.SpeedL.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.SpeedL.Response.FromString,
+ _registered_method=True)
+ self.servoJ = channel.unary_unary(
+ '/cmvr.api.ArmService/servoJ',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.ServoJ.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.ServoJ.Response.FromString,
+ _registered_method=True)
+ self.stopMotion = channel.unary_unary(
+ '/cmvr.api.ArmService/stopMotion',
+ 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.getJointState = channel.unary_unary(
+ '/cmvr.api.ArmService/getJointState',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.JointRequest.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.JointResponse.FromString,
+ _registered_method=True)
+ self.getPose = channel.unary_unary(
+ '/cmvr.api.ArmService/getPose',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.GetPose.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.GetPose.Response.FromString,
+ _registered_method=True)
+ self.calibrateZeroQ = channel.unary_unary(
+ '/cmvr.api.ArmService/calibrateZeroQ',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Response.FromString,
+ _registered_method=True)
+ self.getPoseMatrix = channel.unary_unary(
+ '/cmvr.api.ArmService/getPoseMatrix',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Response.FromString,
+ _registered_method=True)
+ self.computeForwardKinematics = channel.unary_unary(
+ '/cmvr.api.ArmService/computeForwardKinematics',
+ request_serializer=cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Response.FromString,
+ _registered_method=True)
+
+
+class ArmServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def torqueOff(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def torqueOn(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def moveJ(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def moveL(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def speedJ(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def speedL(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def servoJ(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def stopMotion(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def getJointState(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def getPose(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def calibrateZeroQ(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def getPoseMatrix(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def computeForwardKinematics(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_ArmServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'torqueOff': grpc.unary_unary_rpc_method_handler(
+ servicer.torqueOff,
+ request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
+ ),
+ 'torqueOn': grpc.unary_unary_rpc_method_handler(
+ servicer.torqueOn,
+ request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
+ ),
+ 'moveJ': grpc.unary_unary_rpc_method_handler(
+ servicer.moveJ,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.MoveJ.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.MoveJ.Response.SerializeToString,
+ ),
+ 'moveL': grpc.unary_unary_rpc_method_handler(
+ servicer.moveL,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.MoveL.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.MoveL.Response.SerializeToString,
+ ),
+ 'speedJ': grpc.unary_unary_rpc_method_handler(
+ servicer.speedJ,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Response.SerializeToString,
+ ),
+ 'speedL': grpc.unary_unary_rpc_method_handler(
+ servicer.speedL,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.SpeedL.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.SpeedL.Response.SerializeToString,
+ ),
+ 'servoJ': grpc.unary_unary_rpc_method_handler(
+ servicer.servoJ,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.ServoJ.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.ServoJ.Response.SerializeToString,
+ ),
+ 'stopMotion': grpc.unary_unary_rpc_method_handler(
+ servicer.stopMotion,
+ request_deserializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_common__pb2.CommandHeader.Feedback.SerializeToString,
+ ),
+ 'getJointState': grpc.unary_unary_rpc_method_handler(
+ servicer.getJointState,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.JointRequest.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.JointResponse.SerializeToString,
+ ),
+ 'getPose': grpc.unary_unary_rpc_method_handler(
+ servicer.getPose,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.GetPose.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.GetPose.Response.SerializeToString,
+ ),
+ 'calibrateZeroQ': grpc.unary_unary_rpc_method_handler(
+ servicer.calibrateZeroQ,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Response.SerializeToString,
+ ),
+ 'getPoseMatrix': grpc.unary_unary_rpc_method_handler(
+ servicer.getPoseMatrix,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Response.SerializeToString,
+ ),
+ 'computeForwardKinematics': grpc.unary_unary_rpc_method_handler(
+ servicer.computeForwardKinematics,
+ request_deserializer=cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Response.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.ArmService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.ArmService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class ArmService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def torqueOff(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.ArmService/torqueOff',
+ 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 torqueOn(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.ArmService/torqueOn',
+ 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 moveJ(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.ArmService/moveJ',
+ cmvr_dot_api_dot_arm__command__pb2.MoveJ.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.MoveJ.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def moveL(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.ArmService/moveL',
+ cmvr_dot_api_dot_arm__command__pb2.MoveL.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.MoveL.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def speedJ(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.ArmService/speedJ',
+ cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.SpeedJ.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def speedL(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.ArmService/speedL',
+ cmvr_dot_api_dot_arm__command__pb2.SpeedL.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.SpeedL.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def servoJ(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.ArmService/servoJ',
+ cmvr_dot_api_dot_arm__command__pb2.ServoJ.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.ServoJ.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def stopMotion(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.ArmService/stopMotion',
+ 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 getJointState(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.ArmService/getJointState',
+ cmvr_dot_api_dot_arm__command__pb2.JointRequest.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.JointResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def getPose(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.ArmService/getPose',
+ cmvr_dot_api_dot_arm__command__pb2.GetPose.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.GetPose.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def calibrateZeroQ(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.ArmService/calibrateZeroQ',
+ cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.CalibrateZeroQ.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def getPoseMatrix(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.ArmService/getPoseMatrix',
+ cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.GetPoseMatrix.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def computeForwardKinematics(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.ArmService/computeForwardKinematics',
+ cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Request.SerializeToString,
+ cmvr_dot_api_dot_arm__command__pb2.ComputeForwardKinematics.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/biohead_command_pb2.py b/src/cmvr/api/biohead_command_pb2.py
new file mode 100644
index 0000000..6e9bfa1
--- /dev/null
+++ b/src/cmvr/api/biohead_command_pb2.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/biohead_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/biohead_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/biohead_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"\xdd\x08\n\x10\x46\x61\x63ialExpression\x12\x33\n\x07\x65yebrow\x18\x03 \x01(\x0b\x32\".cmvr.api.FacialExpression.Eyebrow\x12\x31\n\x06\x65yelid\x18\x04 \x01(\x0b\x32!.cmvr.api.FacialExpression.Eyelid\x12\x33\n\x07\x65yeball\x18\x05 \x01(\x0b\x32\".cmvr.api.FacialExpression.Eyeball\x12-\n\x04nose\x18\x06 \x01(\x0b\x32\x1f.cmvr.api.FacialExpression.Nose\x12/\n\x05mouth\x18\x07 \x01(\x0b\x32 .cmvr.api.FacialExpression.Mouth\x12+\n\x03jaw\x18\x08 \x01(\x0b\x32\x1e.cmvr.api.FacialExpression.Jaw\x1ai\n\x07\x45yebrow\x12\x16\n\x0eleft_outside_y\x18\x01 \x01(\x02\x12\x15\n\rleft_inside_y\x18\x02 \x01(\x02\x12\x17\n\x0fright_outside_y\x18\x03 \x01(\x02\x12\x16\n\x0eright_inside_y\x18\x04 \x01(\x02\x1a\x62\n\x06\x45yelid\x12\x14\n\x0cleft_upper_y\x18\x01 \x01(\x02\x12\x14\n\x0cleft_lower_y\x18\x02 \x01(\x02\x12\x15\n\rright_upper_y\x18\x03 \x01(\x02\x12\x15\n\rright_lower_y\x18\x04 \x01(\x02\x1aK\n\x07\x45yeball\x12\x0e\n\x06left_x\x18\x01 \x01(\x02\x12\x0e\n\x06left_y\x18\x02 \x01(\x02\x12\x0f\n\x07right_x\x18\x03 \x01(\x02\x12\x0f\n\x07right_y\x18\x04 \x01(\x02\x1a\'\n\x04Nose\x12\x0e\n\x06left_y\x18\x01 \x01(\x02\x12\x0f\n\x07right_y\x18\x02 \x01(\x02\x1a\xbc\x03\n\x05Mouth\x12\x13\n\x0bupper_lip_y\x18\x01 \x01(\x02\x12\x13\n\x0bupper_lip_z\x18\x02 \x01(\x02\x12\x13\n\x0blower_lip_y\x18\x03 \x01(\x02\x12\x13\n\x0blower_lip_z\x18\x04 \x01(\x02\x12:\n\x08left_lip\x18\x05 \x01(\x0b\x32(.cmvr.api.FacialExpression.Mouth.LeftLip\x12<\n\tright_lip\x18\x06 \x01(\x0b\x32).cmvr.api.FacialExpression.Mouth.RightLip\x1aq\n\x07LeftLip\x12\x0f\n\x07upper_x\x18\x01 \x01(\x02\x12\x0f\n\x07upper_y\x18\x02 \x01(\x02\x12\x10\n\x08\x63orner_x\x18\x03 \x01(\x02\x12\x10\n\x08\x63orner_y\x18\x04 \x01(\x02\x12\x0f\n\x07lower_x\x18\x05 \x01(\x02\x12\x0f\n\x07lower_y\x18\x06 \x01(\x02\x1ar\n\x08RightLip\x12\x0f\n\x07upper_x\x18\x01 \x01(\x02\x12\x0f\n\x07upper_y\x18\x02 \x01(\x02\x12\x10\n\x08\x63orner_x\x18\x03 \x01(\x02\x12\x10\n\x08\x63orner_y\x18\x04 \x01(\x02\x12\x0f\n\x07lower_x\x18\x05 \x01(\x02\x12\x0f\n\x07lower_y\x18\x06 \x01(\x02\x1a\x1b\n\x03Jaw\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\"\xf0\x01\n\x13SetFacialExpression\x1aj\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12.\n\nexpression\x18\x02 \x01(\x0b\x32\x1a.cmvr.api.FacialExpression\x1am\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x14\n\x0c\x65xecution_id\x18\x02 \x01(\t\x12\x19\n\x11\x65xecution_time_ms\x18\x03 \x01(\x02\"\xf8\x01\n\x16StreamFacialExpression\x1aq\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12(\n\x04\x65xpr\x18\x02 \x01(\x0b\x32\x1a.cmvr.api.FacialExpression\x12\x0b\n\x03\x65of\x18\x03 \x01(\x08\x1ak\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12-\n\texpr_diff\x18\x02 \x01(\x0b\x32\x1a.cmvr.api.FacialExpression\"\x84\x02\n\tGetStatus\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\xba\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x11\n\tis_moving\x18\x02 \x01(\x08\x12\x17\n\x0flast_request_id\x18\x03 \x01(\t\x12\x19\n\x11\x63urrent_positions\x18\x04 \x03(\x02\x12\x18\n\x10\x63\x61mera_recording\x18\x05 \x01(\x08\x12\x1b\n\x13\x61\x63tive_recording_id\x18\x06 \x01(\t\"\xa4\x01\n\rEmergencyStop\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1aW\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x19\n\x11stopped_processes\x18\x02 \x01(\t\"\x86\x01\n\nSpeakStart\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x85\x01\n\tSpeakStop\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x81\x01\n\x05Happy\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x84\x01\n\x08Surprise\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8b\x01\n\x0f\x45xpressionTired\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8b\x01\n\x0f\x45xpressionAngry\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8d\x01\n\x11\x45xpressionSadness\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8a\x01\n\x0e\x45xpressionYawn\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedbackb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.biohead_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_FACIALEXPRESSION']._serialized_start=68
+ _globals['_FACIALEXPRESSION']._serialized_end=1185
+ _globals['_FACIALEXPRESSION_EYEBROW']._serialized_start=386
+ _globals['_FACIALEXPRESSION_EYEBROW']._serialized_end=491
+ _globals['_FACIALEXPRESSION_EYELID']._serialized_start=493
+ _globals['_FACIALEXPRESSION_EYELID']._serialized_end=591
+ _globals['_FACIALEXPRESSION_EYEBALL']._serialized_start=593
+ _globals['_FACIALEXPRESSION_EYEBALL']._serialized_end=668
+ _globals['_FACIALEXPRESSION_NOSE']._serialized_start=670
+ _globals['_FACIALEXPRESSION_NOSE']._serialized_end=709
+ _globals['_FACIALEXPRESSION_MOUTH']._serialized_start=712
+ _globals['_FACIALEXPRESSION_MOUTH']._serialized_end=1156
+ _globals['_FACIALEXPRESSION_MOUTH_LEFTLIP']._serialized_start=927
+ _globals['_FACIALEXPRESSION_MOUTH_LEFTLIP']._serialized_end=1040
+ _globals['_FACIALEXPRESSION_MOUTH_RIGHTLIP']._serialized_start=1042
+ _globals['_FACIALEXPRESSION_MOUTH_RIGHTLIP']._serialized_end=1156
+ _globals['_FACIALEXPRESSION_JAW']._serialized_start=1158
+ _globals['_FACIALEXPRESSION_JAW']._serialized_end=1185
+ _globals['_SETFACIALEXPRESSION']._serialized_start=1188
+ _globals['_SETFACIALEXPRESSION']._serialized_end=1428
+ _globals['_SETFACIALEXPRESSION_REQUEST']._serialized_start=1211
+ _globals['_SETFACIALEXPRESSION_REQUEST']._serialized_end=1317
+ _globals['_SETFACIALEXPRESSION_FEEDBACK']._serialized_start=1319
+ _globals['_SETFACIALEXPRESSION_FEEDBACK']._serialized_end=1428
+ _globals['_STREAMFACIALEXPRESSION']._serialized_start=1431
+ _globals['_STREAMFACIALEXPRESSION']._serialized_end=1679
+ _globals['_STREAMFACIALEXPRESSION_REQUEST']._serialized_start=1457
+ _globals['_STREAMFACIALEXPRESSION_REQUEST']._serialized_end=1570
+ _globals['_STREAMFACIALEXPRESSION_FEEDBACK']._serialized_start=1572
+ _globals['_STREAMFACIALEXPRESSION_FEEDBACK']._serialized_end=1679
+ _globals['_GETSTATUS']._serialized_start=1682
+ _globals['_GETSTATUS']._serialized_end=1942
+ _globals['_GETSTATUS_REQUEST']._serialized_start=1211
+ _globals['_GETSTATUS_REQUEST']._serialized_end=1269
+ _globals['_GETSTATUS_FEEDBACK']._serialized_start=1756
+ _globals['_GETSTATUS_FEEDBACK']._serialized_end=1942
+ _globals['_EMERGENCYSTOP']._serialized_start=1945
+ _globals['_EMERGENCYSTOP']._serialized_end=2109
+ _globals['_EMERGENCYSTOP_REQUEST']._serialized_start=1211
+ _globals['_EMERGENCYSTOP_REQUEST']._serialized_end=1269
+ _globals['_EMERGENCYSTOP_FEEDBACK']._serialized_start=2022
+ _globals['_EMERGENCYSTOP_FEEDBACK']._serialized_end=2109
+ _globals['_SPEAKSTART']._serialized_start=2112
+ _globals['_SPEAKSTART']._serialized_end=2246
+ _globals['_SPEAKSTART_REQUEST']._serialized_start=1211
+ _globals['_SPEAKSTART_REQUEST']._serialized_end=1269
+ _globals['_SPEAKSTART_FEEDBACK']._serialized_start=1319
+ _globals['_SPEAKSTART_FEEDBACK']._serialized_end=1379
+ _globals['_SPEAKSTOP']._serialized_start=2249
+ _globals['_SPEAKSTOP']._serialized_end=2382
+ _globals['_SPEAKSTOP_REQUEST']._serialized_start=1211
+ _globals['_SPEAKSTOP_REQUEST']._serialized_end=1269
+ _globals['_SPEAKSTOP_FEEDBACK']._serialized_start=1319
+ _globals['_SPEAKSTOP_FEEDBACK']._serialized_end=1379
+ _globals['_HAPPY']._serialized_start=2385
+ _globals['_HAPPY']._serialized_end=2514
+ _globals['_HAPPY_REQUEST']._serialized_start=1211
+ _globals['_HAPPY_REQUEST']._serialized_end=1269
+ _globals['_HAPPY_FEEDBACK']._serialized_start=1319
+ _globals['_HAPPY_FEEDBACK']._serialized_end=1379
+ _globals['_SURPRISE']._serialized_start=2517
+ _globals['_SURPRISE']._serialized_end=2649
+ _globals['_SURPRISE_REQUEST']._serialized_start=1211
+ _globals['_SURPRISE_REQUEST']._serialized_end=1269
+ _globals['_SURPRISE_FEEDBACK']._serialized_start=1319
+ _globals['_SURPRISE_FEEDBACK']._serialized_end=1379
+ _globals['_EXPRESSIONTIRED']._serialized_start=2652
+ _globals['_EXPRESSIONTIRED']._serialized_end=2791
+ _globals['_EXPRESSIONTIRED_REQUEST']._serialized_start=1211
+ _globals['_EXPRESSIONTIRED_REQUEST']._serialized_end=1269
+ _globals['_EXPRESSIONTIRED_FEEDBACK']._serialized_start=1319
+ _globals['_EXPRESSIONTIRED_FEEDBACK']._serialized_end=1379
+ _globals['_EXPRESSIONANGRY']._serialized_start=2794
+ _globals['_EXPRESSIONANGRY']._serialized_end=2933
+ _globals['_EXPRESSIONANGRY_REQUEST']._serialized_start=1211
+ _globals['_EXPRESSIONANGRY_REQUEST']._serialized_end=1269
+ _globals['_EXPRESSIONANGRY_FEEDBACK']._serialized_start=1319
+ _globals['_EXPRESSIONANGRY_FEEDBACK']._serialized_end=1379
+ _globals['_EXPRESSIONSADNESS']._serialized_start=2936
+ _globals['_EXPRESSIONSADNESS']._serialized_end=3077
+ _globals['_EXPRESSIONSADNESS_REQUEST']._serialized_start=1211
+ _globals['_EXPRESSIONSADNESS_REQUEST']._serialized_end=1269
+ _globals['_EXPRESSIONSADNESS_FEEDBACK']._serialized_start=1319
+ _globals['_EXPRESSIONSADNESS_FEEDBACK']._serialized_end=1379
+ _globals['_EXPRESSIONYAWN']._serialized_start=3080
+ _globals['_EXPRESSIONYAWN']._serialized_end=3218
+ _globals['_EXPRESSIONYAWN_REQUEST']._serialized_start=1211
+ _globals['_EXPRESSIONYAWN_REQUEST']._serialized_end=1269
+ _globals['_EXPRESSIONYAWN_FEEDBACK']._serialized_start=1319
+ _globals['_EXPRESSIONYAWN_FEEDBACK']._serialized_end=1379
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/biohead_command_pb2.pyi b/src/cmvr/api/biohead_command_pb2.pyi
new file mode 100644
index 0000000..a53f237
--- /dev/null
+++ b/src/cmvr/api/biohead_command_pb2.pyi
@@ -0,0 +1,308 @@
+from cmvr.api import common_pb2 as _common_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 FacialExpression(_message.Message):
+ __slots__ = ("eyebrow", "eyelid", "eyeball", "nose", "mouth", "jaw")
+ class Eyebrow(_message.Message):
+ __slots__ = ("left_outside_y", "left_inside_y", "right_outside_y", "right_inside_y")
+ LEFT_OUTSIDE_Y_FIELD_NUMBER: _ClassVar[int]
+ LEFT_INSIDE_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_OUTSIDE_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_INSIDE_Y_FIELD_NUMBER: _ClassVar[int]
+ left_outside_y: float
+ left_inside_y: float
+ right_outside_y: float
+ right_inside_y: float
+ def __init__(self, left_outside_y: _Optional[float] = ..., left_inside_y: _Optional[float] = ..., right_outside_y: _Optional[float] = ..., right_inside_y: _Optional[float] = ...) -> None: ...
+ class Eyelid(_message.Message):
+ __slots__ = ("left_upper_y", "left_lower_y", "right_upper_y", "right_lower_y")
+ LEFT_UPPER_Y_FIELD_NUMBER: _ClassVar[int]
+ LEFT_LOWER_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_UPPER_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_LOWER_Y_FIELD_NUMBER: _ClassVar[int]
+ left_upper_y: float
+ left_lower_y: float
+ right_upper_y: float
+ right_lower_y: float
+ def __init__(self, left_upper_y: _Optional[float] = ..., left_lower_y: _Optional[float] = ..., right_upper_y: _Optional[float] = ..., right_lower_y: _Optional[float] = ...) -> None: ...
+ class Eyeball(_message.Message):
+ __slots__ = ("left_x", "left_y", "right_x", "right_y")
+ LEFT_X_FIELD_NUMBER: _ClassVar[int]
+ LEFT_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_X_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_Y_FIELD_NUMBER: _ClassVar[int]
+ left_x: float
+ left_y: float
+ right_x: float
+ right_y: float
+ def __init__(self, left_x: _Optional[float] = ..., left_y: _Optional[float] = ..., right_x: _Optional[float] = ..., right_y: _Optional[float] = ...) -> None: ...
+ class Nose(_message.Message):
+ __slots__ = ("left_y", "right_y")
+ LEFT_Y_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_Y_FIELD_NUMBER: _ClassVar[int]
+ left_y: float
+ right_y: float
+ def __init__(self, left_y: _Optional[float] = ..., right_y: _Optional[float] = ...) -> None: ...
+ class Mouth(_message.Message):
+ __slots__ = ("upper_lip_y", "upper_lip_z", "lower_lip_y", "lower_lip_z", "left_lip", "right_lip")
+ class LeftLip(_message.Message):
+ __slots__ = ("upper_x", "upper_y", "corner_x", "corner_y", "lower_x", "lower_y")
+ UPPER_X_FIELD_NUMBER: _ClassVar[int]
+ UPPER_Y_FIELD_NUMBER: _ClassVar[int]
+ CORNER_X_FIELD_NUMBER: _ClassVar[int]
+ CORNER_Y_FIELD_NUMBER: _ClassVar[int]
+ LOWER_X_FIELD_NUMBER: _ClassVar[int]
+ LOWER_Y_FIELD_NUMBER: _ClassVar[int]
+ upper_x: float
+ upper_y: float
+ corner_x: float
+ corner_y: float
+ lower_x: float
+ lower_y: float
+ def __init__(self, upper_x: _Optional[float] = ..., upper_y: _Optional[float] = ..., corner_x: _Optional[float] = ..., corner_y: _Optional[float] = ..., lower_x: _Optional[float] = ..., lower_y: _Optional[float] = ...) -> None: ...
+ class RightLip(_message.Message):
+ __slots__ = ("upper_x", "upper_y", "corner_x", "corner_y", "lower_x", "lower_y")
+ UPPER_X_FIELD_NUMBER: _ClassVar[int]
+ UPPER_Y_FIELD_NUMBER: _ClassVar[int]
+ CORNER_X_FIELD_NUMBER: _ClassVar[int]
+ CORNER_Y_FIELD_NUMBER: _ClassVar[int]
+ LOWER_X_FIELD_NUMBER: _ClassVar[int]
+ LOWER_Y_FIELD_NUMBER: _ClassVar[int]
+ upper_x: float
+ upper_y: float
+ corner_x: float
+ corner_y: float
+ lower_x: float
+ lower_y: float
+ def __init__(self, upper_x: _Optional[float] = ..., upper_y: _Optional[float] = ..., corner_x: _Optional[float] = ..., corner_y: _Optional[float] = ..., lower_x: _Optional[float] = ..., lower_y: _Optional[float] = ...) -> None: ...
+ UPPER_LIP_Y_FIELD_NUMBER: _ClassVar[int]
+ UPPER_LIP_Z_FIELD_NUMBER: _ClassVar[int]
+ LOWER_LIP_Y_FIELD_NUMBER: _ClassVar[int]
+ LOWER_LIP_Z_FIELD_NUMBER: _ClassVar[int]
+ LEFT_LIP_FIELD_NUMBER: _ClassVar[int]
+ RIGHT_LIP_FIELD_NUMBER: _ClassVar[int]
+ upper_lip_y: float
+ upper_lip_z: float
+ lower_lip_y: float
+ lower_lip_z: float
+ left_lip: FacialExpression.Mouth.LeftLip
+ right_lip: FacialExpression.Mouth.RightLip
+ def __init__(self, upper_lip_y: _Optional[float] = ..., upper_lip_z: _Optional[float] = ..., lower_lip_y: _Optional[float] = ..., lower_lip_z: _Optional[float] = ..., left_lip: _Optional[_Union[FacialExpression.Mouth.LeftLip, _Mapping]] = ..., right_lip: _Optional[_Union[FacialExpression.Mouth.RightLip, _Mapping]] = ...) -> None: ...
+ class Jaw(_message.Message):
+ __slots__ = ("x", "y")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ...) -> None: ...
+ EYEBROW_FIELD_NUMBER: _ClassVar[int]
+ EYELID_FIELD_NUMBER: _ClassVar[int]
+ EYEBALL_FIELD_NUMBER: _ClassVar[int]
+ NOSE_FIELD_NUMBER: _ClassVar[int]
+ MOUTH_FIELD_NUMBER: _ClassVar[int]
+ JAW_FIELD_NUMBER: _ClassVar[int]
+ eyebrow: FacialExpression.Eyebrow
+ eyelid: FacialExpression.Eyelid
+ eyeball: FacialExpression.Eyeball
+ nose: FacialExpression.Nose
+ mouth: FacialExpression.Mouth
+ jaw: FacialExpression.Jaw
+ def __init__(self, eyebrow: _Optional[_Union[FacialExpression.Eyebrow, _Mapping]] = ..., eyelid: _Optional[_Union[FacialExpression.Eyelid, _Mapping]] = ..., eyeball: _Optional[_Union[FacialExpression.Eyeball, _Mapping]] = ..., nose: _Optional[_Union[FacialExpression.Nose, _Mapping]] = ..., mouth: _Optional[_Union[FacialExpression.Mouth, _Mapping]] = ..., jaw: _Optional[_Union[FacialExpression.Jaw, _Mapping]] = ...) -> None: ...
+
+class SetFacialExpression(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "expression")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EXPRESSION_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ expression: FacialExpression
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., expression: _Optional[_Union[FacialExpression, _Mapping]] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "execution_id", "execution_time_ms")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EXECUTION_ID_FIELD_NUMBER: _ClassVar[int]
+ EXECUTION_TIME_MS_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ execution_id: str
+ execution_time_ms: float
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., execution_id: _Optional[str] = ..., execution_time_ms: _Optional[float] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class StreamFacialExpression(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "expr", "eof")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EXPR_FIELD_NUMBER: _ClassVar[int]
+ EOF_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ expr: FacialExpression
+ eof: bool
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., expr: _Optional[_Union[FacialExpression, _Mapping]] = ..., eof: _Optional[bool] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "expr_diff")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EXPR_DIFF_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ expr_diff: FacialExpression
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., expr_diff: _Optional[_Union[FacialExpression, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetStatus(_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", "is_moving", "last_request_id", "current_positions", "camera_recording", "active_recording_id")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ IS_MOVING_FIELD_NUMBER: _ClassVar[int]
+ LAST_REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_POSITIONS_FIELD_NUMBER: _ClassVar[int]
+ CAMERA_RECORDING_FIELD_NUMBER: _ClassVar[int]
+ ACTIVE_RECORDING_ID_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ is_moving: bool
+ last_request_id: str
+ current_positions: _containers.RepeatedScalarFieldContainer[float]
+ camera_recording: bool
+ active_recording_id: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., is_moving: _Optional[bool] = ..., last_request_id: _Optional[str] = ..., current_positions: _Optional[_Iterable[float]] = ..., camera_recording: _Optional[bool] = ..., active_recording_id: _Optional[str] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class EmergencyStop(_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", "stopped_processes")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ STOPPED_PROCESSES_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ stopped_processes: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., stopped_processes: _Optional[str] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class SpeakStart(_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",)
+ 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 SpeakStop(_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",)
+ 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 Happy(_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",)
+ 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 Surprise(_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",)
+ 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 ExpressionTired(_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",)
+ 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 ExpressionAngry(_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",)
+ 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 ExpressionSadness(_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",)
+ 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 ExpressionYawn(_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",)
+ 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: ...
diff --git a/src/cmvr/api/biohead_command_pb2_grpc.py b/src/cmvr/api/biohead_command_pb2_grpc.py
new file mode 100644
index 0000000..dec6b6d
--- /dev/null
+++ b/src/cmvr/api/biohead_command_pb2_grpc.py
@@ -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/biohead_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}.'
+ )
diff --git a/src/cmvr/api/biohead_service_pb2.py b/src/cmvr/api/biohead_service_pb2.py
new file mode 100644
index 0000000..9d96131
--- /dev/null
+++ b/src/cmvr/api/biohead_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/biohead_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/biohead_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import biohead_command_pb2 as cmvr_dot_api_dot_biohead__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/biohead_service.proto\x12\x08\x63mvr.api\x1a\x1e\x63mvr/api/biohead_command.proto2\x94\x08\n\x0e\x42ioHeadService\x12`\n\rSetExpression\x12%.cmvr.api.SetFacialExpression.Request\x1a&.cmvr.api.SetFacialExpression.Feedback\"\x00\x12k\n\x10StreamExpression\x12(.cmvr.api.StreamFacialExpression.Request\x1a).cmvr.api.StreamFacialExpression.Feedback(\x01\x30\x01\x12N\n\x0fGetSystemStatus\x12\x1b.cmvr.api.GetStatus.Request\x1a\x1c.cmvr.api.GetStatus.Feedback\"\x00\x12T\n\rEmergencyStop\x12\x1f.cmvr.api.EmergencyStop.Request\x1a .cmvr.api.EmergencyStop.Feedback\"\x00\x12K\n\nSpeakStart\x12\x1c.cmvr.api.SpeakStart.Request\x1a\x1d.cmvr.api.SpeakStart.Feedback\"\x00\x12H\n\tSpeakStop\x12\x1b.cmvr.api.SpeakStop.Request\x1a\x1c.cmvr.api.SpeakStop.Feedback\"\x00\x12<\n\x05Happy\x12\x17.cmvr.api.Happy.Request\x1a\x18.cmvr.api.Happy.Feedback\"\x00\x12\x45\n\x08Surprise\x12\x1a.cmvr.api.Surprise.Request\x1a\x1b.cmvr.api.Surprise.Feedback\"\x00\x12Z\n\x0f\x45xpressionTired\x12!.cmvr.api.ExpressionTired.Request\x1a\".cmvr.api.ExpressionTired.Feedback\"\x00\x12Z\n\x0f\x45xpressionAngry\x12!.cmvr.api.ExpressionAngry.Request\x1a\".cmvr.api.ExpressionAngry.Feedback\"\x00\x12`\n\x11\x45xpressionSadness\x12#.cmvr.api.ExpressionSadness.Request\x1a$.cmvr.api.ExpressionSadness.Feedback\"\x00\x12W\n\x0e\x45xpressionYawn\x12 .cmvr.api.ExpressionYawn.Request\x1a!.cmvr.api.ExpressionYawn.Feedback\"\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.biohead_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_BIOHEADSERVICE']._serialized_start=77
+ _globals['_BIOHEADSERVICE']._serialized_end=1121
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/biohead_service_pb2.pyi b/src/cmvr/api/biohead_service_pb2.pyi
new file mode 100644
index 0000000..b20ed1e
--- /dev/null
+++ b/src/cmvr/api/biohead_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import biohead_command_pb2 as _biohead_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/biohead_service_pb2_grpc.py b/src/cmvr/api/biohead_service_pb2_grpc.py
new file mode 100644
index 0000000..41adafe
--- /dev/null
+++ b/src/cmvr/api/biohead_service_pb2_grpc.py
@@ -0,0 +1,581 @@
+# 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 biohead_command_pb2 as cmvr_dot_api_dot_biohead__command__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/biohead_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 BioHeadServiceStub:
+ """生物头部机器人服务接口
+ """
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.SetExpression = channel.unary_unary(
+ '/cmvr.api.BioHeadService/SetExpression',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Feedback.FromString,
+ _registered_method=True)
+ self.StreamExpression = channel.stream_stream(
+ '/cmvr.api.BioHeadService/StreamExpression',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Feedback.FromString,
+ _registered_method=True)
+ self.GetSystemStatus = channel.unary_unary(
+ '/cmvr.api.BioHeadService/GetSystemStatus',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.GetStatus.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.GetStatus.Feedback.FromString,
+ _registered_method=True)
+ self.EmergencyStop = channel.unary_unary(
+ '/cmvr.api.BioHeadService/EmergencyStop',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Feedback.FromString,
+ _registered_method=True)
+ self.SpeakStart = channel.unary_unary(
+ '/cmvr.api.BioHeadService/SpeakStart',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Feedback.FromString,
+ _registered_method=True)
+ self.SpeakStop = channel.unary_unary(
+ '/cmvr.api.BioHeadService/SpeakStop',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Feedback.FromString,
+ _registered_method=True)
+ self.Happy = channel.unary_unary(
+ '/cmvr.api.BioHeadService/Happy',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.Happy.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.Happy.Feedback.FromString,
+ _registered_method=True)
+ self.Surprise = channel.unary_unary(
+ '/cmvr.api.BioHeadService/Surprise',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.Surprise.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.Surprise.Feedback.FromString,
+ _registered_method=True)
+ self.ExpressionTired = channel.unary_unary(
+ '/cmvr.api.BioHeadService/ExpressionTired',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Feedback.FromString,
+ _registered_method=True)
+ self.ExpressionAngry = channel.unary_unary(
+ '/cmvr.api.BioHeadService/ExpressionAngry',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Feedback.FromString,
+ _registered_method=True)
+ self.ExpressionSadness = channel.unary_unary(
+ '/cmvr.api.BioHeadService/ExpressionSadness',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Feedback.FromString,
+ _registered_method=True)
+ self.ExpressionYawn = channel.unary_unary(
+ '/cmvr.api.BioHeadService/ExpressionYawn',
+ request_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Feedback.FromString,
+ _registered_method=True)
+
+
+class BioHeadServiceServicer:
+ """生物头部机器人服务接口
+ """
+
+ def SetExpression(self, request, context):
+ """设置面部表情
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StreamExpression(self, request_iterator, context):
+ """流式表情控制
+ rpc StreamExpression(StreamFacialExpression.Request) returns (StreamFacialExpression.Feedback){};
+
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetSystemStatus(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 SpeakStart(self, request, context):
+ """开始说话
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SpeakStop(self, request, context):
+ """停止说话
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def Happy(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def Surprise(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ExpressionTired(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ExpressionAngry(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ExpressionSadness(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ExpressionYawn(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_BioHeadServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'SetExpression': grpc.unary_unary_rpc_method_handler(
+ servicer.SetExpression,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Feedback.SerializeToString,
+ ),
+ 'StreamExpression': grpc.stream_stream_rpc_method_handler(
+ servicer.StreamExpression,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Feedback.SerializeToString,
+ ),
+ 'GetSystemStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetSystemStatus,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.GetStatus.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.GetStatus.Feedback.SerializeToString,
+ ),
+ 'EmergencyStop': grpc.unary_unary_rpc_method_handler(
+ servicer.EmergencyStop,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Feedback.SerializeToString,
+ ),
+ 'SpeakStart': grpc.unary_unary_rpc_method_handler(
+ servicer.SpeakStart,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Feedback.SerializeToString,
+ ),
+ 'SpeakStop': grpc.unary_unary_rpc_method_handler(
+ servicer.SpeakStop,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Feedback.SerializeToString,
+ ),
+ 'Happy': grpc.unary_unary_rpc_method_handler(
+ servicer.Happy,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.Happy.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.Happy.Feedback.SerializeToString,
+ ),
+ 'Surprise': grpc.unary_unary_rpc_method_handler(
+ servicer.Surprise,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.Surprise.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.Surprise.Feedback.SerializeToString,
+ ),
+ 'ExpressionTired': grpc.unary_unary_rpc_method_handler(
+ servicer.ExpressionTired,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Feedback.SerializeToString,
+ ),
+ 'ExpressionAngry': grpc.unary_unary_rpc_method_handler(
+ servicer.ExpressionAngry,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Feedback.SerializeToString,
+ ),
+ 'ExpressionSadness': grpc.unary_unary_rpc_method_handler(
+ servicer.ExpressionSadness,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Feedback.SerializeToString,
+ ),
+ 'ExpressionYawn': grpc.unary_unary_rpc_method_handler(
+ servicer.ExpressionYawn,
+ request_deserializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.BioHeadService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.BioHeadService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class BioHeadService:
+ """生物头部机器人服务接口
+ """
+
+ @staticmethod
+ def SetExpression(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.BioHeadService/SetExpression',
+ cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.SetFacialExpression.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StreamExpression(request_iterator,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.stream_stream(
+ request_iterator,
+ target,
+ '/cmvr.api.BioHeadService/StreamExpression',
+ cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.StreamFacialExpression.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetSystemStatus(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.BioHeadService/GetSystemStatus',
+ cmvr_dot_api_dot_biohead__command__pb2.GetStatus.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.GetStatus.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.BioHeadService/EmergencyStop',
+ cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.EmergencyStop.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SpeakStart(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.BioHeadService/SpeakStart',
+ cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.SpeakStart.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SpeakStop(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.BioHeadService/SpeakStop',
+ cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.SpeakStop.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def Happy(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.BioHeadService/Happy',
+ cmvr_dot_api_dot_biohead__command__pb2.Happy.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.Happy.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def Surprise(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.BioHeadService/Surprise',
+ cmvr_dot_api_dot_biohead__command__pb2.Surprise.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.Surprise.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ExpressionTired(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.BioHeadService/ExpressionTired',
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionTired.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ExpressionAngry(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.BioHeadService/ExpressionAngry',
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionAngry.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ExpressionSadness(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.BioHeadService/ExpressionSadness',
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionSadness.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ExpressionYawn(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.BioHeadService/ExpressionYawn',
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Request.SerializeToString,
+ cmvr_dot_api_dot_biohead__command__pb2.ExpressionYawn.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/camera_command_pb2.py b/src/cmvr/api/camera_command_pb2.py
new file mode 100644
index 0000000..2b194f2
--- /dev/null
+++ b/src/cmvr/api/camera_command_pb2.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/camera_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/camera_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x63mvr/api/camera_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"\xd7\x01\n\tFrameData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\r\n\x05width\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x05\x12+\n\x04type\x18\x04 \x01(\x0e\x32\x1d.cmvr.api.FrameData.FrameType\x12\r\n\x05\x63odec\x18\x05 \x01(\t\x12\x14\n\x0cis_key_frame\x18\x06 \x01(\x08\"K\n\tFrameType\x12\x08\n\x04U8C1\x10\x00\x12\t\n\x05U16C1\x10\x01\x12\x08\n\x04U8C3\x10\x02\x12\t\n\x05U16C3\x10\x03\x12\t\n\x05\x46\x31\x36\x43\x31\x10\x04\x12\t\n\x05\x46\x33\x32\x43\x31\x10\x05\"V\n\x10\x43\x61meraIntrinsics\x12\n\n\x02\x63x\x18\x01 \x01(\x02\x12\n\n\x02\x63y\x18\x02 \x01(\x02\x12\n\n\x02\x66x\x18\x03 \x01(\x02\x12\n\n\x02\x66y\x18\x04 \x01(\x02\x12\x12\n\x06\x63oeffs\x18\x05 \x03(\x02\x42\x02\x10\x01\"\xb9\x01\n\x0b\x43\x61meraState\x12\x16\n\x0eis_initialized\x18\x01 \x01(\x08\x12\x11\n\tis_opened\x18\x02 \x01(\x08\x12\x14\n\x0cis_streaming\x18\x03 \x01(\x08\x12\x14\n\x0cis_recording\x18\x04 \x01(\x08\x12\x10\n\x08is_error\x18\x05 \x01(\x08\x12\x15\n\rerror_message\x18\x06 \x01(\t\x12\x0b\n\x03\x66ps\x18\x07 \x01(\x05\x12\r\n\x05width\x18\x08 \x01(\x05\x12\x0e\n\x06height\x18\t \x01(\x05\"\xb7\x01\n\x15GetCameraStateCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x62\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\x15.cmvr.api.CameraState\"\x8e\x01\n\x12StartCameraCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8d\x01\n\x11StopCameraCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xe9\x01\n\x12GetRGBImageCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x96\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x63olor_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x03 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\"\xeb\x01\n\x14GetDepthImageCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x96\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x64\x65pth_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x03 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\"\x95\x02\n\x14GetRGBDImagesCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\xc0\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x63olor_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12(\n\x0b\x64\x65pth_frame\x18\x03 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x04 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\"\xab\x01\n\x1bStartCameraRecordingCommand\x1aN\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x12\n\nvideo_path\x18\x02 \x01(\t\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x96\x01\n\x1aStopCameraRecordingCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8c\x02\n\x18GetRGBImageStreamCommand\x1aG\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x0b\n\x03\x65of\x18\x02 \x01(\x08\x1a\xa6\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x63olor_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x03 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\x12\x0e\n\x06seq_no\x18\x04 \x01(\x05\"\x8e\x02\n\x1aGetDepthImageStreamCommand\x1aG\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x0b\n\x03\x65of\x18\x02 \x01(\x08\x1a\xa6\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x64\x65pth_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x03 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\x12\x0e\n\x06seq_no\x18\x04 \x01(\x05\"\xb8\x02\n\x1aGetRGBDImagesStreamCommand\x1aG\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x0b\n\x03\x65of\x18\x02 \x01(\x08\x1a\xd0\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12(\n\x0b\x63olor_frame\x18\x02 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12(\n\x0b\x64\x65pth_frame\x18\x03 \x01(\x0b\x32\x13.cmvr.api.FrameData\x12.\n\nintrinsics\x18\x04 \x01(\x0b\x32\x1a.cmvr.api.CameraIntrinsics\x12\x0e\n\x06seq_no\x18\x05 \x01(\x05\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.camera_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CAMERAINTRINSICS'].fields_by_name['coeffs']._loaded_options = None
+ _globals['_CAMERAINTRINSICS'].fields_by_name['coeffs']._serialized_options = b'\020\001'
+ _globals['_FRAMEDATA']._serialized_start=67
+ _globals['_FRAMEDATA']._serialized_end=282
+ _globals['_FRAMEDATA_FRAMETYPE']._serialized_start=207
+ _globals['_FRAMEDATA_FRAMETYPE']._serialized_end=282
+ _globals['_CAMERAINTRINSICS']._serialized_start=284
+ _globals['_CAMERAINTRINSICS']._serialized_end=370
+ _globals['_CAMERASTATE']._serialized_start=373
+ _globals['_CAMERASTATE']._serialized_end=558
+ _globals['_GETCAMERASTATECOMMAND']._serialized_start=561
+ _globals['_GETCAMERASTATECOMMAND']._serialized_end=744
+ _globals['_GETCAMERASTATECOMMAND_REQUEST']._serialized_start=586
+ _globals['_GETCAMERASTATECOMMAND_REQUEST']._serialized_end=644
+ _globals['_GETCAMERASTATECOMMAND_FEEDBACK']._serialized_start=646
+ _globals['_GETCAMERASTATECOMMAND_FEEDBACK']._serialized_end=744
+ _globals['_STARTCAMERACOMMAND']._serialized_start=747
+ _globals['_STARTCAMERACOMMAND']._serialized_end=889
+ _globals['_STARTCAMERACOMMAND_REQUEST']._serialized_start=586
+ _globals['_STARTCAMERACOMMAND_REQUEST']._serialized_end=644
+ _globals['_STARTCAMERACOMMAND_FEEDBACK']._serialized_start=646
+ _globals['_STARTCAMERACOMMAND_FEEDBACK']._serialized_end=706
+ _globals['_STOPCAMERACOMMAND']._serialized_start=892
+ _globals['_STOPCAMERACOMMAND']._serialized_end=1033
+ _globals['_STOPCAMERACOMMAND_REQUEST']._serialized_start=586
+ _globals['_STOPCAMERACOMMAND_REQUEST']._serialized_end=644
+ _globals['_STOPCAMERACOMMAND_FEEDBACK']._serialized_start=646
+ _globals['_STOPCAMERACOMMAND_FEEDBACK']._serialized_end=706
+ _globals['_GETRGBIMAGECOMMAND']._serialized_start=1036
+ _globals['_GETRGBIMAGECOMMAND']._serialized_end=1269
+ _globals['_GETRGBIMAGECOMMAND_REQUEST']._serialized_start=586
+ _globals['_GETRGBIMAGECOMMAND_REQUEST']._serialized_end=644
+ _globals['_GETRGBIMAGECOMMAND_FEEDBACK']._serialized_start=1119
+ _globals['_GETRGBIMAGECOMMAND_FEEDBACK']._serialized_end=1269
+ _globals['_GETDEPTHIMAGECOMMAND']._serialized_start=1272
+ _globals['_GETDEPTHIMAGECOMMAND']._serialized_end=1507
+ _globals['_GETDEPTHIMAGECOMMAND_REQUEST']._serialized_start=586
+ _globals['_GETDEPTHIMAGECOMMAND_REQUEST']._serialized_end=644
+ _globals['_GETDEPTHIMAGECOMMAND_FEEDBACK']._serialized_start=1357
+ _globals['_GETDEPTHIMAGECOMMAND_FEEDBACK']._serialized_end=1507
+ _globals['_GETRGBDIMAGESCOMMAND']._serialized_start=1510
+ _globals['_GETRGBDIMAGESCOMMAND']._serialized_end=1787
+ _globals['_GETRGBDIMAGESCOMMAND_REQUEST']._serialized_start=586
+ _globals['_GETRGBDIMAGESCOMMAND_REQUEST']._serialized_end=644
+ _globals['_GETRGBDIMAGESCOMMAND_FEEDBACK']._serialized_start=1595
+ _globals['_GETRGBDIMAGESCOMMAND_FEEDBACK']._serialized_end=1787
+ _globals['_STARTCAMERARECORDINGCOMMAND']._serialized_start=1790
+ _globals['_STARTCAMERARECORDINGCOMMAND']._serialized_end=1961
+ _globals['_STARTCAMERARECORDINGCOMMAND_REQUEST']._serialized_start=1821
+ _globals['_STARTCAMERARECORDINGCOMMAND_REQUEST']._serialized_end=1899
+ _globals['_STARTCAMERARECORDINGCOMMAND_FEEDBACK']._serialized_start=646
+ _globals['_STARTCAMERARECORDINGCOMMAND_FEEDBACK']._serialized_end=706
+ _globals['_STOPCAMERARECORDINGCOMMAND']._serialized_start=1964
+ _globals['_STOPCAMERARECORDINGCOMMAND']._serialized_end=2114
+ _globals['_STOPCAMERARECORDINGCOMMAND_REQUEST']._serialized_start=586
+ _globals['_STOPCAMERARECORDINGCOMMAND_REQUEST']._serialized_end=644
+ _globals['_STOPCAMERARECORDINGCOMMAND_FEEDBACK']._serialized_start=646
+ _globals['_STOPCAMERARECORDINGCOMMAND_FEEDBACK']._serialized_end=706
+ _globals['_GETRGBIMAGESTREAMCOMMAND']._serialized_start=2117
+ _globals['_GETRGBIMAGESTREAMCOMMAND']._serialized_end=2385
+ _globals['_GETRGBIMAGESTREAMCOMMAND_REQUEST']._serialized_start=2145
+ _globals['_GETRGBIMAGESTREAMCOMMAND_REQUEST']._serialized_end=2216
+ _globals['_GETRGBIMAGESTREAMCOMMAND_FEEDBACK']._serialized_start=2219
+ _globals['_GETRGBIMAGESTREAMCOMMAND_FEEDBACK']._serialized_end=2385
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND']._serialized_start=2388
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND']._serialized_end=2658
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND_REQUEST']._serialized_start=2145
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND_REQUEST']._serialized_end=2216
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND_FEEDBACK']._serialized_start=2492
+ _globals['_GETDEPTHIMAGESTREAMCOMMAND_FEEDBACK']._serialized_end=2658
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND']._serialized_start=2661
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND']._serialized_end=2973
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND_REQUEST']._serialized_start=2145
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND_REQUEST']._serialized_end=2216
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND_FEEDBACK']._serialized_start=2765
+ _globals['_GETRGBDIMAGESSTREAMCOMMAND_FEEDBACK']._serialized_end=2973
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/camera_command_pb2.pyi b/src/cmvr/api/camera_command_pb2.pyi
new file mode 100644
index 0000000..2a3bc1f
--- /dev/null
+++ b/src/cmvr/api/camera_command_pb2.pyi
@@ -0,0 +1,273 @@
+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 FrameData(_message.Message):
+ __slots__ = ("data", "width", "height", "type", "codec", "is_key_frame")
+ class FrameType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ U8C1: _ClassVar[FrameData.FrameType]
+ U16C1: _ClassVar[FrameData.FrameType]
+ U8C3: _ClassVar[FrameData.FrameType]
+ U16C3: _ClassVar[FrameData.FrameType]
+ F16C1: _ClassVar[FrameData.FrameType]
+ F32C1: _ClassVar[FrameData.FrameType]
+ U8C1: FrameData.FrameType
+ U16C1: FrameData.FrameType
+ U8C3: FrameData.FrameType
+ U16C3: FrameData.FrameType
+ F16C1: FrameData.FrameType
+ F32C1: FrameData.FrameType
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ CODEC_FIELD_NUMBER: _ClassVar[int]
+ IS_KEY_FRAME_FIELD_NUMBER: _ClassVar[int]
+ data: bytes
+ width: int
+ height: int
+ type: FrameData.FrameType
+ codec: str
+ is_key_frame: bool
+ def __init__(self, data: _Optional[bytes] = ..., width: _Optional[int] = ..., height: _Optional[int] = ..., type: _Optional[_Union[FrameData.FrameType, str]] = ..., codec: _Optional[str] = ..., is_key_frame: _Optional[bool] = ...) -> None: ...
+
+class CameraIntrinsics(_message.Message):
+ __slots__ = ("cx", "cy", "fx", "fy", "coeffs")
+ CX_FIELD_NUMBER: _ClassVar[int]
+ CY_FIELD_NUMBER: _ClassVar[int]
+ FX_FIELD_NUMBER: _ClassVar[int]
+ FY_FIELD_NUMBER: _ClassVar[int]
+ COEFFS_FIELD_NUMBER: _ClassVar[int]
+ cx: float
+ cy: float
+ fx: float
+ fy: float
+ coeffs: _containers.RepeatedScalarFieldContainer[float]
+ def __init__(self, cx: _Optional[float] = ..., cy: _Optional[float] = ..., fx: _Optional[float] = ..., fy: _Optional[float] = ..., coeffs: _Optional[_Iterable[float]] = ...) -> None: ...
+
+class CameraState(_message.Message):
+ __slots__ = ("is_initialized", "is_opened", "is_streaming", "is_recording", "is_error", "error_message", "fps", "width", "height")
+ IS_INITIALIZED_FIELD_NUMBER: _ClassVar[int]
+ IS_OPENED_FIELD_NUMBER: _ClassVar[int]
+ IS_STREAMING_FIELD_NUMBER: _ClassVar[int]
+ IS_RECORDING_FIELD_NUMBER: _ClassVar[int]
+ IS_ERROR_FIELD_NUMBER: _ClassVar[int]
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ FPS_FIELD_NUMBER: _ClassVar[int]
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ is_initialized: bool
+ is_opened: bool
+ is_streaming: bool
+ is_recording: bool
+ is_error: bool
+ error_message: str
+ fps: int
+ width: int
+ height: int
+ def __init__(self, is_initialized: _Optional[bool] = ..., is_opened: _Optional[bool] = ..., is_streaming: _Optional[bool] = ..., is_recording: _Optional[bool] = ..., is_error: _Optional[bool] = ..., error_message: _Optional[str] = ..., fps: _Optional[int] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ...
+
+class GetCameraStateCommand(_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: CameraState
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[CameraState, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class StartCameraCommand(_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",)
+ 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 StopCameraCommand(_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",)
+ 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 GetRGBImageCommand(_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", "color_frame", "intrinsics")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ COLOR_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ color_frame: FrameData
+ intrinsics: CameraIntrinsics
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., color_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetDepthImageCommand(_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", "depth_frame", "intrinsics")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ DEPTH_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ depth_frame: FrameData
+ intrinsics: CameraIntrinsics
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., depth_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetRGBDImagesCommand(_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", "color_frame", "depth_frame", "intrinsics")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ COLOR_FRAME_FIELD_NUMBER: _ClassVar[int]
+ DEPTH_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ color_frame: FrameData
+ depth_frame: FrameData
+ intrinsics: CameraIntrinsics
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., color_frame: _Optional[_Union[FrameData, _Mapping]] = ..., depth_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class StartCameraRecordingCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "video_path")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VIDEO_PATH_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ video_path: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., video_path: _Optional[str] = ...) -> 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 StopCameraRecordingCommand(_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",)
+ 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 GetRGBImageStreamCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "eof")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EOF_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ eof: bool
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., eof: _Optional[bool] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "color_frame", "intrinsics", "seq_no")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ COLOR_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ SEQ_NO_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ color_frame: FrameData
+ intrinsics: CameraIntrinsics
+ seq_no: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., color_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ..., seq_no: _Optional[int] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetDepthImageStreamCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "eof")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EOF_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ eof: bool
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., eof: _Optional[bool] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "depth_frame", "intrinsics", "seq_no")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ DEPTH_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ SEQ_NO_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ depth_frame: FrameData
+ intrinsics: CameraIntrinsics
+ seq_no: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., depth_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ..., seq_no: _Optional[int] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetRGBDImagesStreamCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "eof")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ EOF_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ eof: bool
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., eof: _Optional[bool] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "color_frame", "depth_frame", "intrinsics", "seq_no")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ COLOR_FRAME_FIELD_NUMBER: _ClassVar[int]
+ DEPTH_FRAME_FIELD_NUMBER: _ClassVar[int]
+ INTRINSICS_FIELD_NUMBER: _ClassVar[int]
+ SEQ_NO_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ color_frame: FrameData
+ depth_frame: FrameData
+ intrinsics: CameraIntrinsics
+ seq_no: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., color_frame: _Optional[_Union[FrameData, _Mapping]] = ..., depth_frame: _Optional[_Union[FrameData, _Mapping]] = ..., intrinsics: _Optional[_Union[CameraIntrinsics, _Mapping]] = ..., seq_no: _Optional[int] = ...) -> None: ...
+ def __init__(self) -> None: ...
diff --git a/src/cmvr/api/camera_command_pb2_grpc.py b/src/cmvr/api/camera_command_pb2_grpc.py
new file mode 100644
index 0000000..3be0c2d
--- /dev/null
+++ b/src/cmvr/api/camera_command_pb2_grpc.py
@@ -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/camera_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}.'
+ )
diff --git a/src/cmvr/api/camera_service_pb2.py b/src/cmvr/api/camera_service_pb2.py
new file mode 100644
index 0000000..39b45c2
--- /dev/null
+++ b/src/cmvr/api/camera_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/camera_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/camera_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import camera_command_pb2 as cmvr_dot_api_dot_camera__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x63mvr/api/camera_service.proto\x12\x08\x63mvr.api\x1a\x1d\x63mvr/api/camera_command.proto2\x9b\t\n\rCameraService\x12`\n\tGetStatus\x12\'.cmvr.api.GetCameraStateCommand.Request\x1a(.cmvr.api.GetCameraStateCommand.Feedback\"\x00\x12\\\n\x0bStartCamera\x12$.cmvr.api.StartCameraCommand.Request\x1a%.cmvr.api.StartCameraCommand.Feedback\"\x00\x12Y\n\nStopCamera\x12#.cmvr.api.StopCameraCommand.Request\x1a$.cmvr.api.StopCameraCommand.Feedback\"\x00\x12\\\n\x0bGetRGBImage\x12$.cmvr.api.GetRGBImageCommand.Request\x1a%.cmvr.api.GetRGBImageCommand.Feedback\"\x00\x12\x62\n\rGetDepthImage\x12&.cmvr.api.GetDepthImageCommand.Request\x1a\'.cmvr.api.GetDepthImageCommand.Feedback\"\x00\x12\x62\n\rGetRGBDImages\x12&.cmvr.api.GetRGBDImagesCommand.Request\x1a\'.cmvr.api.GetRGBDImagesCommand.Feedback\"\x00\x12q\n\x0eStartRecording\x12-.cmvr.api.StartCameraRecordingCommand.Request\x1a..cmvr.api.StartCameraRecordingCommand.Feedback\"\x00\x12n\n\rStopRecording\x12,.cmvr.api.StopCameraRecordingCommand.Request\x1a-.cmvr.api.StopCameraRecordingCommand.Feedback\"\x00\x12r\n\x11GetRGBImageStream\x12*.cmvr.api.GetRGBImageStreamCommand.Request\x1a+.cmvr.api.GetRGBImageStreamCommand.Feedback\"\x00(\x01\x30\x01\x12x\n\x13GetDepthImageStream\x12,.cmvr.api.GetDepthImageStreamCommand.Request\x1a-.cmvr.api.GetDepthImageStreamCommand.Feedback\"\x00(\x01\x30\x01\x12x\n\x13GetRGBDImagesStream\x12,.cmvr.api.GetRGBDImagesStreamCommand.Request\x1a-.cmvr.api.GetRGBDImagesStreamCommand.Feedback\"\x00(\x01\x30\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.camera_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CAMERASERVICE']._serialized_start=75
+ _globals['_CAMERASERVICE']._serialized_end=1254
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/camera_service_pb2.pyi b/src/cmvr/api/camera_service_pb2.pyi
new file mode 100644
index 0000000..5496655
--- /dev/null
+++ b/src/cmvr/api/camera_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import camera_command_pb2 as _camera_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/camera_service_pb2_grpc.py b/src/cmvr/api/camera_service_pb2_grpc.py
new file mode 100644
index 0000000..cb89e35
--- /dev/null
+++ b/src/cmvr/api/camera_service_pb2_grpc.py
@@ -0,0 +1,527 @@
+# 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 camera_command_pb2 as cmvr_dot_api_dot_camera__command__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/camera_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 CameraServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetStatus = channel.unary_unary(
+ '/cmvr.api.CameraService/GetStatus',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StartCamera = channel.unary_unary(
+ '/cmvr.api.CameraService/StartCamera',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StopCamera = channel.unary_unary(
+ '/cmvr.api.CameraService/StopCamera',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetRGBImage = channel.unary_unary(
+ '/cmvr.api.CameraService/GetRGBImage',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetDepthImage = channel.unary_unary(
+ '/cmvr.api.CameraService/GetDepthImage',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetRGBDImages = channel.unary_unary(
+ '/cmvr.api.CameraService/GetRGBDImages',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StartRecording = channel.unary_unary(
+ '/cmvr.api.CameraService/StartRecording',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StopRecording = channel.unary_unary(
+ '/cmvr.api.CameraService/StopRecording',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetRGBImageStream = channel.stream_stream(
+ '/cmvr.api.CameraService/GetRGBImageStream',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetDepthImageStream = channel.stream_stream(
+ '/cmvr.api.CameraService/GetDepthImageStream',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetRGBDImagesStream = channel.stream_stream(
+ '/cmvr.api.CameraService/GetRGBDImagesStream',
+ request_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Feedback.FromString,
+ _registered_method=True)
+
+
+class CameraServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def GetStatus(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StartCamera(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StopCamera(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRGBImage(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetDepthImage(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRGBDImages(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StartRecording(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StopRecording(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRGBImageStream(self, request_iterator, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetDepthImageStream(self, request_iterator, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRGBDImagesStream(self, request_iterator, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_CameraServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetStatus,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Feedback.SerializeToString,
+ ),
+ 'StartCamera': grpc.unary_unary_rpc_method_handler(
+ servicer.StartCamera,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Feedback.SerializeToString,
+ ),
+ 'StopCamera': grpc.unary_unary_rpc_method_handler(
+ servicer.StopCamera,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Feedback.SerializeToString,
+ ),
+ 'GetRGBImage': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRGBImage,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Feedback.SerializeToString,
+ ),
+ 'GetDepthImage': grpc.unary_unary_rpc_method_handler(
+ servicer.GetDepthImage,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Feedback.SerializeToString,
+ ),
+ 'GetRGBDImages': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRGBDImages,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Feedback.SerializeToString,
+ ),
+ 'StartRecording': grpc.unary_unary_rpc_method_handler(
+ servicer.StartRecording,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'StopRecording': grpc.unary_unary_rpc_method_handler(
+ servicer.StopRecording,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'GetRGBImageStream': grpc.stream_stream_rpc_method_handler(
+ servicer.GetRGBImageStream,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Feedback.SerializeToString,
+ ),
+ 'GetDepthImageStream': grpc.stream_stream_rpc_method_handler(
+ servicer.GetDepthImageStream,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Feedback.SerializeToString,
+ ),
+ 'GetRGBDImagesStream': grpc.stream_stream_rpc_method_handler(
+ servicer.GetRGBDImagesStream,
+ request_deserializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.CameraService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.CameraService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class CameraService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetStatus(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.CameraService/GetStatus',
+ cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetCameraStateCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StartCamera(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.CameraService/StartCamera',
+ cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.StartCameraCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StopCamera(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.CameraService/StopCamera',
+ cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.StopCameraCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRGBImage(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.CameraService/GetRGBImage',
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBImageCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetDepthImage(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.CameraService/GetDepthImage',
+ cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetDepthImageCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRGBDImages(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.CameraService/GetRGBDImages',
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StartRecording(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.CameraService/StartRecording',
+ cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.StartCameraRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StopRecording(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.CameraService/StopRecording',
+ cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.StopCameraRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRGBImageStream(request_iterator,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.stream_stream(
+ request_iterator,
+ target,
+ '/cmvr.api.CameraService/GetRGBImageStream',
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBImageStreamCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetDepthImageStream(request_iterator,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.stream_stream(
+ request_iterator,
+ target,
+ '/cmvr.api.CameraService/GetDepthImageStream',
+ cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetDepthImageStreamCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRGBDImagesStream(request_iterator,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.stream_stream(
+ request_iterator,
+ target,
+ '/cmvr.api.CameraService/GetRGBDImagesStream',
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_camera__command__pb2.GetRGBDImagesStreamCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/common_pb2.py b/src/cmvr/api/common_pb2.py
new file mode 100644
index 0000000..261a618
--- /dev/null
+++ b/src/cmvr/api/common_pb2.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/common.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/common.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63mvr/api/common.proto\x12\x08\x63mvr.api\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb8\x01\n\x0f\x44\x65viceLifecycle\x12\x32\n\x05state\x18\x01 \x01(\x0e\x32#.cmvr.api.DeviceLifecycle.Lifecycle\"q\n\tLifecycle\x12\x0e\n\nSTATE_INIT\x10\x00\x12\x0f\n\x0bSTATE_READY\x10\x01\x12\x11\n\rSTATE_RUNNING\x10\x02\x12\x0f\n\x0bSTATE_ERROR\x10\x03\x12\x0f\n\x0bSTATE_ESTOP\x10\x04\x12\x0e\n\nSTATE_STOP\x10\x05\"\xbf\x01\n\rCommandHeader\x1aK\n\x07Request\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a\x61\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa2\x01\n\x0b\x43onfigParam\x12\x12\n\nparam_name\x18\x01 \x01(\t\x12\x13\n\tint_value\x18\x02 \x01(\x05H\x00\x12\x16\n\x0c\x64ouble_value\x18\x03 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x42\r\n\x0bparam_valueb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.common_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_DEVICELIFECYCLE']._serialized_start=69
+ _globals['_DEVICELIFECYCLE']._serialized_end=253
+ _globals['_DEVICELIFECYCLE_LIFECYCLE']._serialized_start=140
+ _globals['_DEVICELIFECYCLE_LIFECYCLE']._serialized_end=253
+ _globals['_COMMANDHEADER']._serialized_start=256
+ _globals['_COMMANDHEADER']._serialized_end=447
+ _globals['_COMMANDHEADER_REQUEST']._serialized_start=273
+ _globals['_COMMANDHEADER_REQUEST']._serialized_end=348
+ _globals['_COMMANDHEADER_FEEDBACK']._serialized_start=350
+ _globals['_COMMANDHEADER_FEEDBACK']._serialized_end=447
+ _globals['_CONFIGPARAM']._serialized_start=450
+ _globals['_CONFIGPARAM']._serialized_end=612
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/common_pb2.pyi b/src/cmvr/api/common_pb2.pyi
new file mode 100644
index 0000000..183eec5
--- /dev/null
+++ b/src/cmvr/api/common_pb2.pyi
@@ -0,0 +1,66 @@
+import datetime
+
+from google.protobuf import timestamp_pb2 as _timestamp_pb2
+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 Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class DeviceLifecycle(_message.Message):
+ __slots__ = ("state",)
+ class Lifecycle(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ STATE_INIT: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_READY: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_RUNNING: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_ERROR: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_ESTOP: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_STOP: _ClassVar[DeviceLifecycle.Lifecycle]
+ STATE_INIT: DeviceLifecycle.Lifecycle
+ STATE_READY: DeviceLifecycle.Lifecycle
+ STATE_RUNNING: DeviceLifecycle.Lifecycle
+ STATE_ERROR: DeviceLifecycle.Lifecycle
+ STATE_ESTOP: DeviceLifecycle.Lifecycle
+ STATE_STOP: DeviceLifecycle.Lifecycle
+ STATE_FIELD_NUMBER: _ClassVar[int]
+ state: DeviceLifecycle.Lifecycle
+ def __init__(self, state: _Optional[_Union[DeviceLifecycle.Lifecycle, str]] = ...) -> None: ...
+
+class CommandHeader(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("device_id", "timestamp")
+ DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ device_id: str
+ timestamp: _timestamp_pb2.Timestamp
+ def __init__(self, device_id: _Optional[str] = ..., timestamp: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("success", "error_message", "timestamp")
+ SUCCESS_FIELD_NUMBER: _ClassVar[int]
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ success: bool
+ error_message: str
+ timestamp: _timestamp_pb2.Timestamp
+ def __init__(self, success: _Optional[bool] = ..., error_message: _Optional[str] = ..., timestamp: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class ConfigParam(_message.Message):
+ __slots__ = ("param_name", "int_value", "double_value", "string_value", "bool_value", "bytes_value")
+ PARAM_NAME_FIELD_NUMBER: _ClassVar[int]
+ INT_VALUE_FIELD_NUMBER: _ClassVar[int]
+ DOUBLE_VALUE_FIELD_NUMBER: _ClassVar[int]
+ STRING_VALUE_FIELD_NUMBER: _ClassVar[int]
+ BOOL_VALUE_FIELD_NUMBER: _ClassVar[int]
+ BYTES_VALUE_FIELD_NUMBER: _ClassVar[int]
+ param_name: str
+ int_value: int
+ double_value: float
+ string_value: str
+ bool_value: bool
+ bytes_value: bytes
+ def __init__(self, param_name: _Optional[str] = ..., int_value: _Optional[int] = ..., double_value: _Optional[float] = ..., string_value: _Optional[str] = ..., bool_value: _Optional[bool] = ..., bytes_value: _Optional[bytes] = ...) -> None: ...
diff --git a/src/cmvr/api/common_pb2_grpc.py b/src/cmvr/api/common_pb2_grpc.py
new file mode 100644
index 0000000..8c1ed82
--- /dev/null
+++ b/src/cmvr/api/common_pb2_grpc.py
@@ -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/common_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}.'
+ )
diff --git a/src/cmvr/api/dexhand_command_pb2.py b/src/cmvr/api/dexhand_command_pb2.py
new file mode 100644
index 0000000..67c3e88
--- /dev/null
+++ b/src/cmvr/api/dexhand_command_pb2.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/dexhand_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/dexhand_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/dexhand_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\")\n\x0c\x46reedomValue\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x02\"\xa9\x01\n\x0c\x46reedomState\x12\x0e\n\x06\x64of_id\x18\x01 \x01(\x05\x12\r\n\x05\x61ngle\x18\x02 \x01(\x05\x12\r\n\x05speed\x18\x03 \x01(\x05\x12\r\n\x05\x66orce\x18\x04 \x01(\x05\x12\x10\n\x08position\x18\x05 \x01(\x05\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x05\x12\x13\n\x0btemperature\x18\x07 \x01(\x05\x12\r\n\x05\x65rror\x18\x08 \x01(\x05\x12\x15\n\rerror_message\x18\t \x03(\t\"\x90\x03\n\nSensorData\x12\x34\n\x0b\x66inger_type\x18\x04 \x01(\x0e\x32\x1f.cmvr.api.SensorData.FingerType\x12\x30\n\tpart_type\x18\x05 \x01(\x0e\x32\x1d.cmvr.api.SensorData.PartType\x12\x13\n\x0bsensor_name\x18\x06 \x01(\t\x12*\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x1c.cmvr.api.SensorData.RowData\x12\x0c\n\x04rows\x18\x02 \x01(\x05\x12\x0c\n\x04\x63ols\x18\x03 \x01(\x05\x1a\x1d\n\x07RowData\x12\x12\n\x06values\x18\x01 \x03(\x05\x42\x02\x10\x01\"T\n\nFingerType\x12\t\n\x05PINKY\x10\x00\x12\x08\n\x04RING\x10\x01\x12\x11\n\rMIDDLE_FINGER\x10\x02\x12\t\n\x05INDEX\x10\x03\x12\t\n\x05THUMB\x10\x04\x12\x08\n\x04PALM\x10\x05\"H\n\x08PartType\x12\x07\n\x03TIP\x10\x00\x12\n\n\x06\x46INGER\x10\x01\x12\x07\n\x03PAD\x10\x02\x12\x10\n\x0cTHUMB_MIDDLE\x10\x03\x12\x0c\n\x08PALM_PAD\x10\x04\"M\n\x0c\x44\x65xHandState\x12\x16\n\x0eis_initialized\x18\x01 \x01(\x08\x12%\n\x05hands\x18\x02 \x03(\x0b\x32\x16.cmvr.api.FreedomState\"\xb9\x01\n\x16GetDexHandStateCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x63\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\x16.cmvr.api.DexHandState\"\xbe\x01\n\x1aSetDexHandPositionsCommand\x1a\x62\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12&\n\x06values\x18\x02 \x03(\x0b\x32\x16.cmvr.api.FreedomValue\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\x17SetDexHandAnglesCommand\x1a\x62\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12&\n\x06values\x18\x02 \x03(\x0b\x32\x16.cmvr.api.FreedomValue\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xba\x01\n\x16SetDexHandForceCommand\x1a\x62\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12&\n\x06values\x18\x02 \x03(\x0b\x32\x16.cmvr.api.FreedomValue\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xba\x01\n\x16SetDexHandSpeedCommand\x1a\x62\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12&\n\x06values\x18\x02 \x03(\x0b\x32\x16.cmvr.api.FreedomValue\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xab\x01\n\x1aSetDexHandPresetActCommand\x1aO\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x13\n\x0bpresetActId\x18\x02 \x01(\x05\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xb6\x01\n\x14GetSensorDataCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x62\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12$\n\x06sensor\x18\x02 \x03(\x0b\x32\x14.cmvr.api.SensorData\"\xbc\x01\n\x1aGetSensorDataStreamCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x62\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12$\n\x06sensor\x18\x02 \x03(\x0b\x32\x14.cmvr.api.SensorDatab\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.dexhand_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_SENSORDATA_ROWDATA'].fields_by_name['values']._loaded_options = None
+ _globals['_SENSORDATA_ROWDATA'].fields_by_name['values']._serialized_options = b'\020\001'
+ _globals['_FREEDOMVALUE']._serialized_start=67
+ _globals['_FREEDOMVALUE']._serialized_end=108
+ _globals['_FREEDOMSTATE']._serialized_start=111
+ _globals['_FREEDOMSTATE']._serialized_end=280
+ _globals['_SENSORDATA']._serialized_start=283
+ _globals['_SENSORDATA']._serialized_end=683
+ _globals['_SENSORDATA_ROWDATA']._serialized_start=494
+ _globals['_SENSORDATA_ROWDATA']._serialized_end=523
+ _globals['_SENSORDATA_FINGERTYPE']._serialized_start=525
+ _globals['_SENSORDATA_FINGERTYPE']._serialized_end=609
+ _globals['_SENSORDATA_PARTTYPE']._serialized_start=611
+ _globals['_SENSORDATA_PARTTYPE']._serialized_end=683
+ _globals['_DEXHANDSTATE']._serialized_start=685
+ _globals['_DEXHANDSTATE']._serialized_end=762
+ _globals['_GETDEXHANDSTATECOMMAND']._serialized_start=765
+ _globals['_GETDEXHANDSTATECOMMAND']._serialized_end=950
+ _globals['_GETDEXHANDSTATECOMMAND_REQUEST']._serialized_start=791
+ _globals['_GETDEXHANDSTATECOMMAND_REQUEST']._serialized_end=849
+ _globals['_GETDEXHANDSTATECOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_GETDEXHANDSTATECOMMAND_FEEDBACK']._serialized_end=950
+ _globals['_SETDEXHANDPOSITIONSCOMMAND']._serialized_start=953
+ _globals['_SETDEXHANDPOSITIONSCOMMAND']._serialized_end=1143
+ _globals['_SETDEXHANDPOSITIONSCOMMAND_REQUEST']._serialized_start=983
+ _globals['_SETDEXHANDPOSITIONSCOMMAND_REQUEST']._serialized_end=1081
+ _globals['_SETDEXHANDPOSITIONSCOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_SETDEXHANDPOSITIONSCOMMAND_FEEDBACK']._serialized_end=911
+ _globals['_SETDEXHANDANGLESCOMMAND']._serialized_start=1146
+ _globals['_SETDEXHANDANGLESCOMMAND']._serialized_end=1333
+ _globals['_SETDEXHANDANGLESCOMMAND_REQUEST']._serialized_start=983
+ _globals['_SETDEXHANDANGLESCOMMAND_REQUEST']._serialized_end=1081
+ _globals['_SETDEXHANDANGLESCOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_SETDEXHANDANGLESCOMMAND_FEEDBACK']._serialized_end=911
+ _globals['_SETDEXHANDFORCECOMMAND']._serialized_start=1336
+ _globals['_SETDEXHANDFORCECOMMAND']._serialized_end=1522
+ _globals['_SETDEXHANDFORCECOMMAND_REQUEST']._serialized_start=983
+ _globals['_SETDEXHANDFORCECOMMAND_REQUEST']._serialized_end=1081
+ _globals['_SETDEXHANDFORCECOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_SETDEXHANDFORCECOMMAND_FEEDBACK']._serialized_end=911
+ _globals['_SETDEXHANDSPEEDCOMMAND']._serialized_start=1525
+ _globals['_SETDEXHANDSPEEDCOMMAND']._serialized_end=1711
+ _globals['_SETDEXHANDSPEEDCOMMAND_REQUEST']._serialized_start=983
+ _globals['_SETDEXHANDSPEEDCOMMAND_REQUEST']._serialized_end=1081
+ _globals['_SETDEXHANDSPEEDCOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_SETDEXHANDSPEEDCOMMAND_FEEDBACK']._serialized_end=911
+ _globals['_SETDEXHANDPRESETACTCOMMAND']._serialized_start=1714
+ _globals['_SETDEXHANDPRESETACTCOMMAND']._serialized_end=1885
+ _globals['_SETDEXHANDPRESETACTCOMMAND_REQUEST']._serialized_start=1744
+ _globals['_SETDEXHANDPRESETACTCOMMAND_REQUEST']._serialized_end=1823
+ _globals['_SETDEXHANDPRESETACTCOMMAND_FEEDBACK']._serialized_start=851
+ _globals['_SETDEXHANDPRESETACTCOMMAND_FEEDBACK']._serialized_end=911
+ _globals['_GETSENSORDATACOMMAND']._serialized_start=1888
+ _globals['_GETSENSORDATACOMMAND']._serialized_end=2070
+ _globals['_GETSENSORDATACOMMAND_REQUEST']._serialized_start=791
+ _globals['_GETSENSORDATACOMMAND_REQUEST']._serialized_end=849
+ _globals['_GETSENSORDATACOMMAND_FEEDBACK']._serialized_start=1972
+ _globals['_GETSENSORDATACOMMAND_FEEDBACK']._serialized_end=2070
+ _globals['_GETSENSORDATASTREAMCOMMAND']._serialized_start=2073
+ _globals['_GETSENSORDATASTREAMCOMMAND']._serialized_end=2261
+ _globals['_GETSENSORDATASTREAMCOMMAND_REQUEST']._serialized_start=791
+ _globals['_GETSENSORDATASTREAMCOMMAND_REQUEST']._serialized_end=849
+ _globals['_GETSENSORDATASTREAMCOMMAND_FEEDBACK']._serialized_start=1972
+ _globals['_GETSENSORDATASTREAMCOMMAND_FEEDBACK']._serialized_end=2070
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/dexhand_command_pb2.pyi b/src/cmvr/api/dexhand_command_pb2.pyi
new file mode 100644
index 0000000..3490141
--- /dev/null
+++ b/src/cmvr/api/dexhand_command_pb2.pyi
@@ -0,0 +1,222 @@
+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 FreedomValue(_message.Message):
+ __slots__ = ("id", "value")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ id: int
+ value: float
+ def __init__(self, id: _Optional[int] = ..., value: _Optional[float] = ...) -> None: ...
+
+class FreedomState(_message.Message):
+ __slots__ = ("dof_id", "angle", "speed", "force", "position", "current", "temperature", "error", "error_message")
+ DOF_ID_FIELD_NUMBER: _ClassVar[int]
+ ANGLE_FIELD_NUMBER: _ClassVar[int]
+ SPEED_FIELD_NUMBER: _ClassVar[int]
+ FORCE_FIELD_NUMBER: _ClassVar[int]
+ POSITION_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_FIELD_NUMBER: _ClassVar[int]
+ TEMPERATURE_FIELD_NUMBER: _ClassVar[int]
+ ERROR_FIELD_NUMBER: _ClassVar[int]
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ dof_id: int
+ angle: int
+ speed: int
+ force: int
+ position: int
+ current: int
+ temperature: int
+ error: int
+ error_message: _containers.RepeatedScalarFieldContainer[str]
+ def __init__(self, dof_id: _Optional[int] = ..., angle: _Optional[int] = ..., speed: _Optional[int] = ..., force: _Optional[int] = ..., position: _Optional[int] = ..., current: _Optional[int] = ..., temperature: _Optional[int] = ..., error: _Optional[int] = ..., error_message: _Optional[_Iterable[str]] = ...) -> None: ...
+
+class SensorData(_message.Message):
+ __slots__ = ("finger_type", "part_type", "sensor_name", "data", "rows", "cols")
+ class FingerType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PINKY: _ClassVar[SensorData.FingerType]
+ RING: _ClassVar[SensorData.FingerType]
+ MIDDLE_FINGER: _ClassVar[SensorData.FingerType]
+ INDEX: _ClassVar[SensorData.FingerType]
+ THUMB: _ClassVar[SensorData.FingerType]
+ PALM: _ClassVar[SensorData.FingerType]
+ PINKY: SensorData.FingerType
+ RING: SensorData.FingerType
+ MIDDLE_FINGER: SensorData.FingerType
+ INDEX: SensorData.FingerType
+ THUMB: SensorData.FingerType
+ PALM: SensorData.FingerType
+ class PartType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TIP: _ClassVar[SensorData.PartType]
+ FINGER: _ClassVar[SensorData.PartType]
+ PAD: _ClassVar[SensorData.PartType]
+ THUMB_MIDDLE: _ClassVar[SensorData.PartType]
+ PALM_PAD: _ClassVar[SensorData.PartType]
+ TIP: SensorData.PartType
+ FINGER: SensorData.PartType
+ PAD: SensorData.PartType
+ THUMB_MIDDLE: SensorData.PartType
+ PALM_PAD: SensorData.PartType
+ class RowData(_message.Message):
+ __slots__ = ("values",)
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ values: _containers.RepeatedScalarFieldContainer[int]
+ def __init__(self, values: _Optional[_Iterable[int]] = ...) -> None: ...
+ FINGER_TYPE_FIELD_NUMBER: _ClassVar[int]
+ PART_TYPE_FIELD_NUMBER: _ClassVar[int]
+ SENSOR_NAME_FIELD_NUMBER: _ClassVar[int]
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ ROWS_FIELD_NUMBER: _ClassVar[int]
+ COLS_FIELD_NUMBER: _ClassVar[int]
+ finger_type: SensorData.FingerType
+ part_type: SensorData.PartType
+ sensor_name: str
+ data: _containers.RepeatedCompositeFieldContainer[SensorData.RowData]
+ rows: int
+ cols: int
+ def __init__(self, finger_type: _Optional[_Union[SensorData.FingerType, str]] = ..., part_type: _Optional[_Union[SensorData.PartType, str]] = ..., sensor_name: _Optional[str] = ..., data: _Optional[_Iterable[_Union[SensorData.RowData, _Mapping]]] = ..., rows: _Optional[int] = ..., cols: _Optional[int] = ...) -> None: ...
+
+class DexHandState(_message.Message):
+ __slots__ = ("is_initialized", "hands")
+ IS_INITIALIZED_FIELD_NUMBER: _ClassVar[int]
+ HANDS_FIELD_NUMBER: _ClassVar[int]
+ is_initialized: bool
+ hands: _containers.RepeatedCompositeFieldContainer[FreedomState]
+ def __init__(self, is_initialized: _Optional[bool] = ..., hands: _Optional[_Iterable[_Union[FreedomState, _Mapping]]] = ...) -> None: ...
+
+class GetDexHandStateCommand(_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: DexHandState
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[DexHandState, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class SetDexHandPositionsCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "values")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ values: _containers.RepeatedCompositeFieldContainer[FreedomValue]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., values: _Optional[_Iterable[_Union[FreedomValue, _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 SetDexHandAnglesCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "values")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ values: _containers.RepeatedCompositeFieldContainer[FreedomValue]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., values: _Optional[_Iterable[_Union[FreedomValue, _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 SetDexHandForceCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "values")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ values: _containers.RepeatedCompositeFieldContainer[FreedomValue]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., values: _Optional[_Iterable[_Union[FreedomValue, _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 SetDexHandSpeedCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "values")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ values: _containers.RepeatedCompositeFieldContainer[FreedomValue]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., values: _Optional[_Iterable[_Union[FreedomValue, _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 SetDexHandPresetActCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "presetActId")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ PRESETACTID_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ presetActId: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., presetActId: _Optional[int] = ...) -> 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 GetSensorDataCommand(_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", "sensor")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ SENSOR_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ sensor: _containers.RepeatedCompositeFieldContainer[SensorData]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., sensor: _Optional[_Iterable[_Union[SensorData, _Mapping]]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetSensorDataStreamCommand(_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", "sensor")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ SENSOR_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ sensor: _containers.RepeatedCompositeFieldContainer[SensorData]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., sensor: _Optional[_Iterable[_Union[SensorData, _Mapping]]] = ...) -> None: ...
+ def __init__(self) -> None: ...
diff --git a/src/cmvr/api/dexhand_command_pb2_grpc.py b/src/cmvr/api/dexhand_command_pb2_grpc.py
new file mode 100644
index 0000000..cbcc7b8
--- /dev/null
+++ b/src/cmvr/api/dexhand_command_pb2_grpc.py
@@ -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/dexhand_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}.'
+ )
diff --git a/src/cmvr/api/dexhand_service_pb2.py b/src/cmvr/api/dexhand_service_pb2.py
new file mode 100644
index 0000000..4398ead
--- /dev/null
+++ b/src/cmvr/api/dexhand_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/dexhand_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/dexhand_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import dexhand_command_pb2 as cmvr_dot_api_dot_dexhand__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/dexhand_service.proto\x12\x08\x63mvr.api\x1a\x1e\x63mvr/api/dexhand_command.proto2\xe8\x06\n\x0e\x44\x65xHandService\x12`\n\tGetStatus\x12(.cmvr.api.GetDexHandStateCommand.Request\x1a).cmvr.api.GetDexHandStateCommand.Feedback\x12l\n\rSetDexHandPos\x12,.cmvr.api.SetDexHandPositionsCommand.Request\x1a-.cmvr.api.SetDexHandPositionsCommand.Feedback\x12h\n\x0fSetDexHandAngle\x12).cmvr.api.SetDexHandAnglesCommand.Request\x1a*.cmvr.api.SetDexHandAnglesCommand.Feedback\x12\x66\n\x0fSetDexHandForce\x12(.cmvr.api.SetDexHandForceCommand.Request\x1a).cmvr.api.SetDexHandForceCommand.Feedback\x12\x66\n\x0fSetDexHandSpeed\x12(.cmvr.api.SetDexHandSpeedCommand.Request\x1a).cmvr.api.SetDexHandSpeedCommand.Feedback\x12r\n\x13SetDexHandPresetAct\x12,.cmvr.api.SetDexHandPresetActCommand.Request\x1a-.cmvr.api.SetDexHandPresetActCommand.Feedback\x12`\n\rGetSensorData\x12&.cmvr.api.GetSensorDataCommand.Request\x1a\'.cmvr.api.GetSensorDataCommand.Feedback\x12v\n\x13GetSensorDataStream\x12,.cmvr.api.GetSensorDataStreamCommand.Request\x1a-.cmvr.api.GetSensorDataStreamCommand.Feedback(\x01\x30\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.dexhand_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_DEXHANDSERVICE']._serialized_start=77
+ _globals['_DEXHANDSERVICE']._serialized_end=949
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/dexhand_service_pb2.pyi b/src/cmvr/api/dexhand_service_pb2.pyi
new file mode 100644
index 0000000..4b691ab
--- /dev/null
+++ b/src/cmvr/api/dexhand_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import dexhand_command_pb2 as _dexhand_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/dexhand_service_pb2_grpc.py b/src/cmvr/api/dexhand_service_pb2_grpc.py
new file mode 100644
index 0000000..0f19a24
--- /dev/null
+++ b/src/cmvr/api/dexhand_service_pb2_grpc.py
@@ -0,0 +1,399 @@
+# 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 dexhand_command_pb2 as cmvr_dot_api_dot_dexhand__command__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/dexhand_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 DexHandServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetStatus = channel.unary_unary(
+ '/cmvr.api.DexHandService/GetStatus',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetDexHandPos = channel.unary_unary(
+ '/cmvr.api.DexHandService/SetDexHandPos',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetDexHandAngle = channel.unary_unary(
+ '/cmvr.api.DexHandService/SetDexHandAngle',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetDexHandForce = channel.unary_unary(
+ '/cmvr.api.DexHandService/SetDexHandForce',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetDexHandSpeed = channel.unary_unary(
+ '/cmvr.api.DexHandService/SetDexHandSpeed',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetDexHandPresetAct = channel.unary_unary(
+ '/cmvr.api.DexHandService/SetDexHandPresetAct',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetSensorData = channel.unary_unary(
+ '/cmvr.api.DexHandService/GetSensorData',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetSensorDataStream = channel.stream_stream(
+ '/cmvr.api.DexHandService/GetSensorDataStream',
+ request_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Feedback.FromString,
+ _registered_method=True)
+
+
+class DexHandServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def GetStatus(self, request, context):
+ """基本控制
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetDexHandPos(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetDexHandAngle(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetDexHandForce(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetDexHandSpeed(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetDexHandPresetAct(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetSensorData(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetSensorDataStream(self, request_iterator, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_DexHandServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetStatus,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Feedback.SerializeToString,
+ ),
+ 'SetDexHandPos': grpc.unary_unary_rpc_method_handler(
+ servicer.SetDexHandPos,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Feedback.SerializeToString,
+ ),
+ 'SetDexHandAngle': grpc.unary_unary_rpc_method_handler(
+ servicer.SetDexHandAngle,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Feedback.SerializeToString,
+ ),
+ 'SetDexHandForce': grpc.unary_unary_rpc_method_handler(
+ servicer.SetDexHandForce,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Feedback.SerializeToString,
+ ),
+ 'SetDexHandSpeed': grpc.unary_unary_rpc_method_handler(
+ servicer.SetDexHandSpeed,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Feedback.SerializeToString,
+ ),
+ 'SetDexHandPresetAct': grpc.unary_unary_rpc_method_handler(
+ servicer.SetDexHandPresetAct,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Feedback.SerializeToString,
+ ),
+ 'GetSensorData': grpc.unary_unary_rpc_method_handler(
+ servicer.GetSensorData,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Feedback.SerializeToString,
+ ),
+ 'GetSensorDataStream': grpc.stream_stream_rpc_method_handler(
+ servicer.GetSensorDataStream,
+ request_deserializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.DexHandService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.DexHandService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class DexHandService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetStatus(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.DexHandService/GetStatus',
+ cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.GetDexHandStateCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetDexHandPos(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.DexHandService/SetDexHandPos',
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPositionsCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetDexHandAngle(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.DexHandService/SetDexHandAngle',
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandAnglesCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetDexHandForce(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.DexHandService/SetDexHandForce',
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandForceCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetDexHandSpeed(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.DexHandService/SetDexHandSpeed',
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandSpeedCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetDexHandPresetAct(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.DexHandService/SetDexHandPresetAct',
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.SetDexHandPresetActCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetSensorData(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.DexHandService/GetSensorData',
+ cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetSensorDataStream(request_iterator,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.stream_stream(
+ request_iterator,
+ target,
+ '/cmvr.api.DexHandService/GetSensorDataStream',
+ cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_dexhand__command__pb2.GetSensorDataStreamCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/hlc_command_pb2.py b/src/cmvr/api/hlc_command_pb2.py
new file mode 100644
index 0000000..a28be26
--- /dev/null
+++ b/src/cmvr/api/hlc_command_pb2.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/hlc_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/hlc_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/hlc_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"\xaa\x01\n\x05Touch\x1a\x63\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\t\n\x01u\x18\x02 \x01(\x05\x12\t\n\x01v\x18\x03 \x01(\x05\x12\x11\n\tmax_force\x18\x04 \x01(\x01\x1a<\n\x08Response\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedbackb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.hlc_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOUCH']._serialized_start=64
+ _globals['_TOUCH']._serialized_end=234
+ _globals['_TOUCH_REQUEST']._serialized_start=73
+ _globals['_TOUCH_REQUEST']._serialized_end=172
+ _globals['_TOUCH_RESPONSE']._serialized_start=174
+ _globals['_TOUCH_RESPONSE']._serialized_end=234
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/hlc_command_pb2.pyi b/src/cmvr/api/hlc_command_pb2.pyi
new file mode 100644
index 0000000..a254639
--- /dev/null
+++ b/src/cmvr/api/hlc_command_pb2.pyi
@@ -0,0 +1,27 @@
+from cmvr.api import common_pb2 as _common_pb2
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class Touch(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "u", "v", "max_force")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ U_FIELD_NUMBER: _ClassVar[int]
+ V_FIELD_NUMBER: _ClassVar[int]
+ MAX_FORCE_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ u: int
+ v: int
+ max_force: float
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., u: _Optional[int] = ..., v: _Optional[int] = ..., max_force: _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: ...
diff --git a/src/cmvr/api/hlc_command_pb2_grpc.py b/src/cmvr/api/hlc_command_pb2_grpc.py
new file mode 100644
index 0000000..d5672e0
--- /dev/null
+++ b/src/cmvr/api/hlc_command_pb2_grpc.py
@@ -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/hlc_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}.'
+ )
diff --git a/src/cmvr/api/hlc_service_pb2.py b/src/cmvr/api/hlc_service_pb2.py
new file mode 100644
index 0000000..c010d37
--- /dev/null
+++ b/src/cmvr/api/hlc_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/hlc_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/hlc_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import hlc_command_pb2 as cmvr_dot_api_dot_hlc__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/api/hlc_service.proto\x12\x08\x63mvr.api\x1a\x1a\x63mvr/api/hlc_command.proto2H\n\nHlcService\x12:\n\x05touch\x12\x17.cmvr.api.Touch.Request\x1a\x18.cmvr.api.Touch.Responseb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.hlc_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_HLCSERVICE']._serialized_start=68
+ _globals['_HLCSERVICE']._serialized_end=140
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/hlc_service_pb2.pyi b/src/cmvr/api/hlc_service_pb2.pyi
new file mode 100644
index 0000000..db31920
--- /dev/null
+++ b/src/cmvr/api/hlc_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import hlc_command_pb2 as _hlc_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/hlc_service_pb2_grpc.py b/src/cmvr/api/hlc_service_pb2_grpc.py
new file mode 100644
index 0000000..91ab8a1
--- /dev/null
+++ b/src/cmvr/api/hlc_service_pb2_grpc.py
@@ -0,0 +1,97 @@
+# 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 hlc_command_pb2 as cmvr_dot_api_dot_hlc__command__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/hlc_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 HlcServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.touch = channel.unary_unary(
+ '/cmvr.api.HlcService/touch',
+ request_serializer=cmvr_dot_api_dot_hlc__command__pb2.Touch.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_hlc__command__pb2.Touch.Response.FromString,
+ _registered_method=True)
+
+
+class HlcServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def touch(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_HlcServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'touch': grpc.unary_unary_rpc_method_handler(
+ servicer.touch,
+ request_deserializer=cmvr_dot_api_dot_hlc__command__pb2.Touch.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_hlc__command__pb2.Touch.Response.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.HlcService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.HlcService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class HlcService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def touch(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.HlcService/touch',
+ cmvr_dot_api_dot_hlc__command__pb2.Touch.Request.SerializeToString,
+ cmvr_dot_api_dot_hlc__command__pb2.Touch.Response.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/microphone_command_pb2.py b/src/cmvr/api/microphone_command_pb2.py
new file mode 100644
index 0000000..2b78e0e
--- /dev/null
+++ b/src/cmvr/api/microphone_command_pb2.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/microphone_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/microphone_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!cmvr/api/microphone_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"s\n\x08MicState\x12\x16\n\x0eis_initialized\x18\x01 \x01(\x08\x12\x12\n\nis_running\x18\x02 \x01(\x08\x12\x14\n\x0cis_recording\x18\x03 \x01(\x08\x12\x0e\n\x06volume\x18\x04 \x01(\x05\x12\x15\n\rerror_message\x18\x05 \x01(\t\"\xb1\x01\n\x12GetMicStateCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a_\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\x12.cmvr.api.MicState\"\xa7\x01\n\x18StartMicRecordingCommand\x1aM\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x11\n\tfile_path\x18\x02 \x01(\t\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x93\x01\n\x17StopMicRecordingCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x94\x01\n\x18PauseMicRecordingCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x95\x01\n\x19ResumeMicRecordingCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xa4\x01\n\x18SetMicPhoneVolumeCommand\x1aJ\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x0e\n\x06volume\x18\x02 \x01(\x05\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xa4\x01\n\x18GetMicPhoneVolumeCommand\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\x0e\n\x06volume\x18\x02 \x01(\x05\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.microphone_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_MICSTATE']._serialized_start=70
+ _globals['_MICSTATE']._serialized_end=185
+ _globals['_GETMICSTATECOMMAND']._serialized_start=188
+ _globals['_GETMICSTATECOMMAND']._serialized_end=365
+ _globals['_GETMICSTATECOMMAND_REQUEST']._serialized_start=210
+ _globals['_GETMICSTATECOMMAND_REQUEST']._serialized_end=268
+ _globals['_GETMICSTATECOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_GETMICSTATECOMMAND_FEEDBACK']._serialized_end=365
+ _globals['_STARTMICRECORDINGCOMMAND']._serialized_start=368
+ _globals['_STARTMICRECORDINGCOMMAND']._serialized_end=535
+ _globals['_STARTMICRECORDINGCOMMAND_REQUEST']._serialized_start=396
+ _globals['_STARTMICRECORDINGCOMMAND_REQUEST']._serialized_end=473
+ _globals['_STARTMICRECORDINGCOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_STARTMICRECORDINGCOMMAND_FEEDBACK']._serialized_end=330
+ _globals['_STOPMICRECORDINGCOMMAND']._serialized_start=538
+ _globals['_STOPMICRECORDINGCOMMAND']._serialized_end=685
+ _globals['_STOPMICRECORDINGCOMMAND_REQUEST']._serialized_start=210
+ _globals['_STOPMICRECORDINGCOMMAND_REQUEST']._serialized_end=268
+ _globals['_STOPMICRECORDINGCOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_STOPMICRECORDINGCOMMAND_FEEDBACK']._serialized_end=330
+ _globals['_PAUSEMICRECORDINGCOMMAND']._serialized_start=688
+ _globals['_PAUSEMICRECORDINGCOMMAND']._serialized_end=836
+ _globals['_PAUSEMICRECORDINGCOMMAND_REQUEST']._serialized_start=210
+ _globals['_PAUSEMICRECORDINGCOMMAND_REQUEST']._serialized_end=268
+ _globals['_PAUSEMICRECORDINGCOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_PAUSEMICRECORDINGCOMMAND_FEEDBACK']._serialized_end=330
+ _globals['_RESUMEMICRECORDINGCOMMAND']._serialized_start=839
+ _globals['_RESUMEMICRECORDINGCOMMAND']._serialized_end=988
+ _globals['_RESUMEMICRECORDINGCOMMAND_REQUEST']._serialized_start=210
+ _globals['_RESUMEMICRECORDINGCOMMAND_REQUEST']._serialized_end=268
+ _globals['_RESUMEMICRECORDINGCOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_RESUMEMICRECORDINGCOMMAND_FEEDBACK']._serialized_end=330
+ _globals['_SETMICPHONEVOLUMECOMMAND']._serialized_start=991
+ _globals['_SETMICPHONEVOLUMECOMMAND']._serialized_end=1155
+ _globals['_SETMICPHONEVOLUMECOMMAND_REQUEST']._serialized_start=1019
+ _globals['_SETMICPHONEVOLUMECOMMAND_REQUEST']._serialized_end=1093
+ _globals['_SETMICPHONEVOLUMECOMMAND_FEEDBACK']._serialized_start=270
+ _globals['_SETMICPHONEVOLUMECOMMAND_FEEDBACK']._serialized_end=330
+ _globals['_GETMICPHONEVOLUMECOMMAND']._serialized_start=1158
+ _globals['_GETMICPHONEVOLUMECOMMAND']._serialized_end=1322
+ _globals['_GETMICPHONEVOLUMECOMMAND_REQUEST']._serialized_start=210
+ _globals['_GETMICPHONEVOLUMECOMMAND_REQUEST']._serialized_end=268
+ _globals['_GETMICPHONEVOLUMECOMMAND_FEEDBACK']._serialized_start=1246
+ _globals['_GETMICPHONEVOLUMECOMMAND_FEEDBACK']._serialized_end=1322
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/microphone_command_pb2.pyi b/src/cmvr/api/microphone_command_pb2.pyi
new file mode 100644
index 0000000..1a7ecc2
--- /dev/null
+++ b/src/cmvr/api/microphone_command_pb2.pyi
@@ -0,0 +1,127 @@
+from cmvr.api import common_pb2 as _common_pb2
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class MicState(_message.Message):
+ __slots__ = ("is_initialized", "is_running", "is_recording", "volume", "error_message")
+ IS_INITIALIZED_FIELD_NUMBER: _ClassVar[int]
+ IS_RUNNING_FIELD_NUMBER: _ClassVar[int]
+ IS_RECORDING_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ is_initialized: bool
+ is_running: bool
+ is_recording: bool
+ volume: int
+ error_message: str
+ def __init__(self, is_initialized: _Optional[bool] = ..., is_running: _Optional[bool] = ..., is_recording: _Optional[bool] = ..., volume: _Optional[int] = ..., error_message: _Optional[str] = ...) -> None: ...
+
+class GetMicStateCommand(_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: MicState
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[MicState, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class StartMicRecordingCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "file_path")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ FILE_PATH_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ file_path: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., file_path: _Optional[str] = ...) -> 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 StopMicRecordingCommand(_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",)
+ 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 PauseMicRecordingCommand(_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",)
+ 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 ResumeMicRecordingCommand(_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",)
+ 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 SetMicPhoneVolumeCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "volume")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ volume: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., volume: _Optional[int] = ...) -> 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 GetMicPhoneVolumeCommand(_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", "volume")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ volume: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., volume: _Optional[int] = ...) -> None: ...
+ def __init__(self) -> None: ...
diff --git a/src/cmvr/api/microphone_command_pb2_grpc.py b/src/cmvr/api/microphone_command_pb2_grpc.py
new file mode 100644
index 0000000..97f9f1f
--- /dev/null
+++ b/src/cmvr/api/microphone_command_pb2_grpc.py
@@ -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/microphone_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}.'
+ )
diff --git a/src/cmvr/api/microphone_service_pb2.py b/src/cmvr/api/microphone_service_pb2.py
new file mode 100644
index 0000000..9f29ca1
--- /dev/null
+++ b/src/cmvr/api/microphone_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/microphone_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/microphone_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import microphone_command_pb2 as cmvr_dot_api_dot_microphone__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!cmvr/api/microphone_service.proto\x12\x08\x63mvr.api\x1a!cmvr/api/microphone_command.proto2\xd7\x05\n\x0fMicPhoneService\x12X\n\tGetStatus\x12$.cmvr.api.GetMicStateCommand.Request\x1a%.cmvr.api.GetMicStateCommand.Feedback\x12\x66\n\x0bStartRecord\x12*.cmvr.api.StartMicRecordingCommand.Request\x1a+.cmvr.api.StartMicRecordingCommand.Feedback\x12\x63\n\nStopRecord\x12).cmvr.api.StopMicRecordingCommand.Request\x1a*.cmvr.api.StopMicRecordingCommand.Feedback\x12\x66\n\x0bPauseRecord\x12*.cmvr.api.PauseMicRecordingCommand.Request\x1a+.cmvr.api.PauseMicRecordingCommand.Feedback\x12i\n\x0cResumeRecord\x12+.cmvr.api.ResumeMicRecordingCommand.Request\x1a,.cmvr.api.ResumeMicRecordingCommand.Feedback\x12\x64\n\tSetVolume\x12*.cmvr.api.SetMicPhoneVolumeCommand.Request\x1a+.cmvr.api.SetMicPhoneVolumeCommand.Feedback\x12\x64\n\tGetVolume\x12*.cmvr.api.GetMicPhoneVolumeCommand.Request\x1a+.cmvr.api.GetMicPhoneVolumeCommand.Feedbackb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.microphone_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_MICPHONESERVICE']._serialized_start=83
+ _globals['_MICPHONESERVICE']._serialized_end=810
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/microphone_service_pb2.pyi b/src/cmvr/api/microphone_service_pb2.pyi
new file mode 100644
index 0000000..38e96fd
--- /dev/null
+++ b/src/cmvr/api/microphone_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import microphone_command_pb2 as _microphone_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/microphone_service_pb2_grpc.py b/src/cmvr/api/microphone_service_pb2_grpc.py
new file mode 100644
index 0000000..3fde576
--- /dev/null
+++ b/src/cmvr/api/microphone_service_pb2_grpc.py
@@ -0,0 +1,357 @@
+# 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 microphone_command_pb2 as cmvr_dot_api_dot_microphone__command__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/microphone_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 MicPhoneServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetStatus = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/GetStatus',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StartRecord = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/StartRecord',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StopRecord = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/StopRecord',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.PauseRecord = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/PauseRecord',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.ResumeRecord = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/ResumeRecord',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetVolume = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/SetVolume',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetVolume = channel.unary_unary(
+ '/cmvr.api.MicPhoneService/GetVolume',
+ request_serializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Feedback.FromString,
+ _registered_method=True)
+
+
+class MicPhoneServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def GetStatus(self, request, context):
+ """基本控制
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StartRecord(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StopRecord(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def PauseRecord(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ResumeRecord(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetVolume(self, request, context):
+ """音量控制
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetVolume(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_MicPhoneServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetStatus,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Feedback.SerializeToString,
+ ),
+ 'StartRecord': grpc.unary_unary_rpc_method_handler(
+ servicer.StartRecord,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'StopRecord': grpc.unary_unary_rpc_method_handler(
+ servicer.StopRecord,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'PauseRecord': grpc.unary_unary_rpc_method_handler(
+ servicer.PauseRecord,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'ResumeRecord': grpc.unary_unary_rpc_method_handler(
+ servicer.ResumeRecord,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Feedback.SerializeToString,
+ ),
+ 'SetVolume': grpc.unary_unary_rpc_method_handler(
+ servicer.SetVolume,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Feedback.SerializeToString,
+ ),
+ 'GetVolume': grpc.unary_unary_rpc_method_handler(
+ servicer.GetVolume,
+ request_deserializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.MicPhoneService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.MicPhoneService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class MicPhoneService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetStatus(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.MicPhoneService/GetStatus',
+ cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.GetMicStateCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StartRecord(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.MicPhoneService/StartRecord',
+ cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.StartMicRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StopRecord(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.MicPhoneService/StopRecord',
+ cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.StopMicRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def PauseRecord(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.MicPhoneService/PauseRecord',
+ cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.PauseMicRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ResumeRecord(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.MicPhoneService/ResumeRecord',
+ cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.ResumeMicRecordingCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetVolume(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.MicPhoneService/SetVolume',
+ cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.SetMicPhoneVolumeCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetVolume(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.MicPhoneService/GetVolume',
+ cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_microphone__command__pb2.GetMicPhoneVolumeCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/speaker_command_pb2.py b/src/cmvr/api/speaker_command_pb2.py
new file mode 100644
index 0000000..e67e0bf
--- /dev/null
+++ b/src/cmvr/api/speaker_command_pb2.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/speaker_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/speaker_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/speaker_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"\xb3\x01\n\tAudioData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x13\n\x0bsample_rate\x18\x02 \x01(\x05\x12\x10\n\x08\x63hannels\x18\x03 \x01(\x05\x12/\n\x06\x66ormat\x18\x04 \x01(\x0e\x32\x1f.cmvr.api.AudioData.AudioFormat\x12\r\n\x05\x63odec\x18\x05 \x01(\t\"1\n\x0b\x41udioFormat\x12\x07\n\x03PCM\x10\x00\x12\x07\n\x03MP3\x10\x01\x12\x07\n\x03\x41\x41\x43\x10\x02\x12\x07\n\x03WAV\x10\x03\"\x89\x01\n\x0cSpeakerState\x12\x16\n\x0eis_initialized\x18\x01 \x01(\x08\x12\x12\n\nis_running\x18\x02 \x01(\x08\x12\x13\n\x0bis_decoding\x18\x03 \x01(\x08\x12\x11\n\tis_paused\x18\x05 \x01(\x08\x12\x0e\n\x06volume\x18\x06 \x01(\x05\x12\x15\n\rerror_message\x18\x07 \x01(\t\"\xb9\x01\n\x16GetSpeakerStateCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a\x63\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\x16.cmvr.api.SpeakerState\"\xa0\x01\n\x10PlayAudioCommand\x1aN\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x12\n\naudio_path\x18\x02 \x01(\t\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8e\x01\n\x12StopSpeakerCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8f\x01\n\x13PauseSpeakerCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x90\x01\n\x14ResumeSpeakerCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xa3\x01\n\x17SetSpeakerVolumeCommand\x1aJ\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12\x0e\n\x06volume\x18\x02 \x01(\x05\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\xa3\x01\n\x17GetSpeakerVolumeCommand\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\x0e\n\x06volume\x18\x02 \x01(\x05\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.speaker_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_AUDIODATA']._serialized_start=68
+ _globals['_AUDIODATA']._serialized_end=247
+ _globals['_AUDIODATA_AUDIOFORMAT']._serialized_start=198
+ _globals['_AUDIODATA_AUDIOFORMAT']._serialized_end=247
+ _globals['_SPEAKERSTATE']._serialized_start=250
+ _globals['_SPEAKERSTATE']._serialized_end=387
+ _globals['_GETSPEAKERSTATECOMMAND']._serialized_start=390
+ _globals['_GETSPEAKERSTATECOMMAND']._serialized_end=575
+ _globals['_GETSPEAKERSTATECOMMAND_REQUEST']._serialized_start=416
+ _globals['_GETSPEAKERSTATECOMMAND_REQUEST']._serialized_end=474
+ _globals['_GETSPEAKERSTATECOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_GETSPEAKERSTATECOMMAND_FEEDBACK']._serialized_end=575
+ _globals['_PLAYAUDIOCOMMAND']._serialized_start=578
+ _globals['_PLAYAUDIOCOMMAND']._serialized_end=738
+ _globals['_PLAYAUDIOCOMMAND_REQUEST']._serialized_start=598
+ _globals['_PLAYAUDIOCOMMAND_REQUEST']._serialized_end=676
+ _globals['_PLAYAUDIOCOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_PLAYAUDIOCOMMAND_FEEDBACK']._serialized_end=536
+ _globals['_STOPSPEAKERCOMMAND']._serialized_start=741
+ _globals['_STOPSPEAKERCOMMAND']._serialized_end=883
+ _globals['_STOPSPEAKERCOMMAND_REQUEST']._serialized_start=416
+ _globals['_STOPSPEAKERCOMMAND_REQUEST']._serialized_end=474
+ _globals['_STOPSPEAKERCOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_STOPSPEAKERCOMMAND_FEEDBACK']._serialized_end=536
+ _globals['_PAUSESPEAKERCOMMAND']._serialized_start=886
+ _globals['_PAUSESPEAKERCOMMAND']._serialized_end=1029
+ _globals['_PAUSESPEAKERCOMMAND_REQUEST']._serialized_start=416
+ _globals['_PAUSESPEAKERCOMMAND_REQUEST']._serialized_end=474
+ _globals['_PAUSESPEAKERCOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_PAUSESPEAKERCOMMAND_FEEDBACK']._serialized_end=536
+ _globals['_RESUMESPEAKERCOMMAND']._serialized_start=1032
+ _globals['_RESUMESPEAKERCOMMAND']._serialized_end=1176
+ _globals['_RESUMESPEAKERCOMMAND_REQUEST']._serialized_start=416
+ _globals['_RESUMESPEAKERCOMMAND_REQUEST']._serialized_end=474
+ _globals['_RESUMESPEAKERCOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_RESUMESPEAKERCOMMAND_FEEDBACK']._serialized_end=536
+ _globals['_SETSPEAKERVOLUMECOMMAND']._serialized_start=1179
+ _globals['_SETSPEAKERVOLUMECOMMAND']._serialized_end=1342
+ _globals['_SETSPEAKERVOLUMECOMMAND_REQUEST']._serialized_start=1206
+ _globals['_SETSPEAKERVOLUMECOMMAND_REQUEST']._serialized_end=1280
+ _globals['_SETSPEAKERVOLUMECOMMAND_FEEDBACK']._serialized_start=476
+ _globals['_SETSPEAKERVOLUMECOMMAND_FEEDBACK']._serialized_end=536
+ _globals['_GETSPEAKERVOLUMECOMMAND']._serialized_start=1345
+ _globals['_GETSPEAKERVOLUMECOMMAND']._serialized_end=1508
+ _globals['_GETSPEAKERVOLUMECOMMAND_REQUEST']._serialized_start=416
+ _globals['_GETSPEAKERVOLUMECOMMAND_REQUEST']._serialized_end=474
+ _globals['_GETSPEAKERVOLUMECOMMAND_FEEDBACK']._serialized_start=1432
+ _globals['_GETSPEAKERVOLUMECOMMAND_FEEDBACK']._serialized_end=1508
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/speaker_command_pb2.pyi b/src/cmvr/api/speaker_command_pb2.pyi
new file mode 100644
index 0000000..016cd2e
--- /dev/null
+++ b/src/cmvr/api/speaker_command_pb2.pyi
@@ -0,0 +1,154 @@
+from cmvr.api import common_pb2 as _common_pb2
+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 Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class AudioData(_message.Message):
+ __slots__ = ("data", "sample_rate", "channels", "format", "codec")
+ class AudioFormat(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PCM: _ClassVar[AudioData.AudioFormat]
+ MP3: _ClassVar[AudioData.AudioFormat]
+ AAC: _ClassVar[AudioData.AudioFormat]
+ WAV: _ClassVar[AudioData.AudioFormat]
+ PCM: AudioData.AudioFormat
+ MP3: AudioData.AudioFormat
+ AAC: AudioData.AudioFormat
+ WAV: AudioData.AudioFormat
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ SAMPLE_RATE_FIELD_NUMBER: _ClassVar[int]
+ CHANNELS_FIELD_NUMBER: _ClassVar[int]
+ FORMAT_FIELD_NUMBER: _ClassVar[int]
+ CODEC_FIELD_NUMBER: _ClassVar[int]
+ data: bytes
+ sample_rate: int
+ channels: int
+ format: AudioData.AudioFormat
+ codec: str
+ def __init__(self, data: _Optional[bytes] = ..., sample_rate: _Optional[int] = ..., channels: _Optional[int] = ..., format: _Optional[_Union[AudioData.AudioFormat, str]] = ..., codec: _Optional[str] = ...) -> None: ...
+
+class SpeakerState(_message.Message):
+ __slots__ = ("is_initialized", "is_running", "is_decoding", "is_paused", "volume", "error_message")
+ IS_INITIALIZED_FIELD_NUMBER: _ClassVar[int]
+ IS_RUNNING_FIELD_NUMBER: _ClassVar[int]
+ IS_DECODING_FIELD_NUMBER: _ClassVar[int]
+ IS_PAUSED_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ is_initialized: bool
+ is_running: bool
+ is_decoding: bool
+ is_paused: bool
+ volume: int
+ error_message: str
+ def __init__(self, is_initialized: _Optional[bool] = ..., is_running: _Optional[bool] = ..., is_decoding: _Optional[bool] = ..., is_paused: _Optional[bool] = ..., volume: _Optional[int] = ..., error_message: _Optional[str] = ...) -> None: ...
+
+class GetSpeakerStateCommand(_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: SpeakerState
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., state: _Optional[_Union[SpeakerState, _Mapping]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class PlayAudioCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "audio_path")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ AUDIO_PATH_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ audio_path: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., audio_path: _Optional[str] = ...) -> 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 StopSpeakerCommand(_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",)
+ 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 PauseSpeakerCommand(_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",)
+ 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 ResumeSpeakerCommand(_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",)
+ 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 SetSpeakerVolumeCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "volume")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ volume: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., volume: _Optional[int] = ...) -> 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 GetSpeakerVolumeCommand(_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", "volume")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ volume: int
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., volume: _Optional[int] = ...) -> None: ...
+ def __init__(self) -> None: ...
diff --git a/src/cmvr/api/speaker_command_pb2_grpc.py b/src/cmvr/api/speaker_command_pb2_grpc.py
new file mode 100644
index 0000000..de6592f
--- /dev/null
+++ b/src/cmvr/api/speaker_command_pb2_grpc.py
@@ -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/speaker_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}.'
+ )
diff --git a/src/cmvr/api/speaker_service_pb2.py b/src/cmvr/api/speaker_service_pb2.py
new file mode 100644
index 0000000..6f8cd65
--- /dev/null
+++ b/src/cmvr/api/speaker_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/speaker_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/speaker_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import speaker_command_pb2 as cmvr_dot_api_dot_speaker__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63mvr/api/speaker_service.proto\x12\x08\x63mvr.api\x1a\x1e\x63mvr/api/speaker_command.proto2\xb0\x05\n\x0eSpeakerService\x12`\n\tGetStatus\x12(.cmvr.api.GetSpeakerStateCommand.Request\x1a).cmvr.api.GetSpeakerStateCommand.Feedback\x12T\n\tPlayAudio\x12\".cmvr.api.PlayAudioCommand.Request\x1a#.cmvr.api.PlayAudioCommand.Feedback\x12[\n\x0cStopPlayback\x12$.cmvr.api.StopSpeakerCommand.Request\x1a%.cmvr.api.StopSpeakerCommand.Feedback\x12^\n\rPausePlayback\x12%.cmvr.api.PauseSpeakerCommand.Request\x1a&.cmvr.api.PauseSpeakerCommand.Feedback\x12\x61\n\x0eResumePlayback\x12&.cmvr.api.ResumeSpeakerCommand.Request\x1a\'.cmvr.api.ResumeSpeakerCommand.Feedback\x12\x62\n\tSetVolume\x12).cmvr.api.SetSpeakerVolumeCommand.Request\x1a*.cmvr.api.SetSpeakerVolumeCommand.Feedback\x12\x62\n\tGetVolume\x12).cmvr.api.GetSpeakerVolumeCommand.Request\x1a*.cmvr.api.GetSpeakerVolumeCommand.Feedbackb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.speaker_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_SPEAKERSERVICE']._serialized_start=77
+ _globals['_SPEAKERSERVICE']._serialized_end=765
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/speaker_service_pb2.pyi b/src/cmvr/api/speaker_service_pb2.pyi
new file mode 100644
index 0000000..b32548a
--- /dev/null
+++ b/src/cmvr/api/speaker_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import speaker_command_pb2 as _speaker_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/speaker_service_pb2_grpc.py b/src/cmvr/api/speaker_service_pb2_grpc.py
new file mode 100644
index 0000000..5914f50
--- /dev/null
+++ b/src/cmvr/api/speaker_service_pb2_grpc.py
@@ -0,0 +1,357 @@
+# 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 speaker_command_pb2 as cmvr_dot_api_dot_speaker__command__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/speaker_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 SpeakerServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetStatus = channel.unary_unary(
+ '/cmvr.api.SpeakerService/GetStatus',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Feedback.FromString,
+ _registered_method=True)
+ self.PlayAudio = channel.unary_unary(
+ '/cmvr.api.SpeakerService/PlayAudio',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StopPlayback = channel.unary_unary(
+ '/cmvr.api.SpeakerService/StopPlayback',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Feedback.FromString,
+ _registered_method=True)
+ self.PausePlayback = channel.unary_unary(
+ '/cmvr.api.SpeakerService/PausePlayback',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Feedback.FromString,
+ _registered_method=True)
+ self.ResumePlayback = channel.unary_unary(
+ '/cmvr.api.SpeakerService/ResumePlayback',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Feedback.FromString,
+ _registered_method=True)
+ self.SetVolume = channel.unary_unary(
+ '/cmvr.api.SpeakerService/SetVolume',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetVolume = channel.unary_unary(
+ '/cmvr.api.SpeakerService/GetVolume',
+ request_serializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Feedback.FromString,
+ _registered_method=True)
+
+
+class SpeakerServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def GetStatus(self, request, context):
+ """基本控制
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def PlayAudio(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StopPlayback(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def PausePlayback(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def ResumePlayback(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def SetVolume(self, request, context):
+ """音量控制
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetVolume(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_SpeakerServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetStatus,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Feedback.SerializeToString,
+ ),
+ 'PlayAudio': grpc.unary_unary_rpc_method_handler(
+ servicer.PlayAudio,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Feedback.SerializeToString,
+ ),
+ 'StopPlayback': grpc.unary_unary_rpc_method_handler(
+ servicer.StopPlayback,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Feedback.SerializeToString,
+ ),
+ 'PausePlayback': grpc.unary_unary_rpc_method_handler(
+ servicer.PausePlayback,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Feedback.SerializeToString,
+ ),
+ 'ResumePlayback': grpc.unary_unary_rpc_method_handler(
+ servicer.ResumePlayback,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Feedback.SerializeToString,
+ ),
+ 'SetVolume': grpc.unary_unary_rpc_method_handler(
+ servicer.SetVolume,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Feedback.SerializeToString,
+ ),
+ 'GetVolume': grpc.unary_unary_rpc_method_handler(
+ servicer.GetVolume,
+ request_deserializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.SpeakerService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.SpeakerService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class SpeakerService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetStatus(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.SpeakerService/GetStatus',
+ cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerStateCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def PlayAudio(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.SpeakerService/PlayAudio',
+ cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.PlayAudioCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StopPlayback(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.SpeakerService/StopPlayback',
+ cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.StopSpeakerCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def PausePlayback(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.SpeakerService/PausePlayback',
+ cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.PauseSpeakerCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def ResumePlayback(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.SpeakerService/ResumePlayback',
+ cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.ResumeSpeakerCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def SetVolume(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.SpeakerService/SetVolume',
+ cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.SetSpeakerVolumeCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetVolume(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.SpeakerService/GetVolume',
+ cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_speaker__command__pb2.GetSpeakerVolumeCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/system_command_pb2.py b/src/cmvr/api/system_command_pb2.py
new file mode 100644
index 0000000..ca596b5
--- /dev/null
+++ b/src/cmvr/api/system_command_pb2.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/system_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/system_command.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import common_pb2 as cmvr_dot_api_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x63mvr/api/system_command.proto\x12\x08\x63mvr.api\x1a\x15\x63mvr/api/common.proto\"J\n\nDeviceList\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12)\n\x0b\x64\x65vice_type\x18\x02 \x01(\x0e\x32\x14.cmvr.api.DeviceType\"\xd5\x01\n\x14GetSystemInfoCommand\x1a\t\n\x07Request\x1a\xb1\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x13\n\x0bsystem_name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\n\n\x02os\x18\x05 \x01(\t\x12\x16\n\x0ekernel_version\x18\x06 \x01(\t\x12\x14\n\x0c\x61rchitecture\x18\x07 \x01(\t\"\xf8\x01\n\x16GetSystemStatusCommand\x1a\t\n\x07Request\x1a\xd2\x01\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\x12\x11\n\tcpu_usage\x18\x02 \x01(\x02\x12\x14\n\x0cmem_total_mb\x18\x03 \x01(\x02\x12\x13\n\x0bmem_used_mb\x18\x04 \x01(\x02\x12\x15\n\rdisk_total_gb\x18\x05 \x01(\x02\x12\x14\n\x0c\x64isk_used_gb\x18\x06 \x01(\x02\x12)\n\x0b\x64\x65vice_list\x18\x07 \x03(\x0b\x32\x14.cmvr.api.DeviceList\"\xb6\x01\n\x13UpdateParamsCommand\x1a\x61\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x12%\n\x06params\x18\x02 \x03(\x0b\x32\x15.cmvr.api.ConfigParam\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback\"\x8a\x01\n\x0eStopAllCommand\x1a:\n\x07Request\x12/\n\x06header\x18\x01 \x01(\x0b\x32\x1f.cmvr.api.CommandHeader.Request\x1a<\n\x08\x46\x65\x65\x64\x62\x61\x63k\x12\x30\n\x06header\x18\x01 \x01(\x0b\x32 .cmvr.api.CommandHeader.Feedback*}\n\nDeviceType\x12\x07\n\x03\x41GV\x10\x00\x12\x0b\n\x07\x42\x61ttery\x10\x01\x12\n\n\x06\x43\x61mera\x10\x02\x12\x0b\n\x07\x44\x65xHand\x10\x03\x12\x0b\n\x07Gripper\x10\x04\x12\x0e\n\nMicrophone\x10\x05\x12\t\n\x05Robot\x10\x06\x12\x0b\n\x07Speaker\x10\x07\x12\x0b\n\x07Unknown\x10\x14\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.system_command_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_DEVICETYPE']._serialized_start=935
+ _globals['_DEVICETYPE']._serialized_end=1060
+ _globals['_DEVICELIST']._serialized_start=66
+ _globals['_DEVICELIST']._serialized_end=140
+ _globals['_GETSYSTEMINFOCOMMAND']._serialized_start=143
+ _globals['_GETSYSTEMINFOCOMMAND']._serialized_end=356
+ _globals['_GETSYSTEMINFOCOMMAND_REQUEST']._serialized_start=167
+ _globals['_GETSYSTEMINFOCOMMAND_REQUEST']._serialized_end=176
+ _globals['_GETSYSTEMINFOCOMMAND_FEEDBACK']._serialized_start=179
+ _globals['_GETSYSTEMINFOCOMMAND_FEEDBACK']._serialized_end=356
+ _globals['_GETSYSTEMSTATUSCOMMAND']._serialized_start=359
+ _globals['_GETSYSTEMSTATUSCOMMAND']._serialized_end=607
+ _globals['_GETSYSTEMSTATUSCOMMAND_REQUEST']._serialized_start=167
+ _globals['_GETSYSTEMSTATUSCOMMAND_REQUEST']._serialized_end=176
+ _globals['_GETSYSTEMSTATUSCOMMAND_FEEDBACK']._serialized_start=397
+ _globals['_GETSYSTEMSTATUSCOMMAND_FEEDBACK']._serialized_end=607
+ _globals['_UPDATEPARAMSCOMMAND']._serialized_start=610
+ _globals['_UPDATEPARAMSCOMMAND']._serialized_end=792
+ _globals['_UPDATEPARAMSCOMMAND_REQUEST']._serialized_start=633
+ _globals['_UPDATEPARAMSCOMMAND_REQUEST']._serialized_end=730
+ _globals['_UPDATEPARAMSCOMMAND_FEEDBACK']._serialized_start=179
+ _globals['_UPDATEPARAMSCOMMAND_FEEDBACK']._serialized_end=239
+ _globals['_STOPALLCOMMAND']._serialized_start=795
+ _globals['_STOPALLCOMMAND']._serialized_end=933
+ _globals['_STOPALLCOMMAND_REQUEST']._serialized_start=633
+ _globals['_STOPALLCOMMAND_REQUEST']._serialized_end=691
+ _globals['_STOPALLCOMMAND_FEEDBACK']._serialized_start=179
+ _globals['_STOPALLCOMMAND_FEEDBACK']._serialized_end=239
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/system_command_pb2.pyi b/src/cmvr/api/system_command_pb2.pyi
new file mode 100644
index 0000000..6190153
--- /dev/null
+++ b/src/cmvr/api/system_command_pb2.pyi
@@ -0,0 +1,116 @@
+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 DeviceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ AGV: _ClassVar[DeviceType]
+ Battery: _ClassVar[DeviceType]
+ Camera: _ClassVar[DeviceType]
+ DexHand: _ClassVar[DeviceType]
+ Gripper: _ClassVar[DeviceType]
+ Microphone: _ClassVar[DeviceType]
+ Robot: _ClassVar[DeviceType]
+ Speaker: _ClassVar[DeviceType]
+ Unknown: _ClassVar[DeviceType]
+AGV: DeviceType
+Battery: DeviceType
+Camera: DeviceType
+DexHand: DeviceType
+Gripper: DeviceType
+Microphone: DeviceType
+Robot: DeviceType
+Speaker: DeviceType
+Unknown: DeviceType
+
+class DeviceList(_message.Message):
+ __slots__ = ("device_id", "device_type")
+ DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
+ DEVICE_TYPE_FIELD_NUMBER: _ClassVar[int]
+ device_id: str
+ device_type: DeviceType
+ def __init__(self, device_id: _Optional[str] = ..., device_type: _Optional[_Union[DeviceType, str]] = ...) -> None: ...
+
+class GetSystemInfoCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ()
+ def __init__(self) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "system_name", "version", "description", "os", "kernel_version", "architecture")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ SYSTEM_NAME_FIELD_NUMBER: _ClassVar[int]
+ VERSION_FIELD_NUMBER: _ClassVar[int]
+ DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
+ OS_FIELD_NUMBER: _ClassVar[int]
+ KERNEL_VERSION_FIELD_NUMBER: _ClassVar[int]
+ ARCHITECTURE_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ system_name: str
+ version: str
+ description: str
+ os: str
+ kernel_version: str
+ architecture: str
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., system_name: _Optional[str] = ..., version: _Optional[str] = ..., description: _Optional[str] = ..., os: _Optional[str] = ..., kernel_version: _Optional[str] = ..., architecture: _Optional[str] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class GetSystemStatusCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ()
+ def __init__(self) -> None: ...
+ class Feedback(_message.Message):
+ __slots__ = ("header", "cpu_usage", "mem_total_mb", "mem_used_mb", "disk_total_gb", "disk_used_gb", "device_list")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ CPU_USAGE_FIELD_NUMBER: _ClassVar[int]
+ MEM_TOTAL_MB_FIELD_NUMBER: _ClassVar[int]
+ MEM_USED_MB_FIELD_NUMBER: _ClassVar[int]
+ DISK_TOTAL_GB_FIELD_NUMBER: _ClassVar[int]
+ DISK_USED_GB_FIELD_NUMBER: _ClassVar[int]
+ DEVICE_LIST_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Feedback
+ cpu_usage: float
+ mem_total_mb: float
+ mem_used_mb: float
+ disk_total_gb: float
+ disk_used_gb: float
+ device_list: _containers.RepeatedCompositeFieldContainer[DeviceList]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Feedback, _Mapping]] = ..., cpu_usage: _Optional[float] = ..., mem_total_mb: _Optional[float] = ..., mem_used_mb: _Optional[float] = ..., disk_total_gb: _Optional[float] = ..., disk_used_gb: _Optional[float] = ..., device_list: _Optional[_Iterable[_Union[DeviceList, _Mapping]]] = ...) -> None: ...
+ def __init__(self) -> None: ...
+
+class UpdateParamsCommand(_message.Message):
+ __slots__ = ()
+ class Request(_message.Message):
+ __slots__ = ("header", "params")
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ PARAMS_FIELD_NUMBER: _ClassVar[int]
+ header: _common_pb2.CommandHeader.Request
+ params: _containers.RepeatedCompositeFieldContainer[_common_pb2.ConfigParam]
+ def __init__(self, header: _Optional[_Union[_common_pb2.CommandHeader.Request, _Mapping]] = ..., params: _Optional[_Iterable[_Union[_common_pb2.ConfigParam, _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 StopAllCommand(_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",)
+ 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: ...
diff --git a/src/cmvr/api/system_command_pb2_grpc.py b/src/cmvr/api/system_command_pb2_grpc.py
new file mode 100644
index 0000000..e13624b
--- /dev/null
+++ b/src/cmvr/api/system_command_pb2_grpc.py
@@ -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/system_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}.'
+ )
diff --git a/src/cmvr/api/system_service_pb2.py b/src/cmvr/api/system_service_pb2.py
new file mode 100644
index 0000000..859ba7f
--- /dev/null
+++ b/src/cmvr/api/system_service_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/system_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/system_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.api import system_command_pb2 as cmvr_dot_api_dot_system__command__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x63mvr/api/system_service.proto\x12\x08\x63mvr.api\x1a\x1d\x63mvr/api/system_command.proto2\x90\x03\n\rSystemService\x12\x62\n\rGetSystemInfo\x12&.cmvr.api.GetSystemInfoCommand.Request\x1a\'.cmvr.api.GetSystemInfoCommand.Feedback\"\x00\x12h\n\x0fGetSystemStatus\x12(.cmvr.api.GetSystemStatusCommand.Request\x1a).cmvr.api.GetSystemStatusCommand.Feedback\"\x00\x12_\n\x0cUpdateParams\x12%.cmvr.api.UpdateParamsCommand.Request\x1a&.cmvr.api.UpdateParamsCommand.Feedback\"\x00\x12P\n\x07StopAll\x12 .cmvr.api.StopAllCommand.Request\x1a!.cmvr.api.StopAllCommand.Feedback\"\x00\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.system_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_SYSTEMSERVICE']._serialized_start=75
+ _globals['_SYSTEMSERVICE']._serialized_end=475
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/system_service_pb2.pyi b/src/cmvr/api/system_service_pb2.pyi
new file mode 100644
index 0000000..d696a2d
--- /dev/null
+++ b/src/cmvr/api/system_service_pb2.pyi
@@ -0,0 +1,5 @@
+from cmvr.api import system_command_pb2 as _system_command_pb2
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
diff --git a/src/cmvr/api/system_service_pb2_grpc.py b/src/cmvr/api/system_service_pb2_grpc.py
new file mode 100644
index 0000000..145c99c
--- /dev/null
+++ b/src/cmvr/api/system_service_pb2_grpc.py
@@ -0,0 +1,226 @@
+# 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 system_command_pb2 as cmvr_dot_api_dot_system__command__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/system_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 SystemServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetSystemInfo = channel.unary_unary(
+ '/cmvr.api.SystemService/GetSystemInfo',
+ request_serializer=cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Feedback.FromString,
+ _registered_method=True)
+ self.GetSystemStatus = channel.unary_unary(
+ '/cmvr.api.SystemService/GetSystemStatus',
+ request_serializer=cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Feedback.FromString,
+ _registered_method=True)
+ self.UpdateParams = channel.unary_unary(
+ '/cmvr.api.SystemService/UpdateParams',
+ request_serializer=cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Feedback.FromString,
+ _registered_method=True)
+ self.StopAll = channel.unary_unary(
+ '/cmvr.api.SystemService/StopAll',
+ request_serializer=cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Request.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Feedback.FromString,
+ _registered_method=True)
+
+
+class SystemServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def GetSystemInfo(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetSystemStatus(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def UpdateParams(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StopAll(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_SystemServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetSystemInfo': grpc.unary_unary_rpc_method_handler(
+ servicer.GetSystemInfo,
+ request_deserializer=cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Feedback.SerializeToString,
+ ),
+ 'GetSystemStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetSystemStatus,
+ request_deserializer=cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Feedback.SerializeToString,
+ ),
+ 'UpdateParams': grpc.unary_unary_rpc_method_handler(
+ servicer.UpdateParams,
+ request_deserializer=cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Feedback.SerializeToString,
+ ),
+ 'StopAll': grpc.unary_unary_rpc_method_handler(
+ servicer.StopAll,
+ request_deserializer=cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Request.FromString,
+ response_serializer=cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Feedback.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.SystemService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.SystemService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class SystemService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetSystemInfo(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.SystemService/GetSystemInfo',
+ cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_system__command__pb2.GetSystemInfoCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetSystemStatus(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.SystemService/GetSystemStatus',
+ cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_system__command__pb2.GetSystemStatusCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def UpdateParams(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.SystemService/UpdateParams',
+ cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_system__command__pb2.UpdateParamsCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StopAll(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.SystemService/StopAll',
+ cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Request.SerializeToString,
+ cmvr_dot_api_dot_system__command__pb2.StopAllCommand.Feedback.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/api/test_service_pb2.py b/src/cmvr/api/test_service_pb2.py
new file mode 100644
index 0000000..2241a7d
--- /dev/null
+++ b/src/cmvr/api/test_service_pb2.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/api/test_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/test_service.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x63mvr/api/test_service.proto\x12\x08\x63mvr.api\"\x1b\n\x0bTestReqeust\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\"\x1c\n\x0cTestResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t2D\n\x0bTestService\x12\x35\n\x04\x43\x61ll\x12\x15.cmvr.api.TestReqeust\x1a\x16.cmvr.api.TestResponseb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.api.test_service_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TESTREQEUST']._serialized_start=41
+ _globals['_TESTREQEUST']._serialized_end=68
+ _globals['_TESTRESPONSE']._serialized_start=70
+ _globals['_TESTRESPONSE']._serialized_end=98
+ _globals['_TESTSERVICE']._serialized_start=100
+ _globals['_TESTSERVICE']._serialized_end=168
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/api/test_service_pb2.pyi b/src/cmvr/api/test_service_pb2.pyi
new file mode 100644
index 0000000..586bbd5
--- /dev/null
+++ b/src/cmvr/api/test_service_pb2.pyi
@@ -0,0 +1,17 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from typing import ClassVar as _ClassVar, Optional as _Optional
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class TestReqeust(_message.Message):
+ __slots__ = ("data",)
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ data: str
+ def __init__(self, data: _Optional[str] = ...) -> None: ...
+
+class TestResponse(_message.Message):
+ __slots__ = ("data",)
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ data: str
+ def __init__(self, data: _Optional[str] = ...) -> None: ...
diff --git a/src/cmvr/api/test_service_pb2_grpc.py b/src/cmvr/api/test_service_pb2_grpc.py
new file mode 100644
index 0000000..df27866
--- /dev/null
+++ b/src/cmvr/api/test_service_pb2_grpc.py
@@ -0,0 +1,97 @@
+# 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 test_service_pb2 as cmvr_dot_api_dot_test__service__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/test_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 TestServiceStub:
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.Call = channel.unary_unary(
+ '/cmvr.api.TestService/Call',
+ request_serializer=cmvr_dot_api_dot_test__service__pb2.TestReqeust.SerializeToString,
+ response_deserializer=cmvr_dot_api_dot_test__service__pb2.TestResponse.FromString,
+ _registered_method=True)
+
+
+class TestServiceServicer:
+ """Missing associated documentation comment in .proto file."""
+
+ def Call(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_TestServiceServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'Call': grpc.unary_unary_rpc_method_handler(
+ servicer.Call,
+ request_deserializer=cmvr_dot_api_dot_test__service__pb2.TestReqeust.FromString,
+ response_serializer=cmvr_dot_api_dot_test__service__pb2.TestResponse.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'cmvr.api.TestService', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('cmvr.api.TestService', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class TestService:
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def Call(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.TestService/Call',
+ cmvr_dot_api_dot_test__service__pb2.TestReqeust.SerializeToString,
+ cmvr_dot_api_dot_test__service__pb2.TestResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/src/cmvr/common/__pycache__/geometry_pb2.cpython-310.pyc b/src/cmvr/common/__pycache__/geometry_pb2.cpython-310.pyc
new file mode 100644
index 0000000..4ec4a99
Binary files /dev/null and b/src/cmvr/common/__pycache__/geometry_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/common/__pycache__/geometry_pb2.cpython-312.pyc b/src/cmvr/common/__pycache__/geometry_pb2.cpython-312.pyc
new file mode 100644
index 0000000..780fb0d
Binary files /dev/null and b/src/cmvr/common/__pycache__/geometry_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-310.pyc b/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..062f5c0
Binary files /dev/null and b/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-312.pyc b/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..861a77d
Binary files /dev/null and b/src/cmvr/common/__pycache__/geometry_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/common/geometry_pb2.py b/src/cmvr/common/geometry_pb2.py
new file mode 100644
index 0000000..34864a8
--- /dev/null
+++ b/src/cmvr/common/geometry_pb2.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/common/geometry.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/common/geometry.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/common/geometry.proto\x12\x0b\x63mvr.common\"2\n\x04Vec2\x12\x0e\n\x01x\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0e\n\x01y\x18\x02 \x01(\x01H\x01\x88\x01\x01\x42\x04\n\x02_xB\x04\n\x02_y\"H\n\x04Vec3\x12\x0e\n\x01x\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0e\n\x01y\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x0e\n\x01z\x18\x03 \x01(\x01H\x02\x88\x01\x01\x42\x04\n\x02_xB\x04\n\x02_yB\x04\n\x02_z\"\x90\x01\n\x04Vec6\x12\x0e\n\x01x\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0e\n\x01y\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x0e\n\x01z\x18\x03 \x01(\x01H\x02\x88\x01\x01\x12\x0f\n\x02rx\x18\x04 \x01(\x01H\x03\x88\x01\x01\x12\x0f\n\x02ry\x18\x05 \x01(\x01H\x04\x88\x01\x01\x12\x0f\n\x02rz\x18\x06 \x01(\x01H\x05\x88\x01\x01\x42\x04\n\x02_xB\x04\n\x02_yB\x04\n\x02_zB\x05\n\x03_rxB\x05\n\x03_ryB\x05\n\x03_rz\"^\n\x04Quat\x12\x0e\n\x01w\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0e\n\x01x\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x0e\n\x01y\x18\x03 \x01(\x01H\x02\x88\x01\x01\x12\x0e\n\x01z\x18\x04 \x01(\x01H\x03\x88\x01\x01\x42\x04\n\x02_wB\x04\n\x02_xB\x04\n\x02_yB\x04\n\x02_z\"O\n\x05\x45uler\x12\x0f\n\x02rx\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0f\n\x02ry\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x0f\n\x02rz\x18\x03 \x01(\x01H\x02\x88\x01\x01\x42\x05\n\x03_rxB\x05\n\x03_ryB\x05\n\x03_rz\"w\n\x06Pose3d\x12#\n\x08position\x18\x01 \x01(\x0b\x32\x11.cmvr.common.Vec3\x12%\n\nquaternion\x18\x02 \x01(\x0b\x32\x11.cmvr.common.Quat\x12!\n\x05\x65uler\x18\x03 \x01(\x0b\x32\x12.cmvr.common.Euler\"R\n\x06Pose2d\x12\x0e\n\x01x\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x0e\n\x01y\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x12\n\x05theta\x18\x03 \x01(\x01H\x02\x88\x01\x01\x42\x04\n\x02_xB\x04\n\x02_yB\x08\n\x06_theta\"\xf0\x01\n\x04Mat3\x12\x10\n\x03m00\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03m01\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03m02\x18\x03 \x01(\x01H\x02\x88\x01\x01\x12\x10\n\x03m10\x18\x04 \x01(\x01H\x03\x88\x01\x01\x12\x10\n\x03m11\x18\x05 \x01(\x01H\x04\x88\x01\x01\x12\x10\n\x03m12\x18\x06 \x01(\x01H\x05\x88\x01\x01\x12\x10\n\x03m20\x18\x07 \x01(\x01H\x06\x88\x01\x01\x12\x10\n\x03m21\x18\x08 \x01(\x01H\x07\x88\x01\x01\x12\x10\n\x03m22\x18\t \x01(\x01H\x08\x88\x01\x01\x42\x06\n\x04_m00B\x06\n\x04_m01B\x06\n\x04_m02B\x06\n\x04_m10B\x06\n\x04_m11B\x06\n\x04_m12B\x06\n\x04_m20B\x06\n\x04_m21B\x06\n\x04_m22\"\xa6\x03\n\x04Mat4\x12\x10\n\x03m00\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x03m01\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03m02\x18\x03 \x01(\x01H\x02\x88\x01\x01\x12\x10\n\x03m03\x18\x04 \x01(\x01H\x03\x88\x01\x01\x12\x10\n\x03m10\x18\x05 \x01(\x01H\x04\x88\x01\x01\x12\x10\n\x03m11\x18\x06 \x01(\x01H\x05\x88\x01\x01\x12\x10\n\x03m12\x18\x07 \x01(\x01H\x06\x88\x01\x01\x12\x10\n\x03m13\x18\x08 \x01(\x01H\x07\x88\x01\x01\x12\x10\n\x03m20\x18\t \x01(\x01H\x08\x88\x01\x01\x12\x10\n\x03m21\x18\n \x01(\x01H\t\x88\x01\x01\x12\x10\n\x03m22\x18\x0b \x01(\x01H\n\x88\x01\x01\x12\x10\n\x03m23\x18\x0c \x01(\x01H\x0b\x88\x01\x01\x12\x10\n\x03m30\x18\r \x01(\x01H\x0c\x88\x01\x01\x12\x10\n\x03m31\x18\x0e \x01(\x01H\r\x88\x01\x01\x12\x10\n\x03m32\x18\x0f \x01(\x01H\x0e\x88\x01\x01\x12\x10\n\x03m33\x18\x10 \x01(\x01H\x0f\x88\x01\x01\x42\x06\n\x04_m00B\x06\n\x04_m01B\x06\n\x04_m02B\x06\n\x04_m03B\x06\n\x04_m10B\x06\n\x04_m11B\x06\n\x04_m12B\x06\n\x04_m13B\x06\n\x04_m20B\x06\n\x04_m21B\x06\n\x04_m22B\x06\n\x04_m23B\x06\n\x04_m30B\x06\n\x04_m31B\x06\n\x04_m32B\x06\n\x04_m33b\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.common.geometry_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_VEC2']._serialized_start=43
+ _globals['_VEC2']._serialized_end=93
+ _globals['_VEC3']._serialized_start=95
+ _globals['_VEC3']._serialized_end=167
+ _globals['_VEC6']._serialized_start=170
+ _globals['_VEC6']._serialized_end=314
+ _globals['_QUAT']._serialized_start=316
+ _globals['_QUAT']._serialized_end=410
+ _globals['_EULER']._serialized_start=412
+ _globals['_EULER']._serialized_end=491
+ _globals['_POSE3D']._serialized_start=493
+ _globals['_POSE3D']._serialized_end=612
+ _globals['_POSE2D']._serialized_start=614
+ _globals['_POSE2D']._serialized_end=696
+ _globals['_MAT3']._serialized_start=699
+ _globals['_MAT3']._serialized_end=939
+ _globals['_MAT4']._serialized_start=942
+ _globals['_MAT4']._serialized_end=1364
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/common/geometry_pb2.pyi b/src/cmvr/common/geometry_pb2.pyi
new file mode 100644
index 0000000..0606137
--- /dev/null
+++ b/src/cmvr/common/geometry_pb2.pyi
@@ -0,0 +1,140 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class Vec2(_message.Message):
+ __slots__ = ("x", "y")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ...) -> None: ...
+
+class Vec3(_message.Message):
+ __slots__ = ("x", "y", "z")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ Z_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ z: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ...) -> None: ...
+
+class Vec6(_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 Quat(_message.Message):
+ __slots__ = ("w", "x", "y", "z")
+ W_FIELD_NUMBER: _ClassVar[int]
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ Z_FIELD_NUMBER: _ClassVar[int]
+ w: float
+ x: float
+ y: float
+ z: float
+ def __init__(self, w: _Optional[float] = ..., x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ...) -> None: ...
+
+class Euler(_message.Message):
+ __slots__ = ("rx", "ry", "rz")
+ RX_FIELD_NUMBER: _ClassVar[int]
+ RY_FIELD_NUMBER: _ClassVar[int]
+ RZ_FIELD_NUMBER: _ClassVar[int]
+ rx: float
+ ry: float
+ rz: float
+ def __init__(self, rx: _Optional[float] = ..., ry: _Optional[float] = ..., rz: _Optional[float] = ...) -> None: ...
+
+class Pose3d(_message.Message):
+ __slots__ = ("position", "quaternion", "euler")
+ POSITION_FIELD_NUMBER: _ClassVar[int]
+ QUATERNION_FIELD_NUMBER: _ClassVar[int]
+ EULER_FIELD_NUMBER: _ClassVar[int]
+ position: Vec3
+ quaternion: Quat
+ euler: Euler
+ def __init__(self, position: _Optional[_Union[Vec3, _Mapping]] = ..., quaternion: _Optional[_Union[Quat, _Mapping]] = ..., euler: _Optional[_Union[Euler, _Mapping]] = ...) -> None: ...
+
+class Pose2d(_message.Message):
+ __slots__ = ("x", "y", "theta")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ THETA_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ theta: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., theta: _Optional[float] = ...) -> None: ...
+
+class Mat3(_message.Message):
+ __slots__ = ("m00", "m01", "m02", "m10", "m11", "m12", "m20", "m21", "m22")
+ M00_FIELD_NUMBER: _ClassVar[int]
+ M01_FIELD_NUMBER: _ClassVar[int]
+ M02_FIELD_NUMBER: _ClassVar[int]
+ M10_FIELD_NUMBER: _ClassVar[int]
+ M11_FIELD_NUMBER: _ClassVar[int]
+ M12_FIELD_NUMBER: _ClassVar[int]
+ M20_FIELD_NUMBER: _ClassVar[int]
+ M21_FIELD_NUMBER: _ClassVar[int]
+ M22_FIELD_NUMBER: _ClassVar[int]
+ m00: float
+ m01: float
+ m02: float
+ m10: float
+ m11: float
+ m12: float
+ m20: float
+ m21: float
+ m22: float
+ def __init__(self, m00: _Optional[float] = ..., m01: _Optional[float] = ..., m02: _Optional[float] = ..., m10: _Optional[float] = ..., m11: _Optional[float] = ..., m12: _Optional[float] = ..., m20: _Optional[float] = ..., m21: _Optional[float] = ..., m22: _Optional[float] = ...) -> None: ...
+
+class Mat4(_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: ...
diff --git a/src/cmvr/common/geometry_pb2_grpc.py b/src/cmvr/common/geometry_pb2_grpc.py
new file mode 100644
index 0000000..180a532
--- /dev/null
+++ b/src/cmvr/common/geometry_pb2_grpc.py
@@ -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/common/geometry_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}.'
+ )
diff --git a/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b6ef8ae
Binary files /dev/null and b/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..a2b79a5
Binary files /dev/null and b/src/cmvr/config/__pycache__/joint_limits_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..8208bb9
Binary files /dev/null and b/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..9b5ca54
Binary files /dev/null and b/src/cmvr/config/__pycache__/joint_limits_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..bba24e0
Binary files /dev/null and b/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..22102d7
Binary files /dev/null and b/src/cmvr/config/__pycache__/lawba_ik_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..f11d9b5
Binary files /dev/null and b/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..4b8f610
Binary files /dev/null and b/src/cmvr/config/__pycache__/lawba_ik_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..7e1c0e9
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..07d9116
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1830a58
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..410981b
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_dls_ik_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..62f3955
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..9d00d9a
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1fc36a5
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..b8efde0
Binary files /dev/null and b/src/cmvr/config/__pycache__/pinocchio_qp_ik_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..a9f8cd3
Binary files /dev/null and b/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..04db026
Binary files /dev/null and b/src/cmvr/config/__pycache__/srs_ik_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..b420eae
Binary files /dev/null and b/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..e818646
Binary files /dev/null and b/src/cmvr/config/__pycache__/srs_ik_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..efa4719
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..73cd09b
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..96dd86f
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..5761af9
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_algorithm_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-310.pyc b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b6ce929
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-312.pyc b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..68c155d
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1d70193
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..d736658
Binary files /dev/null and b/src/cmvr/config/__pycache__/touch_screen_common_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-310.pyc b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..c815843
Binary files /dev/null and b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-312.pyc b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..eb0bc52
Binary files /dev/null and b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..eae9d16
Binary files /dev/null and b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..df5581f
Binary files /dev/null and b/src/cmvr/config/agv_config/__pycache__/agv_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/agv_config/agv_config_pb2.py b/src/cmvr/config/agv_config/agv_config_pb2.py
new file mode 100644
index 0000000..36e373e
--- /dev/null
+++ b/src/cmvr/config/agv_config/agv_config_pb2.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/agv_config/agv_config.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/config/agv_config/agv_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'cmvr/config/agv_config/agv_config.proto\x12\x0b\x63mvr.config\"3\n\x0bMyAgvConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\x05\"\xc7\x03\n\x10Src1100AgvConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0e\n\x06\x65nable\x18\x03 \x01(\x08\x12\x13\n\x0bport_status\x18\x04 \x01(\x05\x12\x14\n\x0cport_control\x18\x05 \x01(\x05\x12\x10\n\x08port_nav\x18\x06 \x01(\x05\x12\x13\n\x0bport_config\x18\x07 \x01(\x05\x12\x12\n\nport_other\x18\x08 \x01(\x05\x12\x11\n\tport_push\x18\t \x01(\x05\x12\x1a\n\x12\x63onnect_timeout_ms\x18\n \x01(\x05\x12\x17\n\x0frecv_timeout_ms\x18\x0b \x01(\x05\x12\x19\n\x11\x65nable_state_push\x18\x0c \x01(\x08\x12\x1e\n\x16state_push_interval_ms\x18\r \x01(\x05\x12\"\n\x1astate_push_included_fields\x18\x0e \x03(\t\x12\"\n\x1astate_push_excluded_fields\x18\x0f \x03(\t\x12\x19\n\x11\x65nable_map_update\x18\x10 \x01(\x08\x12\x1e\n\x16map_update_interval_ms\x18\x11 \x01(\x05\x12\x1f\n\x17map_update_history_size\x18\x12 \x01(\r\"\x8a\x01\n\x0f\x41GVDeviceConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12*\n\x06my_agv\x18\n \x01(\x0b\x32\x18.cmvr.config.MyAgvConfigH\x00\x12\x34\n\x0bsrc1100_agv\x18\x0b \x01(\x0b\x32\x1d.cmvr.config.Src1100AgvConfigH\x00\x42\t\n\x07\x62\x61\x63kend\"7\n\tAGVConfig\x12*\n\x04\x61gvs\x18\x01 \x03(\x0b\x32\x1c.cmvr.config.AGVDeviceConfig\"4\n\rAGVRootConfig\x12#\n\x03\x61gv\x18\x01 \x01(\x0b\x32\x16.cmvr.config.AGVConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.agv_config.agv_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_MYAGVCONFIG']._serialized_start=56
+ _globals['_MYAGVCONFIG']._serialized_end=107
+ _globals['_SRC1100AGVCONFIG']._serialized_start=110
+ _globals['_SRC1100AGVCONFIG']._serialized_end=565
+ _globals['_AGVDEVICECONFIG']._serialized_start=568
+ _globals['_AGVDEVICECONFIG']._serialized_end=706
+ _globals['_AGVCONFIG']._serialized_start=708
+ _globals['_AGVCONFIG']._serialized_end=763
+ _globals['_AGVROOTCONFIG']._serialized_start=765
+ _globals['_AGVROOTCONFIG']._serialized_end=817
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/agv_config/agv_config_pb2.pyi b/src/cmvr/config/agv_config/agv_config_pb2.pyi
new file mode 100644
index 0000000..b83665f
--- /dev/null
+++ b/src/cmvr/config/agv_config/agv_config_pb2.pyi
@@ -0,0 +1,79 @@
+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 MyAgvConfig(_message.Message):
+ __slots__ = ("id", "ip", "port")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ PORT_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ip: str
+ port: int
+ def __init__(self, id: _Optional[str] = ..., ip: _Optional[str] = ..., port: _Optional[int] = ...) -> None: ...
+
+class Src1100AgvConfig(_message.Message):
+ __slots__ = ("id", "ip", "enable", "port_status", "port_control", "port_nav", "port_config", "port_other", "port_push", "connect_timeout_ms", "recv_timeout_ms", "enable_state_push", "state_push_interval_ms", "state_push_included_fields", "state_push_excluded_fields", "enable_map_update", "map_update_interval_ms", "map_update_history_size")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_FIELD_NUMBER: _ClassVar[int]
+ PORT_STATUS_FIELD_NUMBER: _ClassVar[int]
+ PORT_CONTROL_FIELD_NUMBER: _ClassVar[int]
+ PORT_NAV_FIELD_NUMBER: _ClassVar[int]
+ PORT_CONFIG_FIELD_NUMBER: _ClassVar[int]
+ PORT_OTHER_FIELD_NUMBER: _ClassVar[int]
+ PORT_PUSH_FIELD_NUMBER: _ClassVar[int]
+ CONNECT_TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int]
+ RECV_TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_STATE_PUSH_FIELD_NUMBER: _ClassVar[int]
+ STATE_PUSH_INTERVAL_MS_FIELD_NUMBER: _ClassVar[int]
+ STATE_PUSH_INCLUDED_FIELDS_FIELD_NUMBER: _ClassVar[int]
+ STATE_PUSH_EXCLUDED_FIELDS_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_MAP_UPDATE_FIELD_NUMBER: _ClassVar[int]
+ MAP_UPDATE_INTERVAL_MS_FIELD_NUMBER: _ClassVar[int]
+ MAP_UPDATE_HISTORY_SIZE_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ip: str
+ enable: bool
+ port_status: int
+ port_control: int
+ port_nav: int
+ port_config: int
+ port_other: int
+ port_push: int
+ connect_timeout_ms: int
+ recv_timeout_ms: int
+ enable_state_push: bool
+ state_push_interval_ms: int
+ state_push_included_fields: _containers.RepeatedScalarFieldContainer[str]
+ state_push_excluded_fields: _containers.RepeatedScalarFieldContainer[str]
+ enable_map_update: bool
+ map_update_interval_ms: int
+ map_update_history_size: int
+ def __init__(self, id: _Optional[str] = ..., ip: _Optional[str] = ..., enable: _Optional[bool] = ..., port_status: _Optional[int] = ..., port_control: _Optional[int] = ..., port_nav: _Optional[int] = ..., port_config: _Optional[int] = ..., port_other: _Optional[int] = ..., port_push: _Optional[int] = ..., connect_timeout_ms: _Optional[int] = ..., recv_timeout_ms: _Optional[int] = ..., enable_state_push: _Optional[bool] = ..., state_push_interval_ms: _Optional[int] = ..., state_push_included_fields: _Optional[_Iterable[str]] = ..., state_push_excluded_fields: _Optional[_Iterable[str]] = ..., enable_map_update: _Optional[bool] = ..., map_update_interval_ms: _Optional[int] = ..., map_update_history_size: _Optional[int] = ...) -> None: ...
+
+class AGVDeviceConfig(_message.Message):
+ __slots__ = ("id", "my_agv", "src1100_agv")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ MY_AGV_FIELD_NUMBER: _ClassVar[int]
+ SRC1100_AGV_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ my_agv: MyAgvConfig
+ src1100_agv: Src1100AgvConfig
+ def __init__(self, id: _Optional[str] = ..., my_agv: _Optional[_Union[MyAgvConfig, _Mapping]] = ..., src1100_agv: _Optional[_Union[Src1100AgvConfig, _Mapping]] = ...) -> None: ...
+
+class AGVConfig(_message.Message):
+ __slots__ = ("agvs",)
+ AGVS_FIELD_NUMBER: _ClassVar[int]
+ agvs: _containers.RepeatedCompositeFieldContainer[AGVDeviceConfig]
+ def __init__(self, agvs: _Optional[_Iterable[_Union[AGVDeviceConfig, _Mapping]]] = ...) -> None: ...
+
+class AGVRootConfig(_message.Message):
+ __slots__ = ("agv",)
+ AGV_FIELD_NUMBER: _ClassVar[int]
+ agv: AGVConfig
+ def __init__(self, agv: _Optional[_Union[AGVConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/agv_config/agv_config_pb2_grpc.py b/src/cmvr/config/agv_config/agv_config_pb2_grpc.py
new file mode 100644
index 0000000..18b89e7
--- /dev/null
+++ b/src/cmvr/config/agv_config/agv_config_pb2_grpc.py
@@ -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/config/agv_config/agv_config_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}.'
+ )
diff --git a/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-310.pyc b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..081e2b3
Binary files /dev/null and b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-312.pyc b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..c897c71
Binary files /dev/null and b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..ea2f6c8
Binary files /dev/null and b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..e0418da
Binary files /dev/null and b/src/cmvr/config/arm_config/__pycache__/arm_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/arm_config/arm_config_pb2.py b/src/cmvr/config/arm_config/arm_config_pb2.py
new file mode 100644
index 0000000..c5ec716
--- /dev/null
+++ b/src/cmvr/config/arm_config/arm_config_pb2.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/arm_config/arm_config.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/config/arm_config/arm_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.common import geometry_pb2 as cmvr_dot_common_dot_geometry__pb2
+from cmvr.config import joint_limits_config_pb2 as cmvr_dot_config_dot_joint__limits__config__pb2
+from cmvr.config import lawba_ik_config_pb2 as cmvr_dot_config_dot_lawba__ik__config__pb2
+from cmvr.config import pinocchio_dls_ik_config_pb2 as cmvr_dot_config_dot_pinocchio__dls__ik__config__pb2
+from cmvr.config import pinocchio_qp_ik_config_pb2 as cmvr_dot_config_dot_pinocchio__qp__ik__config__pb2
+from cmvr.config import srs_ik_config_pb2 as cmvr_dot_config_dot_srs__ik__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'cmvr/config/arm_config/arm_config.proto\x12\x0b\x63mvr.config\x1a\x1a\x63mvr/common/geometry.proto\x1a%cmvr/config/joint_limits_config.proto\x1a!cmvr/config/lawba_ik_config.proto\x1a)cmvr/config/pinocchio_dls_ik_config.proto\x1a(cmvr/config/pinocchio_qp_ik_config.proto\x1a\x1f\x63mvr/config/srs_ik_config.proto\"\xc1\x01\n\x1aMotorRobotArmBackendConfig\x12\x17\n\x0fmotor_system_id\x18\x02 \x01(\t\x12\x0b\n\x03\x64of\x18\x03 \x01(\x05\x12\x13\n\x0bjoint_names\x18\x04 \x03(\t\x12\x10\n\x08upd_freq\x18\x05 \x01(\x05\x12\x13\n\x0b\x62uffer_size\x18\x06 \x01(\x05\x12\x13\n\x0b\x64\x65\x66\x61ult_vel\x18\x07 \x01(\x01\x12\x13\n\x0b\x64\x65\x66\x61ult_acc\x18\x08 \x01(\x01\x12\x17\n\x0fmotor_group_ids\x18\t \x03(\t\"\xe5\x01\n\x1bVendorRobotArmBackendConfig\x12/\n\x05\x62rand\x18\x01 \x01(\x0e\x32 .cmvr.config.VendorRobotArmBrand\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\r\n\x05model\x18\x04 \x01(\t\x12\x0b\n\x03\x64of\x18\x05 \x01(\x05\x12\x13\n\x0bjoint_names\x18\x06 \x03(\t\x12\x12\n\nbase_frame\x18\x07 \x01(\t\x12\x12\n\ntool_frame\x18\x08 \x01(\t\x12\x10\n\x08username\x18\t \x01(\t\x12\x10\n\x08password\x18\n \x01(\t\"s\n\x1bQpJointLimitAvoidanceConfig\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\x12\x14\n\x0cmargin_ratio\x18\x02 \x01(\x01\x12\x0c\n\x04gain\x18\x03 \x01(\x01\x12\x10\n\x08max_push\x18\x04 \x01(\x01\x12\x0e\n\x06weight\x18\x05 \x01(\x01\"\x9f\x02\n\x19\x43\x61rtesianVelocityQpConfig\x12\x34\n\x0cjoint_limits\x18\x01 \x01(\x0b\x32\x1e.cmvr.config.JointLimitsConfig\x12\x30\n\x15twist_tracking_weight\x18\x02 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x1b\n\x13qdot_regularization\x18\x03 \x01(\x01\x12 \n\x18prev_qdot_regularization\x18\x04 \x01(\x01\x12\x12\n\nsolver_eps\x18\x05 \x01(\x01\x12G\n\x15joint_limit_avoidance\x18\x06 \x01(\x0b\x32(.cmvr.config.QpJointLimitAvoidanceConfig\"\xd6\x04\n\x13SpeedLPlannerConfig\x12\x1b\n\x13linear_velocity_max\x18\x01 \x01(\x01\x12\x1f\n\x17linear_acceleration_max\x18\x02 \x01(\x01\x12\x17\n\x0flinear_jerk_max\x18\x03 \x01(\x01\x12\x1c\n\x14\x61ngular_velocity_max\x18\x04 \x01(\x01\x12 \n\x18\x61ngular_acceleration_max\x18\x05 \x01(\x01\x12\x18\n\x10\x61ngular_jerk_max\x18\x06 \x01(\x01\x12\x1e\n\x16joint_acceleration_max\x18\x07 \x03(\x01\x12&\n\x1elinear_target_replan_threshold\x18\x08 \x01(\x01\x12\'\n\x1f\x61ngular_target_replan_threshold\x18\t \x01(\x01\x12$\n\x1clinear_reverse_cos_threshold\x18\n \x01(\x01\x12-\n%linear_reverse_switch_speed_threshold\x18\x0b \x01(\x01\x12&\n\x1enormal_direction_deviation_deg\x18\x0c \x01(\x01\x12$\n\x1cmild_direction_deviation_deg\x18\r \x01(\x01\x12&\n\x1esevere_direction_deviation_deg\x18\x0e \x01(\x01\x12\x1e\n\x16linear_min_speed_ratio\x18\x0f \x01(\x01\x12\x32\n\x02qp\x18\x10 \x01(\x0b\x32&.cmvr.config.CartesianVelocityQpConfig\"\x9f\x01\n!CartesianVelocityControllerConfig\x12\x18\n\x10\x63ontrol_period_s\x18\x01 \x01(\x01\x12\x17\n\x0fstop_twist_norm\x18\x02 \x01(\x01\x12\"\n\x1astop_command_velocity_norm\x18\x03 \x01(\x01\x12#\n\x1bstop_measured_velocity_norm\x18\x04 \x01(\x01\"\x94\x01\n\x1eToppraJointMotionPlannerConfig\x12.\n\tpath_type\x18\x01 \x01(\x0e\x32\x1b.cmvr.config.ToppraPathType\x12\x17\n\x0fsample_period_s\x18\x02 \x01(\x01\x12\x11\n\tgrid_size\x18\x03 \x01(\x05\x12\x16\n\x0ehigh_grid_size\x18\x04 \x01(\x05\"n\n\x0bMoveJConfig\x12R\n\x1btoppra_joint_motion_planner\x18\x01 \x01(\x0b\x32+.cmvr.config.ToppraJointMotionPlannerConfigH\x00\x42\x0b\n\talgorithm\"\x8f\x01\n\x12MoveLPlannerConfig\x12\x17\n\x0fsample_period_s\x18\x01 \x01(\x01\x12\x15\n\rposition_gain\x18\x02 \x01(\x01\x12\x15\n\rrotation_gain\x18\x03 \x01(\x01\x12\x32\n\x02qp\x18\x04 \x01(\x0b\x32&.cmvr.config.CartesianVelocityQpConfig\"\xbf\x01\n\x0bMoveLConfig\x12Q\n&pinocchio_dls_cartesian_motion_planner\x18\x01 \x01(\x0b\x32\x1f.cmvr.config.MoveLPlannerConfigH\x00\x12P\n%pinocchio_qp_cartesian_motion_planner\x18\x02 \x01(\x0b\x32\x1f.cmvr.config.MoveLPlannerConfigH\x00\x42\x0b\n\talgorithm\"~\n\x16SpeedLControllerConfig\x12W\n\x1d\x63\x61rtesian_velocity_controller\x18\x01 \x01(\x0b\x32..cmvr.config.CartesianVelocityControllerConfigH\x00\x42\x0b\n\talgorithm\"\x83\x02\n\x0cSpeedLConfig\x12R\n&pinocchio_dls_cartesian_motion_planner\x18\x01 \x01(\x0b\x32 .cmvr.config.SpeedLPlannerConfigH\x00\x12Q\n%pinocchio_qp_cartesian_motion_planner\x18\x02 \x01(\x0b\x32 .cmvr.config.SpeedLPlannerConfigH\x00\x12?\n\x12speed_l_controller\x18\n \x01(\x0b\x32#.cmvr.config.SpeedLControllerConfigB\x0b\n\talgorithm\"\x96\x02\n\x13\x41rmKinematicsConfig\x12\x44\n\x17pinocchio_dls_ik_solver\x18\n \x01(\x0b\x32!.cmvr.config.PinocchioDlsIKConfigH\x00\x12\x42\n\x16pinocchio_qp_ik_solver\x18\x0b \x01(\x0b\x32 .cmvr.config.PinocchioQpIKConfigH\x00\x12\x31\n\rsrs_ik_solver\x18\x0c \x01(\x0b\x32\x18.cmvr.config.SrsIKConfigH\x00\x12\x35\n\x0flawba_ik_solver\x18\r \x01(\x0b\x32\x1a.cmvr.config.LawbaIKConfigH\x00\x42\x0b\n\talgorithm\"\x97\x01\n\x0f\x41rmMotionConfig\x12(\n\x06move_j\x18\x01 \x01(\x0b\x32\x18.cmvr.config.MoveJConfig\x12(\n\x06move_l\x18\x02 \x01(\x0b\x32\x18.cmvr.config.MoveLConfig\x12*\n\x07speed_l\x18\x04 \x01(\x0b\x32\x19.cmvr.config.SpeedLConfigJ\x04\x08\x03\x10\x04\"\x87\x02\n\x0eRobotArmConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x38\n\x05motor\x18\n \x01(\x0b\x32\'.cmvr.config.MotorRobotArmBackendConfigH\x00\x12:\n\x06vendor\x18\x0b \x01(\x0b\x32(.cmvr.config.VendorRobotArmBackendConfigH\x00\x12\x34\n\nkinematics\x18\x14 \x01(\x0b\x32 .cmvr.config.ArmKinematicsConfig\x12,\n\x06motion\x18\x15 \x01(\x0b\x32\x1c.cmvr.config.ArmMotionConfigB\t\n\x07\x62\x61\x63kendJ\x04\x08\x02\x10\x03\"<\n\tArmConfig\x12/\n\nrobot_arms\x18\x01 \x03(\x0b\x32\x1b.cmvr.config.RobotArmConfig\"4\n\rArmRootConfig\x12#\n\x03\x61rm\x18\x01 \x01(\x0b\x32\x16.cmvr.config.ArmConfig*\xab\x01\n\x0eToppraPathType\x12\x1c\n\x18TOPPRA_PATH_TYPE_UNKNOWN\x10\x00\x12\x1b\n\x17TOPPRA_PATH_TYPE_LINEAR\x10\x01\x12\"\n\x1eTOPPRA_PATH_TYPE_CUBIC_HERMITE\x10\x02\x12\x1c\n\x18TOPPRA_PATH_TYPE_QUINTIC\x10\x03\x12\x1c\n\x18TOPPRA_PATH_TYPE_NATURAL\x10\x04*\x85\x01\n\x13VendorRobotArmBrand\x12\"\n\x1eVENDOR_ROBOT_ARM_BRAND_UNKNOWN\x10\x00\x12#\n\x1fVENDOR_ROBOT_ARM_BRAND_AUBO_ARM\x10\x01\x12%\n!VENDOR_ROBOT_ARM_BRAND_HUAYAN_ARM\x10\x02\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.arm_config.arm_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOPPRAPATHTYPE']._serialized_start=3685
+ _globals['_TOPPRAPATHTYPE']._serialized_end=3856
+ _globals['_VENDORROBOTARMBRAND']._serialized_start=3859
+ _globals['_VENDORROBOTARMBRAND']._serialized_end=3992
+ _globals['_MOTORROBOTARMBACKENDCONFIG']._serialized_start=277
+ _globals['_MOTORROBOTARMBACKENDCONFIG']._serialized_end=470
+ _globals['_VENDORROBOTARMBACKENDCONFIG']._serialized_start=473
+ _globals['_VENDORROBOTARMBACKENDCONFIG']._serialized_end=702
+ _globals['_QPJOINTLIMITAVOIDANCECONFIG']._serialized_start=704
+ _globals['_QPJOINTLIMITAVOIDANCECONFIG']._serialized_end=819
+ _globals['_CARTESIANVELOCITYQPCONFIG']._serialized_start=822
+ _globals['_CARTESIANVELOCITYQPCONFIG']._serialized_end=1109
+ _globals['_SPEEDLPLANNERCONFIG']._serialized_start=1112
+ _globals['_SPEEDLPLANNERCONFIG']._serialized_end=1710
+ _globals['_CARTESIANVELOCITYCONTROLLERCONFIG']._serialized_start=1713
+ _globals['_CARTESIANVELOCITYCONTROLLERCONFIG']._serialized_end=1872
+ _globals['_TOPPRAJOINTMOTIONPLANNERCONFIG']._serialized_start=1875
+ _globals['_TOPPRAJOINTMOTIONPLANNERCONFIG']._serialized_end=2023
+ _globals['_MOVEJCONFIG']._serialized_start=2025
+ _globals['_MOVEJCONFIG']._serialized_end=2135
+ _globals['_MOVELPLANNERCONFIG']._serialized_start=2138
+ _globals['_MOVELPLANNERCONFIG']._serialized_end=2281
+ _globals['_MOVELCONFIG']._serialized_start=2284
+ _globals['_MOVELCONFIG']._serialized_end=2475
+ _globals['_SPEEDLCONTROLLERCONFIG']._serialized_start=2477
+ _globals['_SPEEDLCONTROLLERCONFIG']._serialized_end=2603
+ _globals['_SPEEDLCONFIG']._serialized_start=2606
+ _globals['_SPEEDLCONFIG']._serialized_end=2865
+ _globals['_ARMKINEMATICSCONFIG']._serialized_start=2868
+ _globals['_ARMKINEMATICSCONFIG']._serialized_end=3146
+ _globals['_ARMMOTIONCONFIG']._serialized_start=3149
+ _globals['_ARMMOTIONCONFIG']._serialized_end=3300
+ _globals['_ROBOTARMCONFIG']._serialized_start=3303
+ _globals['_ROBOTARMCONFIG']._serialized_end=3566
+ _globals['_ARMCONFIG']._serialized_start=3568
+ _globals['_ARMCONFIG']._serialized_end=3628
+ _globals['_ARMROOTCONFIG']._serialized_start=3630
+ _globals['_ARMROOTCONFIG']._serialized_end=3682
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/arm_config/arm_config_pb2.pyi b/src/cmvr/config/arm_config/arm_config_pb2.pyi
new file mode 100644
index 0000000..b5c4aa8
--- /dev/null
+++ b/src/cmvr/config/arm_config/arm_config_pb2.pyi
@@ -0,0 +1,260 @@
+from cmvr.common import geometry_pb2 as _geometry_pb2
+from cmvr.config import joint_limits_config_pb2 as _joint_limits_config_pb2
+from cmvr.config import lawba_ik_config_pb2 as _lawba_ik_config_pb2
+from cmvr.config import pinocchio_dls_ik_config_pb2 as _pinocchio_dls_ik_config_pb2
+from cmvr.config import pinocchio_qp_ik_config_pb2 as _pinocchio_qp_ik_config_pb2
+from cmvr.config import srs_ik_config_pb2 as _srs_ik_config_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 ToppraPathType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOPPRA_PATH_TYPE_UNKNOWN: _ClassVar[ToppraPathType]
+ TOPPRA_PATH_TYPE_LINEAR: _ClassVar[ToppraPathType]
+ TOPPRA_PATH_TYPE_CUBIC_HERMITE: _ClassVar[ToppraPathType]
+ TOPPRA_PATH_TYPE_QUINTIC: _ClassVar[ToppraPathType]
+ TOPPRA_PATH_TYPE_NATURAL: _ClassVar[ToppraPathType]
+
+class VendorRobotArmBrand(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ VENDOR_ROBOT_ARM_BRAND_UNKNOWN: _ClassVar[VendorRobotArmBrand]
+ VENDOR_ROBOT_ARM_BRAND_AUBO_ARM: _ClassVar[VendorRobotArmBrand]
+ VENDOR_ROBOT_ARM_BRAND_HUAYAN_ARM: _ClassVar[VendorRobotArmBrand]
+TOPPRA_PATH_TYPE_UNKNOWN: ToppraPathType
+TOPPRA_PATH_TYPE_LINEAR: ToppraPathType
+TOPPRA_PATH_TYPE_CUBIC_HERMITE: ToppraPathType
+TOPPRA_PATH_TYPE_QUINTIC: ToppraPathType
+TOPPRA_PATH_TYPE_NATURAL: ToppraPathType
+VENDOR_ROBOT_ARM_BRAND_UNKNOWN: VendorRobotArmBrand
+VENDOR_ROBOT_ARM_BRAND_AUBO_ARM: VendorRobotArmBrand
+VENDOR_ROBOT_ARM_BRAND_HUAYAN_ARM: VendorRobotArmBrand
+
+class MotorRobotArmBackendConfig(_message.Message):
+ __slots__ = ("motor_system_id", "dof", "joint_names", "upd_freq", "buffer_size", "default_vel", "default_acc", "motor_group_ids")
+ MOTOR_SYSTEM_ID_FIELD_NUMBER: _ClassVar[int]
+ DOF_FIELD_NUMBER: _ClassVar[int]
+ JOINT_NAMES_FIELD_NUMBER: _ClassVar[int]
+ UPD_FREQ_FIELD_NUMBER: _ClassVar[int]
+ BUFFER_SIZE_FIELD_NUMBER: _ClassVar[int]
+ DEFAULT_VEL_FIELD_NUMBER: _ClassVar[int]
+ DEFAULT_ACC_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_GROUP_IDS_FIELD_NUMBER: _ClassVar[int]
+ motor_system_id: str
+ dof: int
+ joint_names: _containers.RepeatedScalarFieldContainer[str]
+ upd_freq: int
+ buffer_size: int
+ default_vel: float
+ default_acc: float
+ motor_group_ids: _containers.RepeatedScalarFieldContainer[str]
+ def __init__(self, motor_system_id: _Optional[str] = ..., dof: _Optional[int] = ..., joint_names: _Optional[_Iterable[str]] = ..., upd_freq: _Optional[int] = ..., buffer_size: _Optional[int] = ..., default_vel: _Optional[float] = ..., default_acc: _Optional[float] = ..., motor_group_ids: _Optional[_Iterable[str]] = ...) -> None: ...
+
+class VendorRobotArmBackendConfig(_message.Message):
+ __slots__ = ("brand", "ip", "port", "model", "dof", "joint_names", "base_frame", "tool_frame", "username", "password")
+ BRAND_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ PORT_FIELD_NUMBER: _ClassVar[int]
+ MODEL_FIELD_NUMBER: _ClassVar[int]
+ DOF_FIELD_NUMBER: _ClassVar[int]
+ JOINT_NAMES_FIELD_NUMBER: _ClassVar[int]
+ BASE_FRAME_FIELD_NUMBER: _ClassVar[int]
+ TOOL_FRAME_FIELD_NUMBER: _ClassVar[int]
+ USERNAME_FIELD_NUMBER: _ClassVar[int]
+ PASSWORD_FIELD_NUMBER: _ClassVar[int]
+ brand: VendorRobotArmBrand
+ ip: str
+ port: int
+ model: str
+ dof: int
+ joint_names: _containers.RepeatedScalarFieldContainer[str]
+ base_frame: str
+ tool_frame: str
+ username: str
+ password: str
+ def __init__(self, brand: _Optional[_Union[VendorRobotArmBrand, str]] = ..., ip: _Optional[str] = ..., port: _Optional[int] = ..., model: _Optional[str] = ..., dof: _Optional[int] = ..., joint_names: _Optional[_Iterable[str]] = ..., base_frame: _Optional[str] = ..., tool_frame: _Optional[str] = ..., username: _Optional[str] = ..., password: _Optional[str] = ...) -> None: ...
+
+class QpJointLimitAvoidanceConfig(_message.Message):
+ __slots__ = ("enable", "margin_ratio", "gain", "max_push", "weight")
+ ENABLE_FIELD_NUMBER: _ClassVar[int]
+ MARGIN_RATIO_FIELD_NUMBER: _ClassVar[int]
+ GAIN_FIELD_NUMBER: _ClassVar[int]
+ MAX_PUSH_FIELD_NUMBER: _ClassVar[int]
+ WEIGHT_FIELD_NUMBER: _ClassVar[int]
+ enable: bool
+ margin_ratio: float
+ gain: float
+ max_push: float
+ weight: float
+ def __init__(self, enable: _Optional[bool] = ..., margin_ratio: _Optional[float] = ..., gain: _Optional[float] = ..., max_push: _Optional[float] = ..., weight: _Optional[float] = ...) -> None: ...
+
+class CartesianVelocityQpConfig(_message.Message):
+ __slots__ = ("joint_limits", "twist_tracking_weight", "qdot_regularization", "prev_qdot_regularization", "solver_eps", "joint_limit_avoidance")
+ JOINT_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ TWIST_TRACKING_WEIGHT_FIELD_NUMBER: _ClassVar[int]
+ QDOT_REGULARIZATION_FIELD_NUMBER: _ClassVar[int]
+ PREV_QDOT_REGULARIZATION_FIELD_NUMBER: _ClassVar[int]
+ SOLVER_EPS_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMIT_AVOIDANCE_FIELD_NUMBER: _ClassVar[int]
+ joint_limits: _joint_limits_config_pb2.JointLimitsConfig
+ twist_tracking_weight: _geometry_pb2.Vec6
+ qdot_regularization: float
+ prev_qdot_regularization: float
+ solver_eps: float
+ joint_limit_avoidance: QpJointLimitAvoidanceConfig
+ def __init__(self, joint_limits: _Optional[_Union[_joint_limits_config_pb2.JointLimitsConfig, _Mapping]] = ..., twist_tracking_weight: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., qdot_regularization: _Optional[float] = ..., prev_qdot_regularization: _Optional[float] = ..., solver_eps: _Optional[float] = ..., joint_limit_avoidance: _Optional[_Union[QpJointLimitAvoidanceConfig, _Mapping]] = ...) -> None: ...
+
+class SpeedLPlannerConfig(_message.Message):
+ __slots__ = ("linear_velocity_max", "linear_acceleration_max", "linear_jerk_max", "angular_velocity_max", "angular_acceleration_max", "angular_jerk_max", "joint_acceleration_max", "linear_target_replan_threshold", "angular_target_replan_threshold", "linear_reverse_cos_threshold", "linear_reverse_switch_speed_threshold", "normal_direction_deviation_deg", "mild_direction_deviation_deg", "severe_direction_deviation_deg", "linear_min_speed_ratio", "qp")
+ LINEAR_VELOCITY_MAX_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_ACCELERATION_MAX_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_JERK_MAX_FIELD_NUMBER: _ClassVar[int]
+ ANGULAR_VELOCITY_MAX_FIELD_NUMBER: _ClassVar[int]
+ ANGULAR_ACCELERATION_MAX_FIELD_NUMBER: _ClassVar[int]
+ ANGULAR_JERK_MAX_FIELD_NUMBER: _ClassVar[int]
+ JOINT_ACCELERATION_MAX_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_TARGET_REPLAN_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ ANGULAR_TARGET_REPLAN_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_REVERSE_COS_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_REVERSE_SWITCH_SPEED_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ NORMAL_DIRECTION_DEVIATION_DEG_FIELD_NUMBER: _ClassVar[int]
+ MILD_DIRECTION_DEVIATION_DEG_FIELD_NUMBER: _ClassVar[int]
+ SEVERE_DIRECTION_DEVIATION_DEG_FIELD_NUMBER: _ClassVar[int]
+ LINEAR_MIN_SPEED_RATIO_FIELD_NUMBER: _ClassVar[int]
+ QP_FIELD_NUMBER: _ClassVar[int]
+ linear_velocity_max: float
+ linear_acceleration_max: float
+ linear_jerk_max: float
+ angular_velocity_max: float
+ angular_acceleration_max: float
+ angular_jerk_max: float
+ joint_acceleration_max: _containers.RepeatedScalarFieldContainer[float]
+ linear_target_replan_threshold: float
+ angular_target_replan_threshold: float
+ linear_reverse_cos_threshold: float
+ linear_reverse_switch_speed_threshold: float
+ normal_direction_deviation_deg: float
+ mild_direction_deviation_deg: float
+ severe_direction_deviation_deg: float
+ linear_min_speed_ratio: float
+ qp: CartesianVelocityQpConfig
+ def __init__(self, linear_velocity_max: _Optional[float] = ..., linear_acceleration_max: _Optional[float] = ..., linear_jerk_max: _Optional[float] = ..., angular_velocity_max: _Optional[float] = ..., angular_acceleration_max: _Optional[float] = ..., angular_jerk_max: _Optional[float] = ..., joint_acceleration_max: _Optional[_Iterable[float]] = ..., linear_target_replan_threshold: _Optional[float] = ..., angular_target_replan_threshold: _Optional[float] = ..., linear_reverse_cos_threshold: _Optional[float] = ..., linear_reverse_switch_speed_threshold: _Optional[float] = ..., normal_direction_deviation_deg: _Optional[float] = ..., mild_direction_deviation_deg: _Optional[float] = ..., severe_direction_deviation_deg: _Optional[float] = ..., linear_min_speed_ratio: _Optional[float] = ..., qp: _Optional[_Union[CartesianVelocityQpConfig, _Mapping]] = ...) -> None: ...
+
+class CartesianVelocityControllerConfig(_message.Message):
+ __slots__ = ("control_period_s", "stop_twist_norm", "stop_command_velocity_norm", "stop_measured_velocity_norm")
+ CONTROL_PERIOD_S_FIELD_NUMBER: _ClassVar[int]
+ STOP_TWIST_NORM_FIELD_NUMBER: _ClassVar[int]
+ STOP_COMMAND_VELOCITY_NORM_FIELD_NUMBER: _ClassVar[int]
+ STOP_MEASURED_VELOCITY_NORM_FIELD_NUMBER: _ClassVar[int]
+ control_period_s: float
+ stop_twist_norm: float
+ stop_command_velocity_norm: float
+ stop_measured_velocity_norm: float
+ def __init__(self, control_period_s: _Optional[float] = ..., stop_twist_norm: _Optional[float] = ..., stop_command_velocity_norm: _Optional[float] = ..., stop_measured_velocity_norm: _Optional[float] = ...) -> None: ...
+
+class ToppraJointMotionPlannerConfig(_message.Message):
+ __slots__ = ("path_type", "sample_period_s", "grid_size", "high_grid_size")
+ PATH_TYPE_FIELD_NUMBER: _ClassVar[int]
+ SAMPLE_PERIOD_S_FIELD_NUMBER: _ClassVar[int]
+ GRID_SIZE_FIELD_NUMBER: _ClassVar[int]
+ HIGH_GRID_SIZE_FIELD_NUMBER: _ClassVar[int]
+ path_type: ToppraPathType
+ sample_period_s: float
+ grid_size: int
+ high_grid_size: int
+ def __init__(self, path_type: _Optional[_Union[ToppraPathType, str]] = ..., sample_period_s: _Optional[float] = ..., grid_size: _Optional[int] = ..., high_grid_size: _Optional[int] = ...) -> None: ...
+
+class MoveJConfig(_message.Message):
+ __slots__ = ("toppra_joint_motion_planner",)
+ TOPPRA_JOINT_MOTION_PLANNER_FIELD_NUMBER: _ClassVar[int]
+ toppra_joint_motion_planner: ToppraJointMotionPlannerConfig
+ def __init__(self, toppra_joint_motion_planner: _Optional[_Union[ToppraJointMotionPlannerConfig, _Mapping]] = ...) -> None: ...
+
+class MoveLPlannerConfig(_message.Message):
+ __slots__ = ("sample_period_s", "position_gain", "rotation_gain", "qp")
+ SAMPLE_PERIOD_S_FIELD_NUMBER: _ClassVar[int]
+ POSITION_GAIN_FIELD_NUMBER: _ClassVar[int]
+ ROTATION_GAIN_FIELD_NUMBER: _ClassVar[int]
+ QP_FIELD_NUMBER: _ClassVar[int]
+ sample_period_s: float
+ position_gain: float
+ rotation_gain: float
+ qp: CartesianVelocityQpConfig
+ def __init__(self, sample_period_s: _Optional[float] = ..., position_gain: _Optional[float] = ..., rotation_gain: _Optional[float] = ..., qp: _Optional[_Union[CartesianVelocityQpConfig, _Mapping]] = ...) -> None: ...
+
+class MoveLConfig(_message.Message):
+ __slots__ = ("pinocchio_dls_cartesian_motion_planner", "pinocchio_qp_cartesian_motion_planner")
+ PINOCCHIO_DLS_CARTESIAN_MOTION_PLANNER_FIELD_NUMBER: _ClassVar[int]
+ PINOCCHIO_QP_CARTESIAN_MOTION_PLANNER_FIELD_NUMBER: _ClassVar[int]
+ pinocchio_dls_cartesian_motion_planner: MoveLPlannerConfig
+ pinocchio_qp_cartesian_motion_planner: MoveLPlannerConfig
+ def __init__(self, pinocchio_dls_cartesian_motion_planner: _Optional[_Union[MoveLPlannerConfig, _Mapping]] = ..., pinocchio_qp_cartesian_motion_planner: _Optional[_Union[MoveLPlannerConfig, _Mapping]] = ...) -> None: ...
+
+class SpeedLControllerConfig(_message.Message):
+ __slots__ = ("cartesian_velocity_controller",)
+ CARTESIAN_VELOCITY_CONTROLLER_FIELD_NUMBER: _ClassVar[int]
+ cartesian_velocity_controller: CartesianVelocityControllerConfig
+ def __init__(self, cartesian_velocity_controller: _Optional[_Union[CartesianVelocityControllerConfig, _Mapping]] = ...) -> None: ...
+
+class SpeedLConfig(_message.Message):
+ __slots__ = ("pinocchio_dls_cartesian_motion_planner", "pinocchio_qp_cartesian_motion_planner", "speed_l_controller")
+ PINOCCHIO_DLS_CARTESIAN_MOTION_PLANNER_FIELD_NUMBER: _ClassVar[int]
+ PINOCCHIO_QP_CARTESIAN_MOTION_PLANNER_FIELD_NUMBER: _ClassVar[int]
+ SPEED_L_CONTROLLER_FIELD_NUMBER: _ClassVar[int]
+ pinocchio_dls_cartesian_motion_planner: SpeedLPlannerConfig
+ pinocchio_qp_cartesian_motion_planner: SpeedLPlannerConfig
+ speed_l_controller: SpeedLControllerConfig
+ def __init__(self, pinocchio_dls_cartesian_motion_planner: _Optional[_Union[SpeedLPlannerConfig, _Mapping]] = ..., pinocchio_qp_cartesian_motion_planner: _Optional[_Union[SpeedLPlannerConfig, _Mapping]] = ..., speed_l_controller: _Optional[_Union[SpeedLControllerConfig, _Mapping]] = ...) -> None: ...
+
+class ArmKinematicsConfig(_message.Message):
+ __slots__ = ("pinocchio_dls_ik_solver", "pinocchio_qp_ik_solver", "srs_ik_solver", "lawba_ik_solver")
+ PINOCCHIO_DLS_IK_SOLVER_FIELD_NUMBER: _ClassVar[int]
+ PINOCCHIO_QP_IK_SOLVER_FIELD_NUMBER: _ClassVar[int]
+ SRS_IK_SOLVER_FIELD_NUMBER: _ClassVar[int]
+ LAWBA_IK_SOLVER_FIELD_NUMBER: _ClassVar[int]
+ pinocchio_dls_ik_solver: _pinocchio_dls_ik_config_pb2.PinocchioDlsIKConfig
+ pinocchio_qp_ik_solver: _pinocchio_qp_ik_config_pb2.PinocchioQpIKConfig
+ srs_ik_solver: _srs_ik_config_pb2.SrsIKConfig
+ lawba_ik_solver: _lawba_ik_config_pb2.LawbaIKConfig
+ def __init__(self, pinocchio_dls_ik_solver: _Optional[_Union[_pinocchio_dls_ik_config_pb2.PinocchioDlsIKConfig, _Mapping]] = ..., pinocchio_qp_ik_solver: _Optional[_Union[_pinocchio_qp_ik_config_pb2.PinocchioQpIKConfig, _Mapping]] = ..., srs_ik_solver: _Optional[_Union[_srs_ik_config_pb2.SrsIKConfig, _Mapping]] = ..., lawba_ik_solver: _Optional[_Union[_lawba_ik_config_pb2.LawbaIKConfig, _Mapping]] = ...) -> None: ...
+
+class ArmMotionConfig(_message.Message):
+ __slots__ = ("move_j", "move_l", "speed_l")
+ MOVE_J_FIELD_NUMBER: _ClassVar[int]
+ MOVE_L_FIELD_NUMBER: _ClassVar[int]
+ SPEED_L_FIELD_NUMBER: _ClassVar[int]
+ move_j: MoveJConfig
+ move_l: MoveLConfig
+ speed_l: SpeedLConfig
+ def __init__(self, move_j: _Optional[_Union[MoveJConfig, _Mapping]] = ..., move_l: _Optional[_Union[MoveLConfig, _Mapping]] = ..., speed_l: _Optional[_Union[SpeedLConfig, _Mapping]] = ...) -> None: ...
+
+class RobotArmConfig(_message.Message):
+ __slots__ = ("id", "motor", "vendor", "kinematics", "motion")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_FIELD_NUMBER: _ClassVar[int]
+ VENDOR_FIELD_NUMBER: _ClassVar[int]
+ KINEMATICS_FIELD_NUMBER: _ClassVar[int]
+ MOTION_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ motor: MotorRobotArmBackendConfig
+ vendor: VendorRobotArmBackendConfig
+ kinematics: ArmKinematicsConfig
+ motion: ArmMotionConfig
+ def __init__(self, id: _Optional[str] = ..., motor: _Optional[_Union[MotorRobotArmBackendConfig, _Mapping]] = ..., vendor: _Optional[_Union[VendorRobotArmBackendConfig, _Mapping]] = ..., kinematics: _Optional[_Union[ArmKinematicsConfig, _Mapping]] = ..., motion: _Optional[_Union[ArmMotionConfig, _Mapping]] = ...) -> None: ...
+
+class ArmConfig(_message.Message):
+ __slots__ = ("robot_arms",)
+ ROBOT_ARMS_FIELD_NUMBER: _ClassVar[int]
+ robot_arms: _containers.RepeatedCompositeFieldContainer[RobotArmConfig]
+ def __init__(self, robot_arms: _Optional[_Iterable[_Union[RobotArmConfig, _Mapping]]] = ...) -> None: ...
+
+class ArmRootConfig(_message.Message):
+ __slots__ = ("arm",)
+ ARM_FIELD_NUMBER: _ClassVar[int]
+ arm: ArmConfig
+ def __init__(self, arm: _Optional[_Union[ArmConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/arm_config/arm_config_pb2_grpc.py b/src/cmvr/config/arm_config/arm_config_pb2_grpc.py
new file mode 100644
index 0000000..e75605d
--- /dev/null
+++ b/src/cmvr/config/arm_config/arm_config_pb2_grpc.py
@@ -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/config/arm_config/arm_config_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}.'
+ )
diff --git a/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-310.pyc b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..5bde9c3
Binary files /dev/null and b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-312.pyc b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..e297722
Binary files /dev/null and b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..4a55020
Binary files /dev/null and b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..7a7aa1a
Binary files /dev/null and b/src/cmvr/config/aubo_robot_config/__pycache__/aubo_robot_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.py b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.py
new file mode 100644
index 0000000..6816aa9
--- /dev/null
+++ b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/aubo_robot_config/aubo_robot_config.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/config/aubo_robot_config/aubo_robot_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5cmvr/config/aubo_robot_config/aubo_robot_config.proto\x12\x0b\x63mvr.config\"[\n\x0f\x41uboRobotConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\x10\n\x08username\x18\x04 \x01(\t\x12\x10\n\x08password\x18\x05 \x01(\tb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.aubo_robot_config.aubo_robot_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_AUBOROBOTCONFIG']._serialized_start=70
+ _globals['_AUBOROBOTCONFIG']._serialized_end=161
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.pyi b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.pyi
new file mode 100644
index 0000000..10d0a6f
--- /dev/null
+++ b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.pyi
@@ -0,0 +1,19 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from typing import ClassVar as _ClassVar, Optional as _Optional
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class AuboRobotConfig(_message.Message):
+ __slots__ = ("id", "ip", "port", "username", "password")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ PORT_FIELD_NUMBER: _ClassVar[int]
+ USERNAME_FIELD_NUMBER: _ClassVar[int]
+ PASSWORD_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ip: str
+ port: int
+ username: str
+ password: str
+ def __init__(self, id: _Optional[str] = ..., ip: _Optional[str] = ..., port: _Optional[int] = ..., username: _Optional[str] = ..., password: _Optional[str] = ...) -> None: ...
diff --git a/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2_grpc.py b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2_grpc.py
new file mode 100644
index 0000000..390b828
--- /dev/null
+++ b/src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2_grpc.py
@@ -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/config/aubo_robot_config/aubo_robot_config_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}.'
+ )
diff --git a/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-310.pyc b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..a902be1
Binary files /dev/null and b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-312.pyc b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..fc23359
Binary files /dev/null and b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..c51de0e
Binary files /dev/null and b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..0267183
Binary files /dev/null and b/src/cmvr/config/biohead_config/__pycache__/biohead_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/biohead_config/biohead_config_pb2.py b/src/cmvr/config/biohead_config/biohead_config_pb2.py
new file mode 100644
index 0000000..d39c9de
--- /dev/null
+++ b/src/cmvr/config/biohead_config/biohead_config_pb2.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/biohead_config/biohead_config.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/config/biohead_config/biohead_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/cmvr/config/biohead_config/biohead_config.proto\x12\x0b\x63mvr.config\"\x9c\x01\n\x17\x42ioHeadServoGroupConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\x05\x12\x15\n\rchannel_start\x18\x03 \x01(\x05\x12\x13\n\x0b\x63hannel_end\x18\x04 \x01(\x05\x12\x0e\n\x06offset\x18\x05 \x03(\x01\x12\x12\n\nmin_angles\x18\x06 \x03(\x01\x12\x12\n\nmax_angles\x18\x07 \x03(\x01\"\x84\x01\n\x12\x42ioHeadRobotConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0bserial_port\x18\x02 \x01(\t\x12\x11\n\tbaud_rate\x18\x03 \x01(\x05\x12:\n\x0cservo_groups\x18\x04 \x03(\x0b\x32$.cmvr.config.BioHeadServoGroupConfig\"K\n\x16\x42ioHeadRobotRootConfig\x12\x31\n\x08\x62io_head\x18\x01 \x01(\x0b\x32\x1f.cmvr.config.BioHeadRobotConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.biohead_config.biohead_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_BIOHEADSERVOGROUPCONFIG']._serialized_start=65
+ _globals['_BIOHEADSERVOGROUPCONFIG']._serialized_end=221
+ _globals['_BIOHEADROBOTCONFIG']._serialized_start=224
+ _globals['_BIOHEADROBOTCONFIG']._serialized_end=356
+ _globals['_BIOHEADROBOTROOTCONFIG']._serialized_start=358
+ _globals['_BIOHEADROBOTROOTCONFIG']._serialized_end=433
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/biohead_config/biohead_config_pb2.pyi b/src/cmvr/config/biohead_config/biohead_config_pb2.pyi
new file mode 100644
index 0000000..34787c3
--- /dev/null
+++ b/src/cmvr/config/biohead_config/biohead_config_pb2.pyi
@@ -0,0 +1,43 @@
+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 BioHeadServoGroupConfig(_message.Message):
+ __slots__ = ("name", "address", "channel_start", "channel_end", "offset", "min_angles", "max_angles")
+ NAME_FIELD_NUMBER: _ClassVar[int]
+ ADDRESS_FIELD_NUMBER: _ClassVar[int]
+ CHANNEL_START_FIELD_NUMBER: _ClassVar[int]
+ CHANNEL_END_FIELD_NUMBER: _ClassVar[int]
+ OFFSET_FIELD_NUMBER: _ClassVar[int]
+ MIN_ANGLES_FIELD_NUMBER: _ClassVar[int]
+ MAX_ANGLES_FIELD_NUMBER: _ClassVar[int]
+ name: str
+ address: int
+ channel_start: int
+ channel_end: int
+ offset: _containers.RepeatedScalarFieldContainer[float]
+ min_angles: _containers.RepeatedScalarFieldContainer[float]
+ max_angles: _containers.RepeatedScalarFieldContainer[float]
+ def __init__(self, name: _Optional[str] = ..., address: _Optional[int] = ..., channel_start: _Optional[int] = ..., channel_end: _Optional[int] = ..., offset: _Optional[_Iterable[float]] = ..., min_angles: _Optional[_Iterable[float]] = ..., max_angles: _Optional[_Iterable[float]] = ...) -> None: ...
+
+class BioHeadRobotConfig(_message.Message):
+ __slots__ = ("id", "serial_port", "baud_rate", "servo_groups")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ SERIAL_PORT_FIELD_NUMBER: _ClassVar[int]
+ BAUD_RATE_FIELD_NUMBER: _ClassVar[int]
+ SERVO_GROUPS_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ serial_port: str
+ baud_rate: int
+ servo_groups: _containers.RepeatedCompositeFieldContainer[BioHeadServoGroupConfig]
+ def __init__(self, id: _Optional[str] = ..., serial_port: _Optional[str] = ..., baud_rate: _Optional[int] = ..., servo_groups: _Optional[_Iterable[_Union[BioHeadServoGroupConfig, _Mapping]]] = ...) -> None: ...
+
+class BioHeadRobotRootConfig(_message.Message):
+ __slots__ = ("bio_head",)
+ BIO_HEAD_FIELD_NUMBER: _ClassVar[int]
+ bio_head: BioHeadRobotConfig
+ def __init__(self, bio_head: _Optional[_Union[BioHeadRobotConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/biohead_config/biohead_config_pb2_grpc.py b/src/cmvr/config/biohead_config/biohead_config_pb2_grpc.py
new file mode 100644
index 0000000..4bc14cb
--- /dev/null
+++ b/src/cmvr/config/biohead_config/biohead_config_pb2_grpc.py
@@ -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/config/biohead_config/biohead_config_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}.'
+ )
diff --git a/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-310.pyc b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..d17b87f
Binary files /dev/null and b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-312.pyc b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..233c6cc
Binary files /dev/null and b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..4b25031
Binary files /dev/null and b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..80e1bd5
Binary files /dev/null and b/src/cmvr/config/camera_config/__pycache__/camera_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/camera_config/camera_config_pb2.py b/src/cmvr/config/camera_config/camera_config_pb2.py
new file mode 100644
index 0000000..ae2aa4e
--- /dev/null
+++ b/src/cmvr/config/camera_config/camera_config_pb2.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/camera_config/camera_config.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/config/camera_config/camera_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-cmvr/config/camera_config/camera_config.proto\x12\x0b\x63mvr.config\"\xd2\x02\n\x15RealSenseCameraConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0cserialNumber\x18\x02 \x01(\t\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\x12\x0b\n\x03\x66ps\x18\x05 \x01(\x05\x12\r\n\x05\x63odec\x18\x06 \x01(\t\x12,\n\x0b\x63\x61mera_mode\x18\x07 \x01(\x0e\x32\x17.cmvr.config.CameraMode\x12,\n\x0bstream_mode\x18\x08 \x01(\x0e\x32\x17.cmvr.config.StreamMode\x12*\n\nalign_mode\x18\t \x01(\x0e\x32\x16.cmvr.config.AlignMode\x12\x13\n\x0b\x62uffer_size\x18\n \x01(\x05\x12\x0c\n\x04sync\x18\x0b \x01(\x08\x12\x14\n\x0c\x65ncode_width\x18\r \x01(\x05\x12\x15\n\rencode_height\x18\x0e \x01(\x05J\x04\x08\x0c\x10\r\"\x89\x02\n\x0fUVCCameraConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03usb\x18\x02 \x01(\t\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\x12\x0b\n\x03\x66ps\x18\x05 \x01(\x05\x12\r\n\x05\x63odec\x18\x06 \x01(\t\x12,\n\x0b\x63\x61mera_mode\x18\x07 \x01(\x0e\x32\x17.cmvr.config.CameraMode\x12,\n\x0bstream_mode\x18\x08 \x01(\x0e\x32\x17.cmvr.config.StreamMode\x12\x13\n\x0b\x62uffer_size\x18\t \x01(\x05\x12\x14\n\x0c\x65ncode_width\x18\x0b \x01(\x05\x12\x15\n\rencode_height\x18\x0c \x01(\x05J\x04\x08\n\x10\x0b\"h\n\x14MechMindCameraConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x12\n\x05\x61lign\x18\x03 \x01(\x08H\x00\x88\x01\x01\x12\x14\n\x0cimage2d_type\x18\x04 \x01(\tB\x08\n\x06_alignJ\x04\x08\x05\x10\x06\"\xce\x01\n\x12\x43\x61meraDeviceConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12+\n\x03uvc\x18\n \x01(\x0b\x32\x1c.cmvr.config.UVCCameraConfigH\x00\x12\x37\n\trealsense\x18\x0b \x01(\x0b\x32\".cmvr.config.RealSenseCameraConfigH\x00\x12\x35\n\x08mechmind\x18\x0c \x01(\x0b\x32!.cmvr.config.MechMindCameraConfigH\x00\x42\t\n\x07\x62\x61\x63kendJ\x04\x08\x02\x10\x03\"@\n\x0c\x43\x61meraConfig\x12\x30\n\x07\x63\x61meras\x18\x01 \x03(\x0b\x32\x1f.cmvr.config.CameraDeviceConfig\"=\n\x10\x43\x61meraRootConfig\x12)\n\x06\x63\x61mera\x18\x01 \x01(\x0b\x32\x19.cmvr.config.CameraConfig*:\n\nCameraMode\x12\x15\n\x11\x43\x41MERA_MODE_PHOTO\x10\x00\x12\x15\n\x11\x43\x41MERA_MODE_VIDEO\x10\x01*N\n\nStreamMode\x12\x13\n\x0fSTREAM_MODE_RGB\x10\x00\x12\x14\n\x10STREAM_MODE_RGBD\x10\x01\x12\x15\n\x11STREAM_MODE_DEPTH\x10\x02*7\n\tAlignMode\x12\x14\n\x10\x41LIGN_MODE_COLOR\x10\x00\x12\x14\n\x10\x41LIGN_MODE_DEPTH\x10\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.camera_config.camera_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CAMERAMODE']._serialized_start=1115
+ _globals['_CAMERAMODE']._serialized_end=1173
+ _globals['_STREAMMODE']._serialized_start=1175
+ _globals['_STREAMMODE']._serialized_end=1253
+ _globals['_ALIGNMODE']._serialized_start=1255
+ _globals['_ALIGNMODE']._serialized_end=1310
+ _globals['_REALSENSECAMERACONFIG']._serialized_start=63
+ _globals['_REALSENSECAMERACONFIG']._serialized_end=401
+ _globals['_UVCCAMERACONFIG']._serialized_start=404
+ _globals['_UVCCAMERACONFIG']._serialized_end=669
+ _globals['_MECHMINDCAMERACONFIG']._serialized_start=671
+ _globals['_MECHMINDCAMERACONFIG']._serialized_end=775
+ _globals['_CAMERADEVICECONFIG']._serialized_start=778
+ _globals['_CAMERADEVICECONFIG']._serialized_end=984
+ _globals['_CAMERACONFIG']._serialized_start=986
+ _globals['_CAMERACONFIG']._serialized_end=1050
+ _globals['_CAMERAROOTCONFIG']._serialized_start=1052
+ _globals['_CAMERAROOTCONFIG']._serialized_end=1113
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/camera_config/camera_config_pb2.pyi b/src/cmvr/config/camera_config/camera_config_pb2.pyi
new file mode 100644
index 0000000..96d41b9
--- /dev/null
+++ b/src/cmvr/config/camera_config/camera_config_pb2.pyi
@@ -0,0 +1,123 @@
+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 CameraMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ CAMERA_MODE_PHOTO: _ClassVar[CameraMode]
+ CAMERA_MODE_VIDEO: _ClassVar[CameraMode]
+
+class StreamMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ STREAM_MODE_RGB: _ClassVar[StreamMode]
+ STREAM_MODE_RGBD: _ClassVar[StreamMode]
+ STREAM_MODE_DEPTH: _ClassVar[StreamMode]
+
+class AlignMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ ALIGN_MODE_COLOR: _ClassVar[AlignMode]
+ ALIGN_MODE_DEPTH: _ClassVar[AlignMode]
+CAMERA_MODE_PHOTO: CameraMode
+CAMERA_MODE_VIDEO: CameraMode
+STREAM_MODE_RGB: StreamMode
+STREAM_MODE_RGBD: StreamMode
+STREAM_MODE_DEPTH: StreamMode
+ALIGN_MODE_COLOR: AlignMode
+ALIGN_MODE_DEPTH: AlignMode
+
+class RealSenseCameraConfig(_message.Message):
+ __slots__ = ("id", "serialNumber", "width", "height", "fps", "codec", "camera_mode", "stream_mode", "align_mode", "buffer_size", "sync", "encode_width", "encode_height")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ SERIALNUMBER_FIELD_NUMBER: _ClassVar[int]
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ FPS_FIELD_NUMBER: _ClassVar[int]
+ CODEC_FIELD_NUMBER: _ClassVar[int]
+ CAMERA_MODE_FIELD_NUMBER: _ClassVar[int]
+ STREAM_MODE_FIELD_NUMBER: _ClassVar[int]
+ ALIGN_MODE_FIELD_NUMBER: _ClassVar[int]
+ BUFFER_SIZE_FIELD_NUMBER: _ClassVar[int]
+ SYNC_FIELD_NUMBER: _ClassVar[int]
+ ENCODE_WIDTH_FIELD_NUMBER: _ClassVar[int]
+ ENCODE_HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ serialNumber: str
+ width: int
+ height: int
+ fps: int
+ codec: str
+ camera_mode: CameraMode
+ stream_mode: StreamMode
+ align_mode: AlignMode
+ buffer_size: int
+ sync: bool
+ encode_width: int
+ encode_height: int
+ def __init__(self, id: _Optional[str] = ..., serialNumber: _Optional[str] = ..., width: _Optional[int] = ..., height: _Optional[int] = ..., fps: _Optional[int] = ..., codec: _Optional[str] = ..., camera_mode: _Optional[_Union[CameraMode, str]] = ..., stream_mode: _Optional[_Union[StreamMode, str]] = ..., align_mode: _Optional[_Union[AlignMode, str]] = ..., buffer_size: _Optional[int] = ..., sync: _Optional[bool] = ..., encode_width: _Optional[int] = ..., encode_height: _Optional[int] = ...) -> None: ...
+
+class UVCCameraConfig(_message.Message):
+ __slots__ = ("id", "usb", "width", "height", "fps", "codec", "camera_mode", "stream_mode", "buffer_size", "encode_width", "encode_height")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ USB_FIELD_NUMBER: _ClassVar[int]
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ FPS_FIELD_NUMBER: _ClassVar[int]
+ CODEC_FIELD_NUMBER: _ClassVar[int]
+ CAMERA_MODE_FIELD_NUMBER: _ClassVar[int]
+ STREAM_MODE_FIELD_NUMBER: _ClassVar[int]
+ BUFFER_SIZE_FIELD_NUMBER: _ClassVar[int]
+ ENCODE_WIDTH_FIELD_NUMBER: _ClassVar[int]
+ ENCODE_HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ usb: str
+ width: int
+ height: int
+ fps: int
+ codec: str
+ camera_mode: CameraMode
+ stream_mode: StreamMode
+ buffer_size: int
+ encode_width: int
+ encode_height: int
+ def __init__(self, id: _Optional[str] = ..., usb: _Optional[str] = ..., width: _Optional[int] = ..., height: _Optional[int] = ..., fps: _Optional[int] = ..., codec: _Optional[str] = ..., camera_mode: _Optional[_Union[CameraMode, str]] = ..., stream_mode: _Optional[_Union[StreamMode, str]] = ..., buffer_size: _Optional[int] = ..., encode_width: _Optional[int] = ..., encode_height: _Optional[int] = ...) -> None: ...
+
+class MechMindCameraConfig(_message.Message):
+ __slots__ = ("id", "ip", "align", "image2d_type")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ ALIGN_FIELD_NUMBER: _ClassVar[int]
+ IMAGE2D_TYPE_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ip: str
+ align: bool
+ image2d_type: str
+ def __init__(self, id: _Optional[str] = ..., ip: _Optional[str] = ..., align: _Optional[bool] = ..., image2d_type: _Optional[str] = ...) -> None: ...
+
+class CameraDeviceConfig(_message.Message):
+ __slots__ = ("id", "uvc", "realsense", "mechmind")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ UVC_FIELD_NUMBER: _ClassVar[int]
+ REALSENSE_FIELD_NUMBER: _ClassVar[int]
+ MECHMIND_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ uvc: UVCCameraConfig
+ realsense: RealSenseCameraConfig
+ mechmind: MechMindCameraConfig
+ def __init__(self, id: _Optional[str] = ..., uvc: _Optional[_Union[UVCCameraConfig, _Mapping]] = ..., realsense: _Optional[_Union[RealSenseCameraConfig, _Mapping]] = ..., mechmind: _Optional[_Union[MechMindCameraConfig, _Mapping]] = ...) -> None: ...
+
+class CameraConfig(_message.Message):
+ __slots__ = ("cameras",)
+ CAMERAS_FIELD_NUMBER: _ClassVar[int]
+ cameras: _containers.RepeatedCompositeFieldContainer[CameraDeviceConfig]
+ def __init__(self, cameras: _Optional[_Iterable[_Union[CameraDeviceConfig, _Mapping]]] = ...) -> None: ...
+
+class CameraRootConfig(_message.Message):
+ __slots__ = ("camera",)
+ CAMERA_FIELD_NUMBER: _ClassVar[int]
+ camera: CameraConfig
+ def __init__(self, camera: _Optional[_Union[CameraConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/camera_config/camera_config_pb2_grpc.py b/src/cmvr/config/camera_config/camera_config_pb2_grpc.py
new file mode 100644
index 0000000..f9f5ad4
--- /dev/null
+++ b/src/cmvr/config/camera_config/camera_config_pb2_grpc.py
@@ -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/config/camera_config/camera_config_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}.'
+ )
diff --git a/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-310.pyc b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..22266c8
Binary files /dev/null and b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-312.pyc b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..0f81662
Binary files /dev/null and b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1d5bcb8
Binary files /dev/null and b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..907b13d
Binary files /dev/null and b/src/cmvr/config/cmvr_es_config/__pycache__/cmvr_es_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.py b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.py
new file mode 100644
index 0000000..d357f3a
--- /dev/null
+++ b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/cmvr_es_config/cmvr_es_config.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/config/cmvr_es_config/cmvr_es_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/cmvr/config/cmvr_es_config/cmvr_es_config.proto\x12\x0b\x63mvr.config\"\x9d\x01\n\x0c\x43MVRESConfig\x12\x1a\n\x12logger_config_file\x18\x01 \x01(\t\x12\"\n\x1a\x64\x65vice_manager_config_file\x18\x02 \x01(\t\x12 \n\x18task_manager_config_file\x18\x04 \x01(\t\x12\x1f\n\x17grpc_server_config_file\x18\x06 \x01(\tJ\x04\x08\x03\x10\x04J\x04\x08\x05\x10\x06\">\n\x10\x43MVRESRootConfig\x12*\n\x07\x63mvr_es\x18\x01 \x01(\x0b\x32\x19.cmvr.config.CMVRESConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.cmvr_es_config.cmvr_es_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CMVRESCONFIG']._serialized_start=65
+ _globals['_CMVRESCONFIG']._serialized_end=222
+ _globals['_CMVRESROOTCONFIG']._serialized_start=224
+ _globals['_CMVRESROOTCONFIG']._serialized_end=286
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.pyi b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.pyi
new file mode 100644
index 0000000..ce0019e
--- /dev/null
+++ b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.pyi
@@ -0,0 +1,24 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class CMVRESConfig(_message.Message):
+ __slots__ = ("logger_config_file", "device_manager_config_file", "task_manager_config_file", "grpc_server_config_file")
+ LOGGER_CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ DEVICE_MANAGER_CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ TASK_MANAGER_CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ GRPC_SERVER_CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ logger_config_file: str
+ device_manager_config_file: str
+ task_manager_config_file: str
+ grpc_server_config_file: str
+ def __init__(self, logger_config_file: _Optional[str] = ..., device_manager_config_file: _Optional[str] = ..., task_manager_config_file: _Optional[str] = ..., grpc_server_config_file: _Optional[str] = ...) -> None: ...
+
+class CMVRESRootConfig(_message.Message):
+ __slots__ = ("cmvr_es",)
+ CMVR_ES_FIELD_NUMBER: _ClassVar[int]
+ cmvr_es: CMVRESConfig
+ def __init__(self, cmvr_es: _Optional[_Union[CMVRESConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2_grpc.py b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2_grpc.py
new file mode 100644
index 0000000..05e5367
--- /dev/null
+++ b/src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2_grpc.py
@@ -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/config/cmvr_es_config/cmvr_es_config_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}.'
+ )
diff --git a/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-310.pyc b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b528c6d
Binary files /dev/null and b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-312.pyc b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..cdff94d
Binary files /dev/null and b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..4197d06
Binary files /dev/null and b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..0bef689
Binary files /dev/null and b/src/cmvr/config/device_manager_config/__pycache__/device_manager_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/device_manager_config/device_manager_config_pb2.py b/src/cmvr/config/device_manager_config/device_manager_config_pb2.py
new file mode 100644
index 0000000..b487e0e
--- /dev/null
+++ b/src/cmvr/config/device_manager_config/device_manager_config_pb2.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/device_manager_config/device_manager_config.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/config/device_manager_config/device_manager_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n=cmvr/config/device_manager_config/device_manager_config.proto\x12\x0b\x63mvr.config\"\xa4\x05\n\x11\x44\x65viceConfigEntry\x12\n\n\x02id\x18\x01 \x01(\t\x12\x37\n\x04type\x18\x02 \x01(\x0e\x32).cmvr.config.DeviceConfigEntry.DeviceType\x12\x13\n\x0b\x63onfig_file\x18\x03 \x01(\t\x12\x0e\n\x06\x65nable\x18\x04 \x01(\x08\"\xa4\x04\n\nDeviceType\x12\x17\n\x13\x44\x45VICE_TYPE_UNKNOWN\x10\x00\x12\x1e\n\x1a\x44\x45VICE_TYPE_BIO_HEAD_ROBOT\x10\x08\x12\x1c\n\x18\x44\x45VICE_TYPE_MOTOR_SYSTEM\x10\t\x12\x19\n\x15\x44\x45VICE_TYPE_ROBOT_ARM\x10\x0c\x12\x16\n\x12\x44\x45VICE_TYPE_CAMERA\x10\r\x12\x17\n\x13\x44\x45VICE_TYPE_DEXHAND\x10\x0e\x12\x1a\n\x16\x44\x45VICE_TYPE_MICROPHONE\x10\x0f\x12\x17\n\x13\x44\x45VICE_TYPE_SPEAKER\x10\x10\x12\x13\n\x0f\x44\x45VICE_TYPE_AGV\x10\x11\"\x04\x08\x01\x10\x01\"\x04\x08\x02\x10\x02\"\x04\x08\x03\x10\x03\"\x04\x08\x04\x10\x04\"\x04\x08\x05\x10\x05\"\x04\x08\x06\x10\x06\"\x04\x08\x07\x10\x07\"\x04\x08\n\x10\n\"\x04\x08\x0b\x10\x0b*\x16\x44\x45VICE_TYPE_UVC_CAMERA*\x1c\x44\x45VICE_TYPE_REALSENSE_CAMERA*\x1c\x44\x45VICE_TYPE_RH56DFTP_DEXHAND*\x16\x44\x45VICE_TYPE_PX6AX_GEN3*\x1d\x44\x45VICE_TYPE_FFMPEG_MICROPHONE*\x1a\x44\x45VICE_TYPE_FFMPEG_SPEAKER*\x1a\x44\x45VICE_TYPE_HUMANOID_ROBOT*\x15\x44\x45VICE_TYPE_MOTOR_ARM*\x19\x44\x45VICE_TYPE_CARTESIAN_ARM\"z\n\x13\x44\x65viceManagerConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12/\n\x07\x64\x65vices\x18\x04 \x03(\x0b\x32\x1e.cmvr.config.DeviceConfigEntry\"S\n\x17\x44\x65viceManagerRootConfig\x12\x38\n\x0e\x64\x65vice_manager\x18\x01 \x01(\x0b\x32 .cmvr.config.DeviceManagerConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.device_manager_config.device_manager_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_DEVICECONFIGENTRY']._serialized_start=79
+ _globals['_DEVICECONFIGENTRY']._serialized_end=755
+ _globals['_DEVICECONFIGENTRY_DEVICETYPE']._serialized_start=207
+ _globals['_DEVICECONFIGENTRY_DEVICETYPE']._serialized_end=755
+ _globals['_DEVICEMANAGERCONFIG']._serialized_start=757
+ _globals['_DEVICEMANAGERCONFIG']._serialized_end=879
+ _globals['_DEVICEMANAGERROOTCONFIG']._serialized_start=881
+ _globals['_DEVICEMANAGERROOTCONFIG']._serialized_end=964
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/device_manager_config/device_manager_config_pb2.pyi b/src/cmvr/config/device_manager_config/device_manager_config_pb2.pyi
new file mode 100644
index 0000000..92801ce
--- /dev/null
+++ b/src/cmvr/config/device_manager_config/device_manager_config_pb2.pyi
@@ -0,0 +1,58 @@
+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 DeviceConfigEntry(_message.Message):
+ __slots__ = ("id", "type", "config_file", "enable")
+ class DeviceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ DEVICE_TYPE_UNKNOWN: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_BIO_HEAD_ROBOT: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_MOTOR_SYSTEM: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_ROBOT_ARM: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_CAMERA: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_DEXHAND: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_MICROPHONE: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_SPEAKER: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_AGV: _ClassVar[DeviceConfigEntry.DeviceType]
+ DEVICE_TYPE_UNKNOWN: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_BIO_HEAD_ROBOT: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_MOTOR_SYSTEM: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_ROBOT_ARM: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_CAMERA: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_DEXHAND: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_MICROPHONE: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_SPEAKER: DeviceConfigEntry.DeviceType
+ DEVICE_TYPE_AGV: DeviceConfigEntry.DeviceType
+ ID_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ type: DeviceConfigEntry.DeviceType
+ config_file: str
+ enable: bool
+ def __init__(self, id: _Optional[str] = ..., type: _Optional[_Union[DeviceConfigEntry.DeviceType, str]] = ..., config_file: _Optional[str] = ..., enable: _Optional[bool] = ...) -> None: ...
+
+class DeviceManagerConfig(_message.Message):
+ __slots__ = ("name", "version", "description", "devices")
+ NAME_FIELD_NUMBER: _ClassVar[int]
+ VERSION_FIELD_NUMBER: _ClassVar[int]
+ DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
+ DEVICES_FIELD_NUMBER: _ClassVar[int]
+ name: str
+ version: str
+ description: str
+ devices: _containers.RepeatedCompositeFieldContainer[DeviceConfigEntry]
+ def __init__(self, name: _Optional[str] = ..., version: _Optional[str] = ..., description: _Optional[str] = ..., devices: _Optional[_Iterable[_Union[DeviceConfigEntry, _Mapping]]] = ...) -> None: ...
+
+class DeviceManagerRootConfig(_message.Message):
+ __slots__ = ("device_manager",)
+ DEVICE_MANAGER_FIELD_NUMBER: _ClassVar[int]
+ device_manager: DeviceManagerConfig
+ def __init__(self, device_manager: _Optional[_Union[DeviceManagerConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/device_manager_config/device_manager_config_pb2_grpc.py b/src/cmvr/config/device_manager_config/device_manager_config_pb2_grpc.py
new file mode 100644
index 0000000..828fc9d
--- /dev/null
+++ b/src/cmvr/config/device_manager_config/device_manager_config_pb2_grpc.py
@@ -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/config/device_manager_config/device_manager_config_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}.'
+ )
diff --git a/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-310.pyc b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..bb053cc
Binary files /dev/null and b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-312.pyc b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..9002119
Binary files /dev/null and b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..19b8c7a
Binary files /dev/null and b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..5351738
Binary files /dev/null and b/src/cmvr/config/dexhand_config/__pycache__/dexhand_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/dexhand_config/dexhand_config_pb2.py b/src/cmvr/config/dexhand_config/dexhand_config_pb2.py
new file mode 100644
index 0000000..d5fb86e
--- /dev/null
+++ b/src/cmvr/config/dexhand_config/dexhand_config_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/dexhand_config/dexhand_config.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/config/dexhand_config/dexhand_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/cmvr/config/dexhand_config/dexhand_config.proto\x12\x0b\x63mvr.config\"]\n\x15RH56DFTPDexHandConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\x18\n\x10poll_interval_ms\x18\x05 \x01(\x05J\x04\x08\x04\x10\x05\"\xc5\x03\n\tPX6AXGen3\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0bserial_port\x18\x02 \x01(\t\x12\x14\n\x0csensor_model\x18\x03 \x01(\t\x12\x11\n\tmodule_id\x18\x04 \x01(\x05\x12\x11\n\tbaud_rate\x18\x05 \x01(\x05\x12\x1a\n\x12\x64istributed_length\x18\x06 \x01(\x05\x12\x18\n\x10resultant_length\x18\x07 \x01(\x05\x12\x18\n\x10poll_interval_ms\x18\x08 \x01(\x05\x12\x16\n\x0e\x61uto_calibrate\x18\t \x01(\x08\x12\x1b\n\x13response_timeout_ms\x18\x0b \x01(\x05\x12\x14\n\x0ctactile_rows\x18\x0c \x01(\x05\x12\x14\n\x0ctactile_cols\x18\r \x01(\x05\x12\x1d\n\x15response_header_bytes\x18\x0e \x01(\x05\x12\x16\n\x0etactile_finger\x18\x0f \x01(\t\x12\x16\n\x0etactile_region\x18\x10 \x01(\t\x12\x13\n\x0bsensor_name\x18\x11 \x01(\t\x12@\n\x11polling_read_mode\x18\x12 \x01(\x0e\x32%.cmvr.config.PX6AXGen3PollingReadModeJ\x04\x08\n\x10\x0b\"\x99\x01\n\x13\x44\x65xHandDeviceConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x36\n\x08rh56dftp\x18\n \x01(\x0b\x32\".cmvr.config.RH56DFTPDexHandConfigH\x00\x12-\n\x0bpx_6ax_gen3\x18\x0b \x01(\x0b\x32\x16.cmvr.config.PX6AXGen3H\x00\x42\t\n\x07\x62\x61\x63kendJ\x04\x08\x02\x10\x03\"C\n\rDexHandConfig\x12\x32\n\x08\x64\x65xhands\x18\x01 \x03(\x0b\x32 .cmvr.config.DexHandDeviceConfig\"@\n\x11\x44\x65xHandRootConfig\x12+\n\x07\x64\x65xhand\x18\x01 \x01(\x0b\x32\x1a.cmvr.config.DexHandConfig*\xc5\x01\n\x18PX6AXGen3PollingReadMode\x12\x41\n=PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_AND_RESULTANT_FORCE\x10\x00\x12\x33\n/PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_FORCE\x10\x01\x12\x31\n-PX_6AX_GEN3_POLLING_READ_MODE_RESULTANT_FORCE\x10\x02\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.dexhand_config.dexhand_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_PX6AXGEN3POLLINGREADMODE']._serialized_start=907
+ _globals['_PX6AXGEN3POLLINGREADMODE']._serialized_end=1104
+ _globals['_RH56DFTPDEXHANDCONFIG']._serialized_start=64
+ _globals['_RH56DFTPDEXHANDCONFIG']._serialized_end=157
+ _globals['_PX6AXGEN3']._serialized_start=160
+ _globals['_PX6AXGEN3']._serialized_end=613
+ _globals['_DEXHANDDEVICECONFIG']._serialized_start=616
+ _globals['_DEXHANDDEVICECONFIG']._serialized_end=769
+ _globals['_DEXHANDCONFIG']._serialized_start=771
+ _globals['_DEXHANDCONFIG']._serialized_end=838
+ _globals['_DEXHANDROOTCONFIG']._serialized_start=840
+ _globals['_DEXHANDROOTCONFIG']._serialized_end=904
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/dexhand_config/dexhand_config_pb2.pyi b/src/cmvr/config/dexhand_config/dexhand_config_pb2.pyi
new file mode 100644
index 0000000..9e695f5
--- /dev/null
+++ b/src/cmvr/config/dexhand_config/dexhand_config_pb2.pyi
@@ -0,0 +1,89 @@
+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 PX6AXGen3PollingReadMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_AND_RESULTANT_FORCE: _ClassVar[PX6AXGen3PollingReadMode]
+ PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_FORCE: _ClassVar[PX6AXGen3PollingReadMode]
+ PX_6AX_GEN3_POLLING_READ_MODE_RESULTANT_FORCE: _ClassVar[PX6AXGen3PollingReadMode]
+PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_AND_RESULTANT_FORCE: PX6AXGen3PollingReadMode
+PX_6AX_GEN3_POLLING_READ_MODE_DISTRIBUTED_FORCE: PX6AXGen3PollingReadMode
+PX_6AX_GEN3_POLLING_READ_MODE_RESULTANT_FORCE: PX6AXGen3PollingReadMode
+
+class RH56DFTPDexHandConfig(_message.Message):
+ __slots__ = ("id", "ip", "port", "poll_interval_ms")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IP_FIELD_NUMBER: _ClassVar[int]
+ PORT_FIELD_NUMBER: _ClassVar[int]
+ POLL_INTERVAL_MS_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ip: str
+ port: int
+ poll_interval_ms: int
+ def __init__(self, id: _Optional[str] = ..., ip: _Optional[str] = ..., port: _Optional[int] = ..., poll_interval_ms: _Optional[int] = ...) -> None: ...
+
+class PX6AXGen3(_message.Message):
+ __slots__ = ("id", "serial_port", "sensor_model", "module_id", "baud_rate", "distributed_length", "resultant_length", "poll_interval_ms", "auto_calibrate", "response_timeout_ms", "tactile_rows", "tactile_cols", "response_header_bytes", "tactile_finger", "tactile_region", "sensor_name", "polling_read_mode")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ SERIAL_PORT_FIELD_NUMBER: _ClassVar[int]
+ SENSOR_MODEL_FIELD_NUMBER: _ClassVar[int]
+ MODULE_ID_FIELD_NUMBER: _ClassVar[int]
+ BAUD_RATE_FIELD_NUMBER: _ClassVar[int]
+ DISTRIBUTED_LENGTH_FIELD_NUMBER: _ClassVar[int]
+ RESULTANT_LENGTH_FIELD_NUMBER: _ClassVar[int]
+ POLL_INTERVAL_MS_FIELD_NUMBER: _ClassVar[int]
+ AUTO_CALIBRATE_FIELD_NUMBER: _ClassVar[int]
+ RESPONSE_TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int]
+ TACTILE_ROWS_FIELD_NUMBER: _ClassVar[int]
+ TACTILE_COLS_FIELD_NUMBER: _ClassVar[int]
+ RESPONSE_HEADER_BYTES_FIELD_NUMBER: _ClassVar[int]
+ TACTILE_FINGER_FIELD_NUMBER: _ClassVar[int]
+ TACTILE_REGION_FIELD_NUMBER: _ClassVar[int]
+ SENSOR_NAME_FIELD_NUMBER: _ClassVar[int]
+ POLLING_READ_MODE_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ serial_port: str
+ sensor_model: str
+ module_id: int
+ baud_rate: int
+ distributed_length: int
+ resultant_length: int
+ poll_interval_ms: int
+ auto_calibrate: bool
+ response_timeout_ms: int
+ tactile_rows: int
+ tactile_cols: int
+ response_header_bytes: int
+ tactile_finger: str
+ tactile_region: str
+ sensor_name: str
+ polling_read_mode: PX6AXGen3PollingReadMode
+ def __init__(self, id: _Optional[str] = ..., serial_port: _Optional[str] = ..., sensor_model: _Optional[str] = ..., module_id: _Optional[int] = ..., baud_rate: _Optional[int] = ..., distributed_length: _Optional[int] = ..., resultant_length: _Optional[int] = ..., poll_interval_ms: _Optional[int] = ..., auto_calibrate: _Optional[bool] = ..., response_timeout_ms: _Optional[int] = ..., tactile_rows: _Optional[int] = ..., tactile_cols: _Optional[int] = ..., response_header_bytes: _Optional[int] = ..., tactile_finger: _Optional[str] = ..., tactile_region: _Optional[str] = ..., sensor_name: _Optional[str] = ..., polling_read_mode: _Optional[_Union[PX6AXGen3PollingReadMode, str]] = ...) -> None: ...
+
+class DexHandDeviceConfig(_message.Message):
+ __slots__ = ("id", "rh56dftp", "px_6ax_gen3")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ RH56DFTP_FIELD_NUMBER: _ClassVar[int]
+ PX_6AX_GEN3_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ rh56dftp: RH56DFTPDexHandConfig
+ px_6ax_gen3: PX6AXGen3
+ def __init__(self, id: _Optional[str] = ..., rh56dftp: _Optional[_Union[RH56DFTPDexHandConfig, _Mapping]] = ..., px_6ax_gen3: _Optional[_Union[PX6AXGen3, _Mapping]] = ...) -> None: ...
+
+class DexHandConfig(_message.Message):
+ __slots__ = ("dexhands",)
+ DEXHANDS_FIELD_NUMBER: _ClassVar[int]
+ dexhands: _containers.RepeatedCompositeFieldContainer[DexHandDeviceConfig]
+ def __init__(self, dexhands: _Optional[_Iterable[_Union[DexHandDeviceConfig, _Mapping]]] = ...) -> None: ...
+
+class DexHandRootConfig(_message.Message):
+ __slots__ = ("dexhand",)
+ DEXHAND_FIELD_NUMBER: _ClassVar[int]
+ dexhand: DexHandConfig
+ def __init__(self, dexhand: _Optional[_Union[DexHandConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/dexhand_config/dexhand_config_pb2_grpc.py b/src/cmvr/config/dexhand_config/dexhand_config_pb2_grpc.py
new file mode 100644
index 0000000..a750f92
--- /dev/null
+++ b/src/cmvr/config/dexhand_config/dexhand_config_pb2_grpc.py
@@ -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/config/dexhand_config/dexhand_config_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}.'
+ )
diff --git a/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-310.pyc b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..4a82ba4
Binary files /dev/null and b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-312.pyc b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..0def31d
Binary files /dev/null and b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..4f04f4f
Binary files /dev/null and b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..23b3afa
Binary files /dev/null and b/src/cmvr/config/grpc_server_config/__pycache__/grpc_server_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.py b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.py
new file mode 100644
index 0000000..5d41463
--- /dev/null
+++ b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/grpc_server_config/grpc_server_config.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/config/grpc_server_config/grpc_server_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n7cmvr/config/grpc_server_config/grpc_server_config.proto\x12\x0b\x63mvr.config\"U\n\x10GRPCServerConfig\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\t\x12\x19\n\x11\x65nable_reflection\x18\x03 \x01(\x08\x12\n\n\x02id\x18\x04 \x01(\t\"J\n\x14GRPCServerRootConfig\x12\x32\n\x0bgrpc_server\x18\x01 \x01(\x0b\x32\x1d.cmvr.config.GRPCServerConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.grpc_server_config.grpc_server_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_GRPCSERVERCONFIG']._serialized_start=72
+ _globals['_GRPCSERVERCONFIG']._serialized_end=157
+ _globals['_GRPCSERVERROOTCONFIG']._serialized_start=159
+ _globals['_GRPCSERVERROOTCONFIG']._serialized_end=233
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.pyi b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.pyi
new file mode 100644
index 0000000..458e702
--- /dev/null
+++ b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2.pyi
@@ -0,0 +1,24 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class GRPCServerConfig(_message.Message):
+ __slots__ = ("host", "port", "enable_reflection", "id")
+ HOST_FIELD_NUMBER: _ClassVar[int]
+ PORT_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_REFLECTION_FIELD_NUMBER: _ClassVar[int]
+ ID_FIELD_NUMBER: _ClassVar[int]
+ host: str
+ port: str
+ enable_reflection: bool
+ id: str
+ def __init__(self, host: _Optional[str] = ..., port: _Optional[str] = ..., enable_reflection: _Optional[bool] = ..., id: _Optional[str] = ...) -> None: ...
+
+class GRPCServerRootConfig(_message.Message):
+ __slots__ = ("grpc_server",)
+ GRPC_SERVER_FIELD_NUMBER: _ClassVar[int]
+ grpc_server: GRPCServerConfig
+ def __init__(self, grpc_server: _Optional[_Union[GRPCServerConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/grpc_server_config/grpc_server_config_pb2_grpc.py b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2_grpc.py
new file mode 100644
index 0000000..82d5de1
--- /dev/null
+++ b/src/cmvr/config/grpc_server_config/grpc_server_config_pb2_grpc.py
@@ -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/config/grpc_server_config/grpc_server_config_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}.'
+ )
diff --git a/src/cmvr/config/joint_limits_config_pb2.py b/src/cmvr/config/joint_limits_config_pb2.py
new file mode 100644
index 0000000..e4a4cf7
--- /dev/null
+++ b/src/cmvr/config/joint_limits_config_pb2.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/joint_limits_config.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/config/joint_limits_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%cmvr/config/joint_limits_config.proto\x12\x0b\x63mvr.config\"V\n\x10JointLimitConfig\x12\x12\n\njoint_name\x18\x01 \x01(\t\x12\r\n\x05lower\x18\x02 \x01(\x01\x12\r\n\x05upper\x18\x03 \x01(\x01\x12\x10\n\x08velocity\x18\x04 \x01(\x01\"q\n\x11JointLimitsConfig\x12-\n\x06source\x18\x01 \x01(\x0e\x32\x1d.cmvr.config.JointLimitSource\x12-\n\x06joints\x18\x02 \x03(\x0b\x32\x1d.cmvr.config.JointLimitConfig*n\n\x10JointLimitSource\x12\x1e\n\x1aJOINT_LIMIT_SOURCE_UNKNOWN\x10\x00\x12\x1b\n\x17JOINT_LIMIT_SOURCE_URDF\x10\x01\x12\x1d\n\x19JOINT_LIMIT_SOURCE_CUSTOM\x10\x02\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.joint_limits_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_JOINTLIMITSOURCE']._serialized_start=257
+ _globals['_JOINTLIMITSOURCE']._serialized_end=367
+ _globals['_JOINTLIMITCONFIG']._serialized_start=54
+ _globals['_JOINTLIMITCONFIG']._serialized_end=140
+ _globals['_JOINTLIMITSCONFIG']._serialized_start=142
+ _globals['_JOINTLIMITSCONFIG']._serialized_end=255
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/joint_limits_config_pb2.pyi b/src/cmvr/config/joint_limits_config_pb2.pyi
new file mode 100644
index 0000000..5a48aba
--- /dev/null
+++ b/src/cmvr/config/joint_limits_config_pb2.pyi
@@ -0,0 +1,37 @@
+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 JointLimitSource(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ JOINT_LIMIT_SOURCE_UNKNOWN: _ClassVar[JointLimitSource]
+ JOINT_LIMIT_SOURCE_URDF: _ClassVar[JointLimitSource]
+ JOINT_LIMIT_SOURCE_CUSTOM: _ClassVar[JointLimitSource]
+JOINT_LIMIT_SOURCE_UNKNOWN: JointLimitSource
+JOINT_LIMIT_SOURCE_URDF: JointLimitSource
+JOINT_LIMIT_SOURCE_CUSTOM: JointLimitSource
+
+class JointLimitConfig(_message.Message):
+ __slots__ = ("joint_name", "lower", "upper", "velocity")
+ JOINT_NAME_FIELD_NUMBER: _ClassVar[int]
+ LOWER_FIELD_NUMBER: _ClassVar[int]
+ UPPER_FIELD_NUMBER: _ClassVar[int]
+ VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ joint_name: str
+ lower: float
+ upper: float
+ velocity: float
+ def __init__(self, joint_name: _Optional[str] = ..., lower: _Optional[float] = ..., upper: _Optional[float] = ..., velocity: _Optional[float] = ...) -> None: ...
+
+class JointLimitsConfig(_message.Message):
+ __slots__ = ("source", "joints")
+ SOURCE_FIELD_NUMBER: _ClassVar[int]
+ JOINTS_FIELD_NUMBER: _ClassVar[int]
+ source: JointLimitSource
+ joints: _containers.RepeatedCompositeFieldContainer[JointLimitConfig]
+ def __init__(self, source: _Optional[_Union[JointLimitSource, str]] = ..., joints: _Optional[_Iterable[_Union[JointLimitConfig, _Mapping]]] = ...) -> None: ...
diff --git a/src/cmvr/config/joint_limits_config_pb2_grpc.py b/src/cmvr/config/joint_limits_config_pb2_grpc.py
new file mode 100644
index 0000000..de4465e
--- /dev/null
+++ b/src/cmvr/config/joint_limits_config_pb2_grpc.py
@@ -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/config/joint_limits_config_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}.'
+ )
diff --git a/src/cmvr/config/lawba_ik_config_pb2.py b/src/cmvr/config/lawba_ik_config_pb2.py
new file mode 100644
index 0000000..33c6278
--- /dev/null
+++ b/src/cmvr/config/lawba_ik_config_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/lawba_ik_config.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/config/lawba_ik_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.config import srs_ik_config_pb2 as cmvr_dot_config_dot_srs__ik__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!cmvr/config/lawba_ik_config.proto\x12\x0b\x63mvr.config\x1a\x1f\x63mvr/config/srs_ik_config.proto\"\xd5\x01\n\rLawbaIKConfig\x12,\n\nsrs_config\x18\x01 \x01(\x0b\x32\x18.cmvr.config.SrsIKConfig\x12\x10\n\x08lambda_q\x18\x02 \x01(\x01\x12!\n\x19\x63ur_branch_cost_threshold\x18\x03 \x01(\x01\x12\x1c\n\x14opt_psi_update_alpha\x18\x04 \x01(\x01\x12\x19\n\x11opt_psi_max_delta\x18\x05 \x01(\x01\x12\x13\n\x0bopt_psi_min\x18\x06 \x01(\x01\x12\x13\n\x0bopt_psi_eps\x18\x07 \x01(\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.lawba_ik_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_LAWBAIKCONFIG']._serialized_start=84
+ _globals['_LAWBAIKCONFIG']._serialized_end=297
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/lawba_ik_config_pb2.pyi b/src/cmvr/config/lawba_ik_config_pb2.pyi
new file mode 100644
index 0000000..1cf0ccc
--- /dev/null
+++ b/src/cmvr/config/lawba_ik_config_pb2.pyi
@@ -0,0 +1,25 @@
+from cmvr.config import srs_ik_config_pb2 as _srs_ik_config_pb2
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class LawbaIKConfig(_message.Message):
+ __slots__ = ("srs_config", "lambda_q", "cur_branch_cost_threshold", "opt_psi_update_alpha", "opt_psi_max_delta", "opt_psi_min", "opt_psi_eps")
+ SRS_CONFIG_FIELD_NUMBER: _ClassVar[int]
+ LAMBDA_Q_FIELD_NUMBER: _ClassVar[int]
+ CUR_BRANCH_COST_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ OPT_PSI_UPDATE_ALPHA_FIELD_NUMBER: _ClassVar[int]
+ OPT_PSI_MAX_DELTA_FIELD_NUMBER: _ClassVar[int]
+ OPT_PSI_MIN_FIELD_NUMBER: _ClassVar[int]
+ OPT_PSI_EPS_FIELD_NUMBER: _ClassVar[int]
+ srs_config: _srs_ik_config_pb2.SrsIKConfig
+ lambda_q: float
+ cur_branch_cost_threshold: float
+ opt_psi_update_alpha: float
+ opt_psi_max_delta: float
+ opt_psi_min: float
+ opt_psi_eps: float
+ def __init__(self, srs_config: _Optional[_Union[_srs_ik_config_pb2.SrsIKConfig, _Mapping]] = ..., lambda_q: _Optional[float] = ..., cur_branch_cost_threshold: _Optional[float] = ..., opt_psi_update_alpha: _Optional[float] = ..., opt_psi_max_delta: _Optional[float] = ..., opt_psi_min: _Optional[float] = ..., opt_psi_eps: _Optional[float] = ...) -> None: ...
diff --git a/src/cmvr/config/lawba_ik_config_pb2_grpc.py b/src/cmvr/config/lawba_ik_config_pb2_grpc.py
new file mode 100644
index 0000000..820a36d
--- /dev/null
+++ b/src/cmvr/config/lawba_ik_config_pb2_grpc.py
@@ -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/config/lawba_ik_config_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}.'
+ )
diff --git a/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-310.pyc b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..b552b80
Binary files /dev/null and b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-312.pyc b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..08e6a9f
Binary files /dev/null and b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..5e17bd9
Binary files /dev/null and b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..002d0ba
Binary files /dev/null and b/src/cmvr/config/logger_config/__pycache__/logger_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/logger_config/logger_config_pb2.py b/src/cmvr/config/logger_config/logger_config_pb2.py
new file mode 100644
index 0000000..8c4572b
--- /dev/null
+++ b/src/cmvr/config/logger_config/logger_config_pb2.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/logger_config/logger_config.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/config/logger_config/logger_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-cmvr/config/logger_config/logger_config.proto\x12\x0b\x63mvr.config\"P\n\x08LogRoute\x12$\n\x05level\x18\x01 \x01(\x0e\x32\x15.cmvr.config.LogLevel\x12\x10\n\x08terminal\x18\x02 \x01(\x08\x12\x0c\n\x04\x66ile\x18\x03 \x01(\x08\"\xc5\x01\n\tLogFormat\x12\x16\n\tshow_time\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x17\n\nshow_level\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x1b\n\x0eshow_thread_id\x18\x03 \x01(\x08H\x02\x88\x01\x01\x12!\n\x14show_source_location\x18\x04 \x01(\x08H\x03\x88\x01\x01\x42\x0c\n\n_show_timeB\r\n\x0b_show_levelB\x11\n\x0f_show_thread_idB\x17\n\x15_show_source_location\"\xd8\x01\n\x0cLoggerConfig\x12,\n\rminimum_level\x18\x01 \x01(\x0e\x32\x15.cmvr.config.LogLevel\x12%\n\x06routes\x18\x02 \x03(\x0b\x32\x15.cmvr.config.LogRoute\x12\x11\n\tdirectory\x18\x03 \x01(\t\x12\x18\n\x10max_file_size_mb\x18\x04 \x01(\x05\x12\x1e\n\x16\x66lush_interval_seconds\x18\x05 \x01(\x05\x12&\n\x06\x66ormat\x18\x06 \x01(\x0b\x32\x16.cmvr.config.LogFormat\"=\n\x10LoggerRootConfig\x12)\n\x06logger\x18\x01 \x01(\x0b\x32\x19.cmvr.config.LoggerConfig*\x8f\x01\n\x08LogLevel\x12\x19\n\x15LOG_LEVEL_UNSPECIFIED\x10\x00\x12\x13\n\x0fLOG_LEVEL_DEBUG\x10\x01\x12\x12\n\x0eLOG_LEVEL_INFO\x10\x02\x12\x15\n\x11LOG_LEVEL_WARNING\x10\x03\x12\x13\n\x0fLOG_LEVEL_ERROR\x10\x04\x12\x13\n\x0fLOG_LEVEL_FATAL\x10\x05\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.logger_config.logger_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_LOGLEVEL']._serialized_start=627
+ _globals['_LOGLEVEL']._serialized_end=770
+ _globals['_LOGROUTE']._serialized_start=62
+ _globals['_LOGROUTE']._serialized_end=142
+ _globals['_LOGFORMAT']._serialized_start=145
+ _globals['_LOGFORMAT']._serialized_end=342
+ _globals['_LOGGERCONFIG']._serialized_start=345
+ _globals['_LOGGERCONFIG']._serialized_end=561
+ _globals['_LOGGERROOTCONFIG']._serialized_start=563
+ _globals['_LOGGERROOTCONFIG']._serialized_end=624
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/logger_config/logger_config_pb2.pyi b/src/cmvr/config/logger_config/logger_config_pb2.pyi
new file mode 100644
index 0000000..335fd91
--- /dev/null
+++ b/src/cmvr/config/logger_config/logger_config_pb2.pyi
@@ -0,0 +1,67 @@
+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 LogLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ LOG_LEVEL_UNSPECIFIED: _ClassVar[LogLevel]
+ LOG_LEVEL_DEBUG: _ClassVar[LogLevel]
+ LOG_LEVEL_INFO: _ClassVar[LogLevel]
+ LOG_LEVEL_WARNING: _ClassVar[LogLevel]
+ LOG_LEVEL_ERROR: _ClassVar[LogLevel]
+ LOG_LEVEL_FATAL: _ClassVar[LogLevel]
+LOG_LEVEL_UNSPECIFIED: LogLevel
+LOG_LEVEL_DEBUG: LogLevel
+LOG_LEVEL_INFO: LogLevel
+LOG_LEVEL_WARNING: LogLevel
+LOG_LEVEL_ERROR: LogLevel
+LOG_LEVEL_FATAL: LogLevel
+
+class LogRoute(_message.Message):
+ __slots__ = ("level", "terminal", "file")
+ LEVEL_FIELD_NUMBER: _ClassVar[int]
+ TERMINAL_FIELD_NUMBER: _ClassVar[int]
+ FILE_FIELD_NUMBER: _ClassVar[int]
+ level: LogLevel
+ terminal: bool
+ file: bool
+ def __init__(self, level: _Optional[_Union[LogLevel, str]] = ..., terminal: _Optional[bool] = ..., file: _Optional[bool] = ...) -> None: ...
+
+class LogFormat(_message.Message):
+ __slots__ = ("show_time", "show_level", "show_thread_id", "show_source_location")
+ SHOW_TIME_FIELD_NUMBER: _ClassVar[int]
+ SHOW_LEVEL_FIELD_NUMBER: _ClassVar[int]
+ SHOW_THREAD_ID_FIELD_NUMBER: _ClassVar[int]
+ SHOW_SOURCE_LOCATION_FIELD_NUMBER: _ClassVar[int]
+ show_time: bool
+ show_level: bool
+ show_thread_id: bool
+ show_source_location: bool
+ def __init__(self, show_time: _Optional[bool] = ..., show_level: _Optional[bool] = ..., show_thread_id: _Optional[bool] = ..., show_source_location: _Optional[bool] = ...) -> None: ...
+
+class LoggerConfig(_message.Message):
+ __slots__ = ("minimum_level", "routes", "directory", "max_file_size_mb", "flush_interval_seconds", "format")
+ MINIMUM_LEVEL_FIELD_NUMBER: _ClassVar[int]
+ ROUTES_FIELD_NUMBER: _ClassVar[int]
+ DIRECTORY_FIELD_NUMBER: _ClassVar[int]
+ MAX_FILE_SIZE_MB_FIELD_NUMBER: _ClassVar[int]
+ FLUSH_INTERVAL_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ FORMAT_FIELD_NUMBER: _ClassVar[int]
+ minimum_level: LogLevel
+ routes: _containers.RepeatedCompositeFieldContainer[LogRoute]
+ directory: str
+ max_file_size_mb: int
+ flush_interval_seconds: int
+ format: LogFormat
+ def __init__(self, minimum_level: _Optional[_Union[LogLevel, str]] = ..., routes: _Optional[_Iterable[_Union[LogRoute, _Mapping]]] = ..., directory: _Optional[str] = ..., max_file_size_mb: _Optional[int] = ..., flush_interval_seconds: _Optional[int] = ..., format: _Optional[_Union[LogFormat, _Mapping]] = ...) -> None: ...
+
+class LoggerRootConfig(_message.Message):
+ __slots__ = ("logger",)
+ LOGGER_FIELD_NUMBER: _ClassVar[int]
+ logger: LoggerConfig
+ def __init__(self, logger: _Optional[_Union[LoggerConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/logger_config/logger_config_pb2_grpc.py b/src/cmvr/config/logger_config/logger_config_pb2_grpc.py
new file mode 100644
index 0000000..759e3e1
--- /dev/null
+++ b/src/cmvr/config/logger_config/logger_config_pb2_grpc.py
@@ -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/config/logger_config/logger_config_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}.'
+ )
diff --git a/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-310.pyc b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..205054f
Binary files /dev/null and b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-312.pyc b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..2275338
Binary files /dev/null and b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..08e25b1
Binary files /dev/null and b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..36f1b9d
Binary files /dev/null and b/src/cmvr/config/microphone_config/__pycache__/microphone_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/microphone_config/microphone_config_pb2.py b/src/cmvr/config/microphone_config/microphone_config_pb2.py
new file mode 100644
index 0000000..70b6bc2
--- /dev/null
+++ b/src/cmvr/config/microphone_config/microphone_config_pb2.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/microphone_config/microphone_config.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/config/microphone_config/microphone_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5cmvr/config/microphone_config/microphone_config.proto\x12\x0b\x63mvr.config\"v\n\x16\x46\x46MpegMicroPhoneConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08\x63hannels\x18\x02 \x01(\x05\x12\x12\n\nsampleRate\x18\x03 \x01(\x05\x12\x0e\n\x06volume\x18\x04 \x01(\x05\x12\x14\n\x0cinput_device\x18\x06 \x01(\tJ\x04\x08\x05\x10\x06\"l\n\x16MicrophoneDeviceConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x35\n\x06\x66\x66mpeg\x18\n \x01(\x0b\x32#.cmvr.config.FFMpegMicroPhoneConfigH\x00\x42\t\n\x07\x62\x61\x63kendJ\x04\x08\x02\x10\x03\"L\n\x10MicroPhoneConfig\x12\x38\n\x0bmicrophones\x18\x01 \x03(\x0b\x32#.cmvr.config.MicrophoneDeviceConfig\"I\n\x14MicroPhoneRootConfig\x12\x31\n\nmicrophone\x18\x01 \x01(\x0b\x32\x1d.cmvr.config.MicroPhoneConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.microphone_config.microphone_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_FFMPEGMICROPHONECONFIG']._serialized_start=70
+ _globals['_FFMPEGMICROPHONECONFIG']._serialized_end=188
+ _globals['_MICROPHONEDEVICECONFIG']._serialized_start=190
+ _globals['_MICROPHONEDEVICECONFIG']._serialized_end=298
+ _globals['_MICROPHONECONFIG']._serialized_start=300
+ _globals['_MICROPHONECONFIG']._serialized_end=376
+ _globals['_MICROPHONEROOTCONFIG']._serialized_start=378
+ _globals['_MICROPHONEROOTCONFIG']._serialized_end=451
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/microphone_config/microphone_config_pb2.pyi b/src/cmvr/config/microphone_config/microphone_config_pb2.pyi
new file mode 100644
index 0000000..a837c3c
--- /dev/null
+++ b/src/cmvr/config/microphone_config/microphone_config_pb2.pyi
@@ -0,0 +1,41 @@
+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 FFMpegMicroPhoneConfig(_message.Message):
+ __slots__ = ("id", "channels", "sampleRate", "volume", "input_device")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ CHANNELS_FIELD_NUMBER: _ClassVar[int]
+ SAMPLERATE_FIELD_NUMBER: _ClassVar[int]
+ VOLUME_FIELD_NUMBER: _ClassVar[int]
+ INPUT_DEVICE_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ channels: int
+ sampleRate: int
+ volume: int
+ input_device: str
+ def __init__(self, id: _Optional[str] = ..., channels: _Optional[int] = ..., sampleRate: _Optional[int] = ..., volume: _Optional[int] = ..., input_device: _Optional[str] = ...) -> None: ...
+
+class MicrophoneDeviceConfig(_message.Message):
+ __slots__ = ("id", "ffmpeg")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ FFMPEG_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ffmpeg: FFMpegMicroPhoneConfig
+ def __init__(self, id: _Optional[str] = ..., ffmpeg: _Optional[_Union[FFMpegMicroPhoneConfig, _Mapping]] = ...) -> None: ...
+
+class MicroPhoneConfig(_message.Message):
+ __slots__ = ("microphones",)
+ MICROPHONES_FIELD_NUMBER: _ClassVar[int]
+ microphones: _containers.RepeatedCompositeFieldContainer[MicrophoneDeviceConfig]
+ def __init__(self, microphones: _Optional[_Iterable[_Union[MicrophoneDeviceConfig, _Mapping]]] = ...) -> None: ...
+
+class MicroPhoneRootConfig(_message.Message):
+ __slots__ = ("microphone",)
+ MICROPHONE_FIELD_NUMBER: _ClassVar[int]
+ microphone: MicroPhoneConfig
+ def __init__(self, microphone: _Optional[_Union[MicroPhoneConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/microphone_config/microphone_config_pb2_grpc.py b/src/cmvr/config/microphone_config/microphone_config_pb2_grpc.py
new file mode 100644
index 0000000..dba125a
--- /dev/null
+++ b/src/cmvr/config/microphone_config/microphone_config_pb2_grpc.py
@@ -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/config/microphone_config/microphone_config_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}.'
+ )
diff --git a/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-310.pyc b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..d0da46b
Binary files /dev/null and b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-312.pyc b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..023b358
Binary files /dev/null and b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..0c008cb
Binary files /dev/null and b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..9ca1b4f
Binary files /dev/null and b/src/cmvr/config/motor_config/__pycache__/motor_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/motor_config/motor_config_pb2.py b/src/cmvr/config/motor_config/motor_config_pb2.py
new file mode 100644
index 0000000..30a5262
--- /dev/null
+++ b/src/cmvr/config/motor_config/motor_config_pb2.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/motor_config/motor_config.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/config/motor_config/motor_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.config import joint_limits_config_pb2 as cmvr_dot_config_dot_joint__limits__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+cmvr/config/motor_config/motor_config.proto\x12\x0b\x63mvr.config\x1a%cmvr/config/joint_limits_config.proto\"j\n\x0eTi5MotorConfig\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x12\n\njoint_name\x18\x02 \x01(\t\x12\x12\n\nlimit_q_lb\x18\x03 \x01(\x01\x12\x12\n\nlimit_q_ub\x18\x04 \x01(\x01\x12\x10\n\x08limit_qd\x18\x05 \x01(\x01\"5\n\x0fSocketCanConfig\x12\x0e\n\x06\x64\x65v_id\x18\x01 \x01(\t\x12\x12\n\nchannel_id\x18\x02 \x01(\x05\"5\n\x0e\x45therCATConfig\x12\x11\n\tmaster_id\x18\x01 \x01(\t\x12\x10\n\x08\x63ycle_us\x18\x02 \x01(\x05\"\xce\x02\n\x10MotorGroupConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12+\n\x08\x62us_type\x18\x02 \x01(\x0e\x32\x19.cmvr.config.MotorBusType\x12\x12\n\ntool_frame\x18\x03 \x01(\t\x12+\n\x03\x63\x61n\x18\n \x01(\x0b\x32\x1c.cmvr.config.SocketCanConfigH\x00\x12/\n\x08\x65thercat\x18\x0b \x01(\x0b\x32\x1b.cmvr.config.EtherCATConfigH\x00\x12+\n\x06motors\x18\x14 \x03(\x0b\x32\x1b.cmvr.config.Ti5MotorConfig\x12\x34\n\x0cjoint_limits\x18\x1e \x01(\x0b\x32\x1e.cmvr.config.JointLimitsConfig\x12\x1e\n\x16joint_limits_urdf_path\x18\x1f \x01(\tB\x0c\n\nbus_config\"N\n\x0bMotorConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x33\n\x0cmotor_groups\x18\x02 \x03(\x0b\x32\x1d.cmvr.config.MotorGroupConfig\":\n\x0fMotorRootConfig\x12\'\n\x05motor\x18\x01 \x01(\x0b\x32\x18.cmvr.config.MotorConfig*f\n\x0cMotorBusType\x12\x15\n\x11MOTOR_BUS_UNKNOWN\x10\x00\x12\x11\n\rMOTOR_BUS_CAN\x10\x01\x12\x16\n\x12MOTOR_BUS_ETHERCAT\x10\x02\x12\x14\n\x10MOTOR_BUS_MUJOCO\x10\x03\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.motor_config.motor_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_MOTORBUSTYPE']._serialized_start=794
+ _globals['_MOTORBUSTYPE']._serialized_end=896
+ _globals['_TI5MOTORCONFIG']._serialized_start=99
+ _globals['_TI5MOTORCONFIG']._serialized_end=205
+ _globals['_SOCKETCANCONFIG']._serialized_start=207
+ _globals['_SOCKETCANCONFIG']._serialized_end=260
+ _globals['_ETHERCATCONFIG']._serialized_start=262
+ _globals['_ETHERCATCONFIG']._serialized_end=315
+ _globals['_MOTORGROUPCONFIG']._serialized_start=318
+ _globals['_MOTORGROUPCONFIG']._serialized_end=652
+ _globals['_MOTORCONFIG']._serialized_start=654
+ _globals['_MOTORCONFIG']._serialized_end=732
+ _globals['_MOTORROOTCONFIG']._serialized_start=734
+ _globals['_MOTORROOTCONFIG']._serialized_end=792
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/motor_config/motor_config_pb2.pyi b/src/cmvr/config/motor_config/motor_config_pb2.pyi
new file mode 100644
index 0000000..3743b31
--- /dev/null
+++ b/src/cmvr/config/motor_config/motor_config_pb2.pyi
@@ -0,0 +1,84 @@
+from cmvr.config import joint_limits_config_pb2 as _joint_limits_config_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 MotorBusType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ MOTOR_BUS_UNKNOWN: _ClassVar[MotorBusType]
+ MOTOR_BUS_CAN: _ClassVar[MotorBusType]
+ MOTOR_BUS_ETHERCAT: _ClassVar[MotorBusType]
+ MOTOR_BUS_MUJOCO: _ClassVar[MotorBusType]
+MOTOR_BUS_UNKNOWN: MotorBusType
+MOTOR_BUS_CAN: MotorBusType
+MOTOR_BUS_ETHERCAT: MotorBusType
+MOTOR_BUS_MUJOCO: MotorBusType
+
+class Ti5MotorConfig(_message.Message):
+ __slots__ = ("id", "joint_name", "limit_q_lb", "limit_q_ub", "limit_qd")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ JOINT_NAME_FIELD_NUMBER: _ClassVar[int]
+ LIMIT_Q_LB_FIELD_NUMBER: _ClassVar[int]
+ LIMIT_Q_UB_FIELD_NUMBER: _ClassVar[int]
+ LIMIT_QD_FIELD_NUMBER: _ClassVar[int]
+ id: int
+ joint_name: str
+ limit_q_lb: float
+ limit_q_ub: float
+ limit_qd: float
+ def __init__(self, id: _Optional[int] = ..., joint_name: _Optional[str] = ..., limit_q_lb: _Optional[float] = ..., limit_q_ub: _Optional[float] = ..., limit_qd: _Optional[float] = ...) -> None: ...
+
+class SocketCanConfig(_message.Message):
+ __slots__ = ("dev_id", "channel_id")
+ DEV_ID_FIELD_NUMBER: _ClassVar[int]
+ CHANNEL_ID_FIELD_NUMBER: _ClassVar[int]
+ dev_id: str
+ channel_id: int
+ def __init__(self, dev_id: _Optional[str] = ..., channel_id: _Optional[int] = ...) -> None: ...
+
+class EtherCATConfig(_message.Message):
+ __slots__ = ("master_id", "cycle_us")
+ MASTER_ID_FIELD_NUMBER: _ClassVar[int]
+ CYCLE_US_FIELD_NUMBER: _ClassVar[int]
+ master_id: str
+ cycle_us: int
+ def __init__(self, master_id: _Optional[str] = ..., cycle_us: _Optional[int] = ...) -> None: ...
+
+class MotorGroupConfig(_message.Message):
+ __slots__ = ("id", "bus_type", "tool_frame", "can", "ethercat", "motors", "joint_limits", "joint_limits_urdf_path")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ BUS_TYPE_FIELD_NUMBER: _ClassVar[int]
+ TOOL_FRAME_FIELD_NUMBER: _ClassVar[int]
+ CAN_FIELD_NUMBER: _ClassVar[int]
+ ETHERCAT_FIELD_NUMBER: _ClassVar[int]
+ MOTORS_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMITS_URDF_PATH_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ bus_type: MotorBusType
+ tool_frame: str
+ can: SocketCanConfig
+ ethercat: EtherCATConfig
+ motors: _containers.RepeatedCompositeFieldContainer[Ti5MotorConfig]
+ joint_limits: _joint_limits_config_pb2.JointLimitsConfig
+ joint_limits_urdf_path: str
+ def __init__(self, id: _Optional[str] = ..., bus_type: _Optional[_Union[MotorBusType, str]] = ..., tool_frame: _Optional[str] = ..., can: _Optional[_Union[SocketCanConfig, _Mapping]] = ..., ethercat: _Optional[_Union[EtherCATConfig, _Mapping]] = ..., motors: _Optional[_Iterable[_Union[Ti5MotorConfig, _Mapping]]] = ..., joint_limits: _Optional[_Union[_joint_limits_config_pb2.JointLimitsConfig, _Mapping]] = ..., joint_limits_urdf_path: _Optional[str] = ...) -> None: ...
+
+class MotorConfig(_message.Message):
+ __slots__ = ("id", "motor_groups")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_GROUPS_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ motor_groups: _containers.RepeatedCompositeFieldContainer[MotorGroupConfig]
+ def __init__(self, id: _Optional[str] = ..., motor_groups: _Optional[_Iterable[_Union[MotorGroupConfig, _Mapping]]] = ...) -> None: ...
+
+class MotorRootConfig(_message.Message):
+ __slots__ = ("motor",)
+ MOTOR_FIELD_NUMBER: _ClassVar[int]
+ motor: MotorConfig
+ def __init__(self, motor: _Optional[_Union[MotorConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/motor_config/motor_config_pb2_grpc.py b/src/cmvr/config/motor_config/motor_config_pb2_grpc.py
new file mode 100644
index 0000000..f4bdce8
--- /dev/null
+++ b/src/cmvr/config/motor_config/motor_config_pb2_grpc.py
@@ -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/config/motor_config/motor_config_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}.'
+ )
diff --git a/src/cmvr/config/pinocchio_dls_ik_config_pb2.py b/src/cmvr/config/pinocchio_dls_ik_config_pb2.py
new file mode 100644
index 0000000..3b5df7f
--- /dev/null
+++ b/src/cmvr/config/pinocchio_dls_ik_config_pb2.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/pinocchio_dls_ik_config.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/config/pinocchio_dls_ik_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.config import joint_limits_config_pb2 as cmvr_dot_config_dot_joint__limits__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)cmvr/config/pinocchio_dls_ik_config.proto\x12\x0b\x63mvr.config\x1a%cmvr/config/joint_limits_config.proto\"\xb8\x02\n\x14PinocchioDlsIKConfig\x12\x11\n\turdf_path\x18\x01 \x01(\t\x12\x17\n\x0f\x62\x61se_frame_name\x18\x02 \x01(\t\x12\x19\n\x11\x66lange_frame_name\x18\x03 \x01(\t\x12\x16\n\x0etcp_frame_name\x18\x04 \x01(\t\x12\x11\n\tmax_iters\x18\x05 \x01(\x05\x12\x0f\n\x07pos_eps\x18\x06 \x01(\x01\x12\x0f\n\x07rot_eps\x18\x07 \x01(\x01\x12\x0f\n\x07\x64\x61mping\x18\x08 \x01(\x01\x12\x45\n\x15joint_limit_avoidance\x18\t \x01(\x0b\x32&.cmvr.config.JointLimitAvoidanceConfig\x12\x34\n\x0cjoint_limits\x18\n \x01(\x0b\x32\x1e.cmvr.config.JointLimitsConfig\"a\n\x19JointLimitAvoidanceConfig\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\x12\x0c\n\x04gain\x18\x02 \x01(\x01\x12\x14\n\x0cmargin_ratio\x18\x03 \x01(\x01\x12\x10\n\x08max_push\x18\x04 \x01(\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.pinocchio_dls_ik_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_PINOCCHIODLSIKCONFIG']._serialized_start=98
+ _globals['_PINOCCHIODLSIKCONFIG']._serialized_end=410
+ _globals['_JOINTLIMITAVOIDANCECONFIG']._serialized_start=412
+ _globals['_JOINTLIMITAVOIDANCECONFIG']._serialized_end=509
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/pinocchio_dls_ik_config_pb2.pyi b/src/cmvr/config/pinocchio_dls_ik_config_pb2.pyi
new file mode 100644
index 0000000..ade3aba
--- /dev/null
+++ b/src/cmvr/config/pinocchio_dls_ik_config_pb2.pyi
@@ -0,0 +1,43 @@
+from cmvr.config import joint_limits_config_pb2 as _joint_limits_config_pb2
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from collections.abc import Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class PinocchioDlsIKConfig(_message.Message):
+ __slots__ = ("urdf_path", "base_frame_name", "flange_frame_name", "tcp_frame_name", "max_iters", "pos_eps", "rot_eps", "damping", "joint_limit_avoidance", "joint_limits")
+ URDF_PATH_FIELD_NUMBER: _ClassVar[int]
+ BASE_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ FLANGE_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ TCP_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ MAX_ITERS_FIELD_NUMBER: _ClassVar[int]
+ POS_EPS_FIELD_NUMBER: _ClassVar[int]
+ ROT_EPS_FIELD_NUMBER: _ClassVar[int]
+ DAMPING_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMIT_AVOIDANCE_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ urdf_path: str
+ base_frame_name: str
+ flange_frame_name: str
+ tcp_frame_name: str
+ max_iters: int
+ pos_eps: float
+ rot_eps: float
+ damping: float
+ joint_limit_avoidance: JointLimitAvoidanceConfig
+ joint_limits: _joint_limits_config_pb2.JointLimitsConfig
+ def __init__(self, urdf_path: _Optional[str] = ..., base_frame_name: _Optional[str] = ..., flange_frame_name: _Optional[str] = ..., tcp_frame_name: _Optional[str] = ..., max_iters: _Optional[int] = ..., pos_eps: _Optional[float] = ..., rot_eps: _Optional[float] = ..., damping: _Optional[float] = ..., joint_limit_avoidance: _Optional[_Union[JointLimitAvoidanceConfig, _Mapping]] = ..., joint_limits: _Optional[_Union[_joint_limits_config_pb2.JointLimitsConfig, _Mapping]] = ...) -> None: ...
+
+class JointLimitAvoidanceConfig(_message.Message):
+ __slots__ = ("enable", "gain", "margin_ratio", "max_push")
+ ENABLE_FIELD_NUMBER: _ClassVar[int]
+ GAIN_FIELD_NUMBER: _ClassVar[int]
+ MARGIN_RATIO_FIELD_NUMBER: _ClassVar[int]
+ MAX_PUSH_FIELD_NUMBER: _ClassVar[int]
+ enable: bool
+ gain: float
+ margin_ratio: float
+ max_push: float
+ def __init__(self, enable: _Optional[bool] = ..., gain: _Optional[float] = ..., margin_ratio: _Optional[float] = ..., max_push: _Optional[float] = ...) -> None: ...
diff --git a/src/cmvr/config/pinocchio_dls_ik_config_pb2_grpc.py b/src/cmvr/config/pinocchio_dls_ik_config_pb2_grpc.py
new file mode 100644
index 0000000..e44a961
--- /dev/null
+++ b/src/cmvr/config/pinocchio_dls_ik_config_pb2_grpc.py
@@ -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/config/pinocchio_dls_ik_config_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}.'
+ )
diff --git a/src/cmvr/config/pinocchio_qp_ik_config_pb2.py b/src/cmvr/config/pinocchio_qp_ik_config_pb2.py
new file mode 100644
index 0000000..8cf275d
--- /dev/null
+++ b/src/cmvr/config/pinocchio_qp_ik_config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/pinocchio_qp_ik_config.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/config/pinocchio_qp_ik_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(cmvr/config/pinocchio_qp_ik_config.proto\x12\x0b\x63mvr.config\"\xcd\x01\n\x13PinocchioQpIKConfig\x12\x11\n\turdf_path\x18\x01 \x01(\t\x12\x17\n\x0f\x62\x61se_frame_name\x18\x02 \x01(\t\x12\x19\n\x11\x66lange_frame_name\x18\x03 \x01(\t\x12\x16\n\x0etcp_frame_name\x18\x04 \x01(\t\x12\x0e\n\x06lambda\x18\x05 \x01(\x01\x12\x10\n\x08w_posrot\x18\x06 \x01(\x01\x12\x11\n\tmax_iters\x18\x07 \x01(\x05\x12\x0b\n\x03tol\x18\x08 \x01(\x01\x12\x15\n\rqp_time_limit\x18\t \x01(\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.pinocchio_qp_ik_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_PINOCCHIOQPIKCONFIG']._serialized_start=58
+ _globals['_PINOCCHIOQPIKCONFIG']._serialized_end=263
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/pinocchio_qp_ik_config_pb2.pyi b/src/cmvr/config/pinocchio_qp_ik_config_pb2.pyi
new file mode 100644
index 0000000..ba7aeef
--- /dev/null
+++ b/src/cmvr/config/pinocchio_qp_ik_config_pb2.pyi
@@ -0,0 +1,26 @@
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from typing import ClassVar as _ClassVar, Optional as _Optional
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class PinocchioQpIKConfig(_message.Message):
+ __slots__ = ("urdf_path", "base_frame_name", "flange_frame_name", "tcp_frame_name", "w_posrot", "max_iters", "tol", "qp_time_limit")
+ URDF_PATH_FIELD_NUMBER: _ClassVar[int]
+ BASE_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ FLANGE_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ TCP_FRAME_NAME_FIELD_NUMBER: _ClassVar[int]
+ LAMBDA_FIELD_NUMBER: _ClassVar[int]
+ W_POSROT_FIELD_NUMBER: _ClassVar[int]
+ MAX_ITERS_FIELD_NUMBER: _ClassVar[int]
+ TOL_FIELD_NUMBER: _ClassVar[int]
+ QP_TIME_LIMIT_FIELD_NUMBER: _ClassVar[int]
+ urdf_path: str
+ base_frame_name: str
+ flange_frame_name: str
+ tcp_frame_name: str
+ w_posrot: float
+ max_iters: int
+ tol: float
+ qp_time_limit: float
+ def __init__(self, urdf_path: _Optional[str] = ..., base_frame_name: _Optional[str] = ..., flange_frame_name: _Optional[str] = ..., tcp_frame_name: _Optional[str] = ..., w_posrot: _Optional[float] = ..., max_iters: _Optional[int] = ..., tol: _Optional[float] = ..., qp_time_limit: _Optional[float] = ..., **kwargs) -> None: ...
diff --git a/src/cmvr/config/pinocchio_qp_ik_config_pb2_grpc.py b/src/cmvr/config/pinocchio_qp_ik_config_pb2_grpc.py
new file mode 100644
index 0000000..29da68f
--- /dev/null
+++ b/src/cmvr/config/pinocchio_qp_ik_config_pb2_grpc.py
@@ -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/config/pinocchio_qp_ik_config_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}.'
+ )
diff --git a/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-310.pyc b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..a414907
Binary files /dev/null and b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-312.pyc b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..434bac0
Binary files /dev/null and b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..d25d72e
Binary files /dev/null and b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..be1da55
Binary files /dev/null and b/src/cmvr/config/speaker_config/__pycache__/speaker_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/speaker_config/speaker_config_pb2.py b/src/cmvr/config/speaker_config/speaker_config_pb2.py
new file mode 100644
index 0000000..966909c
--- /dev/null
+++ b/src/cmvr/config/speaker_config/speaker_config_pb2.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/speaker_config/speaker_config.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/config/speaker_config/speaker_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/cmvr/config/speaker_config/speaker_config.proto\x12\x0b\x63mvr.config\"\'\n\x13\x46\x46MpegSpeakerConfig\x12\n\n\x02id\x18\x01 \x01(\tJ\x04\x08\x02\x10\x03\"f\n\x13SpeakerDeviceConfig\x12\n\n\x02id\x18\x01 \x01(\t\x12\x32\n\x06\x66\x66mpeg\x18\n \x01(\x0b\x32 .cmvr.config.FFMpegSpeakerConfigH\x00\x42\t\n\x07\x62\x61\x63kendJ\x04\x08\x02\x10\x03\"C\n\rSpeakerConfig\x12\x32\n\x08speakers\x18\x01 \x03(\x0b\x32 .cmvr.config.SpeakerDeviceConfig\"@\n\x11SpeakerRootConfig\x12+\n\x07speaker\x18\x01 \x01(\x0b\x32\x1a.cmvr.config.SpeakerConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.speaker_config.speaker_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_FFMPEGSPEAKERCONFIG']._serialized_start=64
+ _globals['_FFMPEGSPEAKERCONFIG']._serialized_end=103
+ _globals['_SPEAKERDEVICECONFIG']._serialized_start=105
+ _globals['_SPEAKERDEVICECONFIG']._serialized_end=207
+ _globals['_SPEAKERCONFIG']._serialized_start=209
+ _globals['_SPEAKERCONFIG']._serialized_end=276
+ _globals['_SPEAKERROOTCONFIG']._serialized_start=278
+ _globals['_SPEAKERROOTCONFIG']._serialized_end=342
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/speaker_config/speaker_config_pb2.pyi b/src/cmvr/config/speaker_config/speaker_config_pb2.pyi
new file mode 100644
index 0000000..833d735
--- /dev/null
+++ b/src/cmvr/config/speaker_config/speaker_config_pb2.pyi
@@ -0,0 +1,33 @@
+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 FFMpegSpeakerConfig(_message.Message):
+ __slots__ = ("id",)
+ ID_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ def __init__(self, id: _Optional[str] = ...) -> None: ...
+
+class SpeakerDeviceConfig(_message.Message):
+ __slots__ = ("id", "ffmpeg")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ FFMPEG_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ ffmpeg: FFMpegSpeakerConfig
+ def __init__(self, id: _Optional[str] = ..., ffmpeg: _Optional[_Union[FFMpegSpeakerConfig, _Mapping]] = ...) -> None: ...
+
+class SpeakerConfig(_message.Message):
+ __slots__ = ("speakers",)
+ SPEAKERS_FIELD_NUMBER: _ClassVar[int]
+ speakers: _containers.RepeatedCompositeFieldContainer[SpeakerDeviceConfig]
+ def __init__(self, speakers: _Optional[_Iterable[_Union[SpeakerDeviceConfig, _Mapping]]] = ...) -> None: ...
+
+class SpeakerRootConfig(_message.Message):
+ __slots__ = ("speaker",)
+ SPEAKER_FIELD_NUMBER: _ClassVar[int]
+ speaker: SpeakerConfig
+ def __init__(self, speaker: _Optional[_Union[SpeakerConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/speaker_config/speaker_config_pb2_grpc.py b/src/cmvr/config/speaker_config/speaker_config_pb2_grpc.py
new file mode 100644
index 0000000..cb837ad
--- /dev/null
+++ b/src/cmvr/config/speaker_config/speaker_config_pb2_grpc.py
@@ -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/config/speaker_config/speaker_config_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}.'
+ )
diff --git a/src/cmvr/config/srs_ik_config_pb2.py b/src/cmvr/config/srs_ik_config_pb2.py
new file mode 100644
index 0000000..07dc7c9
--- /dev/null
+++ b/src/cmvr/config/srs_ik_config_pb2.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/srs_ik_config.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/config/srs_ik_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x63mvr/config/srs_ik_config.proto\x12\x0b\x63mvr.config\"\xb5\x01\n\x0bSrsIKConfig\x12\x13\n\x0binitial_psi\x18\x01 \x01(\x01\x12\x17\n\x0fshoulder_config\x18\x02 \x01(\x05\x12\x14\n\x0c\x65lbow_config\x18\x03 \x01(\x05\x12\x14\n\x0cwrist_config\x18\x04 \x01(\x05\x12\x14\n\x0clink_lengths\x18\n \x03(\x01\x12\x1a\n\x12joint_lower_limits\x18\x0b \x03(\x01\x12\x1a\n\x12joint_upper_limits\x18\x0c \x03(\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.srs_ik_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_SRSIKCONFIG']._serialized_start=49
+ _globals['_SRSIKCONFIG']._serialized_end=230
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/srs_ik_config_pb2.pyi b/src/cmvr/config/srs_ik_config_pb2.pyi
new file mode 100644
index 0000000..746b87c
--- /dev/null
+++ b/src/cmvr/config/srs_ik_config_pb2.pyi
@@ -0,0 +1,25 @@
+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
+from typing import ClassVar as _ClassVar, Optional as _Optional
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class SrsIKConfig(_message.Message):
+ __slots__ = ("initial_psi", "shoulder_config", "elbow_config", "wrist_config", "link_lengths", "joint_lower_limits", "joint_upper_limits")
+ INITIAL_PSI_FIELD_NUMBER: _ClassVar[int]
+ SHOULDER_CONFIG_FIELD_NUMBER: _ClassVar[int]
+ ELBOW_CONFIG_FIELD_NUMBER: _ClassVar[int]
+ WRIST_CONFIG_FIELD_NUMBER: _ClassVar[int]
+ LINK_LENGTHS_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LOWER_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ JOINT_UPPER_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ initial_psi: float
+ shoulder_config: int
+ elbow_config: int
+ wrist_config: int
+ link_lengths: _containers.RepeatedScalarFieldContainer[float]
+ joint_lower_limits: _containers.RepeatedScalarFieldContainer[float]
+ joint_upper_limits: _containers.RepeatedScalarFieldContainer[float]
+ def __init__(self, initial_psi: _Optional[float] = ..., shoulder_config: _Optional[int] = ..., elbow_config: _Optional[int] = ..., wrist_config: _Optional[int] = ..., link_lengths: _Optional[_Iterable[float]] = ..., joint_lower_limits: _Optional[_Iterable[float]] = ..., joint_upper_limits: _Optional[_Iterable[float]] = ...) -> None: ...
diff --git a/src/cmvr/config/srs_ik_config_pb2_grpc.py b/src/cmvr/config/srs_ik_config_pb2_grpc.py
new file mode 100644
index 0000000..44032ce
--- /dev/null
+++ b/src/cmvr/config/srs_ik_config_pb2_grpc.py
@@ -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/config/srs_ik_config_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}.'
+ )
diff --git a/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-310.pyc b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..156b4d4
Binary files /dev/null and b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-312.pyc b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..e513b8f
Binary files /dev/null and b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..6e5d2da
Binary files /dev/null and b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..bc596b9
Binary files /dev/null and b/src/cmvr/config/task_manager_config/__pycache__/task_manager_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/task_manager_config/task_manager_config_pb2.py b/src/cmvr/config/task_manager_config/task_manager_config_pb2.py
new file mode 100644
index 0000000..a4faaaf
--- /dev/null
+++ b/src/cmvr/config/task_manager_config/task_manager_config_pb2.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/task_manager_config/task_manager_config.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/config/task_manager_config/task_manager_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n9cmvr/config/task_manager_config/task_manager_config.proto\x12\x0b\x63mvr.config\"\xb3\x03\n\x0fTaskConfigEntry\x12\n\n\x02id\x18\x01 \x01(\t\x12\x33\n\x04type\x18\x02 \x01(\x0e\x32%.cmvr.config.TaskConfigEntry.TaskType\x12\x13\n\x0b\x63onfig_file\x18\x03 \x01(\t\x12\x0e\n\x06\x65nable\x18\x04 \x01(\x08\x12:\n\x08run_mode\x18\x05 \x01(\x0e\x32(.cmvr.config.TaskConfigEntry.TaskRunMode\x12\x18\n\x10\x63ontrol_period_s\x18\x06 \x01(\x01\"u\n\x08TaskType\x12\x15\n\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x1a\n\x16TASK_TYPE_TOUCH_SCREEN\x10\x01\x12\x19\n\x15TASK_TYPE_GRPC_SERVER\x10\x03\"\x04\x08\x02\x10\x02*\x15TASK_TYPE_ARM_CONTROL\"m\n\x0bTaskRunMode\x12\x19\n\x15TASK_RUN_MODE_UNKNOWN\x10\x00\x12\x1f\n\x1bTASK_RUN_MODE_PERIODIC_STEP\x10\x01\x12\"\n\x1eTASK_RUN_MODE_BLOCKING_SERVICE\x10\x02\"@\n\x11TaskManagerConfig\x12+\n\x05tasks\x18\x01 \x03(\x0b\x32\x1c.cmvr.config.TaskConfigEntry\"M\n\x15TaskManagerRootConfig\x12\x34\n\x0ctask_manager\x18\x01 \x01(\x0b\x32\x1e.cmvr.config.TaskManagerConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.task_manager_config.task_manager_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TASKCONFIGENTRY']._serialized_start=75
+ _globals['_TASKCONFIGENTRY']._serialized_end=510
+ _globals['_TASKCONFIGENTRY_TASKTYPE']._serialized_start=282
+ _globals['_TASKCONFIGENTRY_TASKTYPE']._serialized_end=399
+ _globals['_TASKCONFIGENTRY_TASKRUNMODE']._serialized_start=401
+ _globals['_TASKCONFIGENTRY_TASKRUNMODE']._serialized_end=510
+ _globals['_TASKMANAGERCONFIG']._serialized_start=512
+ _globals['_TASKMANAGERCONFIG']._serialized_end=576
+ _globals['_TASKMANAGERROOTCONFIG']._serialized_start=578
+ _globals['_TASKMANAGERROOTCONFIG']._serialized_end=655
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/task_manager_config/task_manager_config_pb2.pyi b/src/cmvr/config/task_manager_config/task_manager_config_pb2.pyi
new file mode 100644
index 0000000..ecdd172
--- /dev/null
+++ b/src/cmvr/config/task_manager_config/task_manager_config_pb2.pyi
@@ -0,0 +1,52 @@
+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 TaskConfigEntry(_message.Message):
+ __slots__ = ("id", "type", "config_file", "enable", "run_mode", "control_period_s")
+ class TaskType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TASK_TYPE_UNKNOWN: _ClassVar[TaskConfigEntry.TaskType]
+ TASK_TYPE_TOUCH_SCREEN: _ClassVar[TaskConfigEntry.TaskType]
+ TASK_TYPE_GRPC_SERVER: _ClassVar[TaskConfigEntry.TaskType]
+ TASK_TYPE_UNKNOWN: TaskConfigEntry.TaskType
+ TASK_TYPE_TOUCH_SCREEN: TaskConfigEntry.TaskType
+ TASK_TYPE_GRPC_SERVER: TaskConfigEntry.TaskType
+ class TaskRunMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TASK_RUN_MODE_UNKNOWN: _ClassVar[TaskConfigEntry.TaskRunMode]
+ TASK_RUN_MODE_PERIODIC_STEP: _ClassVar[TaskConfigEntry.TaskRunMode]
+ TASK_RUN_MODE_BLOCKING_SERVICE: _ClassVar[TaskConfigEntry.TaskRunMode]
+ TASK_RUN_MODE_UNKNOWN: TaskConfigEntry.TaskRunMode
+ TASK_RUN_MODE_PERIODIC_STEP: TaskConfigEntry.TaskRunMode
+ TASK_RUN_MODE_BLOCKING_SERVICE: TaskConfigEntry.TaskRunMode
+ ID_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ CONFIG_FILE_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_FIELD_NUMBER: _ClassVar[int]
+ RUN_MODE_FIELD_NUMBER: _ClassVar[int]
+ CONTROL_PERIOD_S_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ type: TaskConfigEntry.TaskType
+ config_file: str
+ enable: bool
+ run_mode: TaskConfigEntry.TaskRunMode
+ control_period_s: float
+ def __init__(self, id: _Optional[str] = ..., type: _Optional[_Union[TaskConfigEntry.TaskType, str]] = ..., config_file: _Optional[str] = ..., enable: _Optional[bool] = ..., run_mode: _Optional[_Union[TaskConfigEntry.TaskRunMode, str]] = ..., control_period_s: _Optional[float] = ...) -> None: ...
+
+class TaskManagerConfig(_message.Message):
+ __slots__ = ("tasks",)
+ TASKS_FIELD_NUMBER: _ClassVar[int]
+ tasks: _containers.RepeatedCompositeFieldContainer[TaskConfigEntry]
+ def __init__(self, tasks: _Optional[_Iterable[_Union[TaskConfigEntry, _Mapping]]] = ...) -> None: ...
+
+class TaskManagerRootConfig(_message.Message):
+ __slots__ = ("task_manager",)
+ TASK_MANAGER_FIELD_NUMBER: _ClassVar[int]
+ task_manager: TaskManagerConfig
+ def __init__(self, task_manager: _Optional[_Union[TaskManagerConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/task_manager_config/task_manager_config_pb2_grpc.py b/src/cmvr/config/task_manager_config/task_manager_config_pb2_grpc.py
new file mode 100644
index 0000000..09ebfa7
--- /dev/null
+++ b/src/cmvr/config/task_manager_config/task_manager_config_pb2_grpc.py
@@ -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/config/task_manager_config/task_manager_config_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}.'
+ )
diff --git a/src/cmvr/config/touch_screen_algorithm_config_pb2.py b/src/cmvr/config/touch_screen_algorithm_config_pb2.py
new file mode 100644
index 0000000..d263353
--- /dev/null
+++ b/src/cmvr/config/touch_screen_algorithm_config_pb2.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/touch_screen_algorithm_config.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/config/touch_screen_algorithm_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.common import geometry_pb2 as cmvr_dot_common_dot_geometry__pb2
+from cmvr.config import touch_screen_common_config_pb2 as cmvr_dot_config_dot_touch__screen__common__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/cmvr/config/touch_screen_algorithm_config.proto\x12\x0b\x63mvr.config\x1a\x1a\x63mvr/common/geometry.proto\x1a,cmvr/config/touch_screen_common_config.proto\"\xf9\x01\n\x19TouchScreenApriltagConfig\x12\x17\n\ntag_size_m\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12>\n\x0c\x64\x65pth_policy\x18\x02 \x01(\x0e\x32#.cmvr.config.TouchScreenDepthPolicyH\x01\x88\x01\x01\x12K\n\x13target_point_method\x18\x03 \x01(\x0e\x32).cmvr.config.TouchScreenTargetPointMethodH\x02\x88\x01\x01\x42\r\n\x0b_tag_size_mB\x0f\n\r_depth_policyB\x16\n\x14_target_point_method\"\xcc\x05\n\x15TouchScreenIbvsConfig\x12\x18\n\x0b\x63\x61mera_link\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06lambda\x18\x02 \x01(\x01H\x01\x88\x01\x01\x12\x0f\n\x02mu\x18\x03 \x01(\x01H\x02\x88\x01\x01\x12\x15\n\x08qdot_max\x18\x04 \x01(\x01H\x03\x88\x01\x01\x12 \n\x05vmax6\x18\x05 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12 \n\x05\x61max6\x18\x06 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x1f\n\x12twist_filter_alpha\x18\x07 \x01(\x01H\x04\x88\x01\x01\x12)\n\x1c\x65nable_joint_limit_avoidance\x18\x08 \x01(\x08H\x05\x88\x01\x01\x12\'\n\x1ajoint_limit_avoidance_gain\x18\t \x01(\x01H\x06\x88\x01\x01\x12/\n\"joint_limit_avoidance_margin_ratio\x18\n \x01(\x01H\x07\x88\x01\x01\x12+\n\x1ejoint_limit_avoidance_max_push\x18\x0b \x01(\x01H\x08\x88\x01\x01\x12+\n\x10r_camera_to_visp\x18\x0c \x01(\x0b\x32\x11.cmvr.common.Mat3\x12+\n\x10r_camera_to_urdf\x18\r \x01(\x0b\x32\x11.cmvr.common.Mat3\x12\x1b\n\x13\x63ontrol_joint_names\x18\x0e \x03(\tB\x0e\n\x0c_camera_linkB\t\n\x07_lambdaB\x05\n\x03_muB\x0b\n\t_qdot_maxB\x15\n\x13_twist_filter_alphaB\x1f\n\x1d_enable_joint_limit_avoidanceB\x1d\n\x1b_joint_limit_avoidance_gainB%\n#_joint_limit_avoidance_margin_ratioB!\n\x1f_joint_limit_avoidance_max_pushb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.touch_screen_algorithm_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOUCHSCREENAPRILTAGCONFIG']._serialized_start=139
+ _globals['_TOUCHSCREENAPRILTAGCONFIG']._serialized_end=388
+ _globals['_TOUCHSCREENIBVSCONFIG']._serialized_start=391
+ _globals['_TOUCHSCREENIBVSCONFIG']._serialized_end=1107
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/touch_screen_algorithm_config_pb2.pyi b/src/cmvr/config/touch_screen_algorithm_config_pb2.pyi
new file mode 100644
index 0000000..3d0c683
--- /dev/null
+++ b/src/cmvr/config/touch_screen_algorithm_config_pb2.pyi
@@ -0,0 +1,50 @@
+from cmvr.common import geometry_pb2 as _geometry_pb2
+from cmvr.config import touch_screen_common_config_pb2 as _touch_screen_common_config_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 TouchScreenApriltagConfig(_message.Message):
+ __slots__ = ("tag_size_m", "depth_policy", "target_point_method")
+ TAG_SIZE_M_FIELD_NUMBER: _ClassVar[int]
+ DEPTH_POLICY_FIELD_NUMBER: _ClassVar[int]
+ TARGET_POINT_METHOD_FIELD_NUMBER: _ClassVar[int]
+ tag_size_m: float
+ depth_policy: _touch_screen_common_config_pb2.TouchScreenDepthPolicy
+ target_point_method: _touch_screen_common_config_pb2.TouchScreenTargetPointMethod
+ def __init__(self, tag_size_m: _Optional[float] = ..., depth_policy: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenDepthPolicy, str]] = ..., target_point_method: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenTargetPointMethod, str]] = ...) -> None: ...
+
+class TouchScreenIbvsConfig(_message.Message):
+ __slots__ = ("camera_link", "mu", "qdot_max", "vmax6", "amax6", "twist_filter_alpha", "enable_joint_limit_avoidance", "joint_limit_avoidance_gain", "joint_limit_avoidance_margin_ratio", "joint_limit_avoidance_max_push", "r_camera_to_visp", "r_camera_to_urdf", "control_joint_names")
+ CAMERA_LINK_FIELD_NUMBER: _ClassVar[int]
+ LAMBDA_FIELD_NUMBER: _ClassVar[int]
+ MU_FIELD_NUMBER: _ClassVar[int]
+ QDOT_MAX_FIELD_NUMBER: _ClassVar[int]
+ VMAX6_FIELD_NUMBER: _ClassVar[int]
+ AMAX6_FIELD_NUMBER: _ClassVar[int]
+ TWIST_FILTER_ALPHA_FIELD_NUMBER: _ClassVar[int]
+ ENABLE_JOINT_LIMIT_AVOIDANCE_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMIT_AVOIDANCE_GAIN_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMIT_AVOIDANCE_MARGIN_RATIO_FIELD_NUMBER: _ClassVar[int]
+ JOINT_LIMIT_AVOIDANCE_MAX_PUSH_FIELD_NUMBER: _ClassVar[int]
+ R_CAMERA_TO_VISP_FIELD_NUMBER: _ClassVar[int]
+ R_CAMERA_TO_URDF_FIELD_NUMBER: _ClassVar[int]
+ CONTROL_JOINT_NAMES_FIELD_NUMBER: _ClassVar[int]
+ camera_link: str
+ mu: float
+ qdot_max: float
+ vmax6: _geometry_pb2.Vec6
+ amax6: _geometry_pb2.Vec6
+ twist_filter_alpha: float
+ enable_joint_limit_avoidance: bool
+ joint_limit_avoidance_gain: float
+ joint_limit_avoidance_margin_ratio: float
+ joint_limit_avoidance_max_push: float
+ r_camera_to_visp: _geometry_pb2.Mat3
+ r_camera_to_urdf: _geometry_pb2.Mat3
+ control_joint_names: _containers.RepeatedScalarFieldContainer[str]
+ def __init__(self, camera_link: _Optional[str] = ..., mu: _Optional[float] = ..., qdot_max: _Optional[float] = ..., vmax6: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., amax6: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., twist_filter_alpha: _Optional[float] = ..., enable_joint_limit_avoidance: _Optional[bool] = ..., joint_limit_avoidance_gain: _Optional[float] = ..., joint_limit_avoidance_margin_ratio: _Optional[float] = ..., joint_limit_avoidance_max_push: _Optional[float] = ..., r_camera_to_visp: _Optional[_Union[_geometry_pb2.Mat3, _Mapping]] = ..., r_camera_to_urdf: _Optional[_Union[_geometry_pb2.Mat3, _Mapping]] = ..., control_joint_names: _Optional[_Iterable[str]] = ..., **kwargs) -> None: ...
diff --git a/src/cmvr/config/touch_screen_algorithm_config_pb2_grpc.py b/src/cmvr/config/touch_screen_algorithm_config_pb2_grpc.py
new file mode 100644
index 0000000..139f733
--- /dev/null
+++ b/src/cmvr/config/touch_screen_algorithm_config_pb2_grpc.py
@@ -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/config/touch_screen_algorithm_config_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}.'
+ )
diff --git a/src/cmvr/config/touch_screen_common_config_pb2.py b/src/cmvr/config/touch_screen_common_config_pb2.py
new file mode 100644
index 0000000..2b9d8e9
--- /dev/null
+++ b/src/cmvr/config/touch_screen_common_config_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/touch_screen_common_config.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/config/touch_screen_common_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,cmvr/config/touch_screen_common_config.proto\x12\x0b\x63mvr.config*\x89\x01\n\x16TouchScreenDepthPolicy\x12\"\n\x1eTOUCH_SCREEN_DEPTH_POLICY_NONE\x10\x00\x12$\n TOUCH_SCREEN_DEPTH_POLICY_PREFER\x10\x01\x12%\n!TOUCH_SCREEN_DEPTH_POLICY_REQUIRE\x10\x02*\x80\x01\n\x1cTouchScreenTargetPointMethod\x12.\n*TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE\x10\x00\x12\x30\n,TOUCH_SCREEN_TARGET_POINT_METHOD_DEPTH_IMAGE\x10\x01*\xcb\x01\n\x15TouchScreenFingerType\x12\"\n\x1eTOUCH_SCREEN_FINGER_TYPE_PINKY\x10\x00\x12!\n\x1dTOUCH_SCREEN_FINGER_TYPE_RING\x10\x01\x12#\n\x1fTOUCH_SCREEN_FINGER_TYPE_MIDDLE\x10\x02\x12\"\n\x1eTOUCH_SCREEN_FINGER_TYPE_INDEX\x10\x03\x12\"\n\x1eTOUCH_SCREEN_FINGER_TYPE_THUMB\x10\x04*\xba\x01\n\x18TouchScreenTactileRegion\x12#\n\x1fTOUCH_SCREEN_TACTILE_REGION_TIP\x10\x00\x12&\n\"TOUCH_SCREEN_TACTILE_REGION_FINGER\x10\x01\x12#\n\x1fTOUCH_SCREEN_TACTILE_REGION_PAD\x10\x02\x12,\n(TOUCH_SCREEN_TACTILE_REGION_THUMB_MIDDLE\x10\x04*r\n\x1bTouchScreenTactileCriterion\x12%\n!TOUCH_SCREEN_TACTILE_CRITERION_FZ\x10\x00\x12,\n(TOUCH_SCREEN_TACTILE_CRITERION_MAGNITUDE\x10\x01*\xa0\x01\n\x14TouchScreenAlignMode\x12-\n)TOUCH_SCREEN_ALIGN_MODE_POSE_AND_POSITION\x10\x00\x12.\n*TOUCH_SCREEN_ALIGN_MODE_RX_RY_AND_POSITION\x10\x01\x12)\n%TOUCH_SCREEN_ALIGN_MODE_POSITION_ONLY\x10\x02\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.touch_screen_common_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOUCHSCREENDEPTHPOLICY']._serialized_start=62
+ _globals['_TOUCHSCREENDEPTHPOLICY']._serialized_end=199
+ _globals['_TOUCHSCREENTARGETPOINTMETHOD']._serialized_start=202
+ _globals['_TOUCHSCREENTARGETPOINTMETHOD']._serialized_end=330
+ _globals['_TOUCHSCREENFINGERTYPE']._serialized_start=333
+ _globals['_TOUCHSCREENFINGERTYPE']._serialized_end=536
+ _globals['_TOUCHSCREENTACTILEREGION']._serialized_start=539
+ _globals['_TOUCHSCREENTACTILEREGION']._serialized_end=725
+ _globals['_TOUCHSCREENTACTILECRITERION']._serialized_start=727
+ _globals['_TOUCHSCREENTACTILECRITERION']._serialized_end=841
+ _globals['_TOUCHSCREENALIGNMODE']._serialized_start=844
+ _globals['_TOUCHSCREENALIGNMODE']._serialized_end=1004
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/touch_screen_common_config_pb2.pyi b/src/cmvr/config/touch_screen_common_config_pb2.pyi
new file mode 100644
index 0000000..dbbcdb3
--- /dev/null
+++ b/src/cmvr/config/touch_screen_common_config_pb2.pyi
@@ -0,0 +1,61 @@
+from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from typing import ClassVar as _ClassVar
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class TouchScreenDepthPolicy(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_DEPTH_POLICY_NONE: _ClassVar[TouchScreenDepthPolicy]
+ TOUCH_SCREEN_DEPTH_POLICY_PREFER: _ClassVar[TouchScreenDepthPolicy]
+ TOUCH_SCREEN_DEPTH_POLICY_REQUIRE: _ClassVar[TouchScreenDepthPolicy]
+
+class TouchScreenTargetPointMethod(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE: _ClassVar[TouchScreenTargetPointMethod]
+ TOUCH_SCREEN_TARGET_POINT_METHOD_DEPTH_IMAGE: _ClassVar[TouchScreenTargetPointMethod]
+
+class TouchScreenFingerType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_FINGER_TYPE_PINKY: _ClassVar[TouchScreenFingerType]
+ TOUCH_SCREEN_FINGER_TYPE_RING: _ClassVar[TouchScreenFingerType]
+ TOUCH_SCREEN_FINGER_TYPE_MIDDLE: _ClassVar[TouchScreenFingerType]
+ TOUCH_SCREEN_FINGER_TYPE_INDEX: _ClassVar[TouchScreenFingerType]
+ TOUCH_SCREEN_FINGER_TYPE_THUMB: _ClassVar[TouchScreenFingerType]
+
+class TouchScreenTactileRegion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_TACTILE_REGION_TIP: _ClassVar[TouchScreenTactileRegion]
+ TOUCH_SCREEN_TACTILE_REGION_FINGER: _ClassVar[TouchScreenTactileRegion]
+ TOUCH_SCREEN_TACTILE_REGION_PAD: _ClassVar[TouchScreenTactileRegion]
+ TOUCH_SCREEN_TACTILE_REGION_THUMB_MIDDLE: _ClassVar[TouchScreenTactileRegion]
+
+class TouchScreenTactileCriterion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_TACTILE_CRITERION_FZ: _ClassVar[TouchScreenTactileCriterion]
+ TOUCH_SCREEN_TACTILE_CRITERION_MAGNITUDE: _ClassVar[TouchScreenTactileCriterion]
+
+class TouchScreenAlignMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ TOUCH_SCREEN_ALIGN_MODE_POSE_AND_POSITION: _ClassVar[TouchScreenAlignMode]
+ TOUCH_SCREEN_ALIGN_MODE_RX_RY_AND_POSITION: _ClassVar[TouchScreenAlignMode]
+ TOUCH_SCREEN_ALIGN_MODE_POSITION_ONLY: _ClassVar[TouchScreenAlignMode]
+TOUCH_SCREEN_DEPTH_POLICY_NONE: TouchScreenDepthPolicy
+TOUCH_SCREEN_DEPTH_POLICY_PREFER: TouchScreenDepthPolicy
+TOUCH_SCREEN_DEPTH_POLICY_REQUIRE: TouchScreenDepthPolicy
+TOUCH_SCREEN_TARGET_POINT_METHOD_TAG_PLANE: TouchScreenTargetPointMethod
+TOUCH_SCREEN_TARGET_POINT_METHOD_DEPTH_IMAGE: TouchScreenTargetPointMethod
+TOUCH_SCREEN_FINGER_TYPE_PINKY: TouchScreenFingerType
+TOUCH_SCREEN_FINGER_TYPE_RING: TouchScreenFingerType
+TOUCH_SCREEN_FINGER_TYPE_MIDDLE: TouchScreenFingerType
+TOUCH_SCREEN_FINGER_TYPE_INDEX: TouchScreenFingerType
+TOUCH_SCREEN_FINGER_TYPE_THUMB: TouchScreenFingerType
+TOUCH_SCREEN_TACTILE_REGION_TIP: TouchScreenTactileRegion
+TOUCH_SCREEN_TACTILE_REGION_FINGER: TouchScreenTactileRegion
+TOUCH_SCREEN_TACTILE_REGION_PAD: TouchScreenTactileRegion
+TOUCH_SCREEN_TACTILE_REGION_THUMB_MIDDLE: TouchScreenTactileRegion
+TOUCH_SCREEN_TACTILE_CRITERION_FZ: TouchScreenTactileCriterion
+TOUCH_SCREEN_TACTILE_CRITERION_MAGNITUDE: TouchScreenTactileCriterion
+TOUCH_SCREEN_ALIGN_MODE_POSE_AND_POSITION: TouchScreenAlignMode
+TOUCH_SCREEN_ALIGN_MODE_RX_RY_AND_POSITION: TouchScreenAlignMode
+TOUCH_SCREEN_ALIGN_MODE_POSITION_ONLY: TouchScreenAlignMode
diff --git a/src/cmvr/config/touch_screen_common_config_pb2_grpc.py b/src/cmvr/config/touch_screen_common_config_pb2_grpc.py
new file mode 100644
index 0000000..332da69
--- /dev/null
+++ b/src/cmvr/config/touch_screen_common_config_pb2_grpc.py
@@ -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/config/touch_screen_common_config_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}.'
+ )
diff --git a/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-310.pyc b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-310.pyc
new file mode 100644
index 0000000..c16fa08
Binary files /dev/null and b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-312.pyc b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-312.pyc
new file mode 100644
index 0000000..1b14773
Binary files /dev/null and b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-310.pyc b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..306860f
Binary files /dev/null and b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-312.pyc b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..8a1cbf8
Binary files /dev/null and b/src/cmvr/config/touch_screen_task_config/__pycache__/touch_screen_task_config_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.py b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.py
new file mode 100644
index 0000000..b2c02fb
--- /dev/null
+++ b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/config/touch_screen_task_config/touch_screen_task_config.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/config/touch_screen_task_config/touch_screen_task_config.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.common import geometry_pb2 as cmvr_dot_common_dot_geometry__pb2
+from cmvr.config import touch_screen_common_config_pb2 as cmvr_dot_config_dot_touch__screen__common__config__pb2
+from cmvr.config import touch_screen_algorithm_config_pb2 as cmvr_dot_config_dot_touch__screen__algorithm__config__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nCcmvr/config/touch_screen_task_config/touch_screen_task_config.proto\x12\x0b\x63mvr.config\x1a\x1a\x63mvr/common/geometry.proto\x1a,cmvr/config/touch_screen_common_config.proto\x1a/cmvr/config/touch_screen_algorithm_config.proto\"]\n\x19TouchScreenInitJointPoint\x12\x17\n\njoint_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x10\n\x03rad\x18\x02 \x01(\x01H\x01\x88\x01\x01\x42\r\n\x0b_joint_nameB\x06\n\x04_rad\"\x9c\x01\n\x1cTouchScreenTaskDevicesConfig\x12\x13\n\x06\x61rm_id\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ndexhand_id\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x16\n\tcamera_id\x18\x03 \x01(\tH\x02\x88\x01\x01\x42\t\n\x07_arm_idB\r\n\x0b_dexhand_idB\x0c\n\n_camera_idJ\x04\x08\x01\x10\x02R\x08robot_id\"\x8e\x02\n#TouchScreenTaskInitializationConfig\x12\x19\n\x0c\x62\x65\x66ore_start\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x19\n\x0c\x61\x66ter_finish\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12?\n\x0fjoint_positions\x18\x03 \x03(\x0b\x32&.cmvr.config.TouchScreenInitJointPoint\x12\x15\n\x08velocity\x18\x04 \x01(\x01H\x02\x88\x01\x01\x12\x19\n\x0c\x61\x63\x63\x65leration\x18\x05 \x01(\x01H\x03\x88\x01\x01\x42\x0f\n\r_before_startB\x0f\n\r_after_finishB\x0b\n\t_velocityB\x0f\n\r_acceleration\"[\n\x1fTouchScreenTaskPerceptionConfig\x12\x38\n\x08\x61priltag\x18\x01 \x01(\x0b\x32&.cmvr.config.TouchScreenApriltagConfig\"\xbc\x01\n TouchScreenAlignmentTargetConfig\x12-\n\x12position_in_camera\x18\x01 \x01(\x0b\x32\x11.cmvr.common.Vec3\x12*\n\x0frotation_vector\x18\x02 \x01(\x0b\x32\x11.cmvr.common.Vec3\x12\x34\n\x04mode\x18\x03 \x01(\x0e\x32!.cmvr.config.TouchScreenAlignModeH\x00\x88\x01\x01\x42\x07\n\x05_mode\"\xdb\x02\n\x1eTouchScreenTaskAlignmentConfig\x12\x30\n\x04ibvs\x18\x02 \x01(\x0b\x32\".cmvr.config.TouchScreenIbvsConfig\x12=\n\x06target\x18\x03 \x01(\x0b\x32-.cmvr.config.TouchScreenAlignmentTargetConfig\x12*\n\x0f\x65rror_threshold\x18\x04 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x1a\n\rstable_frames\x18\x05 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\ttimeout_s\x18\x06 \x01(\x01H\x01\x88\x01\x01\x12\x1f\n\x12pause_when_reached\x18\x07 \x01(\x08H\x02\x88\x01\x01\x42\x10\n\x0e_stable_framesB\x0c\n\n_timeout_sB\x15\n\x13_pause_when_reachedJ\x04\x08\x01\x10\x02R\nkinematics\"\xa1\x01\n\x1cTouchScreenTouchSpeedLConfig\x12%\n\ntwist_tool\x18\x01 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x19\n\x0c\x61\x63\x63\x65leration\x18\x02 \x01(\x01H\x00\x88\x01\x01\x12\x1b\n\x0emax_distance_m\x18\x03 \x01(\x01H\x01\x88\x01\x01\x42\x0f\n\r_accelerationB\x11\n\x0f_max_distance_m\"\xfb\x01\n\x1bTouchScreenTouchMoveLConfig\x12)\n\x0e\x64irection_tool\x18\x01 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x17\n\ndistance_m\x18\x02 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\x08velocity\x18\x03 \x01(\x01H\x01\x88\x01\x01\x12\x19\n\x0c\x61\x63\x63\x65leration\x18\x04 \x01(\x01H\x02\x88\x01\x01\x12\x11\n\x04jerk\x18\x05 \x01(\x01H\x03\x88\x01\x01\x12\x1d\n\x15joint_velocity_limits\x18\x06 \x03(\x01\x42\r\n\x0b_distance_mB\x0b\n\t_velocityB\x0f\n\r_accelerationB\x07\n\x05_jerk\"\xae\x02\n\x1fTouchScreenTactileTriggerConfig\x12\x37\n\x06\x66inger\x18\x01 \x01(\x0e\x32\".cmvr.config.TouchScreenFingerTypeH\x00\x88\x01\x01\x12:\n\x06region\x18\x02 \x01(\x0e\x32%.cmvr.config.TouchScreenTactileRegionH\x01\x88\x01\x01\x12@\n\tcriterion\x18\x03 \x01(\x0e\x32(.cmvr.config.TouchScreenTactileCriterionH\x02\x88\x01\x01\x12\x1c\n\x0f\x66orce_threshold\x18\x04 \x01(\x01H\x03\x88\x01\x01\x42\t\n\x07_fingerB\t\n\x07_regionB\x0c\n\n_criterionB\x12\n\x10_force_threshold\"\x8b\x02\n\x1aTouchScreenTaskTouchConfig\x12<\n\x07speed_l\x18\x01 \x01(\x0b\x32).cmvr.config.TouchScreenTouchSpeedLConfigH\x00\x12:\n\x06move_l\x18\x02 \x01(\x0b\x32(.cmvr.config.TouchScreenTouchMoveLConfigH\x00\x12=\n\x07tactile\x18\x03 \x01(\x0b\x32,.cmvr.config.TouchScreenTactileTriggerConfig\x12\x19\n\x0c\x64well_time_s\x18\x04 \x01(\x01H\x01\x88\x01\x01\x42\x08\n\x06motionB\x0f\n\r_dwell_time_s\"\x99\x01\n\x1cTouchScreenTaskRetractConfig\x12%\n\ntwist_tool\x18\x01 \x01(\x0b\x32\x11.cmvr.common.Vec6\x12\x19\n\x0c\x61\x63\x63\x65leration\x18\x02 \x01(\x01H\x00\x88\x01\x01\x12\x17\n\nduration_s\x18\x03 \x01(\x01H\x01\x88\x01\x01\x42\x0f\n\r_accelerationB\r\n\x0b_duration_s\"\xab\x03\n\x15TouchScreenTaskConfig\x12:\n\x07\x64\x65vices\x18\x01 \x01(\x0b\x32).cmvr.config.TouchScreenTaskDevicesConfig\x12H\n\x0einitialization\x18\x02 \x01(\x0b\x32\x30.cmvr.config.TouchScreenTaskInitializationConfig\x12@\n\nperception\x18\x03 \x01(\x0b\x32,.cmvr.config.TouchScreenTaskPerceptionConfig\x12>\n\talignment\x18\x04 \x01(\x0b\x32+.cmvr.config.TouchScreenTaskAlignmentConfig\x12\x36\n\x05touch\x18\x05 \x01(\x0b\x32\'.cmvr.config.TouchScreenTaskTouchConfig\x12:\n\x07retract\x18\x06 \x01(\x0b\x32).cmvr.config.TouchScreenTaskRetractConfig\x12\x0f\n\x02id\x18\x07 \x01(\tH\x00\x88\x01\x01\x42\x05\n\x03_id\"Z\n\x19TouchScreenTaskRootConfig\x12=\n\x11touch_screen_task\x18\x01 \x01(\x0b\x32\".cmvr.config.TouchScreenTaskConfigb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.config.touch_screen_task_config.touch_screen_task_config_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_TOUCHSCREENINITJOINTPOINT']._serialized_start=207
+ _globals['_TOUCHSCREENINITJOINTPOINT']._serialized_end=300
+ _globals['_TOUCHSCREENTASKDEVICESCONFIG']._serialized_start=303
+ _globals['_TOUCHSCREENTASKDEVICESCONFIG']._serialized_end=459
+ _globals['_TOUCHSCREENTASKINITIALIZATIONCONFIG']._serialized_start=462
+ _globals['_TOUCHSCREENTASKINITIALIZATIONCONFIG']._serialized_end=732
+ _globals['_TOUCHSCREENTASKPERCEPTIONCONFIG']._serialized_start=734
+ _globals['_TOUCHSCREENTASKPERCEPTIONCONFIG']._serialized_end=825
+ _globals['_TOUCHSCREENALIGNMENTTARGETCONFIG']._serialized_start=828
+ _globals['_TOUCHSCREENALIGNMENTTARGETCONFIG']._serialized_end=1016
+ _globals['_TOUCHSCREENTASKALIGNMENTCONFIG']._serialized_start=1019
+ _globals['_TOUCHSCREENTASKALIGNMENTCONFIG']._serialized_end=1366
+ _globals['_TOUCHSCREENTOUCHSPEEDLCONFIG']._serialized_start=1369
+ _globals['_TOUCHSCREENTOUCHSPEEDLCONFIG']._serialized_end=1530
+ _globals['_TOUCHSCREENTOUCHMOVELCONFIG']._serialized_start=1533
+ _globals['_TOUCHSCREENTOUCHMOVELCONFIG']._serialized_end=1784
+ _globals['_TOUCHSCREENTACTILETRIGGERCONFIG']._serialized_start=1787
+ _globals['_TOUCHSCREENTACTILETRIGGERCONFIG']._serialized_end=2089
+ _globals['_TOUCHSCREENTASKTOUCHCONFIG']._serialized_start=2092
+ _globals['_TOUCHSCREENTASKTOUCHCONFIG']._serialized_end=2359
+ _globals['_TOUCHSCREENTASKRETRACTCONFIG']._serialized_start=2362
+ _globals['_TOUCHSCREENTASKRETRACTCONFIG']._serialized_end=2515
+ _globals['_TOUCHSCREENTASKCONFIG']._serialized_start=2518
+ _globals['_TOUCHSCREENTASKCONFIG']._serialized_end=2945
+ _globals['_TOUCHSCREENTASKROOTCONFIG']._serialized_start=2947
+ _globals['_TOUCHSCREENTASKROOTCONFIG']._serialized_end=3037
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.pyi b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.pyi
new file mode 100644
index 0000000..aa1f378
--- /dev/null
+++ b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.pyi
@@ -0,0 +1,158 @@
+from cmvr.common import geometry_pb2 as _geometry_pb2
+from cmvr.config import touch_screen_common_config_pb2 as _touch_screen_common_config_pb2
+from cmvr.config import touch_screen_algorithm_config_pb2 as _touch_screen_algorithm_config_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 TouchScreenInitJointPoint(_message.Message):
+ __slots__ = ("joint_name", "rad")
+ JOINT_NAME_FIELD_NUMBER: _ClassVar[int]
+ RAD_FIELD_NUMBER: _ClassVar[int]
+ joint_name: str
+ rad: float
+ def __init__(self, joint_name: _Optional[str] = ..., rad: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTaskDevicesConfig(_message.Message):
+ __slots__ = ("arm_id", "dexhand_id", "camera_id")
+ ARM_ID_FIELD_NUMBER: _ClassVar[int]
+ DEXHAND_ID_FIELD_NUMBER: _ClassVar[int]
+ CAMERA_ID_FIELD_NUMBER: _ClassVar[int]
+ arm_id: str
+ dexhand_id: str
+ camera_id: str
+ def __init__(self, arm_id: _Optional[str] = ..., dexhand_id: _Optional[str] = ..., camera_id: _Optional[str] = ...) -> None: ...
+
+class TouchScreenTaskInitializationConfig(_message.Message):
+ __slots__ = ("before_start", "after_finish", "joint_positions", "velocity", "acceleration")
+ BEFORE_START_FIELD_NUMBER: _ClassVar[int]
+ AFTER_FINISH_FIELD_NUMBER: _ClassVar[int]
+ JOINT_POSITIONS_FIELD_NUMBER: _ClassVar[int]
+ VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ before_start: bool
+ after_finish: bool
+ joint_positions: _containers.RepeatedCompositeFieldContainer[TouchScreenInitJointPoint]
+ velocity: float
+ acceleration: float
+ def __init__(self, before_start: _Optional[bool] = ..., after_finish: _Optional[bool] = ..., joint_positions: _Optional[_Iterable[_Union[TouchScreenInitJointPoint, _Mapping]]] = ..., velocity: _Optional[float] = ..., acceleration: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTaskPerceptionConfig(_message.Message):
+ __slots__ = ("apriltag",)
+ APRILTAG_FIELD_NUMBER: _ClassVar[int]
+ apriltag: _touch_screen_algorithm_config_pb2.TouchScreenApriltagConfig
+ def __init__(self, apriltag: _Optional[_Union[_touch_screen_algorithm_config_pb2.TouchScreenApriltagConfig, _Mapping]] = ...) -> None: ...
+
+class TouchScreenAlignmentTargetConfig(_message.Message):
+ __slots__ = ("position_in_camera", "rotation_vector", "mode")
+ POSITION_IN_CAMERA_FIELD_NUMBER: _ClassVar[int]
+ ROTATION_VECTOR_FIELD_NUMBER: _ClassVar[int]
+ MODE_FIELD_NUMBER: _ClassVar[int]
+ position_in_camera: _geometry_pb2.Vec3
+ rotation_vector: _geometry_pb2.Vec3
+ mode: _touch_screen_common_config_pb2.TouchScreenAlignMode
+ def __init__(self, position_in_camera: _Optional[_Union[_geometry_pb2.Vec3, _Mapping]] = ..., rotation_vector: _Optional[_Union[_geometry_pb2.Vec3, _Mapping]] = ..., mode: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenAlignMode, str]] = ...) -> None: ...
+
+class TouchScreenTaskAlignmentConfig(_message.Message):
+ __slots__ = ("ibvs", "target", "error_threshold", "stable_frames", "timeout_s", "pause_when_reached")
+ IBVS_FIELD_NUMBER: _ClassVar[int]
+ TARGET_FIELD_NUMBER: _ClassVar[int]
+ ERROR_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ STABLE_FRAMES_FIELD_NUMBER: _ClassVar[int]
+ TIMEOUT_S_FIELD_NUMBER: _ClassVar[int]
+ PAUSE_WHEN_REACHED_FIELD_NUMBER: _ClassVar[int]
+ ibvs: _touch_screen_algorithm_config_pb2.TouchScreenIbvsConfig
+ target: TouchScreenAlignmentTargetConfig
+ error_threshold: _geometry_pb2.Vec6
+ stable_frames: int
+ timeout_s: float
+ pause_when_reached: bool
+ def __init__(self, ibvs: _Optional[_Union[_touch_screen_algorithm_config_pb2.TouchScreenIbvsConfig, _Mapping]] = ..., target: _Optional[_Union[TouchScreenAlignmentTargetConfig, _Mapping]] = ..., error_threshold: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., stable_frames: _Optional[int] = ..., timeout_s: _Optional[float] = ..., pause_when_reached: _Optional[bool] = ...) -> None: ...
+
+class TouchScreenTouchSpeedLConfig(_message.Message):
+ __slots__ = ("twist_tool", "acceleration", "max_distance_m")
+ TWIST_TOOL_FIELD_NUMBER: _ClassVar[int]
+ ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ MAX_DISTANCE_M_FIELD_NUMBER: _ClassVar[int]
+ twist_tool: _geometry_pb2.Vec6
+ acceleration: float
+ max_distance_m: float
+ def __init__(self, twist_tool: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., acceleration: _Optional[float] = ..., max_distance_m: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTouchMoveLConfig(_message.Message):
+ __slots__ = ("direction_tool", "distance_m", "velocity", "acceleration", "jerk", "joint_velocity_limits")
+ DIRECTION_TOOL_FIELD_NUMBER: _ClassVar[int]
+ DISTANCE_M_FIELD_NUMBER: _ClassVar[int]
+ VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ JERK_FIELD_NUMBER: _ClassVar[int]
+ JOINT_VELOCITY_LIMITS_FIELD_NUMBER: _ClassVar[int]
+ direction_tool: _geometry_pb2.Vec6
+ distance_m: float
+ velocity: float
+ acceleration: float
+ jerk: float
+ joint_velocity_limits: _containers.RepeatedScalarFieldContainer[float]
+ def __init__(self, direction_tool: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., distance_m: _Optional[float] = ..., velocity: _Optional[float] = ..., acceleration: _Optional[float] = ..., jerk: _Optional[float] = ..., joint_velocity_limits: _Optional[_Iterable[float]] = ...) -> None: ...
+
+class TouchScreenTactileTriggerConfig(_message.Message):
+ __slots__ = ("finger", "region", "criterion", "force_threshold")
+ FINGER_FIELD_NUMBER: _ClassVar[int]
+ REGION_FIELD_NUMBER: _ClassVar[int]
+ CRITERION_FIELD_NUMBER: _ClassVar[int]
+ FORCE_THRESHOLD_FIELD_NUMBER: _ClassVar[int]
+ finger: _touch_screen_common_config_pb2.TouchScreenFingerType
+ region: _touch_screen_common_config_pb2.TouchScreenTactileRegion
+ criterion: _touch_screen_common_config_pb2.TouchScreenTactileCriterion
+ force_threshold: float
+ def __init__(self, finger: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenFingerType, str]] = ..., region: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenTactileRegion, str]] = ..., criterion: _Optional[_Union[_touch_screen_common_config_pb2.TouchScreenTactileCriterion, str]] = ..., force_threshold: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTaskTouchConfig(_message.Message):
+ __slots__ = ("speed_l", "move_l", "tactile", "dwell_time_s")
+ SPEED_L_FIELD_NUMBER: _ClassVar[int]
+ MOVE_L_FIELD_NUMBER: _ClassVar[int]
+ TACTILE_FIELD_NUMBER: _ClassVar[int]
+ DWELL_TIME_S_FIELD_NUMBER: _ClassVar[int]
+ speed_l: TouchScreenTouchSpeedLConfig
+ move_l: TouchScreenTouchMoveLConfig
+ tactile: TouchScreenTactileTriggerConfig
+ dwell_time_s: float
+ def __init__(self, speed_l: _Optional[_Union[TouchScreenTouchSpeedLConfig, _Mapping]] = ..., move_l: _Optional[_Union[TouchScreenTouchMoveLConfig, _Mapping]] = ..., tactile: _Optional[_Union[TouchScreenTactileTriggerConfig, _Mapping]] = ..., dwell_time_s: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTaskRetractConfig(_message.Message):
+ __slots__ = ("twist_tool", "acceleration", "duration_s")
+ TWIST_TOOL_FIELD_NUMBER: _ClassVar[int]
+ ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ DURATION_S_FIELD_NUMBER: _ClassVar[int]
+ twist_tool: _geometry_pb2.Vec6
+ acceleration: float
+ duration_s: float
+ def __init__(self, twist_tool: _Optional[_Union[_geometry_pb2.Vec6, _Mapping]] = ..., acceleration: _Optional[float] = ..., duration_s: _Optional[float] = ...) -> None: ...
+
+class TouchScreenTaskConfig(_message.Message):
+ __slots__ = ("devices", "initialization", "perception", "alignment", "touch", "retract", "id")
+ DEVICES_FIELD_NUMBER: _ClassVar[int]
+ INITIALIZATION_FIELD_NUMBER: _ClassVar[int]
+ PERCEPTION_FIELD_NUMBER: _ClassVar[int]
+ ALIGNMENT_FIELD_NUMBER: _ClassVar[int]
+ TOUCH_FIELD_NUMBER: _ClassVar[int]
+ RETRACT_FIELD_NUMBER: _ClassVar[int]
+ ID_FIELD_NUMBER: _ClassVar[int]
+ devices: TouchScreenTaskDevicesConfig
+ initialization: TouchScreenTaskInitializationConfig
+ perception: TouchScreenTaskPerceptionConfig
+ alignment: TouchScreenTaskAlignmentConfig
+ touch: TouchScreenTaskTouchConfig
+ retract: TouchScreenTaskRetractConfig
+ id: str
+ def __init__(self, devices: _Optional[_Union[TouchScreenTaskDevicesConfig, _Mapping]] = ..., initialization: _Optional[_Union[TouchScreenTaskInitializationConfig, _Mapping]] = ..., perception: _Optional[_Union[TouchScreenTaskPerceptionConfig, _Mapping]] = ..., alignment: _Optional[_Union[TouchScreenTaskAlignmentConfig, _Mapping]] = ..., touch: _Optional[_Union[TouchScreenTaskTouchConfig, _Mapping]] = ..., retract: _Optional[_Union[TouchScreenTaskRetractConfig, _Mapping]] = ..., id: _Optional[str] = ...) -> None: ...
+
+class TouchScreenTaskRootConfig(_message.Message):
+ __slots__ = ("touch_screen_task",)
+ TOUCH_SCREEN_TASK_FIELD_NUMBER: _ClassVar[int]
+ touch_screen_task: TouchScreenTaskConfig
+ def __init__(self, touch_screen_task: _Optional[_Union[TouchScreenTaskConfig, _Mapping]] = ...) -> None: ...
diff --git a/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2_grpc.py b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2_grpc.py
new file mode 100644
index 0000000..d82d483
--- /dev/null
+++ b/src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2_grpc.py
@@ -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/config/touch_screen_task_config/touch_screen_task_config_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}.'
+ )
diff --git a/src/cmvr/msgs/__pycache__/agv_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/agv_pb2.cpython-310.pyc
new file mode 100644
index 0000000..84b6dce
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/agv_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/agv_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/agv_pb2.cpython-312.pyc
new file mode 100644
index 0000000..d29391b
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/agv_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..d86387e
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..6368637
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/agv_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-310.pyc
new file mode 100644
index 0000000..6582c0d
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-312.pyc
new file mode 100644
index 0000000..658adc2
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..6c6ac91
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..f4df27d
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/can_card_parameter_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-310.pyc
new file mode 100644
index 0000000..bcf1f42
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-312.pyc
new file mode 100644
index 0000000..ce35b32
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/canopen_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..fbb6d75
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..3e3ef29
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/canopen_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-310.pyc
new file mode 100644
index 0000000..e4c264e
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-312.pyc
new file mode 100644
index 0000000..f2d3c72
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/error_code_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..4302bdf
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..907fb66
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/error_code_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/motor_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/motor_pb2.cpython-310.pyc
new file mode 100644
index 0000000..35ad367
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/motor_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/motor_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/motor_pb2.cpython-312.pyc
new file mode 100644
index 0000000..f0bb8d3
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/motor_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..5452f55
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..eb6128c
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/motor_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-310.pyc b/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-310.pyc
new file mode 100644
index 0000000..8bf5a30
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-312.pyc b/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-312.pyc
new file mode 100644
index 0000000..c3e20eb
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/robot_detail_pb2.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-310.pyc b/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-310.pyc
new file mode 100644
index 0000000..1a572e9
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-310.pyc differ
diff --git a/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-312.pyc b/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-312.pyc
new file mode 100644
index 0000000..336061f
Binary files /dev/null and b/src/cmvr/msgs/__pycache__/robot_detail_pb2_grpc.cpython-312.pyc differ
diff --git a/src/cmvr/msgs/agv_pb2.py b/src/cmvr/msgs/agv_pb2.py
new file mode 100644
index 0000000..1da853e
--- /dev/null
+++ b/src/cmvr/msgs/agv_pb2.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/agv.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/msgs/agv.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x63mvr/msgs/agv.proto\x12\tcmvr.msgs\"0\n\tAgvPose2d\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\r\n\x05theta\x18\x03 \x01(\x01\"1\n\x0b\x41gvVelocity\x12\n\n\x02vx\x18\x01 \x01(\x01\x12\n\n\x02vy\x18\x02 \x01(\x01\x12\n\n\x02wz\x18\x03 \x01(\x01\"n\n\x0f\x41gvBatteryState\x12\x12\n\npercentage\x18\x01 \x01(\x01\x12\x0f\n\x07voltage\x18\x02 \x01(\x01\x12\x0f\n\x07\x63urrent\x18\x03 \x01(\x01\x12\x13\n\x0btemperature\x18\x04 \x01(\x01\x12\x10\n\x08\x63harging\x18\x05 \x01(\x08\"\xd4\x01\n\x10\x41gvMotionOptions\x12\x11\n\tmax_speed\x18\x01 \x01(\x01\x12\x19\n\x11max_angular_speed\x18\x02 \x01(\x01\x12\x18\n\x10max_acceleration\x18\x03 \x01(\x01\x12 \n\x18max_angular_acceleration\x18\x04 \x01(\x01\x12\x16\n\x0ereach_distance\x18\x05 \x01(\x01\x12\x13\n\x0breach_angle\x18\x06 \x01(\x01\x12\x13\n\x0bspeed_ratio\x18\x07 \x01(\x01\x12\x14\n\x0c\x61synchronous\x18\x08 \x01(\x08\"z\n\x10\x41gvAdapterParams\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.cmvr.msgs.AgvAdapterParams.ValuesEntry\x1a-\n\x0bValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xcf\x02\n\x0f\x41gvRuntimeState\x12\x11\n\ttimestamp\x18\x01 \x01(\x01\x12\x0c\n\x04mode\x18\x02 \x01(\x05\x12\x11\n\tconnected\x18\x03 \x01(\x08\x12\x11\n\tlocalized\x18\x04 \x01(\x08\x12\x0e\n\x06moving\x18\x05 \x01(\x08\x12\r\n\x05\x66\x61ult\x18\x06 \x01(\x08\x12\x19\n\x11\x65mergency_stopped\x18\x07 \x01(\x08\x12\"\n\x04pose\x18\x08 \x01(\x0b\x32\x14.cmvr.msgs.AgvPose2d\x12(\n\x08velocity\x18\t \x01(\x0b\x32\x16.cmvr.msgs.AgvVelocity\x12+\n\x07\x62\x61ttery\x18\n \x01(\x0b\x32\x1a.cmvr.msgs.AgvBatteryState\x12\x13\n\x0b\x63urrent_map\x18\x0b \x01(\t\x12\x17\n\x0f\x63urrent_station\x18\x0c \x01(\t\x12\x12\n\nlast_error\x18\r \x01(\t\"_\n\nAgvStation\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\"\n\x04pose\x18\x03 \x01(\x0b\x32\x14.cmvr.msgs.AgvPose2d\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"@\n\x0e\x41gvPathSegment\x12\x16\n\x0esource_station\x18\x01 \x01(\t\x12\x16\n\x0etarget_station\x18\x02 \x01(\t\"0\n\rAgvMapPoint3D\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\t\n\x01z\x18\x03 \x01(\x01\"\xc7\x01\n\x0f\x41gvUnifiedMap2D\x12\x10\n\x08\x66rame_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x01\x12\x12\n\nresolution\x18\x03 \x01(\x01\x12\r\n\x05width\x18\x04 \x01(\r\x12\x0e\n\x06height\x18\x05 \x01(\r\x12$\n\x06origin\x18\x06 \x01(\x0b\x32\x14.cmvr.msgs.AgvPose2d\x12\x0c\n\x04\x64\x61ta\x18\x07 \x03(\x05\x12(\n\x07objects\x18\x08 \x03(\x0b\x32\x17.cmvr.msgs.AgvMapObject\"l\n\x13\x41gvMapPointSample3D\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\x12\t\n\x01z\x18\x03 \x01(\x01\x12\x11\n\tintensity\x18\x04 \x01(\x02\x12\x0c\n\x04ring\x18\x05 \x01(\r\x12\x13\n\x0btime_offset\x18\x06 \x01(\x01\"E\n\rAgvMapVoxel3D\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x12\t\n\x01z\x18\x03 \x01(\x05\x12\x13\n\x0bprobability\x18\x04 \x01(\x02\"~\n\rAgvMapPlane3D\x12(\n\x06\x63\x65nter\x18\x01 \x01(\x0b\x32\x18.cmvr.msgs.AgvMapPoint3D\x12(\n\x06normal\x18\x02 \x01(\x0b\x32\x18.cmvr.msgs.AgvMapPoint3D\x12\t\n\x01\x64\x18\x03 \x01(\x01\x12\x0e\n\x06radius\x18\x04 \x01(\x01\"\xf0\x01\n\x0c\x41gvMapObject\x12\n\n\x02id\x18\x01 \x01(\t\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x1b.cmvr.msgs.AgvMapObjectType\x12(\n\x06points\x18\x03 \x03(\x0b\x32\x18.cmvr.msgs.AgvMapPoint3D\x12\x0f\n\x07heading\x18\x04 \x01(\x01\x12;\n\nproperties\x18\x05 \x03(\x0b\x32\'.cmvr.msgs.AgvMapObject.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xfe\x01\n\x0f\x41gvUnifiedMap3D\x12\x10\n\x08\x66rame_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x01\x12\x18\n\x10voxel_resolution\x18\x03 \x01(\x01\x12.\n\x06points\x18\x04 \x03(\x0b\x32\x1e.cmvr.msgs.AgvMapPointSample3D\x12(\n\x06voxels\x18\x05 \x03(\x0b\x32\x18.cmvr.msgs.AgvMapVoxel3D\x12(\n\x06planes\x18\x06 \x03(\x0b\x32\x18.cmvr.msgs.AgvMapPlane3D\x12(\n\x07objects\x18\x07 \x03(\x0b\x32\x17.cmvr.msgs.AgvMapObject\"\xa6\x03\n\x13\x41gvUnifiedMapUpdate\x12\x0e\n\x06map_id\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x10\n\x08sequence\x18\x03 \x01(\x04\x12\x14\n\x0cresume_token\x18\x04 \x01(\t\x12-\n\tdimension\x18\x05 \x01(\x0e\x32\x1a.cmvr.msgs.AgvMapDimension\x12\x30\n\x0bupdate_type\x18\x06 \x01(\x0e\x32\x1b.cmvr.msgs.AgvMapUpdateType\x12\x10\n\x08\x66rame_id\x18\x07 \x01(\t\x12\x11\n\ttimestamp\x18\x08 \x01(\x01\x12\x16\n\x0esnapshot_begin\x18\t \x01(\x08\x12\x14\n\x0csnapshot_end\x18\n \x01(\x08\x12\x13\n\x0b\x63hunk_index\x18\x0b \x01(\r\x12\x13\n\x0b\x63hunk_count\x18\x0c \x01(\r\x12,\n\x06map_2d\x18\x14 \x01(\x0b\x32\x1a.cmvr.msgs.AgvUnifiedMap2DH\x00\x12,\n\x06map_3d\x18\x15 \x01(\x0b\x32\x1a.cmvr.msgs.AgvUnifiedMap3DH\x00\x42\t\n\x07payload\"U\n\x13\x41gvNavigationStatus\x12\r\n\x05state\x18\x01 \x01(\x05\x12\x0c\n\x04type\x18\x02 \x01(\x05\x12\x10\n\x08progress\x18\x03 \x01(\x01\x12\x0f\n\x07message\x18\x04 \x01(\t*k\n\x0f\x41gvMapDimension\x12!\n\x1d\x41GV_MAP_DIMENSION_UNSPECIFIED\x10\x00\x12\x0e\n\nAGV_MAP_2D\x10\x01\x12\x0e\n\nAGV_MAP_3D\x10\x02\x12\x15\n\x11\x41GV_MAP_2D_AND_3D\x10\x03*\x89\x01\n\x10\x41gvMapUpdateType\x12\x1e\n\x1a\x41GV_MAP_UPDATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x41GV_MAP_UPDATE_SNAPSHOT\x10\x01\x12\x1e\n\x1a\x41GV_MAP_UPDATE_INCREMENTAL\x10\x02\x12\x18\n\x14\x41GV_MAP_UPDATE_RESET\x10\x03*\xfe\x01\n\x10\x41gvMapObjectType\x12\x1e\n\x1a\x41GV_MAP_OBJECT_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x41GV_MAP_OBJECT_STATION\x10\x01\x12\x17\n\x13\x41GV_MAP_OBJECT_LINE\x10\x02\x12\x17\n\x13\x41GV_MAP_OBJECT_AREA\x10\x03\x12\x19\n\x15\x41GV_MAP_OBJECT_QR_TAG\x10\x04\x12\x1c\n\x18\x41GV_MAP_OBJECT_REFLECTOR\x10\x05\x12\x1f\n\x1b\x41GV_MAP_OBJECT_BIN_LOCATION\x10\x06\x12\"\n\x1e\x41GV_MAP_OBJECT_EXTERNAL_DEVICE\x10\x07\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.agv_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_AGVADAPTERPARAMS_VALUESENTRY']._loaded_options = None
+ _globals['_AGVADAPTERPARAMS_VALUESENTRY']._serialized_options = b'8\001'
+ _globals['_AGVMAPOBJECT_PROPERTIESENTRY']._loaded_options = None
+ _globals['_AGVMAPOBJECT_PROPERTIESENTRY']._serialized_options = b'8\001'
+ _globals['_AGVMAPDIMENSION']._serialized_start=2660
+ _globals['_AGVMAPDIMENSION']._serialized_end=2767
+ _globals['_AGVMAPUPDATETYPE']._serialized_start=2770
+ _globals['_AGVMAPUPDATETYPE']._serialized_end=2907
+ _globals['_AGVMAPOBJECTTYPE']._serialized_start=2910
+ _globals['_AGVMAPOBJECTTYPE']._serialized_end=3164
+ _globals['_AGVPOSE2D']._serialized_start=34
+ _globals['_AGVPOSE2D']._serialized_end=82
+ _globals['_AGVVELOCITY']._serialized_start=84
+ _globals['_AGVVELOCITY']._serialized_end=133
+ _globals['_AGVBATTERYSTATE']._serialized_start=135
+ _globals['_AGVBATTERYSTATE']._serialized_end=245
+ _globals['_AGVMOTIONOPTIONS']._serialized_start=248
+ _globals['_AGVMOTIONOPTIONS']._serialized_end=460
+ _globals['_AGVADAPTERPARAMS']._serialized_start=462
+ _globals['_AGVADAPTERPARAMS']._serialized_end=584
+ _globals['_AGVADAPTERPARAMS_VALUESENTRY']._serialized_start=539
+ _globals['_AGVADAPTERPARAMS_VALUESENTRY']._serialized_end=584
+ _globals['_AGVRUNTIMESTATE']._serialized_start=587
+ _globals['_AGVRUNTIMESTATE']._serialized_end=922
+ _globals['_AGVSTATION']._serialized_start=924
+ _globals['_AGVSTATION']._serialized_end=1019
+ _globals['_AGVPATHSEGMENT']._serialized_start=1021
+ _globals['_AGVPATHSEGMENT']._serialized_end=1085
+ _globals['_AGVMAPPOINT3D']._serialized_start=1087
+ _globals['_AGVMAPPOINT3D']._serialized_end=1135
+ _globals['_AGVUNIFIEDMAP2D']._serialized_start=1138
+ _globals['_AGVUNIFIEDMAP2D']._serialized_end=1337
+ _globals['_AGVMAPPOINTSAMPLE3D']._serialized_start=1339
+ _globals['_AGVMAPPOINTSAMPLE3D']._serialized_end=1447
+ _globals['_AGVMAPVOXEL3D']._serialized_start=1449
+ _globals['_AGVMAPVOXEL3D']._serialized_end=1518
+ _globals['_AGVMAPPLANE3D']._serialized_start=1520
+ _globals['_AGVMAPPLANE3D']._serialized_end=1646
+ _globals['_AGVMAPOBJECT']._serialized_start=1649
+ _globals['_AGVMAPOBJECT']._serialized_end=1889
+ _globals['_AGVMAPOBJECT_PROPERTIESENTRY']._serialized_start=1840
+ _globals['_AGVMAPOBJECT_PROPERTIESENTRY']._serialized_end=1889
+ _globals['_AGVUNIFIEDMAP3D']._serialized_start=1892
+ _globals['_AGVUNIFIEDMAP3D']._serialized_end=2146
+ _globals['_AGVUNIFIEDMAPUPDATE']._serialized_start=2149
+ _globals['_AGVUNIFIEDMAPUPDATE']._serialized_end=2571
+ _globals['_AGVNAVIGATIONSTATUS']._serialized_start=2573
+ _globals['_AGVNAVIGATIONSTATUS']._serialized_end=2658
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/agv_pb2.pyi b/src/cmvr/msgs/agv_pb2.pyi
new file mode 100644
index 0000000..a69d3c6
--- /dev/null
+++ b/src/cmvr/msgs/agv_pb2.pyi
@@ -0,0 +1,319 @@
+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 AgvMapDimension(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ AGV_MAP_DIMENSION_UNSPECIFIED: _ClassVar[AgvMapDimension]
+ AGV_MAP_2D: _ClassVar[AgvMapDimension]
+ AGV_MAP_3D: _ClassVar[AgvMapDimension]
+ AGV_MAP_2D_AND_3D: _ClassVar[AgvMapDimension]
+
+class AgvMapUpdateType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ AGV_MAP_UPDATE_UNSPECIFIED: _ClassVar[AgvMapUpdateType]
+ AGV_MAP_UPDATE_SNAPSHOT: _ClassVar[AgvMapUpdateType]
+ AGV_MAP_UPDATE_INCREMENTAL: _ClassVar[AgvMapUpdateType]
+ AGV_MAP_UPDATE_RESET: _ClassVar[AgvMapUpdateType]
+
+class AgvMapObjectType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ AGV_MAP_OBJECT_UNSPECIFIED: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_STATION: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_LINE: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_AREA: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_QR_TAG: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_REFLECTOR: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_BIN_LOCATION: _ClassVar[AgvMapObjectType]
+ AGV_MAP_OBJECT_EXTERNAL_DEVICE: _ClassVar[AgvMapObjectType]
+AGV_MAP_DIMENSION_UNSPECIFIED: AgvMapDimension
+AGV_MAP_2D: AgvMapDimension
+AGV_MAP_3D: AgvMapDimension
+AGV_MAP_2D_AND_3D: AgvMapDimension
+AGV_MAP_UPDATE_UNSPECIFIED: AgvMapUpdateType
+AGV_MAP_UPDATE_SNAPSHOT: AgvMapUpdateType
+AGV_MAP_UPDATE_INCREMENTAL: AgvMapUpdateType
+AGV_MAP_UPDATE_RESET: AgvMapUpdateType
+AGV_MAP_OBJECT_UNSPECIFIED: AgvMapObjectType
+AGV_MAP_OBJECT_STATION: AgvMapObjectType
+AGV_MAP_OBJECT_LINE: AgvMapObjectType
+AGV_MAP_OBJECT_AREA: AgvMapObjectType
+AGV_MAP_OBJECT_QR_TAG: AgvMapObjectType
+AGV_MAP_OBJECT_REFLECTOR: AgvMapObjectType
+AGV_MAP_OBJECT_BIN_LOCATION: AgvMapObjectType
+AGV_MAP_OBJECT_EXTERNAL_DEVICE: AgvMapObjectType
+
+class AgvPose2d(_message.Message):
+ __slots__ = ("x", "y", "theta")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ THETA_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ theta: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., theta: _Optional[float] = ...) -> None: ...
+
+class AgvVelocity(_message.Message):
+ __slots__ = ("vx", "vy", "wz")
+ VX_FIELD_NUMBER: _ClassVar[int]
+ VY_FIELD_NUMBER: _ClassVar[int]
+ WZ_FIELD_NUMBER: _ClassVar[int]
+ vx: float
+ vy: float
+ wz: float
+ def __init__(self, vx: _Optional[float] = ..., vy: _Optional[float] = ..., wz: _Optional[float] = ...) -> None: ...
+
+class AgvBatteryState(_message.Message):
+ __slots__ = ("percentage", "voltage", "current", "temperature", "charging")
+ PERCENTAGE_FIELD_NUMBER: _ClassVar[int]
+ VOLTAGE_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_FIELD_NUMBER: _ClassVar[int]
+ TEMPERATURE_FIELD_NUMBER: _ClassVar[int]
+ CHARGING_FIELD_NUMBER: _ClassVar[int]
+ percentage: float
+ voltage: float
+ current: float
+ temperature: float
+ charging: bool
+ def __init__(self, percentage: _Optional[float] = ..., voltage: _Optional[float] = ..., current: _Optional[float] = ..., temperature: _Optional[float] = ..., charging: _Optional[bool] = ...) -> None: ...
+
+class AgvMotionOptions(_message.Message):
+ __slots__ = ("max_speed", "max_angular_speed", "max_acceleration", "max_angular_acceleration", "reach_distance", "reach_angle", "speed_ratio", "asynchronous")
+ MAX_SPEED_FIELD_NUMBER: _ClassVar[int]
+ MAX_ANGULAR_SPEED_FIELD_NUMBER: _ClassVar[int]
+ MAX_ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ MAX_ANGULAR_ACCELERATION_FIELD_NUMBER: _ClassVar[int]
+ REACH_DISTANCE_FIELD_NUMBER: _ClassVar[int]
+ REACH_ANGLE_FIELD_NUMBER: _ClassVar[int]
+ SPEED_RATIO_FIELD_NUMBER: _ClassVar[int]
+ ASYNCHRONOUS_FIELD_NUMBER: _ClassVar[int]
+ max_speed: float
+ max_angular_speed: float
+ max_acceleration: float
+ max_angular_acceleration: float
+ reach_distance: float
+ reach_angle: float
+ speed_ratio: float
+ asynchronous: bool
+ def __init__(self, max_speed: _Optional[float] = ..., max_angular_speed: _Optional[float] = ..., max_acceleration: _Optional[float] = ..., max_angular_acceleration: _Optional[float] = ..., reach_distance: _Optional[float] = ..., reach_angle: _Optional[float] = ..., speed_ratio: _Optional[float] = ..., asynchronous: _Optional[bool] = ...) -> None: ...
+
+class AgvAdapterParams(_message.Message):
+ __slots__ = ("values",)
+ class ValuesEntry(_message.Message):
+ __slots__ = ("key", "value")
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ key: str
+ value: str
+ def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
+ VALUES_FIELD_NUMBER: _ClassVar[int]
+ values: _containers.ScalarMap[str, str]
+ def __init__(self, values: _Optional[_Mapping[str, str]] = ...) -> None: ...
+
+class AgvRuntimeState(_message.Message):
+ __slots__ = ("timestamp", "mode", "connected", "localized", "moving", "fault", "emergency_stopped", "pose", "velocity", "battery", "current_map", "current_station", "last_error")
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ MODE_FIELD_NUMBER: _ClassVar[int]
+ CONNECTED_FIELD_NUMBER: _ClassVar[int]
+ LOCALIZED_FIELD_NUMBER: _ClassVar[int]
+ MOVING_FIELD_NUMBER: _ClassVar[int]
+ FAULT_FIELD_NUMBER: _ClassVar[int]
+ EMERGENCY_STOPPED_FIELD_NUMBER: _ClassVar[int]
+ POSE_FIELD_NUMBER: _ClassVar[int]
+ VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ BATTERY_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_MAP_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_STATION_FIELD_NUMBER: _ClassVar[int]
+ LAST_ERROR_FIELD_NUMBER: _ClassVar[int]
+ timestamp: float
+ mode: int
+ connected: bool
+ localized: bool
+ moving: bool
+ fault: bool
+ emergency_stopped: bool
+ pose: AgvPose2d
+ velocity: AgvVelocity
+ battery: AgvBatteryState
+ current_map: str
+ current_station: str
+ last_error: str
+ def __init__(self, timestamp: _Optional[float] = ..., mode: _Optional[int] = ..., connected: _Optional[bool] = ..., localized: _Optional[bool] = ..., moving: _Optional[bool] = ..., fault: _Optional[bool] = ..., emergency_stopped: _Optional[bool] = ..., pose: _Optional[_Union[AgvPose2d, _Mapping]] = ..., velocity: _Optional[_Union[AgvVelocity, _Mapping]] = ..., battery: _Optional[_Union[AgvBatteryState, _Mapping]] = ..., current_map: _Optional[str] = ..., current_station: _Optional[str] = ..., last_error: _Optional[str] = ...) -> None: ...
+
+class AgvStation(_message.Message):
+ __slots__ = ("id", "type", "pose", "description")
+ ID_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ POSE_FIELD_NUMBER: _ClassVar[int]
+ DESCRIPTION_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ type: str
+ pose: AgvPose2d
+ description: str
+ def __init__(self, id: _Optional[str] = ..., type: _Optional[str] = ..., pose: _Optional[_Union[AgvPose2d, _Mapping]] = ..., description: _Optional[str] = ...) -> None: ...
+
+class AgvPathSegment(_message.Message):
+ __slots__ = ("source_station", "target_station")
+ SOURCE_STATION_FIELD_NUMBER: _ClassVar[int]
+ TARGET_STATION_FIELD_NUMBER: _ClassVar[int]
+ source_station: str
+ target_station: str
+ def __init__(self, source_station: _Optional[str] = ..., target_station: _Optional[str] = ...) -> None: ...
+
+class AgvMapPoint3D(_message.Message):
+ __slots__ = ("x", "y", "z")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ Z_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ z: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ...) -> None: ...
+
+class AgvUnifiedMap2D(_message.Message):
+ __slots__ = ("frame_id", "timestamp", "resolution", "width", "height", "origin", "data", "objects")
+ FRAME_ID_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ RESOLUTION_FIELD_NUMBER: _ClassVar[int]
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
+ ORIGIN_FIELD_NUMBER: _ClassVar[int]
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ OBJECTS_FIELD_NUMBER: _ClassVar[int]
+ frame_id: str
+ timestamp: float
+ resolution: float
+ width: int
+ height: int
+ origin: AgvPose2d
+ data: _containers.RepeatedScalarFieldContainer[int]
+ objects: _containers.RepeatedCompositeFieldContainer[AgvMapObject]
+ def __init__(self, frame_id: _Optional[str] = ..., timestamp: _Optional[float] = ..., resolution: _Optional[float] = ..., width: _Optional[int] = ..., height: _Optional[int] = ..., origin: _Optional[_Union[AgvPose2d, _Mapping]] = ..., data: _Optional[_Iterable[int]] = ..., objects: _Optional[_Iterable[_Union[AgvMapObject, _Mapping]]] = ...) -> None: ...
+
+class AgvMapPointSample3D(_message.Message):
+ __slots__ = ("x", "y", "z", "intensity", "ring", "time_offset")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ Z_FIELD_NUMBER: _ClassVar[int]
+ INTENSITY_FIELD_NUMBER: _ClassVar[int]
+ RING_FIELD_NUMBER: _ClassVar[int]
+ TIME_OFFSET_FIELD_NUMBER: _ClassVar[int]
+ x: float
+ y: float
+ z: float
+ intensity: float
+ ring: int
+ time_offset: float
+ def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ..., intensity: _Optional[float] = ..., ring: _Optional[int] = ..., time_offset: _Optional[float] = ...) -> None: ...
+
+class AgvMapVoxel3D(_message.Message):
+ __slots__ = ("x", "y", "z", "probability")
+ X_FIELD_NUMBER: _ClassVar[int]
+ Y_FIELD_NUMBER: _ClassVar[int]
+ Z_FIELD_NUMBER: _ClassVar[int]
+ PROBABILITY_FIELD_NUMBER: _ClassVar[int]
+ x: int
+ y: int
+ z: int
+ probability: float
+ def __init__(self, x: _Optional[int] = ..., y: _Optional[int] = ..., z: _Optional[int] = ..., probability: _Optional[float] = ...) -> None: ...
+
+class AgvMapPlane3D(_message.Message):
+ __slots__ = ("center", "normal", "d", "radius")
+ CENTER_FIELD_NUMBER: _ClassVar[int]
+ NORMAL_FIELD_NUMBER: _ClassVar[int]
+ D_FIELD_NUMBER: _ClassVar[int]
+ RADIUS_FIELD_NUMBER: _ClassVar[int]
+ center: AgvMapPoint3D
+ normal: AgvMapPoint3D
+ d: float
+ radius: float
+ def __init__(self, center: _Optional[_Union[AgvMapPoint3D, _Mapping]] = ..., normal: _Optional[_Union[AgvMapPoint3D, _Mapping]] = ..., d: _Optional[float] = ..., radius: _Optional[float] = ...) -> None: ...
+
+class AgvMapObject(_message.Message):
+ __slots__ = ("id", "type", "points", "heading", "properties")
+ class PropertiesEntry(_message.Message):
+ __slots__ = ("key", "value")
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ key: str
+ value: str
+ def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
+ ID_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ POINTS_FIELD_NUMBER: _ClassVar[int]
+ HEADING_FIELD_NUMBER: _ClassVar[int]
+ PROPERTIES_FIELD_NUMBER: _ClassVar[int]
+ id: str
+ type: AgvMapObjectType
+ points: _containers.RepeatedCompositeFieldContainer[AgvMapPoint3D]
+ heading: float
+ properties: _containers.ScalarMap[str, str]
+ def __init__(self, id: _Optional[str] = ..., type: _Optional[_Union[AgvMapObjectType, str]] = ..., points: _Optional[_Iterable[_Union[AgvMapPoint3D, _Mapping]]] = ..., heading: _Optional[float] = ..., properties: _Optional[_Mapping[str, str]] = ...) -> None: ...
+
+class AgvUnifiedMap3D(_message.Message):
+ __slots__ = ("frame_id", "timestamp", "voxel_resolution", "points", "voxels", "planes", "objects")
+ FRAME_ID_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ VOXEL_RESOLUTION_FIELD_NUMBER: _ClassVar[int]
+ POINTS_FIELD_NUMBER: _ClassVar[int]
+ VOXELS_FIELD_NUMBER: _ClassVar[int]
+ PLANES_FIELD_NUMBER: _ClassVar[int]
+ OBJECTS_FIELD_NUMBER: _ClassVar[int]
+ frame_id: str
+ timestamp: float
+ voxel_resolution: float
+ points: _containers.RepeatedCompositeFieldContainer[AgvMapPointSample3D]
+ voxels: _containers.RepeatedCompositeFieldContainer[AgvMapVoxel3D]
+ planes: _containers.RepeatedCompositeFieldContainer[AgvMapPlane3D]
+ objects: _containers.RepeatedCompositeFieldContainer[AgvMapObject]
+ def __init__(self, frame_id: _Optional[str] = ..., timestamp: _Optional[float] = ..., voxel_resolution: _Optional[float] = ..., points: _Optional[_Iterable[_Union[AgvMapPointSample3D, _Mapping]]] = ..., voxels: _Optional[_Iterable[_Union[AgvMapVoxel3D, _Mapping]]] = ..., planes: _Optional[_Iterable[_Union[AgvMapPlane3D, _Mapping]]] = ..., objects: _Optional[_Iterable[_Union[AgvMapObject, _Mapping]]] = ...) -> None: ...
+
+class AgvUnifiedMapUpdate(_message.Message):
+ __slots__ = ("map_id", "session_id", "sequence", "resume_token", "dimension", "update_type", "frame_id", "timestamp", "snapshot_begin", "snapshot_end", "chunk_index", "chunk_count", "map_2d", "map_3d")
+ MAP_ID_FIELD_NUMBER: _ClassVar[int]
+ SESSION_ID_FIELD_NUMBER: _ClassVar[int]
+ SEQUENCE_FIELD_NUMBER: _ClassVar[int]
+ RESUME_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ DIMENSION_FIELD_NUMBER: _ClassVar[int]
+ UPDATE_TYPE_FIELD_NUMBER: _ClassVar[int]
+ FRAME_ID_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
+ SNAPSHOT_BEGIN_FIELD_NUMBER: _ClassVar[int]
+ SNAPSHOT_END_FIELD_NUMBER: _ClassVar[int]
+ CHUNK_INDEX_FIELD_NUMBER: _ClassVar[int]
+ CHUNK_COUNT_FIELD_NUMBER: _ClassVar[int]
+ MAP_2D_FIELD_NUMBER: _ClassVar[int]
+ MAP_3D_FIELD_NUMBER: _ClassVar[int]
+ map_id: str
+ session_id: str
+ sequence: int
+ resume_token: str
+ dimension: AgvMapDimension
+ update_type: AgvMapUpdateType
+ frame_id: str
+ timestamp: float
+ snapshot_begin: bool
+ snapshot_end: bool
+ chunk_index: int
+ chunk_count: int
+ map_2d: AgvUnifiedMap2D
+ map_3d: AgvUnifiedMap3D
+ def __init__(self, map_id: _Optional[str] = ..., session_id: _Optional[str] = ..., sequence: _Optional[int] = ..., resume_token: _Optional[str] = ..., dimension: _Optional[_Union[AgvMapDimension, str]] = ..., update_type: _Optional[_Union[AgvMapUpdateType, str]] = ..., frame_id: _Optional[str] = ..., timestamp: _Optional[float] = ..., snapshot_begin: _Optional[bool] = ..., snapshot_end: _Optional[bool] = ..., chunk_index: _Optional[int] = ..., chunk_count: _Optional[int] = ..., map_2d: _Optional[_Union[AgvUnifiedMap2D, _Mapping]] = ..., map_3d: _Optional[_Union[AgvUnifiedMap3D, _Mapping]] = ...) -> None: ...
+
+class AgvNavigationStatus(_message.Message):
+ __slots__ = ("state", "type", "progress", "message")
+ STATE_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ PROGRESS_FIELD_NUMBER: _ClassVar[int]
+ MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ state: int
+ type: int
+ progress: float
+ message: str
+ def __init__(self, state: _Optional[int] = ..., type: _Optional[int] = ..., progress: _Optional[float] = ..., message: _Optional[str] = ...) -> None: ...
diff --git a/src/cmvr/msgs/agv_pb2_grpc.py b/src/cmvr/msgs/agv_pb2_grpc.py
new file mode 100644
index 0000000..13ec237
--- /dev/null
+++ b/src/cmvr/msgs/agv_pb2_grpc.py
@@ -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/msgs/agv_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}.'
+ )
diff --git a/src/cmvr/msgs/can_card_parameter_pb2.py b/src/cmvr/msgs/can_card_parameter_pb2.py
new file mode 100644
index 0000000..6f1d211
--- /dev/null
+++ b/src/cmvr/msgs/can_card_parameter_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/can_card_parameter.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/msgs/can_card_parameter.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"cmvr/msgs/can_card_parameter.proto\x12\tcmvr.msgs\"\x98\x07\n\x10\x43\x41NCardParameter\x12<\n\x05\x62rand\x18\x01 \x01(\x0e\x32(.cmvr.msgs.CANCardParameter.CANCardBrandH\x00\x88\x01\x01\x12:\n\x04type\x18\x02 \x01(\x0e\x32\'.cmvr.msgs.CANCardParameter.CANCardTypeH\x01\x88\x01\x01\x12\x41\n\nchannel_id\x18\x03 \x01(\x0e\x32(.cmvr.msgs.CANCardParameter.CANChannelIdH\x02\x88\x01\x01\x12@\n\tinterface\x18\x04 \x01(\x0e\x32(.cmvr.msgs.CANCardParameter.CANInterfaceH\x03\x88\x01\x01\x12\x16\n\tnum_ports\x18\x05 \x01(\rH\x04\x88\x01\x01\x12;\n\x08\x62\x61udrate\x18\x06 \x01(\x0e\x32$.cmvr.msgs.CANCardParameter.BAUDRATEH\x05\x88\x01\x01\"M\n\x0c\x43\x41NCardBrand\x12\x0c\n\x08\x46\x41KE_CAN\x10\x00\x12\x0b\n\x07\x45SD_CAN\x10\x01\x12\x12\n\x0eSOCKET_CAN_RAW\x10\x02\x12\x0e\n\nHERMES_CAN\x10\x03\")\n\x0b\x43\x41NCardType\x12\x0c\n\x08PCI_CARD\x10\x00\x12\x0c\n\x08USB_CARD\x10\x01\"\xb5\x01\n\x0c\x43\x41NChannelId\x12\x13\n\x0f\x43HANNEL_ID_ZERO\x10\x00\x12\x12\n\x0e\x43HANNEL_ID_ONE\x10\x01\x12\x12\n\x0e\x43HANNEL_ID_TWO\x10\x02\x12\x14\n\x10\x43HANNEL_ID_THREE\x10\x03\x12\x13\n\x0f\x43HANNEL_ID_FOUR\x10\x04\x12\x13\n\x0f\x43HANNEL_ID_FIVE\x10\x05\x12\x12\n\x0e\x43HANNEL_ID_SIX\x10\x06\x12\x14\n\x10\x43HANNEL_ID_SEVEN\x10\x07\"2\n\x0c\x43\x41NInterface\x12\n\n\x06NATIVE\x10\x00\x12\x0b\n\x07VIRTUAL\x10\x01\x12\t\n\x05SLCAN\x10\x02\"\x7f\n\x08\x42\x41UDRATE\x12\x14\n\x10\x42\x43\x41N_BAUDRATE_1M\x10\x00\x12\x16\n\x12\x42\x43\x41N_BAUDRATE_500K\x10\x01\x12\x16\n\x12\x42\x43\x41N_BAUDRATE_250K\x10\x02\x12\x16\n\x12\x42\x43\x41N_BAUDRATE_150K\x10\x03\x12\x15\n\x11\x42\x43\x41N_BAUDRATE_NUM\x10\x04\x42\x08\n\x06_brandB\x07\n\x05_typeB\r\n\x0b_channel_idB\x0c\n\n_interfaceB\x0c\n\n_num_portsB\x0b\n\t_baudrateb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.can_card_parameter_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CANCARDPARAMETER']._serialized_start=50
+ _globals['_CANCARDPARAMETER']._serialized_end=970
+ _globals['_CANCARDPARAMETER_CANCARDBRAND']._serialized_start=410
+ _globals['_CANCARDPARAMETER_CANCARDBRAND']._serialized_end=487
+ _globals['_CANCARDPARAMETER_CANCARDTYPE']._serialized_start=489
+ _globals['_CANCARDPARAMETER_CANCARDTYPE']._serialized_end=530
+ _globals['_CANCARDPARAMETER_CANCHANNELID']._serialized_start=533
+ _globals['_CANCARDPARAMETER_CANCHANNELID']._serialized_end=714
+ _globals['_CANCARDPARAMETER_CANINTERFACE']._serialized_start=716
+ _globals['_CANCARDPARAMETER_CANINTERFACE']._serialized_end=766
+ _globals['_CANCARDPARAMETER_BAUDRATE']._serialized_start=768
+ _globals['_CANCARDPARAMETER_BAUDRATE']._serialized_end=895
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/can_card_parameter_pb2.pyi b/src/cmvr/msgs/can_card_parameter_pb2.pyi
new file mode 100644
index 0000000..97e06f1
--- /dev/null
+++ b/src/cmvr/msgs/can_card_parameter_pb2.pyi
@@ -0,0 +1,76 @@
+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 typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class CANCardParameter(_message.Message):
+ __slots__ = ("brand", "type", "channel_id", "interface", "num_ports", "baudrate")
+ class CANCardBrand(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ FAKE_CAN: _ClassVar[CANCardParameter.CANCardBrand]
+ ESD_CAN: _ClassVar[CANCardParameter.CANCardBrand]
+ SOCKET_CAN_RAW: _ClassVar[CANCardParameter.CANCardBrand]
+ HERMES_CAN: _ClassVar[CANCardParameter.CANCardBrand]
+ FAKE_CAN: CANCardParameter.CANCardBrand
+ ESD_CAN: CANCardParameter.CANCardBrand
+ SOCKET_CAN_RAW: CANCardParameter.CANCardBrand
+ HERMES_CAN: CANCardParameter.CANCardBrand
+ class CANCardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PCI_CARD: _ClassVar[CANCardParameter.CANCardType]
+ USB_CARD: _ClassVar[CANCardParameter.CANCardType]
+ PCI_CARD: CANCardParameter.CANCardType
+ USB_CARD: CANCardParameter.CANCardType
+ class CANChannelId(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ CHANNEL_ID_ZERO: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_ONE: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_TWO: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_THREE: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_FOUR: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_FIVE: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_SIX: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_SEVEN: _ClassVar[CANCardParameter.CANChannelId]
+ CHANNEL_ID_ZERO: CANCardParameter.CANChannelId
+ CHANNEL_ID_ONE: CANCardParameter.CANChannelId
+ CHANNEL_ID_TWO: CANCardParameter.CANChannelId
+ CHANNEL_ID_THREE: CANCardParameter.CANChannelId
+ CHANNEL_ID_FOUR: CANCardParameter.CANChannelId
+ CHANNEL_ID_FIVE: CANCardParameter.CANChannelId
+ CHANNEL_ID_SIX: CANCardParameter.CANChannelId
+ CHANNEL_ID_SEVEN: CANCardParameter.CANChannelId
+ class CANInterface(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ NATIVE: _ClassVar[CANCardParameter.CANInterface]
+ VIRTUAL: _ClassVar[CANCardParameter.CANInterface]
+ SLCAN: _ClassVar[CANCardParameter.CANInterface]
+ NATIVE: CANCardParameter.CANInterface
+ VIRTUAL: CANCardParameter.CANInterface
+ SLCAN: CANCardParameter.CANInterface
+ class BAUDRATE(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ BCAN_BAUDRATE_1M: _ClassVar[CANCardParameter.BAUDRATE]
+ BCAN_BAUDRATE_500K: _ClassVar[CANCardParameter.BAUDRATE]
+ BCAN_BAUDRATE_250K: _ClassVar[CANCardParameter.BAUDRATE]
+ BCAN_BAUDRATE_150K: _ClassVar[CANCardParameter.BAUDRATE]
+ BCAN_BAUDRATE_NUM: _ClassVar[CANCardParameter.BAUDRATE]
+ BCAN_BAUDRATE_1M: CANCardParameter.BAUDRATE
+ BCAN_BAUDRATE_500K: CANCardParameter.BAUDRATE
+ BCAN_BAUDRATE_250K: CANCardParameter.BAUDRATE
+ BCAN_BAUDRATE_150K: CANCardParameter.BAUDRATE
+ BCAN_BAUDRATE_NUM: CANCardParameter.BAUDRATE
+ BRAND_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ CHANNEL_ID_FIELD_NUMBER: _ClassVar[int]
+ INTERFACE_FIELD_NUMBER: _ClassVar[int]
+ NUM_PORTS_FIELD_NUMBER: _ClassVar[int]
+ BAUDRATE_FIELD_NUMBER: _ClassVar[int]
+ brand: CANCardParameter.CANCardBrand
+ type: CANCardParameter.CANCardType
+ channel_id: CANCardParameter.CANChannelId
+ interface: CANCardParameter.CANInterface
+ num_ports: int
+ baudrate: CANCardParameter.BAUDRATE
+ def __init__(self, brand: _Optional[_Union[CANCardParameter.CANCardBrand, str]] = ..., type: _Optional[_Union[CANCardParameter.CANCardType, str]] = ..., channel_id: _Optional[_Union[CANCardParameter.CANChannelId, str]] = ..., interface: _Optional[_Union[CANCardParameter.CANInterface, str]] = ..., num_ports: _Optional[int] = ..., baudrate: _Optional[_Union[CANCardParameter.BAUDRATE, str]] = ...) -> None: ...
diff --git a/src/cmvr/msgs/can_card_parameter_pb2_grpc.py b/src/cmvr/msgs/can_card_parameter_pb2_grpc.py
new file mode 100644
index 0000000..dbac691
--- /dev/null
+++ b/src/cmvr/msgs/can_card_parameter_pb2_grpc.py
@@ -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/msgs/can_card_parameter_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}.'
+ )
diff --git a/src/cmvr/msgs/canopen_pb2.py b/src/cmvr/msgs/canopen_pb2.py
new file mode 100644
index 0000000..2efb4b8
--- /dev/null
+++ b/src/cmvr/msgs/canopen_pb2.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/canopen.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/msgs/canopen.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x63mvr/msgs/canopen.proto\x12\tcmvr.msgs\"\x9f\x01\n\x08SdoFrame\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\'\n\x02\x63s\x18\x02 \x01(\x0e\x32\x1b.cmvr.msgs.CommandSpecifier\x12!\n\x05index\x18\x03 \x01(\x0e\x32\x12.cmvr.msgs.ObIndex\x12(\n\tsub_index\x18\x04 \x01(\x0e\x32\x15.cmvr.msgs.ObSubIndex\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\r*\xe8\x01\n\tPdoBaseId\x12\x1b\n\x17PDO_BASE_ID_UNSPECIFIED\x10\x00\x12\x16\n\x11RPDO1_BASE_ID_200\x10\x80\x04\x12\x16\n\x11RPDO2_BASE_ID_300\x10\x80\x06\x12\x16\n\x11RPDO3_BASE_ID_400\x10\x80\x08\x12\x16\n\x11RPDO4_BASE_ID_500\x10\x80\n\x12\x16\n\x11TPDO1_BASE_ID_180\x10\x80\x03\x12\x16\n\x11TPDO2_BASE_ID_280\x10\x80\x05\x12\x16\n\x11TPDO3_BASE_ID_380\x10\x80\x07\x12\x16\n\x11TPDO4_BASE_ID_480\x10\x80\t*\x9d\x01\n\x10TransmissionType\x12\x15\n\x11SYNC_EVENT_DRIVEN\x10\x00\x12\x0f\n\x0bSYNC_CYCLIC\x10\x01\x12\x10\n\x0bREMOTE_SYNC\x10\xfc\x01\x12\x11\n\x0cREMOTE_ASYNC\x10\xfd\x01\x12 \n\x1b\x41SYNC_MANUFACTURER_SPECIFIC\x10\xfe\x01\x12\x1a\n\x15\x41SYNC_DEVICE_SPECIFIC\x10\xff\x01*\xd1\x02\n\x10\x43ommandSpecifier\x12\t\n\x05\x43S_NO\x10\x00\x12\x15\n\x11\x43S_WRITE_ONE_BYTE\x10/\x12\x16\n\x12\x43S_WRITE_TWO_BYTES\x10+\x12\x18\n\x14\x43S_WRITE_THREE_BYTES\x10\'\x12\x17\n\x13\x43S_WRITE_FOUR_BYTES\x10#\x12\x1d\n\x19\x43S_WRITE_SUCCESS_RESPONSE\x10`\x12\x13\n\x0f\x43S_READ_REQUEST\x10@\x12\x1d\n\x19\x43S_READ_RESPONSE_ONE_BYTE\x10O\x12\x1e\n\x1a\x43S_READ_RESPONSE_TWO_BYTES\x10K\x12 \n\x1c\x43S_READ_RESPONSE_THREE_BYTES\x10G\x12\x1f\n\x1b\x43S_READ_RESPONSE_FOUR_BYTES\x10\x43\x12\x1a\n\x15\x43S_EXCEPTION_RESPONSE\x10\x80\x01*\xa1\x01\n\x08NmtState\x12\x14\n\x10NMT_INITIALIZING\x10\x00\x12\x19\n\x15NMT_RESET_APPLICATION\x10\x01\x12\x12\n\x0eNMT_CONNECTING\x10\x02\x12\x11\n\rNMT_PREPARING\x10\x03\x12\x0f\n\x0bNMT_STOPPED\x10\x04\x12\x13\n\x0fNMT_OPERATIONAL\x10\x05\x12\x17\n\x13NMT_PRE_OPERATIONAL\x10\x7f*\xb1\x01\n\nNmtCommand\x12\x1b\n\x17NMT_COMMAND_UNSPECIFIED\x10\x00\x12\x19\n\x15NMT_START_REMOTE_NODE\x10\x01\x12\x18\n\x14NMT_STOP_REMOTE_NODE\x10\x02\x12\x1e\n\x19NMT_ENTER_PRE_OPERATIONAL\x10\x80\x01\x12\x13\n\x0eNMT_RESET_NODE\x10\x81\x01\x12\x1c\n\x17NMT_RESET_COMMUNICATION\x10\x82\x01*\xce\x08\n\x07ObIndex\x12\x0e\n\nINDEX_ZERO\x10\x00\x12\x18\n\x13USER_SAVE_PARA_2000\x10\x80@\x12\x19\n\x14POSITION_OFFSET_2008\x10\x88@\x12\x15\n\x0f\x45RROR_CODE_6007\x10\x87\xc0\x01\x12\x15\n\x0f\x45RROR_CODE_603F\x10\xbf\xc0\x01\x12\x17\n\x11\x43ONTROL_WORD_6040\x10\xc0\xc0\x01\x12\x16\n\x10STATUS_WORD_6041\x10\xc1\xc0\x01\x12\x19\n\x13OPERATION_MODE_6060\x10\xe0\xc0\x01\x12\x17\n\x11MODE_DISPLAY_6061\x10\xe1\xc0\x01\x12\x1a\n\x14\x41\x43TUAL_POSITION_6064\x10\xe4\xc0\x01\x12\x17\n\x11\x41\x43TUAL_SPEED_606C\x10\xec\xc0\x01\x12\x19\n\x13\x41\x43TUAL_CURRENT_6078\x10\xf8\xc0\x01\x12\x18\n\x12TARGET_TORQUE_6071\x10\xf1\xc0\x01\x12\x15\n\x0fMAX_TORQUE_6072\x10\xf2\xc0\x01\x12\x18\n\x12\x44\x45MAND_TORQUE_6074\x10\xf4\xc0\x01\x12\x1a\n\x14TARGET_POSITION_607A\x10\xfa\xc0\x01\x12\"\n\x1cSOFTWARE_POSITION_LIMIT_607D\x10\xfd\xc0\x01\x12\x14\n\x0eMAX_SPEED_607F\x10\xff\xc0\x01\x12\x18\n\x12PROFILE_SPEED_6081\x10\x81\xc1\x01\x12\x1f\n\x19PROFILE_ACCELERATION_6083\x10\x83\xc1\x01\x12\x1f\n\x19PROFILE_DECELERATION_6084\x10\x84\xc1\x01\x12\x1b\n\x15\x43URRENT_LOOP_PID_60F6\x10\xf6\xc1\x01\x12\x19\n\x13SPEED_LOOP_PID_60F9\x10\xf9\xc1\x01\x12\x1c\n\x16POSITION_LOOP_PID_60FB\x10\xfb\xc1\x01\x12\x17\n\x11TARGET_SPEED_60FF\x10\xff\xc1\x01\x12\x1c\n\x16QUICK_STOP_OPTION_605A\x10\xda\xc0\x01\x12\x1b\n\x15QUICK_STOP_DECEL_6085\x10\x85\xc1\x01\x12\x14\n\x0fRPDO1_COMM_1400\x10\x80(\x12\x14\n\x0fRPDO2_COMM_1401\x10\x81(\x12\x14\n\x0fRPDO3_COMM_1402\x10\x82(\x12\x14\n\x0fRPDO4_COMM_1403\x10\x83(\x12\x14\n\x0fTPDO1_COMM_1800\x10\x80\x30\x12\x14\n\x0fTPDO2_COMM_1801\x10\x81\x30\x12\x14\n\x0fTPDO3_COMM_1802\x10\x82\x30\x12\x14\n\x0fTPDO4_COMM_1803\x10\x83\x30\x12\x13\n\x0eRPDO1_MAP_1600\x10\x80,\x12\x13\n\x0eRPDO2_MAP_1601\x10\x81,\x12\x13\n\x0eRPDO3_MAP_1602\x10\x82,\x12\x13\n\x0eRPDO4_MAP_1603\x10\x83,\x12\x13\n\x0eTPDO1_MAP_1A00\x10\x80\x34\x12\x13\n\x0eTPDO2_MAP_1A01\x10\x81\x34\x12\x13\n\x0eTPDO3_MAP_1A02\x10\x82\x34\x12\x13\n\x0eTPDO4_MAP_1A03\x10\x83\x34\x12\x1c\n\x17PRODUCER_HEARTBEAT_TIME\x10\x97 *\x94\x01\n\nObSubIndex\x12\x0f\n\x0bSUB_INDEX_0\x10\x00\x12\x0f\n\x0bSUB_INDEX_1\x10\x01\x12\x0f\n\x0bSUB_INDEX_2\x10\x02\x12\x0f\n\x0bSUB_INDEX_3\x10\x03\x12\x0f\n\x0bSUB_INDEX_4\x10\x04\x12\x0f\n\x0bSUB_INDEX_5\x10\x05\x12\x0f\n\x0bSUB_INDEX_6\x10\x06\x12\x0f\n\x0bSUB_INDEX_7\x10\x07\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.canopen_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_PDOBASEID']._serialized_start=201
+ _globals['_PDOBASEID']._serialized_end=433
+ _globals['_TRANSMISSIONTYPE']._serialized_start=436
+ _globals['_TRANSMISSIONTYPE']._serialized_end=593
+ _globals['_COMMANDSPECIFIER']._serialized_start=596
+ _globals['_COMMANDSPECIFIER']._serialized_end=933
+ _globals['_NMTSTATE']._serialized_start=936
+ _globals['_NMTSTATE']._serialized_end=1097
+ _globals['_NMTCOMMAND']._serialized_start=1100
+ _globals['_NMTCOMMAND']._serialized_end=1277
+ _globals['_OBINDEX']._serialized_start=1280
+ _globals['_OBINDEX']._serialized_end=2382
+ _globals['_OBSUBINDEX']._serialized_start=2385
+ _globals['_OBSUBINDEX']._serialized_end=2533
+ _globals['_SDOFRAME']._serialized_start=39
+ _globals['_SDOFRAME']._serialized_end=198
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/canopen_pb2.pyi b/src/cmvr/msgs/canopen_pb2.pyi
new file mode 100644
index 0000000..6e64f17
--- /dev/null
+++ b/src/cmvr/msgs/canopen_pb2.pyi
@@ -0,0 +1,225 @@
+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 typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class PdoBaseId(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PDO_BASE_ID_UNSPECIFIED: _ClassVar[PdoBaseId]
+ RPDO1_BASE_ID_200: _ClassVar[PdoBaseId]
+ RPDO2_BASE_ID_300: _ClassVar[PdoBaseId]
+ RPDO3_BASE_ID_400: _ClassVar[PdoBaseId]
+ RPDO4_BASE_ID_500: _ClassVar[PdoBaseId]
+ TPDO1_BASE_ID_180: _ClassVar[PdoBaseId]
+ TPDO2_BASE_ID_280: _ClassVar[PdoBaseId]
+ TPDO3_BASE_ID_380: _ClassVar[PdoBaseId]
+ TPDO4_BASE_ID_480: _ClassVar[PdoBaseId]
+
+class TransmissionType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ SYNC_EVENT_DRIVEN: _ClassVar[TransmissionType]
+ SYNC_CYCLIC: _ClassVar[TransmissionType]
+ REMOTE_SYNC: _ClassVar[TransmissionType]
+ REMOTE_ASYNC: _ClassVar[TransmissionType]
+ ASYNC_MANUFACTURER_SPECIFIC: _ClassVar[TransmissionType]
+ ASYNC_DEVICE_SPECIFIC: _ClassVar[TransmissionType]
+
+class CommandSpecifier(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ CS_NO: _ClassVar[CommandSpecifier]
+ CS_WRITE_ONE_BYTE: _ClassVar[CommandSpecifier]
+ CS_WRITE_TWO_BYTES: _ClassVar[CommandSpecifier]
+ CS_WRITE_THREE_BYTES: _ClassVar[CommandSpecifier]
+ CS_WRITE_FOUR_BYTES: _ClassVar[CommandSpecifier]
+ CS_WRITE_SUCCESS_RESPONSE: _ClassVar[CommandSpecifier]
+ CS_READ_REQUEST: _ClassVar[CommandSpecifier]
+ CS_READ_RESPONSE_ONE_BYTE: _ClassVar[CommandSpecifier]
+ CS_READ_RESPONSE_TWO_BYTES: _ClassVar[CommandSpecifier]
+ CS_READ_RESPONSE_THREE_BYTES: _ClassVar[CommandSpecifier]
+ CS_READ_RESPONSE_FOUR_BYTES: _ClassVar[CommandSpecifier]
+ CS_EXCEPTION_RESPONSE: _ClassVar[CommandSpecifier]
+
+class NmtState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ NMT_INITIALIZING: _ClassVar[NmtState]
+ NMT_RESET_APPLICATION: _ClassVar[NmtState]
+ NMT_CONNECTING: _ClassVar[NmtState]
+ NMT_PREPARING: _ClassVar[NmtState]
+ NMT_STOPPED: _ClassVar[NmtState]
+ NMT_OPERATIONAL: _ClassVar[NmtState]
+ NMT_PRE_OPERATIONAL: _ClassVar[NmtState]
+
+class NmtCommand(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ NMT_COMMAND_UNSPECIFIED: _ClassVar[NmtCommand]
+ NMT_START_REMOTE_NODE: _ClassVar[NmtCommand]
+ NMT_STOP_REMOTE_NODE: _ClassVar[NmtCommand]
+ NMT_ENTER_PRE_OPERATIONAL: _ClassVar[NmtCommand]
+ NMT_RESET_NODE: _ClassVar[NmtCommand]
+ NMT_RESET_COMMUNICATION: _ClassVar[NmtCommand]
+
+class ObIndex(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ INDEX_ZERO: _ClassVar[ObIndex]
+ USER_SAVE_PARA_2000: _ClassVar[ObIndex]
+ POSITION_OFFSET_2008: _ClassVar[ObIndex]
+ ERROR_CODE_6007: _ClassVar[ObIndex]
+ ERROR_CODE_603F: _ClassVar[ObIndex]
+ CONTROL_WORD_6040: _ClassVar[ObIndex]
+ STATUS_WORD_6041: _ClassVar[ObIndex]
+ OPERATION_MODE_6060: _ClassVar[ObIndex]
+ MODE_DISPLAY_6061: _ClassVar[ObIndex]
+ ACTUAL_POSITION_6064: _ClassVar[ObIndex]
+ ACTUAL_SPEED_606C: _ClassVar[ObIndex]
+ ACTUAL_CURRENT_6078: _ClassVar[ObIndex]
+ TARGET_TORQUE_6071: _ClassVar[ObIndex]
+ MAX_TORQUE_6072: _ClassVar[ObIndex]
+ DEMAND_TORQUE_6074: _ClassVar[ObIndex]
+ TARGET_POSITION_607A: _ClassVar[ObIndex]
+ SOFTWARE_POSITION_LIMIT_607D: _ClassVar[ObIndex]
+ MAX_SPEED_607F: _ClassVar[ObIndex]
+ PROFILE_SPEED_6081: _ClassVar[ObIndex]
+ PROFILE_ACCELERATION_6083: _ClassVar[ObIndex]
+ PROFILE_DECELERATION_6084: _ClassVar[ObIndex]
+ CURRENT_LOOP_PID_60F6: _ClassVar[ObIndex]
+ SPEED_LOOP_PID_60F9: _ClassVar[ObIndex]
+ POSITION_LOOP_PID_60FB: _ClassVar[ObIndex]
+ TARGET_SPEED_60FF: _ClassVar[ObIndex]
+ QUICK_STOP_OPTION_605A: _ClassVar[ObIndex]
+ QUICK_STOP_DECEL_6085: _ClassVar[ObIndex]
+ RPDO1_COMM_1400: _ClassVar[ObIndex]
+ RPDO2_COMM_1401: _ClassVar[ObIndex]
+ RPDO3_COMM_1402: _ClassVar[ObIndex]
+ RPDO4_COMM_1403: _ClassVar[ObIndex]
+ TPDO1_COMM_1800: _ClassVar[ObIndex]
+ TPDO2_COMM_1801: _ClassVar[ObIndex]
+ TPDO3_COMM_1802: _ClassVar[ObIndex]
+ TPDO4_COMM_1803: _ClassVar[ObIndex]
+ RPDO1_MAP_1600: _ClassVar[ObIndex]
+ RPDO2_MAP_1601: _ClassVar[ObIndex]
+ RPDO3_MAP_1602: _ClassVar[ObIndex]
+ RPDO4_MAP_1603: _ClassVar[ObIndex]
+ TPDO1_MAP_1A00: _ClassVar[ObIndex]
+ TPDO2_MAP_1A01: _ClassVar[ObIndex]
+ TPDO3_MAP_1A02: _ClassVar[ObIndex]
+ TPDO4_MAP_1A03: _ClassVar[ObIndex]
+ PRODUCER_HEARTBEAT_TIME: _ClassVar[ObIndex]
+
+class ObSubIndex(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ SUB_INDEX_0: _ClassVar[ObSubIndex]
+ SUB_INDEX_1: _ClassVar[ObSubIndex]
+ SUB_INDEX_2: _ClassVar[ObSubIndex]
+ SUB_INDEX_3: _ClassVar[ObSubIndex]
+ SUB_INDEX_4: _ClassVar[ObSubIndex]
+ SUB_INDEX_5: _ClassVar[ObSubIndex]
+ SUB_INDEX_6: _ClassVar[ObSubIndex]
+ SUB_INDEX_7: _ClassVar[ObSubIndex]
+PDO_BASE_ID_UNSPECIFIED: PdoBaseId
+RPDO1_BASE_ID_200: PdoBaseId
+RPDO2_BASE_ID_300: PdoBaseId
+RPDO3_BASE_ID_400: PdoBaseId
+RPDO4_BASE_ID_500: PdoBaseId
+TPDO1_BASE_ID_180: PdoBaseId
+TPDO2_BASE_ID_280: PdoBaseId
+TPDO3_BASE_ID_380: PdoBaseId
+TPDO4_BASE_ID_480: PdoBaseId
+SYNC_EVENT_DRIVEN: TransmissionType
+SYNC_CYCLIC: TransmissionType
+REMOTE_SYNC: TransmissionType
+REMOTE_ASYNC: TransmissionType
+ASYNC_MANUFACTURER_SPECIFIC: TransmissionType
+ASYNC_DEVICE_SPECIFIC: TransmissionType
+CS_NO: CommandSpecifier
+CS_WRITE_ONE_BYTE: CommandSpecifier
+CS_WRITE_TWO_BYTES: CommandSpecifier
+CS_WRITE_THREE_BYTES: CommandSpecifier
+CS_WRITE_FOUR_BYTES: CommandSpecifier
+CS_WRITE_SUCCESS_RESPONSE: CommandSpecifier
+CS_READ_REQUEST: CommandSpecifier
+CS_READ_RESPONSE_ONE_BYTE: CommandSpecifier
+CS_READ_RESPONSE_TWO_BYTES: CommandSpecifier
+CS_READ_RESPONSE_THREE_BYTES: CommandSpecifier
+CS_READ_RESPONSE_FOUR_BYTES: CommandSpecifier
+CS_EXCEPTION_RESPONSE: CommandSpecifier
+NMT_INITIALIZING: NmtState
+NMT_RESET_APPLICATION: NmtState
+NMT_CONNECTING: NmtState
+NMT_PREPARING: NmtState
+NMT_STOPPED: NmtState
+NMT_OPERATIONAL: NmtState
+NMT_PRE_OPERATIONAL: NmtState
+NMT_COMMAND_UNSPECIFIED: NmtCommand
+NMT_START_REMOTE_NODE: NmtCommand
+NMT_STOP_REMOTE_NODE: NmtCommand
+NMT_ENTER_PRE_OPERATIONAL: NmtCommand
+NMT_RESET_NODE: NmtCommand
+NMT_RESET_COMMUNICATION: NmtCommand
+INDEX_ZERO: ObIndex
+USER_SAVE_PARA_2000: ObIndex
+POSITION_OFFSET_2008: ObIndex
+ERROR_CODE_6007: ObIndex
+ERROR_CODE_603F: ObIndex
+CONTROL_WORD_6040: ObIndex
+STATUS_WORD_6041: ObIndex
+OPERATION_MODE_6060: ObIndex
+MODE_DISPLAY_6061: ObIndex
+ACTUAL_POSITION_6064: ObIndex
+ACTUAL_SPEED_606C: ObIndex
+ACTUAL_CURRENT_6078: ObIndex
+TARGET_TORQUE_6071: ObIndex
+MAX_TORQUE_6072: ObIndex
+DEMAND_TORQUE_6074: ObIndex
+TARGET_POSITION_607A: ObIndex
+SOFTWARE_POSITION_LIMIT_607D: ObIndex
+MAX_SPEED_607F: ObIndex
+PROFILE_SPEED_6081: ObIndex
+PROFILE_ACCELERATION_6083: ObIndex
+PROFILE_DECELERATION_6084: ObIndex
+CURRENT_LOOP_PID_60F6: ObIndex
+SPEED_LOOP_PID_60F9: ObIndex
+POSITION_LOOP_PID_60FB: ObIndex
+TARGET_SPEED_60FF: ObIndex
+QUICK_STOP_OPTION_605A: ObIndex
+QUICK_STOP_DECEL_6085: ObIndex
+RPDO1_COMM_1400: ObIndex
+RPDO2_COMM_1401: ObIndex
+RPDO3_COMM_1402: ObIndex
+RPDO4_COMM_1403: ObIndex
+TPDO1_COMM_1800: ObIndex
+TPDO2_COMM_1801: ObIndex
+TPDO3_COMM_1802: ObIndex
+TPDO4_COMM_1803: ObIndex
+RPDO1_MAP_1600: ObIndex
+RPDO2_MAP_1601: ObIndex
+RPDO3_MAP_1602: ObIndex
+RPDO4_MAP_1603: ObIndex
+TPDO1_MAP_1A00: ObIndex
+TPDO2_MAP_1A01: ObIndex
+TPDO3_MAP_1A02: ObIndex
+TPDO4_MAP_1A03: ObIndex
+PRODUCER_HEARTBEAT_TIME: ObIndex
+SUB_INDEX_0: ObSubIndex
+SUB_INDEX_1: ObSubIndex
+SUB_INDEX_2: ObSubIndex
+SUB_INDEX_3: ObSubIndex
+SUB_INDEX_4: ObSubIndex
+SUB_INDEX_5: ObSubIndex
+SUB_INDEX_6: ObSubIndex
+SUB_INDEX_7: ObSubIndex
+
+class SdoFrame(_message.Message):
+ __slots__ = ("node_id", "cs", "index", "sub_index", "data")
+ NODE_ID_FIELD_NUMBER: _ClassVar[int]
+ CS_FIELD_NUMBER: _ClassVar[int]
+ INDEX_FIELD_NUMBER: _ClassVar[int]
+ SUB_INDEX_FIELD_NUMBER: _ClassVar[int]
+ DATA_FIELD_NUMBER: _ClassVar[int]
+ node_id: int
+ cs: CommandSpecifier
+ index: ObIndex
+ sub_index: ObSubIndex
+ data: int
+ def __init__(self, node_id: _Optional[int] = ..., cs: _Optional[_Union[CommandSpecifier, str]] = ..., index: _Optional[_Union[ObIndex, str]] = ..., sub_index: _Optional[_Union[ObSubIndex, str]] = ..., data: _Optional[int] = ...) -> None: ...
diff --git a/src/cmvr/msgs/canopen_pb2_grpc.py b/src/cmvr/msgs/canopen_pb2_grpc.py
new file mode 100644
index 0000000..08208c9
--- /dev/null
+++ b/src/cmvr/msgs/canopen_pb2_grpc.py
@@ -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/msgs/canopen_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}.'
+ )
diff --git a/src/cmvr/msgs/error_code_pb2.py b/src/cmvr/msgs/error_code_pb2.py
new file mode 100644
index 0000000..5ca814d
--- /dev/null
+++ b/src/cmvr/msgs/error_code_pb2.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/error_code.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/msgs/error_code.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x63mvr/msgs/error_code.proto\x12\tcmvr.msgs\"A\n\x08StatusPb\x12(\n\nerror_code\x18\x01 \x01(\x0e\x32\x14.cmvr.msgs.ErrorCode\x12\x0b\n\x03msg\x18\x02 \x01(\t*\x80\x02\n\tErrorCode\x12\x06\n\x02OK\x10\x00\x12\x11\n\x0c\x43\x41NBUS_ERROR\x10\xd0\x0f\x12\x1a\n\x15\x43\x41N_CLIENT_ERROR_BASE\x10\xb4\x10\x12(\n#CAN_CLIENT_ERROR_OPEN_DEVICE_FAILED\x10\xb5\x10\x12\x1f\n\x1a\x43\x41N_CLIENT_ERROR_FRAME_NUM\x10\xb6\x10\x12!\n\x1c\x43\x41N_CLIENT_ERROR_SEND_FAILED\x10\xb7\x10\x12!\n\x1c\x43\x41N_CLIENT_ERROR_RECV_FAILED\x10\xb8\x10\x12\x10\n\x0bMOTOR_ERROR\x10\xb8\x17\x12\x19\n\x14MOTOR_ERROR_SET_ZERO\x10\xb9\x17\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.error_code_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_ERRORCODE']._serialized_start=109
+ _globals['_ERRORCODE']._serialized_end=365
+ _globals['_STATUSPB']._serialized_start=41
+ _globals['_STATUSPB']._serialized_end=106
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/error_code_pb2.pyi b/src/cmvr/msgs/error_code_pb2.pyi
new file mode 100644
index 0000000..54166c3
--- /dev/null
+++ b/src/cmvr/msgs/error_code_pb2.pyi
@@ -0,0 +1,35 @@
+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 typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class ErrorCode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ OK: _ClassVar[ErrorCode]
+ CANBUS_ERROR: _ClassVar[ErrorCode]
+ CAN_CLIENT_ERROR_BASE: _ClassVar[ErrorCode]
+ CAN_CLIENT_ERROR_OPEN_DEVICE_FAILED: _ClassVar[ErrorCode]
+ CAN_CLIENT_ERROR_FRAME_NUM: _ClassVar[ErrorCode]
+ CAN_CLIENT_ERROR_SEND_FAILED: _ClassVar[ErrorCode]
+ CAN_CLIENT_ERROR_RECV_FAILED: _ClassVar[ErrorCode]
+ MOTOR_ERROR: _ClassVar[ErrorCode]
+ MOTOR_ERROR_SET_ZERO: _ClassVar[ErrorCode]
+OK: ErrorCode
+CANBUS_ERROR: ErrorCode
+CAN_CLIENT_ERROR_BASE: ErrorCode
+CAN_CLIENT_ERROR_OPEN_DEVICE_FAILED: ErrorCode
+CAN_CLIENT_ERROR_FRAME_NUM: ErrorCode
+CAN_CLIENT_ERROR_SEND_FAILED: ErrorCode
+CAN_CLIENT_ERROR_RECV_FAILED: ErrorCode
+MOTOR_ERROR: ErrorCode
+MOTOR_ERROR_SET_ZERO: ErrorCode
+
+class StatusPb(_message.Message):
+ __slots__ = ("error_code", "msg")
+ ERROR_CODE_FIELD_NUMBER: _ClassVar[int]
+ MSG_FIELD_NUMBER: _ClassVar[int]
+ error_code: ErrorCode
+ msg: str
+ def __init__(self, error_code: _Optional[_Union[ErrorCode, str]] = ..., msg: _Optional[str] = ...) -> None: ...
diff --git a/src/cmvr/msgs/error_code_pb2_grpc.py b/src/cmvr/msgs/error_code_pb2_grpc.py
new file mode 100644
index 0000000..36bd44d
--- /dev/null
+++ b/src/cmvr/msgs/error_code_pb2_grpc.py
@@ -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/msgs/error_code_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}.'
+ )
diff --git a/src/cmvr/msgs/motor_pb2.py b/src/cmvr/msgs/motor_pb2.py
new file mode 100644
index 0000000..fc7fc95
--- /dev/null
+++ b/src/cmvr/msgs/motor_pb2.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/motor.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/msgs/motor.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.msgs import canopen_pb2 as cmvr_dot_msgs_dot_canopen__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63mvr/msgs/motor.proto\x12\tcmvr.msgs\x1a\x17\x63mvr/msgs/canopen.proto\"\x94\x08\n\x0bMotorStatus\x12$\n\x08run_mode\x18\x01 \x01(\x0e\x32\x12.cmvr.msgs.RunMode\x12\x0f\n\x07\x63urrent\x18\x02 \x01(\x05\x12\x16\n\x0etarget_current\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\x05\x12\x14\n\x0ctarget_speed\x18\x05 \x01(\x05\x12\x10\n\x08position\x18\x06 \x01(\x05\x12\x17\n\x0ftarget_position\x18\x07 \x01(\x05\x12\x13\n\x0b\x65rror_state\x18\x08 \x01(\r\x12\x10\n\x08speed_kp\x18\t \x01(\x05\x12\x10\n\x08speed_ki\x18\n \x01(\x05\x12\x10\n\x08speed_kd\x18\x0b \x01(\x05\x12\x13\n\x0bposition_kp\x18\x0c \x01(\x05\x12\x13\n\x0bposition_ki\x18\r \x01(\x05\x12\x13\n\x0bposition_kd\x18\x0e \x01(\x05\x12\x13\n\x0b\x62us_voltage\x18\x0f \x01(\x05\x12\x17\n\x0fmax_abs_current\x18\x10 \x01(\x05\x12\x17\n\x0fmax_pos_current\x18\x11 \x01(\x05\x12\x17\n\x0fmin_neg_current\x18\x12 \x01(\x05\x12\x15\n\rmax_pos_accel\x18\x13 \x01(\x05\x12\x15\n\rmin_neg_accel\x18\x14 \x01(\x05\x12\x18\n\x10max_pos_velocity\x18\x15 \x01(\x05\x12\x18\n\x10min_neg_velocity\x18\x16 \x01(\x05\x12\x18\n\x10max_pos_position\x18\x17 \x01(\x05\x12\x18\n\x10min_neg_position\x18\x18 \x01(\x05\x12\x12\n\nmotor_temp\x18\x19 \x01(\x05\x12\x12\n\nboard_temp\x18\x1a \x01(\x05\x12\x12\n\ncurrent_kp\x18\x1b \x01(\x05\x12\x12\n\ncurrent_ki\x18\x1c \x01(\x05\x12\x12\n\ncurrent_kd\x18\x1d \x01(\x05\x12\x12\n\nmotor_type\x18\x1e \x01(\x05\x12\x15\n\rmotor_version\x18\x1f \x01(\x05\x12\x18\n\x10software_version\x18 \x01(\x05\x12\x17\n\x0fposition_offset\x18! \x01(\x05\x12\x10\n\x08\x63sp_data\x18\" \x01(\x0c\x12\x17\n\x0f\x65ncoder_voltage\x18# \x01(\x05\x12\x15\n\rencoder_state\x18$ \x01(\x05\x12\x19\n\x11overvoltage_limit\x18% \x01(\x05\x12\x1a\n\x12undervoltage_limit\x18& \x01(\x05\x12\x16\n\x0e\x63oil_over_temp\x18\' \x01(\x05\x12\x18\n\x10\x64river_over_temp\x18( \x01(\x05\x12)\n\x0csdo_response\x18) \x01(\x0b\x32\x13.cmvr.msgs.SdoFrame\x12&\n\tnmt_state\x18* \x01(\x0e\x32\x13.cmvr.msgs.NmtState\x12\x11\n\tctrl_word\x18+ \x01(\r\x12\x13\n\x0bstatus_word\x18, \x01(\r*\xae\x02\n\x07RunMode\x12\x18\n\x14RUN_MODE_UNSPECIFIED\x10\x00\x12\x1d\n\x19RUN_MODE_PROFILE_POSITION\x10\x01\x12\x15\n\x11RUN_MODE_VELOCITY\x10\x02\x12\x1d\n\x19RUN_MODE_PROFILE_VELOCITY\x10\x03\x12\x13\n\x0fRUN_MODE_TORQUE\x10\x04\x12\x13\n\x0fRUN_MODE_HOMING\x10\x05\x12\"\n\x1eRUN_MODE_INTERPOLATED_POSITION\x10\x07\x12!\n\x1dRUN_MODE_CYCLIC_SYNC_POSITION\x10\x08\x12!\n\x1dRUN_MODE_CYCLIC_SYNC_VELOCITY\x10\t\x12 \n\x1cRUN_MODE_CYCLIC_SYNC_CURRENT\x10\nb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.motor_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_RUNMODE']._serialized_start=1109
+ _globals['_RUNMODE']._serialized_end=1411
+ _globals['_MOTORSTATUS']._serialized_start=62
+ _globals['_MOTORSTATUS']._serialized_end=1106
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/motor_pb2.pyi b/src/cmvr/msgs/motor_pb2.pyi
new file mode 100644
index 0000000..23cae5a
--- /dev/null
+++ b/src/cmvr/msgs/motor_pb2.pyi
@@ -0,0 +1,123 @@
+from cmvr.msgs import canopen_pb2 as _canopen_pb2
+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 Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class RunMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ RUN_MODE_UNSPECIFIED: _ClassVar[RunMode]
+ RUN_MODE_PROFILE_POSITION: _ClassVar[RunMode]
+ RUN_MODE_VELOCITY: _ClassVar[RunMode]
+ RUN_MODE_PROFILE_VELOCITY: _ClassVar[RunMode]
+ RUN_MODE_TORQUE: _ClassVar[RunMode]
+ RUN_MODE_HOMING: _ClassVar[RunMode]
+ RUN_MODE_INTERPOLATED_POSITION: _ClassVar[RunMode]
+ RUN_MODE_CYCLIC_SYNC_POSITION: _ClassVar[RunMode]
+ RUN_MODE_CYCLIC_SYNC_VELOCITY: _ClassVar[RunMode]
+ RUN_MODE_CYCLIC_SYNC_CURRENT: _ClassVar[RunMode]
+RUN_MODE_UNSPECIFIED: RunMode
+RUN_MODE_PROFILE_POSITION: RunMode
+RUN_MODE_VELOCITY: RunMode
+RUN_MODE_PROFILE_VELOCITY: RunMode
+RUN_MODE_TORQUE: RunMode
+RUN_MODE_HOMING: RunMode
+RUN_MODE_INTERPOLATED_POSITION: RunMode
+RUN_MODE_CYCLIC_SYNC_POSITION: RunMode
+RUN_MODE_CYCLIC_SYNC_VELOCITY: RunMode
+RUN_MODE_CYCLIC_SYNC_CURRENT: RunMode
+
+class MotorStatus(_message.Message):
+ __slots__ = ("run_mode", "current", "target_current", "speed", "target_speed", "position", "target_position", "error_state", "speed_kp", "speed_ki", "speed_kd", "position_kp", "position_ki", "position_kd", "bus_voltage", "max_abs_current", "max_pos_current", "min_neg_current", "max_pos_accel", "min_neg_accel", "max_pos_velocity", "min_neg_velocity", "max_pos_position", "min_neg_position", "motor_temp", "board_temp", "current_kp", "current_ki", "current_kd", "motor_type", "motor_version", "software_version", "position_offset", "csp_data", "encoder_voltage", "encoder_state", "overvoltage_limit", "undervoltage_limit", "coil_over_temp", "driver_over_temp", "sdo_response", "nmt_state", "ctrl_word", "status_word")
+ RUN_MODE_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_FIELD_NUMBER: _ClassVar[int]
+ TARGET_CURRENT_FIELD_NUMBER: _ClassVar[int]
+ SPEED_FIELD_NUMBER: _ClassVar[int]
+ TARGET_SPEED_FIELD_NUMBER: _ClassVar[int]
+ POSITION_FIELD_NUMBER: _ClassVar[int]
+ TARGET_POSITION_FIELD_NUMBER: _ClassVar[int]
+ ERROR_STATE_FIELD_NUMBER: _ClassVar[int]
+ SPEED_KP_FIELD_NUMBER: _ClassVar[int]
+ SPEED_KI_FIELD_NUMBER: _ClassVar[int]
+ SPEED_KD_FIELD_NUMBER: _ClassVar[int]
+ POSITION_KP_FIELD_NUMBER: _ClassVar[int]
+ POSITION_KI_FIELD_NUMBER: _ClassVar[int]
+ POSITION_KD_FIELD_NUMBER: _ClassVar[int]
+ BUS_VOLTAGE_FIELD_NUMBER: _ClassVar[int]
+ MAX_ABS_CURRENT_FIELD_NUMBER: _ClassVar[int]
+ MAX_POS_CURRENT_FIELD_NUMBER: _ClassVar[int]
+ MIN_NEG_CURRENT_FIELD_NUMBER: _ClassVar[int]
+ MAX_POS_ACCEL_FIELD_NUMBER: _ClassVar[int]
+ MIN_NEG_ACCEL_FIELD_NUMBER: _ClassVar[int]
+ MAX_POS_VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ MIN_NEG_VELOCITY_FIELD_NUMBER: _ClassVar[int]
+ MAX_POS_POSITION_FIELD_NUMBER: _ClassVar[int]
+ MIN_NEG_POSITION_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_TEMP_FIELD_NUMBER: _ClassVar[int]
+ BOARD_TEMP_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_KP_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_KI_FIELD_NUMBER: _ClassVar[int]
+ CURRENT_KD_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_TYPE_FIELD_NUMBER: _ClassVar[int]
+ MOTOR_VERSION_FIELD_NUMBER: _ClassVar[int]
+ SOFTWARE_VERSION_FIELD_NUMBER: _ClassVar[int]
+ POSITION_OFFSET_FIELD_NUMBER: _ClassVar[int]
+ CSP_DATA_FIELD_NUMBER: _ClassVar[int]
+ ENCODER_VOLTAGE_FIELD_NUMBER: _ClassVar[int]
+ ENCODER_STATE_FIELD_NUMBER: _ClassVar[int]
+ OVERVOLTAGE_LIMIT_FIELD_NUMBER: _ClassVar[int]
+ UNDERVOLTAGE_LIMIT_FIELD_NUMBER: _ClassVar[int]
+ COIL_OVER_TEMP_FIELD_NUMBER: _ClassVar[int]
+ DRIVER_OVER_TEMP_FIELD_NUMBER: _ClassVar[int]
+ SDO_RESPONSE_FIELD_NUMBER: _ClassVar[int]
+ NMT_STATE_FIELD_NUMBER: _ClassVar[int]
+ CTRL_WORD_FIELD_NUMBER: _ClassVar[int]
+ STATUS_WORD_FIELD_NUMBER: _ClassVar[int]
+ run_mode: RunMode
+ current: int
+ target_current: int
+ speed: int
+ target_speed: int
+ position: int
+ target_position: int
+ error_state: int
+ speed_kp: int
+ speed_ki: int
+ speed_kd: int
+ position_kp: int
+ position_ki: int
+ position_kd: int
+ bus_voltage: int
+ max_abs_current: int
+ max_pos_current: int
+ min_neg_current: int
+ max_pos_accel: int
+ min_neg_accel: int
+ max_pos_velocity: int
+ min_neg_velocity: int
+ max_pos_position: int
+ min_neg_position: int
+ motor_temp: int
+ board_temp: int
+ current_kp: int
+ current_ki: int
+ current_kd: int
+ motor_type: int
+ motor_version: int
+ software_version: int
+ position_offset: int
+ csp_data: bytes
+ encoder_voltage: int
+ encoder_state: int
+ overvoltage_limit: int
+ undervoltage_limit: int
+ coil_over_temp: int
+ driver_over_temp: int
+ sdo_response: _canopen_pb2.SdoFrame
+ nmt_state: _canopen_pb2.NmtState
+ ctrl_word: int
+ status_word: int
+ def __init__(self, run_mode: _Optional[_Union[RunMode, str]] = ..., current: _Optional[int] = ..., target_current: _Optional[int] = ..., speed: _Optional[int] = ..., target_speed: _Optional[int] = ..., position: _Optional[int] = ..., target_position: _Optional[int] = ..., error_state: _Optional[int] = ..., speed_kp: _Optional[int] = ..., speed_ki: _Optional[int] = ..., speed_kd: _Optional[int] = ..., position_kp: _Optional[int] = ..., position_ki: _Optional[int] = ..., position_kd: _Optional[int] = ..., bus_voltage: _Optional[int] = ..., max_abs_current: _Optional[int] = ..., max_pos_current: _Optional[int] = ..., min_neg_current: _Optional[int] = ..., max_pos_accel: _Optional[int] = ..., min_neg_accel: _Optional[int] = ..., max_pos_velocity: _Optional[int] = ..., min_neg_velocity: _Optional[int] = ..., max_pos_position: _Optional[int] = ..., min_neg_position: _Optional[int] = ..., motor_temp: _Optional[int] = ..., board_temp: _Optional[int] = ..., current_kp: _Optional[int] = ..., current_ki: _Optional[int] = ..., current_kd: _Optional[int] = ..., motor_type: _Optional[int] = ..., motor_version: _Optional[int] = ..., software_version: _Optional[int] = ..., position_offset: _Optional[int] = ..., csp_data: _Optional[bytes] = ..., encoder_voltage: _Optional[int] = ..., encoder_state: _Optional[int] = ..., overvoltage_limit: _Optional[int] = ..., undervoltage_limit: _Optional[int] = ..., coil_over_temp: _Optional[int] = ..., driver_over_temp: _Optional[int] = ..., sdo_response: _Optional[_Union[_canopen_pb2.SdoFrame, _Mapping]] = ..., nmt_state: _Optional[_Union[_canopen_pb2.NmtState, str]] = ..., ctrl_word: _Optional[int] = ..., status_word: _Optional[int] = ...) -> None: ...
diff --git a/src/cmvr/msgs/motor_pb2_grpc.py b/src/cmvr/msgs/motor_pb2_grpc.py
new file mode 100644
index 0000000..e54659b
--- /dev/null
+++ b/src/cmvr/msgs/motor_pb2_grpc.py
@@ -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/msgs/motor_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}.'
+ )
diff --git a/src/cmvr/msgs/robot_detail_pb2.py b/src/cmvr/msgs/robot_detail_pb2.py
new file mode 100644
index 0000000..071df18
--- /dev/null
+++ b/src/cmvr/msgs/robot_detail_pb2.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: cmvr/msgs/robot_detail.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/msgs/robot_detail.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from cmvr.msgs import canopen_pb2 as cmvr_dot_msgs_dot_canopen__pb2
+from cmvr.msgs import motor_pb2 as cmvr_dot_msgs_dot_motor__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x63mvr/msgs/robot_detail.proto\x12\tcmvr.msgs\x1a\x17\x63mvr/msgs/canopen.proto\x1a\x15\x63mvr/msgs/motor.proto\"\x88\x01\n\x0bRobotDetail\x12\x32\n\x06motors\x18\x01 \x03(\x0b\x32\".cmvr.msgs.RobotDetail.MotorsEntry\x1a\x45\n\x0bMotorsEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.cmvr.msgs.MotorStatus:\x02\x38\x01\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cmvr.msgs.robot_detail_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_ROBOTDETAIL_MOTORSENTRY']._loaded_options = None
+ _globals['_ROBOTDETAIL_MOTORSENTRY']._serialized_options = b'8\001'
+ _globals['_ROBOTDETAIL']._serialized_start=92
+ _globals['_ROBOTDETAIL']._serialized_end=228
+ _globals['_ROBOTDETAIL_MOTORSENTRY']._serialized_start=159
+ _globals['_ROBOTDETAIL_MOTORSENTRY']._serialized_end=228
+# @@protoc_insertion_point(module_scope)
diff --git a/src/cmvr/msgs/robot_detail_pb2.pyi b/src/cmvr/msgs/robot_detail_pb2.pyi
new file mode 100644
index 0000000..d662e6e
--- /dev/null
+++ b/src/cmvr/msgs/robot_detail_pb2.pyi
@@ -0,0 +1,22 @@
+from cmvr.msgs import canopen_pb2 as _canopen_pb2
+from cmvr.msgs import motor_pb2 as _motor_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 Mapping as _Mapping
+from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+
+DESCRIPTOR: _descriptor.FileDescriptor
+
+class RobotDetail(_message.Message):
+ __slots__ = ("motors",)
+ class MotorsEntry(_message.Message):
+ __slots__ = ("key", "value")
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ key: int
+ value: _motor_pb2.MotorStatus
+ def __init__(self, key: _Optional[int] = ..., value: _Optional[_Union[_motor_pb2.MotorStatus, _Mapping]] = ...) -> None: ...
+ MOTORS_FIELD_NUMBER: _ClassVar[int]
+ motors: _containers.MessageMap[int, _motor_pb2.MotorStatus]
+ def __init__(self, motors: _Optional[_Mapping[int, _motor_pb2.MotorStatus]] = ...) -> None: ...
diff --git a/src/cmvr/msgs/robot_detail_pb2_grpc.py b/src/cmvr/msgs/robot_detail_pb2_grpc.py
new file mode 100644
index 0000000..feff5ce
--- /dev/null
+++ b/src/cmvr/msgs/robot_detail_pb2_grpc.py
@@ -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/msgs/robot_detail_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}.'
+ )
diff --git a/src/cmvr_edge_ai.egg-info/PKG-INFO b/src/cmvr_edge_ai.egg-info/PKG-INFO
new file mode 100644
index 0000000..2bd0ddf
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/PKG-INFO
@@ -0,0 +1,405 @@
+Metadata-Version: 2.4
+Name: cmvr-edge-ai
+Version: 0.1.0
+Summary: Configuration-driven AI pipeline runtime for CMVR robot edge devices
+Requires-Python: <3.13,>=3.10
+Description-Content-Type: text/markdown
+Requires-Dist: pydantic<3,>=2.8
+Requires-Dist: PyYAML<7,>=6
+Provides-Extra: grpc
+Requires-Dist: grpcio<2,>=1.76; extra == "grpc"
+Requires-Dist: protobuf<7,>=6.31.1; extra == "grpc"
+Provides-Extra: http
+Requires-Dist: httpx<1,>=0.27; extra == "http"
+Provides-Extra: video
+Requires-Dist: av<17,>=12; extra == "video"
+Provides-Extra: image
+Requires-Dist: Pillow<13,>=10; extra == "image"
+Provides-Extra: yolo
+Requires-Dist: numpy<3,>=1.24; extra == "yolo"
+Requires-Dist: ultralytics==8.4.31; extra == "yolo"
+Provides-Extra: yolo-cpu
+Requires-Dist: numpy<3,>=1.24; extra == "yolo-cpu"
+Requires-Dist: torch==2.7.0; extra == "yolo-cpu"
+Requires-Dist: torchvision==0.22.0; extra == "yolo-cpu"
+Requires-Dist: ultralytics==8.4.31; extra == "yolo-cpu"
+
+# 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 模型注册,以及按标签、置信度和最大 FPS 的部署配置;
+- 平台 HTTP JSON Sink,支持有限重试并使用告警 `event_id` 作为幂等键;
+- `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射;
+- 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client;
+- 可直接执行的 smoke、PPE 检测和对话占位配置。
+
+`detect_server/pipeline.yaml` 已实现“相机编码流 -> PyAV 解码 -> 注册模型推理 -> 时间窗口规则 -> HTTP 告警”。VAD/ASR/LLM/TTS 尚未内置;`talk_server/pipeline.yaml` 仍使用模拟音频数据,等待 cmvr-es 音频双向流 proto 落地。
+
+## 架构概览
+
+```mermaid
+flowchart LR
+ A["cmvr-es / 平台
gRPC、HTTP、未来 UDP/QUIC"] --> B["Source 连接器"]
+ B --> C["每条边独立的有界队列"]
+ C --> D["Operator DAG
解码、检测、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 # 相机 -> 解码 -> 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.10~3.12;首次执行时,uv 会在本机没有合适解释器时
+自动安装。先确认已经安装 uv:
+
+```bash
+uv --version
+```
+
+未安装时可使用 uv 官方安装器:
+
+```bash
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
+
+只验证框架和模拟对话链路时,一条命令创建最小环境并自检:
+
+```bash
+cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
+bash scripts/bootstrap.sh --profile core
+```
+
+`uv.lock` 是安装的唯一版本来源;bootstrap 使用 `uv sync --locked`,不会在用户
+机器上重新选择依赖版本。无需 `source .venv/bin/activate`,统一通过
+`uv run --no-sync` 使用已经安装好的环境:
+
+```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:
+ detector:
+ with:
+ attach_frame: true
+ 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
+```
+
+然后验证并运行检测链路,不再要求预先导出环境变量:
+
+```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
+```
+
+运行前需要确认权重文件存在、cmvr-es 已启用 `right_hand_cam`,并且平台的 `/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 个标签可以通过 `cmvr-edge-ai models` 查看。`attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。
+
+`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 只有当前进程内的有限重试,重试耗尽会终止 Pipeline,进程退出或断电仍可能丢失未上报告警。
+- 相机 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-当前限制与演进顺序) 中有更详细说明。
diff --git a/src/cmvr_edge_ai.egg-info/SOURCES.txt b/src/cmvr_edge_ai.egg-info/SOURCES.txt
new file mode 100644
index 0000000..96f277b
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/SOURCES.txt
@@ -0,0 +1,207 @@
+README.md
+pyproject.toml
+src/cmvr/api/agv_command_pb2.py
+src/cmvr/api/agv_command_pb2.pyi
+src/cmvr/api/agv_command_pb2_grpc.py
+src/cmvr/api/agv_service_pb2.py
+src/cmvr/api/agv_service_pb2.pyi
+src/cmvr/api/agv_service_pb2_grpc.py
+src/cmvr/api/arm_command_pb2.py
+src/cmvr/api/arm_command_pb2.pyi
+src/cmvr/api/arm_command_pb2_grpc.py
+src/cmvr/api/arm_service_pb2.py
+src/cmvr/api/arm_service_pb2.pyi
+src/cmvr/api/arm_service_pb2_grpc.py
+src/cmvr/api/biohead_command_pb2.py
+src/cmvr/api/biohead_command_pb2.pyi
+src/cmvr/api/biohead_command_pb2_grpc.py
+src/cmvr/api/biohead_service_pb2.py
+src/cmvr/api/biohead_service_pb2.pyi
+src/cmvr/api/biohead_service_pb2_grpc.py
+src/cmvr/api/camera_command_pb2.py
+src/cmvr/api/camera_command_pb2.pyi
+src/cmvr/api/camera_command_pb2_grpc.py
+src/cmvr/api/camera_service_pb2.py
+src/cmvr/api/camera_service_pb2.pyi
+src/cmvr/api/camera_service_pb2_grpc.py
+src/cmvr/api/common_pb2.py
+src/cmvr/api/common_pb2.pyi
+src/cmvr/api/common_pb2_grpc.py
+src/cmvr/api/dexhand_command_pb2.py
+src/cmvr/api/dexhand_command_pb2.pyi
+src/cmvr/api/dexhand_command_pb2_grpc.py
+src/cmvr/api/dexhand_service_pb2.py
+src/cmvr/api/dexhand_service_pb2.pyi
+src/cmvr/api/dexhand_service_pb2_grpc.py
+src/cmvr/api/hlc_command_pb2.py
+src/cmvr/api/hlc_command_pb2.pyi
+src/cmvr/api/hlc_command_pb2_grpc.py
+src/cmvr/api/hlc_service_pb2.py
+src/cmvr/api/hlc_service_pb2.pyi
+src/cmvr/api/hlc_service_pb2_grpc.py
+src/cmvr/api/microphone_command_pb2.py
+src/cmvr/api/microphone_command_pb2.pyi
+src/cmvr/api/microphone_command_pb2_grpc.py
+src/cmvr/api/microphone_service_pb2.py
+src/cmvr/api/microphone_service_pb2.pyi
+src/cmvr/api/microphone_service_pb2_grpc.py
+src/cmvr/api/speaker_command_pb2.py
+src/cmvr/api/speaker_command_pb2.pyi
+src/cmvr/api/speaker_command_pb2_grpc.py
+src/cmvr/api/speaker_service_pb2.py
+src/cmvr/api/speaker_service_pb2.pyi
+src/cmvr/api/speaker_service_pb2_grpc.py
+src/cmvr/api/system_command_pb2.py
+src/cmvr/api/system_command_pb2.pyi
+src/cmvr/api/system_command_pb2_grpc.py
+src/cmvr/api/system_service_pb2.py
+src/cmvr/api/system_service_pb2.pyi
+src/cmvr/api/system_service_pb2_grpc.py
+src/cmvr/api/test_service_pb2.py
+src/cmvr/api/test_service_pb2.pyi
+src/cmvr/api/test_service_pb2_grpc.py
+src/cmvr/common/geometry_pb2.py
+src/cmvr/common/geometry_pb2.pyi
+src/cmvr/common/geometry_pb2_grpc.py
+src/cmvr/config/joint_limits_config_pb2.py
+src/cmvr/config/joint_limits_config_pb2.pyi
+src/cmvr/config/joint_limits_config_pb2_grpc.py
+src/cmvr/config/lawba_ik_config_pb2.py
+src/cmvr/config/lawba_ik_config_pb2.pyi
+src/cmvr/config/lawba_ik_config_pb2_grpc.py
+src/cmvr/config/pinocchio_dls_ik_config_pb2.py
+src/cmvr/config/pinocchio_dls_ik_config_pb2.pyi
+src/cmvr/config/pinocchio_dls_ik_config_pb2_grpc.py
+src/cmvr/config/pinocchio_qp_ik_config_pb2.py
+src/cmvr/config/pinocchio_qp_ik_config_pb2.pyi
+src/cmvr/config/pinocchio_qp_ik_config_pb2_grpc.py
+src/cmvr/config/srs_ik_config_pb2.py
+src/cmvr/config/srs_ik_config_pb2.pyi
+src/cmvr/config/srs_ik_config_pb2_grpc.py
+src/cmvr/config/touch_screen_algorithm_config_pb2.py
+src/cmvr/config/touch_screen_algorithm_config_pb2.pyi
+src/cmvr/config/touch_screen_algorithm_config_pb2_grpc.py
+src/cmvr/config/touch_screen_common_config_pb2.py
+src/cmvr/config/touch_screen_common_config_pb2.pyi
+src/cmvr/config/touch_screen_common_config_pb2_grpc.py
+src/cmvr/config/agv_config/agv_config_pb2.py
+src/cmvr/config/agv_config/agv_config_pb2.pyi
+src/cmvr/config/agv_config/agv_config_pb2_grpc.py
+src/cmvr/config/arm_config/arm_config_pb2.py
+src/cmvr/config/arm_config/arm_config_pb2.pyi
+src/cmvr/config/arm_config/arm_config_pb2_grpc.py
+src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.py
+src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2.pyi
+src/cmvr/config/aubo_robot_config/aubo_robot_config_pb2_grpc.py
+src/cmvr/config/biohead_config/biohead_config_pb2.py
+src/cmvr/config/biohead_config/biohead_config_pb2.pyi
+src/cmvr/config/biohead_config/biohead_config_pb2_grpc.py
+src/cmvr/config/camera_config/camera_config_pb2.py
+src/cmvr/config/camera_config/camera_config_pb2.pyi
+src/cmvr/config/camera_config/camera_config_pb2_grpc.py
+src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.py
+src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2.pyi
+src/cmvr/config/cmvr_es_config/cmvr_es_config_pb2_grpc.py
+src/cmvr/config/device_manager_config/device_manager_config_pb2.py
+src/cmvr/config/device_manager_config/device_manager_config_pb2.pyi
+src/cmvr/config/device_manager_config/device_manager_config_pb2_grpc.py
+src/cmvr/config/dexhand_config/dexhand_config_pb2.py
+src/cmvr/config/dexhand_config/dexhand_config_pb2.pyi
+src/cmvr/config/dexhand_config/dexhand_config_pb2_grpc.py
+src/cmvr/config/grpc_server_config/grpc_server_config_pb2.py
+src/cmvr/config/grpc_server_config/grpc_server_config_pb2.pyi
+src/cmvr/config/grpc_server_config/grpc_server_config_pb2_grpc.py
+src/cmvr/config/logger_config/logger_config_pb2.py
+src/cmvr/config/logger_config/logger_config_pb2.pyi
+src/cmvr/config/logger_config/logger_config_pb2_grpc.py
+src/cmvr/config/microphone_config/microphone_config_pb2.py
+src/cmvr/config/microphone_config/microphone_config_pb2.pyi
+src/cmvr/config/microphone_config/microphone_config_pb2_grpc.py
+src/cmvr/config/motor_config/motor_config_pb2.py
+src/cmvr/config/motor_config/motor_config_pb2.pyi
+src/cmvr/config/motor_config/motor_config_pb2_grpc.py
+src/cmvr/config/speaker_config/speaker_config_pb2.py
+src/cmvr/config/speaker_config/speaker_config_pb2.pyi
+src/cmvr/config/speaker_config/speaker_config_pb2_grpc.py
+src/cmvr/config/task_manager_config/task_manager_config_pb2.py
+src/cmvr/config/task_manager_config/task_manager_config_pb2.pyi
+src/cmvr/config/task_manager_config/task_manager_config_pb2_grpc.py
+src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.py
+src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2.pyi
+src/cmvr/config/touch_screen_task_config/touch_screen_task_config_pb2_grpc.py
+src/cmvr/msgs/agv_pb2.py
+src/cmvr/msgs/agv_pb2.pyi
+src/cmvr/msgs/agv_pb2_grpc.py
+src/cmvr/msgs/can_card_parameter_pb2.py
+src/cmvr/msgs/can_card_parameter_pb2.pyi
+src/cmvr/msgs/can_card_parameter_pb2_grpc.py
+src/cmvr/msgs/canopen_pb2.py
+src/cmvr/msgs/canopen_pb2.pyi
+src/cmvr/msgs/canopen_pb2_grpc.py
+src/cmvr/msgs/error_code_pb2.py
+src/cmvr/msgs/error_code_pb2.pyi
+src/cmvr/msgs/error_code_pb2_grpc.py
+src/cmvr/msgs/motor_pb2.py
+src/cmvr/msgs/motor_pb2.pyi
+src/cmvr/msgs/motor_pb2_grpc.py
+src/cmvr/msgs/robot_detail_pb2.py
+src/cmvr/msgs/robot_detail_pb2.pyi
+src/cmvr/msgs/robot_detail_pb2_grpc.py
+src/cmvr_edge_ai/__init__.py
+src/cmvr_edge_ai/application.py
+src/cmvr_edge_ai/cli.py
+src/cmvr_edge_ai/compiler.py
+src/cmvr_edge_ai.egg-info/PKG-INFO
+src/cmvr_edge_ai.egg-info/SOURCES.txt
+src/cmvr_edge_ai.egg-info/dependency_links.txt
+src/cmvr_edge_ai.egg-info/entry_points.txt
+src/cmvr_edge_ai.egg-info/requires.txt
+src/cmvr_edge_ai.egg-info/top_level.txt
+src/cmvr_edge_ai/config/__init__.py
+src/cmvr_edge_ai/config/loader.py
+src/cmvr_edge_ai/config/models.py
+src/cmvr_edge_ai/connectors/__init__.py
+src/cmvr_edge_ai/connectors/_context.py
+src/cmvr_edge_ai/connectors/cmvr_es/__init__.py
+src/cmvr_edge_ai/connectors/cmvr_es/agv.py
+src/cmvr_edge_ai/connectors/cmvr_es/camera.py
+src/cmvr_edge_ai/connectors/cmvr_es/errors.py
+src/cmvr_edge_ai/connectors/platform/__init__.py
+src/cmvr_edge_ai/connectors/platform/http_json.py
+src/cmvr_edge_ai/contracts/__init__.py
+src/cmvr_edge_ai/contracts/control.py
+src/cmvr_edge_ai/contracts/events.py
+src/cmvr_edge_ai/contracts/media.py
+src/cmvr_edge_ai/core/__init__.py
+src/cmvr_edge_ai/core/component.py
+src/cmvr_edge_ai/core/edge_queue.py
+src/cmvr_edge_ai/core/envelope.py
+src/cmvr_edge_ai/core/runtime.py
+src/cmvr_edge_ai/detection/__init__.py
+src/cmvr_edge_ai/detection/annotation.py
+src/cmvr_edge_ai/detection/base.py
+src/cmvr_edge_ai/detection/operator.py
+src/cmvr_edge_ai/detection/plugins.py
+src/cmvr_edge_ai/detection/registry.py
+src/cmvr_edge_ai/detection/rules.py
+src/cmvr_edge_ai/detection/video.py
+src/cmvr_edge_ai/detection/models/__init__.py
+src/cmvr_edge_ai/detection/models/ppe_yolo.py
+src/cmvr_edge_ai/detection/models/yolo.py
+src/cmvr_edge_ai/observability/__init__.py
+src/cmvr_edge_ai/observability/logging.py
+src/cmvr_edge_ai/plugins/__init__.py
+src/cmvr_edge_ai/plugins/builtin.py
+src/cmvr_edge_ai/plugins/registry.py
+src/cmvr_edge_ai/transports/__init__.py
+src/cmvr_edge_ai/transports/base.py
+src/cmvr_edge_ai/transports/grpc/__init__.py
+src/cmvr_edge_ai/transports/grpc/bidi.py
+src/cmvr_edge_ai/transports/grpc/channel.py
+src/cmvr_edge_ai/transports/http/__init__.py
+src/cmvr_edge_ai/transports/http/client.py
+src/cmvr_edge_ai/transports/quic/__init__.py
+src/cmvr_edge_ai/transports/udp/__init__.py
+src/cmvr_edge_ai/workers/__init__.py
+src/cmvr_edge_ai/workers/process.py
+src/cmvr_edge_ai/workers/thread.py
\ No newline at end of file
diff --git a/src/cmvr_edge_ai.egg-info/dependency_links.txt b/src/cmvr_edge_ai.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/cmvr_edge_ai.egg-info/entry_points.txt b/src/cmvr_edge_ai.egg-info/entry_points.txt
new file mode 100644
index 0000000..bdb5e17
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+cmvr-edge-ai = cmvr_edge_ai.cli:main
diff --git a/src/cmvr_edge_ai.egg-info/requires.txt b/src/cmvr_edge_ai.egg-info/requires.txt
new file mode 100644
index 0000000..4f0dabc
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/requires.txt
@@ -0,0 +1,25 @@
+pydantic<3,>=2.8
+PyYAML<7,>=6
+
+[grpc]
+grpcio<2,>=1.76
+protobuf<7,>=6.31.1
+
+[http]
+httpx<1,>=0.27
+
+[image]
+Pillow<13,>=10
+
+[video]
+av<17,>=12
+
+[yolo]
+numpy<3,>=1.24
+ultralytics==8.4.31
+
+[yolo-cpu]
+numpy<3,>=1.24
+torch==2.7.0
+torchvision==0.22.0
+ultralytics==8.4.31
diff --git a/src/cmvr_edge_ai.egg-info/top_level.txt b/src/cmvr_edge_ai.egg-info/top_level.txt
new file mode 100644
index 0000000..6cd3893
--- /dev/null
+++ b/src/cmvr_edge_ai.egg-info/top_level.txt
@@ -0,0 +1,2 @@
+cmvr
+cmvr_edge_ai
diff --git a/src/cmvr_edge_ai/__init__.py b/src/cmvr_edge_ai/__init__.py
new file mode 100644
index 0000000..a92af13
--- /dev/null
+++ b/src/cmvr_edge_ai/__init__.py
@@ -0,0 +1,3 @@
+"""CMVR edge AI pipeline runtime."""
+
+__version__ = "0.1.0"
diff --git a/src/cmvr_edge_ai/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..1d6abeb
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..11f94e0
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/application.cpython-310.pyc b/src/cmvr_edge_ai/__pycache__/application.cpython-310.pyc
new file mode 100644
index 0000000..d713bf3
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/application.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/application.cpython-312.pyc b/src/cmvr_edge_ai/__pycache__/application.cpython-312.pyc
new file mode 100644
index 0000000..44ac9e0
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/application.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/cli.cpython-310.pyc b/src/cmvr_edge_ai/__pycache__/cli.cpython-310.pyc
new file mode 100644
index 0000000..36a232b
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/cli.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/cli.cpython-312.pyc b/src/cmvr_edge_ai/__pycache__/cli.cpython-312.pyc
new file mode 100644
index 0000000..119a943
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/cli.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/compiler.cpython-310.pyc b/src/cmvr_edge_ai/__pycache__/compiler.cpython-310.pyc
new file mode 100644
index 0000000..4a56ec0
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/compiler.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/__pycache__/compiler.cpython-312.pyc b/src/cmvr_edge_ai/__pycache__/compiler.cpython-312.pyc
new file mode 100644
index 0000000..93c9ce7
Binary files /dev/null and b/src/cmvr_edge_ai/__pycache__/compiler.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/application.py b/src/cmvr_edge_ai/application.py
new file mode 100644
index 0000000..c2afc37
--- /dev/null
+++ b/src/cmvr_edge_ai/application.py
@@ -0,0 +1,199 @@
+"""Application-level ownership for pipelines and shared network clients."""
+
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Iterable
+from concurrent.futures import ThreadPoolExecutor
+from enum import Enum
+
+from cmvr_edge_ai.compiler import (
+ CompiledPipeline,
+ PipelineCompileError,
+ compile_pipeline,
+ enabled_pipeline_ids,
+)
+from cmvr_edge_ai.config import AppConfig
+from cmvr_edge_ai.connectors import register_connector_plugins
+from cmvr_edge_ai.detection import (
+ DetectionModelRegistry,
+ register_builtin_detection_models,
+ register_detection_plugins,
+)
+from cmvr_edge_ai.plugins import PluginRegistry, register_builtin_plugins
+from cmvr_edge_ai.transports.grpc import GrpcChannelPool
+from cmvr_edge_ai.transports.http import HttpClientPool
+
+
+class ApplicationState(str, Enum):
+ CREATED = "created"
+ STARTING = "starting"
+ RUNNING = "running"
+ STOPPING = "stopping"
+ STOPPED = "stopped"
+
+
+def create_default_model_registry(
+ *, discover_entry_points: bool = True
+) -> DetectionModelRegistry:
+ """Create the artifact registry used by generic detection nodes."""
+
+ registry = DetectionModelRegistry()
+ register_builtin_detection_models(registry)
+ if discover_entry_points:
+ registry.load_entry_points()
+ return registry
+
+
+def create_default_registry(
+ *,
+ discover_entry_points: bool = True,
+ model_registry: DetectionModelRegistry | None = None,
+) -> PluginRegistry:
+ registry = PluginRegistry()
+ register_builtin_plugins(registry)
+ register_connector_plugins(registry)
+ detection_models = model_registry or create_default_model_registry(
+ discover_entry_points=discover_entry_points
+ )
+ register_detection_plugins(registry, detection_models)
+ if discover_entry_points:
+ registry.load_entry_points()
+ return registry
+
+
+def validate_application(
+ config: AppConfig,
+ registry: PluginRegistry,
+ pipeline_ids: Iterable[str] | None = None,
+) -> tuple[CompiledPipeline, ...]:
+ selected = _select_pipeline_ids(config, pipeline_ids)
+ return tuple(
+ compile_pipeline(config, pipeline_id, registry) for pipeline_id in selected
+ )
+
+
+class EdgeAIApplication:
+ """Own shared transports and one or more local pipeline runtimes."""
+
+ def __init__(self, config: AppConfig, registry: PluginRegistry) -> None:
+ self.config = config
+ self.registry = registry
+ self.state = ApplicationState.CREATED
+ self.grpc_channels = GrpcChannelPool()
+ self.http_clients = HttpClientPool()
+ self._compiled: dict[str, CompiledPipeline] = {}
+ self._executor: ThreadPoolExecutor | None = None
+ self._lifecycle_lock = asyncio.Lock()
+
+ @property
+ def pipelines(self) -> tuple[str, ...]:
+ return tuple(self._compiled)
+
+ async def start(self, pipeline_ids: Iterable[str] | None = None) -> None:
+ async with self._lifecycle_lock:
+ if self.state is not ApplicationState.CREATED:
+ raise RuntimeError(
+ f"cannot start application in state {self.state.value}"
+ )
+ selected = _select_pipeline_ids(self.config, pipeline_ids)
+ self.state = ApplicationState.STARTING
+ try:
+ self._executor = ThreadPoolExecutor(
+ max_workers=self.config.runtime.thread_workers,
+ thread_name_prefix="cmvr-edge-ai",
+ )
+ metadata = {
+ "endpoints": self.config.endpoints,
+ "grpc_channel_pool": self.grpc_channels,
+ "http_client_pool": self.http_clients,
+ "thread_executor": self._executor,
+ }
+ for pipeline_id in selected:
+ compiled = compile_pipeline(
+ self.config,
+ pipeline_id,
+ self.registry,
+ metadata=metadata,
+ )
+ self._compiled[pipeline_id] = compiled
+ for compiled in self._compiled.values():
+ await compiled.runtime.start()
+ except BaseException:
+ await self._stop_unlocked(graceful=False)
+ raise
+ self.state = ApplicationState.RUNNING
+
+ async def wait(self) -> None:
+ if self.state is not ApplicationState.RUNNING:
+ if self.state is ApplicationState.STOPPED:
+ return
+ raise RuntimeError("application has not been started")
+ await asyncio.gather(
+ *(compiled.runtime.wait() for compiled in self._compiled.values())
+ )
+
+ async def stop(self, *, graceful: bool = True) -> None:
+ async with self._lifecycle_lock:
+ await self._stop_unlocked(graceful=graceful)
+
+ async def _stop_unlocked(self, *, graceful: bool) -> None:
+ if self.state is ApplicationState.STOPPED:
+ return
+ self.state = ApplicationState.STOPPING
+ await asyncio.gather(
+ *(
+ compiled.runtime.stop(graceful=graceful)
+ for compiled in self._compiled.values()
+ ),
+ return_exceptions=True,
+ )
+ await asyncio.gather(
+ self.grpc_channels.close(),
+ self.http_clients.close(),
+ return_exceptions=True,
+ )
+ if self._executor is not None:
+ self._executor.shutdown(wait=False, cancel_futures=True)
+ self._executor = None
+ self.state = ApplicationState.STOPPED
+
+ async def health(self) -> dict[str, object]:
+ reports = {
+ pipeline_id: await compiled.runtime.health()
+ for pipeline_id, compiled in self._compiled.items()
+ }
+ return {"state": self.state.value, "pipelines": reports}
+
+
+def _select_pipeline_ids(
+ config: AppConfig, pipeline_ids: Iterable[str] | None
+) -> tuple[str, ...]:
+ if pipeline_ids is None:
+ selected = enabled_pipeline_ids(config)
+ else:
+ selected = tuple(dict.fromkeys(pipeline_ids))
+ unknown = sorted(set(selected) - set(config.pipelines))
+ if unknown:
+ raise PipelineCompileError(f"unknown pipeline(s): {', '.join(unknown)}")
+ disabled = sorted(
+ pipeline_id
+ for pipeline_id in selected
+ if not config.pipelines[pipeline_id].enabled
+ )
+ if disabled:
+ raise PipelineCompileError(
+ f"selected pipeline(s) are disabled: {', '.join(disabled)}"
+ )
+ if not selected:
+ raise PipelineCompileError("no enabled pipelines selected")
+ return selected
+
+
+__all__ = [
+ "ApplicationState",
+ "EdgeAIApplication",
+ "create_default_model_registry",
+ "create_default_registry",
+ "validate_application",
+]
diff --git a/src/cmvr_edge_ai/cli.py b/src/cmvr_edge_ai/cli.py
new file mode 100644
index 0000000..498ef97
--- /dev/null
+++ b/src/cmvr_edge_ai/cli.py
@@ -0,0 +1,122 @@
+"""Command-line interface for validating and running edge-AI pipelines."""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import signal
+import sys
+from pathlib import Path
+
+from pydantic import ValidationError
+
+from cmvr_edge_ai.application import (
+ EdgeAIApplication,
+ create_default_model_registry,
+ create_default_registry,
+ validate_application,
+)
+from cmvr_edge_ai.compiler import PipelineCompileError
+from cmvr_edge_ai.config import ConfigLoadError, load_config
+from cmvr_edge_ai.observability import configure_logging
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(prog="cmvr-edge-ai")
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ validate = subparsers.add_parser("validate", help="validate config and DAG")
+ _add_config_arguments(validate)
+
+ run = subparsers.add_parser("run", help="run enabled pipelines")
+ _add_config_arguments(run)
+ run.add_argument("--log-level", default="INFO")
+ run.add_argument("--log-format", choices=("text", "json"), default="text")
+
+ subparsers.add_parser("plugins", help="list registered plugins")
+ subparsers.add_parser("models", help="list registered detection models")
+ return parser
+
+
+def _add_config_arguments(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument("--config", "-c", type=Path, required=True)
+ parser.add_argument(
+ "--pipeline",
+ action="append",
+ dest="pipelines",
+ help="select a pipeline; repeat for multiple pipelines",
+ )
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = build_parser().parse_args(argv)
+ try:
+ model_registry = create_default_model_registry()
+ registry = create_default_registry(model_registry=model_registry)
+ if args.command == "plugins":
+ for plugin_spec in registry.specs():
+ print(
+ f"{plugin_spec.plugin_id}\t{plugin_spec.kind.value}\t"
+ f"{plugin_spec.description}"
+ )
+ return 0
+ if args.command == "models":
+ for model_spec in model_registry.specs():
+ labels = ",".join(model_spec.supported_labels)
+ print(
+ f"{model_spec.model_id}\t{model_spec.name}\t"
+ f"{model_spec.backend}\t{labels}"
+ )
+ return 0
+
+ config = load_config(args.config)
+ if args.command == "validate":
+ compiled = validate_application(config, registry, args.pipelines)
+ names = ", ".join(item.pipeline_id for item in compiled)
+ print(f"configuration is valid; pipelines: {names}")
+ return 0
+
+ configure_logging(args.log_level, args.log_format)
+ return asyncio.run(_run_application(config, registry, args.pipelines))
+ except (ConfigLoadError, ValidationError, PipelineCompileError, ValueError) as exc:
+ print(f"configuration error: {exc}", file=sys.stderr)
+ return 2
+ except KeyboardInterrupt:
+ return 130
+ except Exception as exc:
+ print(f"runtime error: {exc}", file=sys.stderr)
+ return 1
+
+
+async def _run_application(config, registry, pipeline_ids) -> int: # type: ignore[no-untyped-def]
+ application = EdgeAIApplication(config, registry)
+ stop_event = asyncio.Event()
+ loop = asyncio.get_running_loop()
+ for signum in (signal.SIGINT, signal.SIGTERM):
+ try:
+ loop.add_signal_handler(signum, stop_event.set)
+ except NotImplementedError:
+ pass
+
+ await application.start(pipeline_ids)
+ wait_task = asyncio.create_task(application.wait(), name="application-wait")
+ signal_task = asyncio.create_task(stop_event.wait(), name="application-signal")
+ try:
+ done, _ = await asyncio.wait(
+ (wait_task, signal_task), return_when=asyncio.FIRST_COMPLETED
+ )
+ if wait_task in done:
+ await wait_task
+ else:
+ await application.stop(graceful=True)
+ wait_task.cancel()
+ await asyncio.gather(wait_task, return_exceptions=True)
+ finally:
+ signal_task.cancel()
+ await asyncio.gather(signal_task, return_exceptions=True)
+ await application.stop(graceful=True)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/cmvr_edge_ai/compiler.py b/src/cmvr_edge_ai/compiler.py
new file mode 100644
index 0000000..f68d8c1
--- /dev/null
+++ b/src/cmvr_edge_ai/compiler.py
@@ -0,0 +1,375 @@
+"""Compile strict YAML configuration into executable pipeline runtimes."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from typing import Any
+
+from cmvr_edge_ai.config import AppConfig, PipelineConfig
+from cmvr_edge_ai.core import Component, Edge, Operator, OverflowPolicy, Sink, Source
+from cmvr_edge_ai.core.runtime import PipelineRuntime
+from cmvr_edge_ai.plugins import (
+ PluginKind,
+ PluginRegistry,
+ PluginSpec,
+ schemas_compatible,
+)
+
+
+SUPPORTED_API_VERSION = "cmvr.edge.ai/v1"
+
+
+class PipelineCompileError(ValueError):
+ """Raised when a valid YAML document cannot form an executable graph."""
+
+
+@dataclass(frozen=True, slots=True)
+class CompiledPipeline:
+ pipeline_id: str
+ runtime: PipelineRuntime
+ plugin_specs: Mapping[str, PluginSpec]
+
+
+def compile_pipeline(
+ config: AppConfig,
+ pipeline_id: str,
+ registry: PluginRegistry,
+ *,
+ metadata: Mapping[str, Any] | None = None,
+) -> CompiledPipeline:
+ if config.api_version != SUPPORTED_API_VERSION:
+ raise PipelineCompileError(
+ f"unsupported api_version {config.api_version!r}; "
+ f"expected {SUPPORTED_API_VERSION!r}"
+ )
+ try:
+ pipeline = config.pipelines[pipeline_id]
+ except KeyError as exc:
+ raise PipelineCompileError(f"unknown pipeline: {pipeline_id}") from exc
+ if not pipeline.enabled:
+ raise PipelineCompileError(f"pipeline is disabled: {pipeline_id}")
+
+ _validate_supported_runtime_config(config)
+ _validate_supported_pipeline_config(pipeline_id, pipeline)
+
+ components: dict[str, Component] = {}
+ specs: dict[str, PluginSpec] = {}
+ for node_id, node_config in pipeline.nodes.items():
+ if not node_config.enabled:
+ raise PipelineCompileError(
+ f"active pipeline {pipeline_id!r} contains disabled node {node_id!r}; "
+ "disable the whole pipeline or remove its edges"
+ )
+ _validate_supported_node_config(node_id, node_config)
+ try:
+ spec = registry.resolve(node_config.uses)
+ component = registry.create(
+ node_config.uses, node_id=node_id, params=node_config.params
+ )
+ except Exception as exc:
+ raise PipelineCompileError(
+ f"failed to construct node {node_id!r} from {node_config.uses!r}: {exc}"
+ ) from exc
+ _validate_component_kind(node_id, component, spec)
+ _validate_endpoint_reference(config, node_id, node_config.params, spec)
+ components[node_id] = component
+ specs[node_id] = spec
+
+ runtime_edges: list[Edge] = []
+ for index, edge_config in enumerate(pipeline.edges):
+ source_node, source_port = edge_config.source.split(".", maxsplit=1)
+ target_node, target_port = edge_config.target.split(".", maxsplit=1)
+ source_spec = specs[source_node]
+ target_spec = specs[target_node]
+
+ source_schema = _port_schema(
+ source_spec.outputs, source_port, edge_config.source, "output"
+ )
+ target_schema = _port_schema(
+ target_spec.inputs, target_port, edge_config.target, "input"
+ )
+ if not schemas_compatible(source_schema, target_schema):
+ raise PipelineCompileError(
+ f"schema mismatch on edge {edge_config.source!r} -> "
+ f"{edge_config.target!r}: {source_schema!r} is not compatible "
+ f"with {target_schema!r}"
+ )
+
+ if edge_config.qos.max_age_ms is not None:
+ raise PipelineCompileError(
+ f"edge {edge_config.source!r} -> {edge_config.target!r} sets "
+ "max_age_ms, which is reserved for the deadline-aware runtime"
+ )
+ if edge_config.qos.put_timeout_ms is not None:
+ raise PipelineCompileError(
+ f"edge {edge_config.source!r} -> {edge_config.target!r} sets "
+ "put_timeout_ms, which is not implemented in runtime v1"
+ )
+ _validate_qos_profile(edge_config.source, edge_config.target, edge_config.qos)
+
+ overflow = edge_config.qos.overflow
+ if overflow == "error":
+ overflow = OverflowPolicy.REJECT.value
+ runtime_edges.append(
+ Edge(
+ source_node=source_node,
+ source_port=source_port,
+ target_node=target_node,
+ target_port=target_port,
+ capacity=edge_config.qos.capacity,
+ overflow=overflow,
+ name=f"{pipeline_id}:{index}:{edge_config.source}->{edge_config.target}",
+ )
+ )
+
+ _validate_detection_rule_paths(pipeline, specs, components)
+ _validate_actuator_paths(pipeline, specs)
+ runtime_metadata = dict(metadata or {})
+ runtime_metadata.setdefault("endpoints", config.endpoints)
+ runtime_metadata.setdefault("node_configs", pipeline.nodes)
+ runtime = PipelineRuntime(
+ components,
+ runtime_edges,
+ pipeline_id=pipeline_id,
+ shutdown_timeout_s=config.runtime.shutdown_timeout_s,
+ metadata=runtime_metadata,
+ )
+ return CompiledPipeline(pipeline_id, runtime, specs)
+
+
+def enabled_pipeline_ids(config: AppConfig) -> tuple[str, ...]:
+ return tuple(
+ pipeline_id
+ for pipeline_id, pipeline in config.pipelines.items()
+ if pipeline.enabled
+ )
+
+
+def _validate_component_kind(node_id: str, component: Any, spec: PluginSpec) -> None:
+ expected_type: type[Component]
+ if spec.kind is PluginKind.SOURCE:
+ expected_type = Source
+ elif spec.kind is PluginKind.OPERATOR:
+ expected_type = Operator
+ else:
+ expected_type = Sink
+ if not isinstance(component, expected_type):
+ raise PipelineCompileError(
+ f"plugin {spec.plugin_id!r} declares {spec.kind.value} but node "
+ f"{node_id!r} created {type(component).__name__}"
+ )
+
+
+def _port_schema(
+ ports: Mapping[str, str], port: str, reference: str, direction: str
+) -> str:
+ try:
+ return ports[port]
+ except KeyError as exc:
+ available = ", ".join(sorted(ports)) or ""
+ raise PipelineCompileError(
+ f"unknown {direction} port {reference!r}; available: {available}"
+ ) from exc
+
+
+def _validate_endpoint_reference(
+ config: AppConfig,
+ node_id: str,
+ params: Mapping[str, Any],
+ spec: PluginSpec,
+) -> None:
+ endpoint_id = params.get("endpoint")
+ transport_tags = sorted(
+ tag.removeprefix("transport:")
+ for tag in spec.tags
+ if tag.startswith("transport:")
+ )
+ if endpoint_id is None:
+ if transport_tags:
+ raise PipelineCompileError(
+ f"connector node {node_id!r} requires a named endpoint"
+ )
+ return
+ if not isinstance(endpoint_id, str) or endpoint_id not in config.endpoints:
+ raise PipelineCompileError(
+ f"node {node_id!r} references unknown endpoint {endpoint_id!r}"
+ )
+ endpoint = config.endpoints[endpoint_id]
+ if transport_tags and endpoint.transport not in transport_tags:
+ raise PipelineCompileError(
+ f"node {node_id!r} requires transport {transport_tags}, but endpoint "
+ f"{endpoint_id!r} uses {endpoint.transport!r}"
+ )
+
+
+def _validate_actuator_paths(
+ pipeline: PipelineConfig, specs: Mapping[str, PluginSpec]
+) -> None:
+ """Require a trusted safety gate immediately before every actuator.
+
+ Requiring only *some* earlier gate lets an arbitrary transform mutate the
+ approved command after validation. The immediate-boundary rule also makes
+ the trust boundary visible in the graph.
+ """
+
+ predecessors: dict[str, set[str]] = {node_id: set() for node_id in pipeline.nodes}
+ for edge in pipeline.edges:
+ predecessors[edge.target_node].add(edge.source_node)
+
+ unsafe = sorted(
+ node_id
+ for node_id, spec in specs.items()
+ if "actuator" in spec.tags
+ and (
+ not predecessors[node_id]
+ or any(
+ "safety_gate" not in specs[parent].tags
+ for parent in predecessors[node_id]
+ )
+ )
+ )
+ if unsafe:
+ raise PipelineCompileError(
+ "every actuator input must pass through a safety gate directly; "
+ f"unsafe actuator node(s): {', '.join(unsafe)}"
+ )
+
+
+def _validate_supported_runtime_config(config: AppConfig) -> None:
+ reserved = {
+ "max_processes",
+ "process_start_method",
+ "health_bind",
+ "reserved_memory_mb",
+ } & config.runtime.model_fields_set
+ if reserved:
+ raise PipelineCompileError(
+ "runtime field(s) are reserved but not enforced in v1; remove them: "
+ + ", ".join(sorted(reserved))
+ )
+
+
+def _validate_supported_pipeline_config(
+ pipeline_id: str, pipeline: PipelineConfig
+) -> None:
+ if pipeline.priority != "normal":
+ raise PipelineCompileError(
+ f"pipeline {pipeline_id!r} sets priority={pipeline.priority!r}, but "
+ "runtime v1 has no priority scheduler"
+ )
+
+
+def _validate_supported_node_config(node_id: str, node: Any) -> None:
+ execution = node.execution
+ unsupported_execution: list[str] = []
+ if execution.mode not in {"async", "inline"}:
+ unsupported_execution.append(f"mode={execution.mode!r}")
+ if execution.concurrency != 1:
+ unsupported_execution.append(f"concurrency={execution.concurrency}")
+ if execution.max_in_flight is not None:
+ unsupported_execution.append(f"max_in_flight={execution.max_in_flight}")
+ if execution.timeout_s is not None:
+ unsupported_execution.append(f"timeout_s={execution.timeout_s}")
+ if not execution.ordered:
+ unsupported_execution.append("ordered=false")
+ if unsupported_execution:
+ raise PipelineCompileError(
+ f"node {node_id!r} requests execution settings not implemented by "
+ "runtime v1: " + ", ".join(unsupported_execution)
+ )
+
+ resources = node.resources
+ declared_resources = [
+ name
+ for name, value in resources.model_dump().items()
+ if (name == "exclusive_gpu" and value is True)
+ or (name != "exclusive_gpu" and value is not None)
+ ]
+ if declared_resources:
+ raise PipelineCompileError(
+ f"node {node_id!r} declares resource limits that runtime v1 cannot "
+ "enforce: " + ", ".join(declared_resources)
+ )
+
+
+def _validate_qos_profile(source: str, target: str, qos: Any) -> None:
+ allowed_overflow = {
+ "realtime_latest": {"drop_oldest", "drop_newest"},
+ "audio_contiguous": {"block", "reject", "error"},
+ # Inter-frame codecs cannot silently lose arbitrary packets before
+ # decode. Dropping is safe only after complete images are produced.
+ "video_contiguous": {"block", "reject", "error"},
+ "request": {"block", "reject", "error"},
+ "telemetry": {"block", "drop_oldest", "drop_newest", "reject", "error"},
+ }
+ try:
+ allowed = allowed_overflow[qos.profile]
+ except KeyError as exc:
+ raise PipelineCompileError(
+ f"edge {source!r} -> {target!r} uses unknown qos profile "
+ f"{qos.profile!r}; supported profiles: "
+ + ", ".join(sorted(allowed_overflow))
+ ) from exc
+ if qos.overflow not in allowed:
+ raise PipelineCompileError(
+ f"edge {source!r} -> {target!r} profile {qos.profile!r} does not "
+ f"allow overflow={qos.overflow!r}; allowed: {', '.join(sorted(allowed))}"
+ )
+
+
+def _validate_detection_rule_paths(
+ pipeline: PipelineConfig,
+ specs: Mapping[str, PluginSpec],
+ components: Mapping[str, Component],
+) -> None:
+ """Reject rules that can never match their directly connected detector.
+
+ The generic detector validates labels against model registration metadata.
+ This edge-level check closes the remaining configuration gap between its
+ selected output labels and a repeat gate's rules without coupling the core
+ configuration schema to one model backend.
+ """
+
+ for edge in pipeline.edges:
+ source_id = edge.source_node
+ target_id = edge.target_node
+ if specs[source_id].plugin_id != "detection.model@1":
+ continue
+ if specs[target_id].plugin_id != "detection.repeat_gate@1":
+ continue
+ selected = frozenset(
+ str(label)
+ for label in getattr(components[source_id], "selected_labels", ())
+ )
+ rules = getattr(components[target_id], "rules", ())
+ if getattr(components[target_id], "alert_image_enabled", False) and not getattr(
+ components[source_id], "attach_frame", False
+ ):
+ raise PipelineCompileError(
+ f"detection rule node {target_id!r} enables alert_image, but "
+ f"detector {source_id!r} does not set attach_frame=true"
+ )
+ unknown_by_rule = {
+ str(rule.rule_id): sorted(set(rule.labels) - selected)
+ for rule in rules
+ if set(rule.labels) - selected
+ }
+ if unknown_by_rule:
+ details = "; ".join(
+ f"{rule_id}: {', '.join(labels)}"
+ for rule_id, labels in sorted(unknown_by_rule.items())
+ )
+ raise PipelineCompileError(
+ f"detection rule node {target_id!r} references labels not emitted "
+ f"by detector {source_id!r}: {details}"
+ )
+
+
+__all__ = [
+ "CompiledPipeline",
+ "PipelineCompileError",
+ "SUPPORTED_API_VERSION",
+ "compile_pipeline",
+ "enabled_pipeline_ids",
+]
diff --git a/src/cmvr_edge_ai/config/__init__.py b/src/cmvr_edge_ai/config/__init__.py
new file mode 100644
index 0000000..1878ef7
--- /dev/null
+++ b/src/cmvr_edge_ai/config/__init__.py
@@ -0,0 +1,45 @@
+"""Public configuration API."""
+
+from .loader import (
+ ConfigLoadError,
+ MissingEnvironmentVariable,
+ expand_environment,
+ load_config,
+ load_config_data,
+ load_config_text,
+)
+from .models import (
+ AppConfig,
+ EdgeAIConfig,
+ EdgeConfig,
+ EndpointConfig,
+ ExecutionConfig,
+ NodeConfig,
+ PipelineConfig,
+ PipelinePriority,
+ QoSConfig,
+ ResourceConfig,
+ RuntimeConfig,
+ StrictConfigModel,
+)
+
+__all__ = [
+ "AppConfig",
+ "ConfigLoadError",
+ "EdgeAIConfig",
+ "EdgeConfig",
+ "EndpointConfig",
+ "ExecutionConfig",
+ "MissingEnvironmentVariable",
+ "NodeConfig",
+ "PipelineConfig",
+ "PipelinePriority",
+ "QoSConfig",
+ "ResourceConfig",
+ "RuntimeConfig",
+ "StrictConfigModel",
+ "expand_environment",
+ "load_config",
+ "load_config_data",
+ "load_config_text",
+]
diff --git a/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..0e42883
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..dc90f95
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/config/__pycache__/loader.cpython-310.pyc b/src/cmvr_edge_ai/config/__pycache__/loader.cpython-310.pyc
new file mode 100644
index 0000000..e49df8a
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/loader.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/config/__pycache__/loader.cpython-312.pyc b/src/cmvr_edge_ai/config/__pycache__/loader.cpython-312.pyc
new file mode 100644
index 0000000..d1437f6
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/loader.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/config/__pycache__/models.cpython-310.pyc b/src/cmvr_edge_ai/config/__pycache__/models.cpython-310.pyc
new file mode 100644
index 0000000..73cd59d
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/models.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/config/__pycache__/models.cpython-312.pyc b/src/cmvr_edge_ai/config/__pycache__/models.cpython-312.pyc
new file mode 100644
index 0000000..df658b1
Binary files /dev/null and b/src/cmvr_edge_ai/config/__pycache__/models.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/config/loader.py b/src/cmvr_edge_ai/config/loader.py
new file mode 100644
index 0000000..351df51
--- /dev/null
+++ b/src/cmvr_edge_ai/config/loader.py
@@ -0,0 +1,145 @@
+"""YAML loading and environment expansion for edge-AI configuration."""
+
+from __future__ import annotations
+
+import os
+import re
+from collections.abc import Mapping
+from pathlib import Path
+from typing import Any
+
+import yaml # type: ignore[import-untyped]
+
+from .models import AppConfig
+
+
+_ENV_EXACT_PATTERN = re.compile(r"^env://(?P[A-Za-z_][A-Za-z0-9_]*)$")
+_ENV_TEMPLATE_PATTERN = re.compile(
+ r"\$\{(?P[A-Za-z_][A-Za-z0-9_]*)" r"(?::-(?P[^}]*))?\}"
+)
+
+
+class ConfigLoadError(ValueError):
+ """Raised when a configuration document cannot be read or expanded."""
+
+
+class MissingEnvironmentVariable(ConfigLoadError):
+ """Raised when a required environment variable is not available."""
+
+
+def expand_environment(
+ value: Any,
+ environ: Mapping[str, str] | None = None,
+) -> Any:
+ """Recursively expand environment references in a YAML-compatible value.
+
+ Supported string forms are:
+
+ * ``${NAME}`` -- ``NAME`` is required;
+ * ``${NAME:-default}`` -- use ``default`` when ``NAME`` is unset or empty;
+ * ``env://NAME`` -- resolve the entire value from required ``NAME``.
+
+ Expansion traverses mappings, lists, and tuples without mutating the input.
+ Environment substitution deliberately returns strings; Pydantic performs
+ the final conversion according to the target configuration field.
+ """
+
+ environment = os.environ if environ is None else environ
+
+ if isinstance(value, str):
+ return _expand_string(value, environment)
+ if isinstance(value, Mapping):
+ return {
+ key: expand_environment(item, environment) for key, item in value.items()
+ }
+ if isinstance(value, list):
+ return [expand_environment(item, environment) for item in value]
+ if isinstance(value, tuple):
+ return tuple(expand_environment(item, environment) for item in value)
+ return value
+
+
+def load_config_data(
+ data: Mapping[str, Any],
+ *,
+ environ: Mapping[str, str] | None = None,
+) -> AppConfig:
+ """Expand and validate an already parsed configuration mapping."""
+
+ if not isinstance(data, Mapping):
+ raise ConfigLoadError("configuration root must be a YAML mapping")
+ expanded = expand_environment(data, environ)
+ return AppConfig.model_validate(expanded)
+
+
+def load_config_text(
+ text: str,
+ *,
+ environ: Mapping[str, str] | None = None,
+) -> AppConfig:
+ """Parse, expand, and validate a YAML configuration string."""
+
+ try:
+ data = yaml.safe_load(text)
+ except yaml.YAMLError as exc:
+ raise ConfigLoadError(f"invalid YAML configuration: {exc}") from exc
+
+ if not isinstance(data, Mapping):
+ raise ConfigLoadError("configuration root must be a YAML mapping")
+ return load_config_data(data, environ=environ)
+
+
+def load_config(
+ path: str | os.PathLike[str],
+ *,
+ environ: Mapping[str, str] | None = None,
+) -> AppConfig:
+ """Load, expand, and validate an edge-AI YAML configuration file."""
+
+ config_path = Path(path)
+ try:
+ text = config_path.read_text(encoding="utf-8")
+ except OSError as exc:
+ raise ConfigLoadError(
+ f"unable to read configuration file {config_path}: {exc}"
+ ) from exc
+
+ try:
+ return load_config_text(text, environ=environ)
+ except ConfigLoadError as exc:
+ raise ConfigLoadError(f"failed to load {config_path}: {exc}") from exc
+
+
+def _expand_string(value: str, environ: Mapping[str, str]) -> str:
+ exact_match = _ENV_EXACT_PATTERN.fullmatch(value)
+ if exact_match:
+ name = exact_match.group("name")
+ if name not in environ:
+ raise MissingEnvironmentVariable(
+ f"required environment variable {name!r} is not set"
+ )
+ return environ[name]
+
+ def replace(match: re.Match[str]) -> str:
+ name = match.group("name")
+ default = match.group("default")
+ current = environ.get(name)
+ if current is not None and (current != "" or default is None):
+ return current
+ if default is not None:
+ return default
+ raise MissingEnvironmentVariable(
+ f"required environment variable {name!r} is not set"
+ )
+
+ return _ENV_TEMPLATE_PATTERN.sub(replace, value)
+
+
+__all__ = [
+ "ConfigLoadError",
+ "MissingEnvironmentVariable",
+ "expand_environment",
+ "load_config",
+ "load_config_data",
+ "load_config_text",
+]
diff --git a/src/cmvr_edge_ai/config/models.py b/src/cmvr_edge_ai/config/models.py
new file mode 100644
index 0000000..77ac8e1
--- /dev/null
+++ b/src/cmvr_edge_ai/config/models.py
@@ -0,0 +1,363 @@
+"""Strict configuration models for the edge-AI pipeline runtime.
+
+The models in this module intentionally do not import the component registry or
+the runtime. This keeps configuration validation usable by command-line tools,
+tests, and deployment checks without initializing any transports or models.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Any, Literal
+
+from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
+
+
+_IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
+_PORT_REFERENCE_PATTERN = re.compile(
+ r"^(?P[A-Za-z_][A-Za-z0-9_-]*)\." r"(?P[A-Za-z_][A-Za-z0-9_-]*)$"
+)
+
+
+class StrictConfigModel(BaseModel):
+ """Base class shared by all public configuration models."""
+
+ model_config = ConfigDict(
+ extra="forbid",
+ populate_by_name=True,
+ validate_default=True,
+ )
+
+
+class RuntimeConfig(StrictConfigModel):
+ """Process-wide limits and lifecycle settings for the local runtime."""
+
+ thread_workers: int = Field(default=4, ge=1, le=256)
+ max_processes: int = Field(default=1, ge=0, le=64)
+ process_start_method: Literal["spawn", "forkserver", "fork"] = "spawn"
+ shutdown_timeout_s: float = Field(default=10.0, gt=0, le=300.0)
+ health_bind: str | None = None
+ reserved_memory_mb: int = Field(default=0, ge=0)
+
+ @field_validator("health_bind")
+ @classmethod
+ def validate_health_bind(cls, value: str | None) -> str | None:
+ if value is not None and not value.strip():
+ raise ValueError("health_bind must not be empty")
+ return value
+
+
+class EndpointConfig(StrictConfigModel):
+ """A named external endpoint used by one or more connector nodes.
+
+ Transport-specific settings belong under ``options`` so that adding QUIC or
+ a vendor-specific protocol does not require weakening top-level validation.
+ """
+
+ transport: str = Field(min_length=1)
+ target: str | None = None
+ base_url: str | None = None
+ bind: str | None = None
+ tls: bool = False
+ timeout_s: float = Field(default=5.0, gt=0, le=3600.0)
+ metadata: dict[str, str] = Field(default_factory=dict)
+ options: dict[str, Any] = Field(default_factory=dict)
+
+ @field_validator("transport")
+ @classmethod
+ def normalize_transport(cls, value: str) -> str:
+ value = value.strip().lower()
+ if not value:
+ raise ValueError("transport must not be empty")
+ return value
+
+ @field_validator("target", "base_url", "bind")
+ @classmethod
+ def reject_empty_endpoint_values(cls, value: str | None) -> str | None:
+ if value is not None and not value.strip():
+ raise ValueError("endpoint address must not be empty")
+ return value
+
+ @model_validator(mode="after")
+ def validate_transport_address(self) -> EndpointConfig:
+ """Validate the transports implemented by the built-in connectors.
+
+ Unknown transport names remain available to third-party plugins. Once
+ a transport is one of the built-ins, however, accepting an incomplete
+ endpoint here would only defer a deterministic error until startup.
+ """
+
+ if self.transport == "grpc":
+ if self.target is None:
+ raise ValueError("gRPC endpoint requires target")
+ if self.base_url is not None:
+ raise ValueError("gRPC endpoint must not set base_url")
+ channel_options = self.options.get("channel_options", {})
+ if not isinstance(channel_options, dict):
+ raise ValueError("gRPC options.channel_options must be a mapping")
+ invalid_options = sorted(
+ str(key)
+ for key, value in channel_options.items()
+ if not isinstance(key, str)
+ or isinstance(value, bool)
+ or not isinstance(value, (int, str))
+ )
+ if invalid_options:
+ raise ValueError(
+ "gRPC channel option values must be int or str: "
+ + ", ".join(invalid_options)
+ )
+ elif self.transport == "http":
+ if self.base_url is None:
+ raise ValueError("HTTP endpoint requires base_url")
+ if not self.base_url.startswith(("http://", "https://")):
+ raise ValueError("HTTP base_url must start with http:// or https://")
+ if self.target is not None:
+ raise ValueError("HTTP endpoint must not set target")
+ if "tls" in self.model_fields_set:
+ raise ValueError(
+ "HTTP endpoint must not set tls; use an https:// base_url and "
+ "options.verify_tls (default true)"
+ )
+ verify_tls = self.options.get("verify_tls", True)
+ if not isinstance(verify_tls, bool):
+ raise ValueError("HTTP options.verify_tls must be boolean")
+ return self
+
+
+class ExecutionConfig(StrictConfigModel):
+ """How a node is scheduled by the runtime."""
+
+ mode: Literal["async", "inline", "thread", "process", "model_worker"] = "async"
+ concurrency: int = Field(default=1, ge=1, le=1024)
+ max_in_flight: int | None = Field(default=None, ge=1, le=65536)
+ timeout_s: float | None = Field(default=None, gt=0, le=86400.0)
+ ordered: bool = True
+
+
+class ResourceConfig(StrictConfigModel):
+ """Optional resource budget declared by a node."""
+
+ cpu_cores: float | None = Field(default=None, gt=0)
+ memory_mb: int | None = Field(default=None, gt=0)
+ gpu_device: int | str | None = None
+ gpu_memory_mb: int | None = Field(default=None, gt=0)
+ exclusive_gpu: bool = False
+
+ @field_validator("gpu_device")
+ @classmethod
+ def validate_gpu_device(cls, value: int | str | None) -> int | str | None:
+ if isinstance(value, int) and value < 0:
+ raise ValueError("gpu_device must be non-negative")
+ if isinstance(value, str) and not value.strip():
+ raise ValueError("gpu_device must not be empty")
+ return value
+
+
+class QoSConfig(StrictConfigModel):
+ """Bounded-queue policy for a single directed edge."""
+
+ # Lossless backpressure is the safe generic default. Real-time sensor
+ # pipelines must opt into frame dropping explicitly per edge.
+ profile: str = Field(default="request", min_length=1)
+ capacity: int = Field(default=1, ge=1, le=1_000_000)
+ overflow: Literal["drop_oldest", "drop_newest", "block", "reject", "error"] = (
+ "block"
+ )
+ max_age_ms: float | None = Field(default=None, gt=0)
+ put_timeout_ms: float | None = Field(default=None, ge=0)
+
+ @field_validator("profile")
+ @classmethod
+ def validate_profile(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("qos profile must not be empty")
+ return value
+
+
+class NodeConfig(StrictConfigModel):
+ """A configured component instance in a pipeline graph."""
+
+ uses: str = Field(min_length=1)
+ params: dict[str, Any] = Field(default_factory=dict, alias="with")
+ execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
+ resources: ResourceConfig = Field(default_factory=ResourceConfig)
+ enabled: bool = True
+
+ @field_validator("uses")
+ @classmethod
+ def validate_component_id(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("uses must not be empty")
+ return value
+
+
+class EdgeConfig(StrictConfigModel):
+ """A connection from one ``node.port`` to another ``node.port``."""
+
+ source: str = Field(alias="from")
+ target: str = Field(alias="to")
+ qos: QoSConfig = Field(default_factory=QoSConfig)
+
+ @field_validator("source", "target")
+ @classmethod
+ def validate_port_reference(cls, value: str) -> str:
+ if not _PORT_REFERENCE_PATTERN.fullmatch(value):
+ raise ValueError(
+ "port reference must use 'node.port' format; node and port may "
+ "contain letters, digits, '_' or '-'"
+ )
+ return value
+
+ @property
+ def source_node(self) -> str:
+ return self.source.split(".", maxsplit=1)[0]
+
+ @property
+ def target_node(self) -> str:
+ return self.target.split(".", maxsplit=1)[0]
+
+
+PipelinePriority = Literal["low", "normal", "high"] | int
+
+
+class PipelineConfig(StrictConfigModel):
+ """A validated directed acyclic graph of configured nodes."""
+
+ enabled: bool = True
+ priority: PipelinePriority = "normal"
+ nodes: dict[str, NodeConfig]
+ edges: list[EdgeConfig] = Field(default_factory=list)
+
+ @field_validator("priority")
+ @classmethod
+ def validate_priority(cls, value: PipelinePriority) -> PipelinePriority:
+ if isinstance(value, int) and not 0 <= value <= 100:
+ raise ValueError("numeric priority must be between 0 and 100")
+ return value
+
+ @field_validator("nodes")
+ @classmethod
+ def validate_node_names(cls, value: dict[str, NodeConfig]) -> dict[str, NodeConfig]:
+ if not value:
+ raise ValueError("pipeline must contain at least one node")
+ invalid = sorted(
+ name for name in value if not _IDENTIFIER_PATTERN.fullmatch(name)
+ )
+ if invalid:
+ raise ValueError(
+ "invalid node name(s): "
+ + ", ".join(invalid)
+ + "; names may contain letters, digits, '_' or '-' and may not "
+ "start with a digit"
+ )
+ return value
+
+ @model_validator(mode="after")
+ def validate_graph(self) -> PipelineConfig:
+ node_names = set(self.nodes)
+ adjacency: dict[str, set[str]] = {name: set() for name in node_names}
+ indegree = {name: 0 for name in node_names}
+
+ for edge in self.edges:
+ missing = {
+ name
+ for name in (edge.source_node, edge.target_node)
+ if name not in node_names
+ }
+ if missing:
+ raise ValueError(
+ f"edge {edge.source!r} -> {edge.target!r} references unknown "
+ f"node(s): {', '.join(sorted(missing))}"
+ )
+
+ # Multiple ports may connect the same pair of nodes. They represent
+ # one dependency for cycle detection.
+ if edge.target_node not in adjacency[edge.source_node]:
+ adjacency[edge.source_node].add(edge.target_node)
+ indegree[edge.target_node] += 1
+
+ ready = [name for name, degree in indegree.items() if degree == 0]
+ visited = 0
+ while ready:
+ current = ready.pop()
+ visited += 1
+ for target in adjacency[current]:
+ indegree[target] -= 1
+ if indegree[target] == 0:
+ ready.append(target)
+
+ if visited != len(node_names):
+ cycle_nodes = sorted(
+ name for name, degree in indegree.items() if degree > 0
+ )
+ raise ValueError(
+ "pipeline graph must be acyclic; cycle involves: "
+ + ", ".join(cycle_nodes)
+ )
+ return self
+
+
+class AppConfig(StrictConfigModel):
+ """Root configuration document for a cmvr-edge-ai process."""
+
+ api_version: str = Field(min_length=1)
+ runtime: RuntimeConfig = Field(default_factory=RuntimeConfig)
+ endpoints: dict[str, EndpointConfig] = Field(default_factory=dict)
+ pipelines: dict[str, PipelineConfig]
+
+ @field_validator("api_version")
+ @classmethod
+ def validate_api_version(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("api_version must not be empty")
+ return value
+
+ @field_validator("endpoints")
+ @classmethod
+ def validate_endpoint_names(
+ cls, value: dict[str, EndpointConfig]
+ ) -> dict[str, EndpointConfig]:
+ return _validate_named_mapping("endpoint", value)
+
+ @field_validator("pipelines")
+ @classmethod
+ def validate_pipeline_names(
+ cls, value: dict[str, PipelineConfig]
+ ) -> dict[str, PipelineConfig]:
+ if not value:
+ raise ValueError("configuration must contain at least one pipeline")
+ return _validate_named_mapping("pipeline", value)
+
+
+def _validate_named_mapping(name: str, value: dict[str, Any]) -> dict[str, Any]:
+ invalid = sorted(key for key in value if not _IDENTIFIER_PATTERN.fullmatch(key))
+ if invalid:
+ raise ValueError(
+ f"invalid {name} name(s): {', '.join(invalid)}; names may contain "
+ "letters, digits, '_' or '-' and may not start with a digit"
+ )
+ return value
+
+
+# A descriptive alias for callers that prefer the project's full name.
+EdgeAIConfig = AppConfig
+
+
+__all__ = [
+ "AppConfig",
+ "EdgeAIConfig",
+ "EdgeConfig",
+ "EndpointConfig",
+ "ExecutionConfig",
+ "NodeConfig",
+ "PipelineConfig",
+ "PipelinePriority",
+ "QoSConfig",
+ "ResourceConfig",
+ "RuntimeConfig",
+ "StrictConfigModel",
+]
diff --git a/src/cmvr_edge_ai/connectors/__init__.py b/src/cmvr_edge_ai/connectors/__init__.py
new file mode 100644
index 0000000..495b5ff
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/__init__.py
@@ -0,0 +1,42 @@
+"""Protocol-specific boundary connectors and their plugin registrations."""
+
+from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec
+
+from .cmvr_es import CmvrAgvCommandSink, CmvrCameraRgbStreamSource
+from .platform import PlatformHttpJsonSink
+
+
+def register_connector_plugins(registry: PluginRegistry) -> None:
+ registry.register(
+ PluginSpec(
+ plugin_id="cmvr.grpc.camera_rgb_stream@1",
+ kind=PluginKind.SOURCE,
+ factory=CmvrCameraRgbStreamSource,
+ outputs={"frames": "ImageFrame/v1"},
+ description="Start and subscribe to a cmvr-es encoded RGB camera stream",
+ tags=frozenset({"sensor", "transport:grpc"}),
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="cmvr.grpc.agv_command_sink@1",
+ kind=PluginKind.SINK,
+ factory=CmvrAgvCommandSink,
+ inputs={"commands": "ApprovedRobotCommand/v1"},
+ description="Typed, non-retrying cmvr-es AGV command sink",
+ tags=frozenset({"actuator", "transport:grpc"}),
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="platform.http_json_sink@1",
+ kind=PluginKind.SINK,
+ factory=PlatformHttpJsonSink,
+ inputs={"input": "*"},
+ description="POST protocol-neutral envelopes as JSON",
+ tags=frozenset({"telemetry", "transport:http"}),
+ )
+ )
+
+
+__all__ = ["register_connector_plugins"]
diff --git a/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..7aee741
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..26e5629
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-310.pyc b/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-310.pyc
new file mode 100644
index 0000000..20b6e42
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-312.pyc b/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-312.pyc
new file mode 100644
index 0000000..f533d6d
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/__pycache__/_context.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/_context.py b/src/cmvr_edge_ai/connectors/_context.py
new file mode 100644
index 0000000..81c2c40
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/_context.py
@@ -0,0 +1,94 @@
+"""Helpers for resolving named endpoints from component context."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from pathlib import Path
+from typing import Any
+
+from cmvr_edge_ai.core.component import ComponentContext
+from cmvr_edge_ai.transports.grpc import GrpcChannelSettings
+from cmvr_edge_ai.transports.http import HttpClientSettings
+
+
+class ConnectorConfigurationError(ValueError):
+ pass
+
+
+def resolve_endpoint(context: ComponentContext, endpoint_id: str) -> Any:
+ endpoints = context.metadata.get("endpoints")
+ if not isinstance(endpoints, Mapping):
+ raise ConnectorConfigurationError(
+ "component context does not contain a named endpoints mapping"
+ )
+ try:
+ return endpoints[endpoint_id]
+ except KeyError as exc:
+ raise ConnectorConfigurationError(
+ f"unknown endpoint referenced by connector: {endpoint_id}"
+ ) from exc
+
+
+def endpoint_value(endpoint: Any, name: str, default: Any = None) -> Any:
+ if isinstance(endpoint, Mapping):
+ return endpoint.get(name, default)
+ return getattr(endpoint, name, default)
+
+
+def grpc_channel_settings(endpoint: Any) -> GrpcChannelSettings:
+ target = endpoint_value(endpoint, "target")
+ if not target:
+ raise ConnectorConfigurationError("gRPC endpoint requires target")
+ options = endpoint_value(endpoint, "options", {}) or {}
+ if not isinstance(options, Mapping):
+ raise ConnectorConfigurationError("gRPC endpoint options must be a mapping")
+
+ channel_options: list[tuple[str, int | str]] = []
+ configured_options = options.get("channel_options", {})
+ if isinstance(configured_options, Mapping):
+ channel_options.extend(
+ (str(key), value) for key, value in configured_options.items()
+ )
+ max_receive_mb = options.get("max_receive_mb")
+ if max_receive_mb is not None:
+ channel_options.append(
+ (
+ "grpc.max_receive_message_length",
+ int(float(max_receive_mb) * 1024 * 1024),
+ )
+ )
+ max_send_mb = options.get("max_send_mb")
+ if max_send_mb is not None:
+ channel_options.append(
+ ("grpc.max_send_message_length", int(float(max_send_mb) * 1024 * 1024))
+ )
+
+ root_certificates = options.get("root_certificates")
+ return GrpcChannelSettings(
+ target=str(target),
+ tls=bool(endpoint_value(endpoint, "tls", False)),
+ root_certificates=(Path(root_certificates) if root_certificates else None),
+ options=tuple(channel_options),
+ )
+
+
+def http_client_settings(endpoint: Any) -> HttpClientSettings:
+ base_url = endpoint_value(endpoint, "base_url")
+ if not base_url:
+ raise ConnectorConfigurationError("HTTP endpoint requires base_url")
+ metadata = endpoint_value(endpoint, "metadata", {}) or {}
+ options = endpoint_value(endpoint, "options", {}) or {}
+ if not isinstance(options, Mapping):
+ raise ConnectorConfigurationError("HTTP endpoint options must be a mapping")
+ verify_tls = options.get("verify_tls", True)
+ if not isinstance(verify_tls, bool):
+ raise ConnectorConfigurationError("HTTP options.verify_tls must be boolean")
+ return HttpClientSettings(
+ base_url=str(base_url),
+ timeout_s=float(endpoint_value(endpoint, "timeout_s", 5.0)),
+ headers=tuple((str(key), str(value)) for key, value in metadata.items()),
+ # HTTPS verification is secure by default. The generic ``tls`` flag is
+ # used by gRPC channel construction and must never silently disable
+ # certificate verification for an https:// URL.
+ verify_tls=verify_tls,
+ )
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__init__.py b/src/cmvr_edge_ai/connectors/cmvr_es/__init__.py
new file mode 100644
index 0000000..1114b1f
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/cmvr_es/__init__.py
@@ -0,0 +1,13 @@
+"""Semantic adapters for cmvr-es services."""
+
+from .agv import CmvrAgvCommandSink
+from .camera import CmvrCameraRgbStreamSource
+from .errors import CmvrApiPackageError, CmvrConnectorError, CmvrServiceError
+
+__all__ = [
+ "CmvrApiPackageError",
+ "CmvrAgvCommandSink",
+ "CmvrCameraRgbStreamSource",
+ "CmvrConnectorError",
+ "CmvrServiceError",
+]
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..2615670
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..37efdbf
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-310.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-310.pyc
new file mode 100644
index 0000000..1d96842
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-312.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-312.pyc
new file mode 100644
index 0000000..7c4ce01
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/agv.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-310.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-310.pyc
new file mode 100644
index 0000000..5fee445
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-312.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-312.pyc
new file mode 100644
index 0000000..6048494
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/camera.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-310.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-310.pyc
new file mode 100644
index 0000000..19a576d
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-312.pyc b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-312.pyc
new file mode 100644
index 0000000..c1832ae
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/cmvr_es/__pycache__/errors.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/agv.py b/src/cmvr_edge_ai/connectors/cmvr_es/agv.py
new file mode 100644
index 0000000..c2f82ba
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/cmvr_es/agv.py
@@ -0,0 +1,230 @@
+"""Typed, non-retrying AGV command sink for cmvr-es."""
+
+from __future__ import annotations
+
+import logging
+import math
+from collections.abc import Mapping
+from importlib import import_module
+from time import time_ns
+from typing import Any
+
+from cmvr_edge_ai.contracts import ApprovedRobotCommand
+from cmvr_edge_ai.core.component import ComponentContext, Sink
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.transports.grpc import GrpcChannelPool
+
+from .._context import endpoint_value, grpc_channel_settings, resolve_endpoint
+from .errors import CmvrApiPackageError, CmvrServiceError
+
+
+class CmvrAgvCommandSink(Sink):
+ """Map an allowlisted ``RobotCommand`` to explicit AgvService methods.
+
+ The sink deliberately has no generic method-name dispatch and performs no
+ automatic retry. Motion-command retry requires an idempotency contract
+ that the current cmvr-es API does not provide.
+ """
+
+ _DIRECT_ACTIONS = {
+ "emergency_stop": "emergencyStop",
+ "clear_fault": "clearFault",
+ "pause_navigation": "pauseNavigation",
+ "resume_navigation": "resumeNavigation",
+ "cancel_navigation": "cancelNavigation",
+ "stop_velocity_control": "stopVelocityControl",
+ "stop_mapping": "stopMapping",
+ }
+
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ self._node_id = node_id
+ self._endpoint_id = str(params.get("endpoint", "cmvr_es"))
+ configured_device = params.get("device_id")
+ if not isinstance(configured_device, str) or not configured_device.strip():
+ raise ValueError("AGV command sink requires a fixed, non-empty device_id")
+ self._device_id = configured_device.strip()
+ self._timeout_s: float | None = None
+ self._unsafe_allow_unleased_velocity = _strict_bool(
+ params,
+ "unsafe_allow_unleased_velocity",
+ default=False,
+ )
+ self._velocity_limits = _parse_velocity_limits(params.get("velocity_limits"))
+ if self._unsafe_allow_unleased_velocity and self._velocity_limits is None:
+ raise ValueError(
+ "unsafe_allow_unleased_velocity requires explicit velocity_limits"
+ )
+ self._velocity_active = False
+ self._logger = logging.getLogger(__name__)
+ self._stub: Any = None
+ self._common_pb2: Any = None
+ self._agv_command_pb2: Any = None
+ self._agv_pb2: Any = None
+
+ async def setup(self, context: ComponentContext) -> None:
+ endpoint = resolve_endpoint(context, self._endpoint_id)
+ pool = context.metadata.get("grpc_channel_pool")
+ if not isinstance(pool, GrpcChannelPool):
+ raise TypeError("component context is missing GrpcChannelPool")
+ channel = await pool.get(self._endpoint_id, grpc_channel_settings(endpoint))
+ self._timeout_s = float(endpoint_value(endpoint, "timeout_s", 5.0))
+ try:
+ self._common_pb2 = import_module("cmvr.api.common_pb2")
+ self._agv_command_pb2 = import_module("cmvr.api.agv_command_pb2")
+ self._agv_pb2 = import_module("cmvr.msgs.agv_pb2")
+ service = import_module("cmvr.api.agv_service_pb2_grpc")
+ except ImportError as exc:
+ raise CmvrApiPackageError(
+ "cmvr Python API bindings are missing; run "
+ "scripts/generate_cmvr_stubs.py or install the cmvr-api wheel"
+ ) from exc
+ self._stub = service.AgvServiceStub(channel)
+
+ async def consume(
+ self, envelope: Envelope[Any], input_port: str = "commands"
+ ) -> None:
+ del input_port
+ approved = envelope.payload
+ if not isinstance(approved, ApprovedRobotCommand):
+ raise TypeError(
+ f"{self._node_id} expected ApprovedRobotCommand, "
+ f"got {type(approved).__name__}"
+ )
+ command = approved.command
+ if envelope.is_expired() or command.valid_until_ns <= time_ns():
+ return
+ if command.device_id != self._device_id:
+ raise PermissionError(
+ f"command targets {command.device_id!r}, sink is bound to "
+ f"{self._device_id!r}"
+ )
+ if self._stub is None:
+ raise RuntimeError("AGV sink has not been set up")
+
+ remaining_s = (command.valid_until_ns - time_ns()) / 1_000_000_000
+ if remaining_s <= 0:
+ return
+ rpc_timeout = min(self._timeout_s or remaining_s, remaining_s)
+
+ if command.action in self._DIRECT_ACTIONS:
+ request = self._command_header()
+ rpc = getattr(self._stub, self._DIRECT_ACTIONS[command.action])
+ elif command.action == "set_velocity":
+ if not self._unsafe_allow_unleased_velocity:
+ raise PermissionError(
+ "set_velocity is disabled until cmvr-es enforces a server-side "
+ "lease; unsafe override must be explicit"
+ )
+ assert self._velocity_limits is not None
+ vx = _bounded_number(command.arguments, "vx", self._velocity_limits)
+ vy = _bounded_number(command.arguments, "vy", self._velocity_limits)
+ wz = _bounded_number(command.arguments, "wz", self._velocity_limits)
+ request = self._agv_command_pb2.AgvSetVelocityCommand.Request(
+ header=self._command_header(),
+ velocity=self._agv_pb2.AgvVelocity(vx=vx, vy=vy, wz=wz),
+ )
+ rpc = self._stub.setVelocity
+ else:
+ raise PermissionError(f"unsupported AGV action: {command.action}")
+
+ response = await rpc(request, timeout=rpc_timeout)
+ feedback = getattr(response, "header", response)
+ if not feedback.success:
+ raise CmvrServiceError(
+ feedback.error_message
+ or f"cmvr-es rejected AGV action {command.action!r}"
+ )
+ if command.action == "set_velocity":
+ self._velocity_active = True
+ elif command.action in {"stop_velocity_control", "emergency_stop"}:
+ self._velocity_active = False
+
+ async def stop(self) -> None:
+ if not self._velocity_active or self._stub is None:
+ return
+ try:
+ request = self._command_header()
+ response = await self._stub.stopVelocityControl(
+ request, timeout=self._timeout_s
+ )
+ feedback = getattr(response, "header", response)
+ if not feedback.success:
+ self._logger.error(
+ "cmvr-es rejected stopVelocityControl during sink shutdown: %s",
+ feedback.error_message,
+ )
+ except Exception:
+ self._logger.exception(
+ "failed to stop AGV velocity during sink shutdown; "
+ "server-side lease is required for crash safety"
+ )
+ finally:
+ self._velocity_active = False
+
+ def _command_header(self) -> Any:
+ header = self._common_pb2.CommandHeader.Request(device_id=self._device_id)
+ # The current cmvr-es API has no valid-until/lease field. Sending the
+ # request time is useful for diagnostics but is not a server-side lease.
+ header.timestamp.GetCurrentTime()
+ return header
+
+
+def _bounded_number(
+ arguments: Mapping[str, Any],
+ name: str,
+ limits: Mapping[str, tuple[float, float]],
+) -> float:
+ if name not in arguments:
+ raise ValueError(f"set_velocity requires argument {name!r}")
+ value = float(arguments[name])
+ if not math.isfinite(value):
+ raise ValueError(f"set_velocity argument {name!r} must be finite")
+ minimum, maximum = limits[name]
+ if not minimum <= value <= maximum:
+ raise PermissionError(
+ f"set_velocity argument {name!r}={value!r} is outside "
+ f"[{minimum}, {maximum}]"
+ )
+ return value
+
+
+def _parse_velocity_limits(
+ value: Any,
+) -> dict[str, tuple[float, float]] | None:
+ if value is None:
+ return None
+ if not isinstance(value, Mapping):
+ raise ValueError("velocity_limits must be a mapping")
+ missing = {"vx", "vy", "wz"} - set(value)
+ if missing:
+ raise ValueError(
+ "velocity_limits must define vx, vy and wz; missing: "
+ + ", ".join(sorted(missing))
+ )
+ parsed: dict[str, tuple[float, float]] = {}
+ for name in ("vx", "vy", "wz"):
+ bounds = value[name]
+ if (
+ not isinstance(bounds, Mapping)
+ or "min" not in bounds
+ or "max" not in bounds
+ ):
+ raise ValueError(f"velocity_limits.{name} requires min and max")
+ minimum = float(bounds["min"])
+ maximum = float(bounds["max"])
+ if not math.isfinite(minimum) or not math.isfinite(maximum):
+ raise ValueError("velocity limits must be finite")
+ if minimum > maximum:
+ raise ValueError(f"velocity_limits.{name} min exceeds max")
+ parsed[name] = (minimum, maximum)
+ return parsed
+
+
+def _strict_bool(params: Mapping[str, Any], name: str, *, default: bool) -> bool:
+ value = params.get(name, default)
+ if type(value) is not bool:
+ raise ValueError(f"{name} must be a boolean, not {value!r}")
+ return value
+
+
+__all__ = ["CmvrAgvCommandSink"]
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/camera.py b/src/cmvr_edge_ai/connectors/cmvr_es/camera.py
new file mode 100644
index 0000000..7c5c084
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/cmvr_es/camera.py
@@ -0,0 +1,591 @@
+"""Typed adapter for the current cmvr-es RGB camera stream."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+import math
+from collections.abc import Mapping
+from dataclasses import dataclass
+from importlib import import_module
+from time import monotonic_ns, perf_counter
+from typing import Any
+from uuid import uuid4
+
+from cmvr_edge_ai.contracts import CameraIntrinsics, ImageFrame
+from cmvr_edge_ai.core.component import ComponentContext, Emission, Source
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.transports.grpc import GrpcChannelPool
+
+from .._context import endpoint_value, grpc_channel_settings, resolve_endpoint
+from .errors import CmvrApiPackageError, CmvrServiceError
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass(slots=True)
+class _StreamStats:
+ session_id: str
+ opened_ns: int
+ window_started_ns: int
+ frames: int = 0
+ received_bytes: int = 0
+ key_frames: int = 0
+ window_frames: int = 0
+ window_bytes: int = 0
+ window_key_frames: int = 0
+ last_frame_ns: int | None = None
+ last_remote_sequence: int | None = None
+ codec: str = "unknown"
+ width: int = 0
+ height: int = 0
+
+
+class CmvrCameraRgbStreamSource(Source):
+ """Start a cmvr-es camera and subscribe to ``GetRGBImageStream``.
+
+ cmvr-es currently reads one subscription request and then streams until the
+ client cancels. The request side remains open until shutdown so this
+ adapter also remains compatible with a future true bidirectional contract.
+ Every initial/reconnect session calls ``StartCamera`` first because the
+ stream RPC starts encoding but does not open the physical camera.
+ """
+
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ allowed_params = {
+ "endpoint",
+ "device_id",
+ "pixel_format",
+ "reconnect",
+ "reconnect_initial_s",
+ "reconnect_max_s",
+ "max_reconnect_attempts",
+ "stream_log_interval_s",
+ }
+ unknown = set(params) - allowed_params
+ if unknown:
+ raise ValueError(
+ "unknown camera source parameter(s): "
+ + ", ".join(sorted(str(value) for value in unknown))
+ )
+ self._node_id = node_id
+ self._endpoint_id = str(params.get("endpoint", "cmvr_es"))
+ device_id = params.get("device_id")
+ if not isinstance(device_id, str) or not device_id.strip():
+ raise ValueError("camera source requires a non-empty device_id")
+ pixel_format = params.get("pixel_format", "BGR8")
+ if not isinstance(pixel_format, str) or not pixel_format.strip():
+ raise ValueError("pixel_format must be a non-empty string")
+ self._device_id = device_id.strip()
+ self._pixel_format = pixel_format.strip()
+ self._reconnect = _strict_bool(params, "reconnect", default=True)
+ self._reconnect_initial_s = _positive_float(
+ params, "reconnect_initial_s", default=0.5
+ )
+ self._reconnect_max_s = _positive_float(params, "reconnect_max_s", default=10.0)
+ if self._reconnect_max_s < self._reconnect_initial_s:
+ raise ValueError("reconnect_max_s must be >= reconnect_initial_s")
+ max_attempts = params.get("max_reconnect_attempts")
+ if max_attempts is None:
+ self._max_reconnect_attempts: int | None = None
+ elif isinstance(max_attempts, bool) or not isinstance(max_attempts, int):
+ raise ValueError("max_reconnect_attempts must be an integer or null")
+ elif max_attempts < 0:
+ raise ValueError("max_reconnect_attempts must not be negative")
+ else:
+ self._max_reconnect_attempts = max_attempts
+ self._stream_log_interval_s = _positive_float(
+ params,
+ "stream_log_interval_s",
+ default=30.0,
+ )
+ self._pool: GrpcChannelPool | None = None
+ self._stub: Any = None
+ self._start_request: Any = None
+ self._stream_request: Any = None
+ self._call: Any = None
+ self._shutdown_event: asyncio.Event | None = None
+ self._rpc_timeout_s: float | None = None
+ self._active_session_id: str | None = None
+ self._total_frames = 0
+ self._total_bytes = 0
+ self._stopped = False
+
+ async def setup(self, context: ComponentContext) -> None:
+ endpoint = resolve_endpoint(context, self._endpoint_id)
+ pool = context.metadata.get("grpc_channel_pool")
+ if not isinstance(pool, GrpcChannelPool):
+ raise TypeError("component context is missing GrpcChannelPool")
+ self._pool = pool
+ channel = await pool.get(self._endpoint_id, grpc_channel_settings(endpoint))
+ self._shutdown_event = context.shutdown_event
+ self._rpc_timeout_s = float(endpoint_value(endpoint, "timeout_s", 5.0))
+
+ try:
+ common_pb2 = import_module("cmvr.api.common_pb2")
+ camera_command_pb2 = import_module("cmvr.api.camera_command_pb2")
+ camera_service_pb2_grpc = import_module("cmvr.api.camera_service_pb2_grpc")
+ except ImportError as exc:
+ raise CmvrApiPackageError(
+ "cmvr Python API bindings are missing; run "
+ "scripts/generate_cmvr_stubs.py or install the cmvr-api wheel"
+ ) from exc
+
+ start_header = common_pb2.CommandHeader.Request(device_id=self._device_id)
+ stream_header = common_pb2.CommandHeader.Request(device_id=self._device_id)
+ self._start_request = camera_command_pb2.StartCameraCommand.Request(
+ header=start_header
+ )
+ self._stream_request = camera_command_pb2.GetRGBImageStreamCommand.Request(
+ header=stream_header
+ )
+ self._stub = camera_service_pb2_grpc.CameraServiceStub(channel)
+ _LOGGER.info(
+ "camera source configured node=%s endpoint=%s device=%s "
+ "rpc_timeout_s=%.3f reconnect=%s reconnect_initial_s=%.3f "
+ "reconnect_max_s=%.3f stream_log_interval_s=%.3f",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ self._rpc_timeout_s,
+ self._reconnect,
+ self._reconnect_initial_s,
+ self._reconnect_max_s,
+ self._stream_log_interval_s,
+ )
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ if (
+ self._stub is None
+ or self._start_request is None
+ or self._stream_request is None
+ or self._shutdown_event is None
+ or self._rpc_timeout_s is None
+ ):
+ raise RuntimeError("camera source has not been set up")
+ shutdown_event = self._shutdown_event
+ reconnect_attempts = 0
+ reconnect_delay_s = self._reconnect_initial_s
+ local_sequence = 0
+
+ while not shutdown_event.is_set():
+ stream_session_id = uuid4().hex
+ self._active_session_id = stream_session_id
+ stats: _StreamStats | None = None
+ progress_task: asyncio.Task[None] | None = None
+
+ async def subscription_requests(): # type: ignore[no-untyped-def]
+ yield self._stream_request
+ await shutdown_event.wait()
+
+ try:
+ await self._start_camera(
+ stream_session_id,
+ reconnect_attempt=reconnect_attempts,
+ )
+ opened_ns = monotonic_ns()
+ stats = _StreamStats(
+ session_id=stream_session_id,
+ opened_ns=opened_ns,
+ window_started_ns=opened_ns,
+ )
+ _LOGGER.info(
+ "camera stream opening node=%s endpoint=%s device=%s "
+ "session=%s reconnect_attempt=%s waiting_for_first_frame=true",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ reconnect_attempts,
+ )
+ self._call = self._stub.GetRGBImageStream(subscription_requests())
+ progress_task = asyncio.create_task(
+ self._log_stream_progress(stats, shutdown_event),
+ name=f"{self._node_id}:camera-stream-progress",
+ )
+ received_in_session = False
+ async for response in self._call:
+ if shutdown_event.is_set():
+ return
+ if not response.header.success:
+ _LOGGER.warning(
+ "camera stream feedback error node=%s endpoint=%s "
+ "device=%s session=%s error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ response.header.error_message
+ or "cmvr-es camera stream failed",
+ )
+ raise CmvrServiceError(
+ response.header.error_message
+ or "cmvr-es camera stream failed"
+ )
+ frame = response.color_frame
+ intrinsics = response.intrinsics
+ frame_data = bytes(frame.data)
+ frame_received_ns = monotonic_ns()
+ remote_sequence = int(response.seq_no)
+ stats.frames += 1
+ stats.received_bytes += len(frame_data)
+ stats.key_frames += int(bool(frame.is_key_frame))
+ stats.window_frames += 1
+ stats.window_bytes += len(frame_data)
+ stats.window_key_frames += int(bool(frame.is_key_frame))
+ stats.last_frame_ns = frame_received_ns
+ stats.last_remote_sequence = remote_sequence
+ stats.codec = frame.codec or "none"
+ stats.width = int(frame.width)
+ stats.height = int(frame.height)
+ self._total_frames += 1
+ self._total_bytes += len(frame_data)
+ if not received_in_session:
+ _LOGGER.info(
+ "camera stream first frame node=%s endpoint=%s device=%s "
+ "session=%s reconnect_attempt=%s time_to_first_frame_ms=%.2f "
+ "remote_sequence=%s codec=%s width=%s height=%s bytes=%s "
+ "key_frame=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ reconnect_attempts,
+ (frame_received_ns - opened_ns) / 1_000_000,
+ remote_sequence,
+ stats.codec,
+ stats.width,
+ stats.height,
+ len(frame_data),
+ bool(frame.is_key_frame),
+ )
+ received_in_session = True
+ reconnect_attempts = 0
+ reconnect_delay_s = self._reconnect_initial_s
+ captured_at_ns = _protobuf_timestamp_ns(response.header.timestamp)
+ payload = ImageFrame(
+ data=frame_data,
+ width=stats.width,
+ height=stats.height,
+ pixel_format=self._pixel_format,
+ codec=stats.codec,
+ is_key_frame=bool(frame.is_key_frame),
+ intrinsics=CameraIntrinsics(
+ fx=float(intrinsics.fx),
+ fy=float(intrinsics.fy),
+ cx=float(intrinsics.cx),
+ cy=float(intrinsics.cy),
+ coefficients=tuple(
+ float(value) for value in intrinsics.coeffs
+ ),
+ ),
+ )
+ sequence = local_sequence
+ local_sequence += 1
+ yield Emission(
+ "frames",
+ Envelope(
+ payload=payload,
+ schema_name="ImageFrame",
+ schema_version=1,
+ source_id=self._device_id,
+ sequence=sequence,
+ captured_at_ns=captured_at_ns,
+ session_id=stream_session_id,
+ attributes={
+ "transport": "grpc",
+ "endpoint": self._endpoint_id,
+ "remote_sequence": remote_sequence,
+ "stream_session_id": stream_session_id,
+ },
+ ),
+ )
+ if shutdown_event.is_set():
+ return
+ detail = (
+ "cmvr-es camera stream ended"
+ if received_in_session
+ else "cmvr-es camera stream ended before the first frame"
+ )
+ raise CmvrServiceError(detail)
+ except asyncio.CancelledError:
+ _LOGGER.info(
+ "camera stream cancelled node=%s endpoint=%s device=%s "
+ "session=%s session_frames=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ stats.frames if stats is not None else 0,
+ )
+ raise
+ except Exception as exc:
+ if shutdown_event.is_set():
+ return
+ if not self._reconnect:
+ _LOGGER.error(
+ "camera stream failed node=%s endpoint=%s device=%s "
+ "session=%s reconnect=false error_type=%s error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ type(exc).__name__,
+ exc,
+ )
+ raise
+ reconnect_attempts += 1
+ if (
+ self._max_reconnect_attempts is not None
+ and reconnect_attempts > self._max_reconnect_attempts
+ ):
+ _LOGGER.error(
+ "camera stream reconnect exhausted node=%s endpoint=%s "
+ "device=%s session=%s attempts=%s error_type=%s error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ reconnect_attempts,
+ type(exc).__name__,
+ exc,
+ )
+ raise CmvrServiceError(
+ "cmvr-es camera stream exceeded max_reconnect_attempts"
+ ) from exc
+ _LOGGER.warning(
+ "camera stream disconnected node=%s endpoint=%s device=%s "
+ "session=%s session_frames=%s attempt=%s retry_in_s=%.3f "
+ "error_type=%s error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ stats.frames if stats is not None else 0,
+ reconnect_attempts,
+ reconnect_delay_s,
+ type(exc).__name__,
+ exc,
+ )
+ try:
+ await asyncio.wait_for(
+ shutdown_event.wait(), timeout=reconnect_delay_s
+ )
+ return
+ except TimeoutError:
+ reconnect_delay_s = min(
+ self._reconnect_max_s, reconnect_delay_s * 2.0
+ )
+ finally:
+ if progress_task is not None:
+ progress_task.cancel()
+ await asyncio.gather(progress_task, return_exceptions=True)
+ if self._call is not None:
+ self._call.cancel()
+ self._call = None
+ if stats is not None:
+ closed_ns = monotonic_ns()
+ _LOGGER.info(
+ "camera stream closed node=%s endpoint=%s device=%s "
+ "session=%s session_frames=%s session_bytes=%s "
+ "uptime_s=%.3f first_frame_received=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stream_session_id,
+ stats.frames,
+ stats.received_bytes,
+ max(0.0, (closed_ns - stats.opened_ns) / 1_000_000_000),
+ stats.frames > 0,
+ )
+ self._active_session_id = None
+
+ async def stop(self) -> None:
+ if self._stopped:
+ return
+ self._stopped = True
+ _LOGGER.info(
+ "camera source stopping node=%s endpoint=%s device=%s "
+ "active_session=%s total_frames=%s total_bytes=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ self._active_session_id or "none",
+ self._total_frames,
+ self._total_bytes,
+ )
+ if self._call is not None:
+ self._call.cancel()
+ self._call = None
+ _LOGGER.info(
+ "camera source stopped node=%s endpoint=%s device=%s "
+ "total_frames=%s total_bytes=%s remote_camera_stopped=false",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ self._total_frames,
+ self._total_bytes,
+ )
+
+ async def _start_camera(
+ self,
+ session_id: str,
+ *,
+ reconnect_attempt: int,
+ ) -> None:
+ assert self._stub is not None
+ assert self._start_request is not None
+ assert self._rpc_timeout_s is not None
+ reason = (
+ "initial"
+ if self._total_frames == 0 and reconnect_attempt == 0
+ else "reconnect"
+ )
+ _LOGGER.info(
+ "camera start requested node=%s endpoint=%s device=%s session=%s "
+ "reason=%s reconnect_attempt=%s timeout_s=%.3f",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ session_id,
+ reason,
+ reconnect_attempt,
+ self._rpc_timeout_s,
+ )
+ started = perf_counter()
+ try:
+ response = await self._stub.StartCamera(
+ self._start_request,
+ timeout=self._rpc_timeout_s,
+ )
+ except asyncio.CancelledError:
+ raise
+ except Exception as exc:
+ _LOGGER.warning(
+ "camera start failed node=%s endpoint=%s device=%s session=%s "
+ "reason=%s error_type=%s error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ session_id,
+ reason,
+ type(exc).__name__,
+ exc,
+ )
+ raise
+
+ feedback = getattr(response, "header", response)
+ if not bool(getattr(feedback, "success", False)):
+ detail = (
+ getattr(feedback, "error_message", "")
+ or "cmvr-es rejected StartCamera"
+ )
+ _LOGGER.warning(
+ "camera start failed node=%s endpoint=%s device=%s session=%s "
+ "reason=%s error_type=CmvrServiceError error=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ session_id,
+ reason,
+ detail,
+ )
+ raise CmvrServiceError(detail)
+
+ _LOGGER.info(
+ "camera start succeeded node=%s endpoint=%s device=%s session=%s "
+ "reason=%s elapsed_ms=%.2f",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ session_id,
+ reason,
+ (perf_counter() - started) * 1000.0,
+ )
+
+ async def _log_stream_progress(
+ self,
+ stats: _StreamStats,
+ shutdown_event: asyncio.Event,
+ ) -> None:
+ while not shutdown_event.is_set():
+ try:
+ await asyncio.wait_for(
+ shutdown_event.wait(),
+ timeout=self._stream_log_interval_s,
+ )
+ return
+ except TimeoutError:
+ now_ns = monotonic_ns()
+ window_s = max(
+ (now_ns - stats.window_started_ns) / 1_000_000_000,
+ 1e-9,
+ )
+ last_frame_age_s = (
+ -1.0
+ if stats.last_frame_ns is None
+ else max(0.0, (now_ns - stats.last_frame_ns) / 1_000_000_000)
+ )
+ _LOGGER.info(
+ "camera stream progress node=%s endpoint=%s device=%s "
+ "session=%s first_frame_received=%s total_frames=%s "
+ "window_frames=%s window_fps=%.2f window_bytes=%s "
+ "bitrate_kbps=%.2f window_key_frames=%s last_remote_sequence=%s "
+ "codec=%s width=%s height=%s last_frame_age_s=%.3f",
+ self._node_id,
+ self._endpoint_id,
+ self._device_id,
+ stats.session_id,
+ stats.frames > 0,
+ stats.frames,
+ stats.window_frames,
+ stats.window_frames / window_s,
+ stats.window_bytes,
+ stats.window_bytes * 8.0 / window_s / 1000.0,
+ stats.window_key_frames,
+ (
+ stats.last_remote_sequence
+ if stats.last_remote_sequence is not None
+ else "none"
+ ),
+ stats.codec,
+ stats.width,
+ stats.height,
+ last_frame_age_s,
+ )
+ stats.window_frames = 0
+ stats.window_bytes = 0
+ stats.window_key_frames = 0
+ stats.window_started_ns = now_ns
+
+
+def _protobuf_timestamp_ns(timestamp: Any) -> int | None:
+ seconds = int(getattr(timestamp, "seconds", 0))
+ nanos = int(getattr(timestamp, "nanos", 0))
+ if seconds == 0 and nanos == 0:
+ return None
+ return seconds * 1_000_000_000 + nanos
+
+
+def _strict_bool(params: Mapping[str, Any], name: str, *, default: bool) -> bool:
+ value = params.get(name, default)
+ if type(value) is not bool:
+ raise ValueError(f"{name} must be a boolean")
+ return value
+
+
+def _positive_float(params: Mapping[str, Any], name: str, *, default: float) -> float:
+ value = params.get(name, default)
+ if isinstance(value, bool):
+ raise ValueError(f"{name} must be a positive number")
+ try:
+ parsed = float(value)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"{name} must be a positive number") from exc
+ if not math.isfinite(parsed) or parsed <= 0:
+ raise ValueError(f"{name} must be a positive number")
+ return parsed
+
+
+__all__ = ["CmvrCameraRgbStreamSource"]
diff --git a/src/cmvr_edge_ai/connectors/cmvr_es/errors.py b/src/cmvr_edge_ai/connectors/cmvr_es/errors.py
new file mode 100644
index 0000000..0ea0b2a
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/cmvr_es/errors.py
@@ -0,0 +1,13 @@
+"""Errors raised by cmvr-es semantic connectors."""
+
+
+class CmvrConnectorError(RuntimeError):
+ pass
+
+
+class CmvrApiPackageError(CmvrConnectorError):
+ pass
+
+
+class CmvrServiceError(CmvrConnectorError):
+ pass
diff --git a/src/cmvr_edge_ai/connectors/platform/__init__.py b/src/cmvr_edge_ai/connectors/platform/__init__.py
new file mode 100644
index 0000000..a081112
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/platform/__init__.py
@@ -0,0 +1,5 @@
+"""Northbound platform connectors."""
+
+from .http_json import PlatformHttpJsonSink
+
+__all__ = ["PlatformHttpJsonSink"]
diff --git a/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..091c4d6
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..79d4d5a
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/platform/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-310.pyc b/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-310.pyc
new file mode 100644
index 0000000..41372dd
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-312.pyc b/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-312.pyc
new file mode 100644
index 0000000..4ff071c
Binary files /dev/null and b/src/cmvr_edge_ai/connectors/platform/__pycache__/http_json.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/connectors/platform/http_json.py b/src/cmvr_edge_ai/connectors/platform/http_json.py
new file mode 100644
index 0000000..e549d99
--- /dev/null
+++ b/src/cmvr_edge_ai/connectors/platform/http_json.py
@@ -0,0 +1,260 @@
+"""HTTP JSON telemetry sink for northbound platforms."""
+
+from __future__ import annotations
+
+import base64
+import dataclasses
+import logging
+import asyncio
+import math
+from collections.abc import Mapping, Sequence
+from enum import Enum
+from typing import Any
+
+from cmvr_edge_ai.contracts import EncodedImage
+from cmvr_edge_ai.core.component import ComponentContext, Sink
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.transports.http import HttpClientPool
+
+from .._context import http_client_settings, resolve_endpoint
+
+
+_LOGGER = logging.getLogger(__name__)
+_DEFAULT_RETRY_STATUSES = frozenset({408, 425, 429, 500, 502, 503, 504})
+
+
+class PlatformHttpJsonSink(Sink):
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ allowed_params = {
+ "endpoint",
+ "path",
+ "failure_mode",
+ "max_attempts",
+ "retry_initial_s",
+ "retry_max_s",
+ "retry_statuses",
+ }
+ unknown = set(params) - allowed_params
+ if unknown:
+ raise ValueError(
+ "unknown HTTP sink parameter(s): "
+ + ", ".join(sorted(str(value) for value in unknown))
+ )
+ self._node_id = node_id
+ self._endpoint_id = str(params["endpoint"])
+ path = params.get("path", "/")
+ if not isinstance(path, str) or not path.startswith("/"):
+ raise ValueError("HTTP sink path must be an absolute path starting with /")
+ self._path = path
+ failure_mode = params.get("failure_mode", "raise")
+ if not isinstance(failure_mode, str) or failure_mode not in {
+ "raise",
+ "log_and_drop",
+ }:
+ raise ValueError("failure_mode must be 'raise' or 'log_and_drop'")
+ self._failure_mode = failure_mode
+ self._max_attempts = _positive_int(params, "max_attempts", default=3)
+ self._retry_initial_s = _non_negative_float(
+ params, "retry_initial_s", default=0.25
+ )
+ self._retry_max_s = _non_negative_float(params, "retry_max_s", default=5.0)
+ if self._retry_max_s < self._retry_initial_s:
+ raise ValueError("retry_max_s must be >= retry_initial_s")
+ retry_statuses = params.get("retry_statuses", _DEFAULT_RETRY_STATUSES)
+ if isinstance(retry_statuses, (str, bytes, bytearray, Mapping)):
+ raise ValueError("retry_statuses must be a sequence of HTTP status codes")
+ try:
+ parsed_statuses = frozenset(int(value) for value in retry_statuses)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(
+ "retry_statuses must be a sequence of HTTP status codes"
+ ) from exc
+ if any(status < 100 or status > 599 for status in parsed_statuses):
+ raise ValueError("retry_statuses values must be between 100 and 599")
+ self._retry_statuses = parsed_statuses
+ self._client: Any = None
+
+ async def setup(self, context: ComponentContext) -> None:
+ endpoint = resolve_endpoint(context, self._endpoint_id)
+ pool = context.metadata.get("http_client_pool")
+ if not isinstance(pool, HttpClientPool):
+ raise TypeError("component context is missing HttpClientPool")
+ self._client = await pool.get(self._endpoint_id, http_client_settings(endpoint))
+
+ async def consume(self, envelope: Envelope[Any], input_port: str = "input") -> None:
+ if self._client is None:
+ raise RuntimeError("HTTP sink has not been set up")
+ body = {
+ "schema": envelope.schema,
+ "source_id": envelope.source_id,
+ "sequence": envelope.sequence,
+ "captured_at_ns": envelope.captured_at_ns,
+ "received_at_ns": envelope.received_at_ns,
+ "trace_id": envelope.trace_id,
+ "session_id": envelope.session_id,
+ "input_port": input_port,
+ "payload": _jsonable(envelope.payload),
+ "attributes": _jsonable(envelope.attributes),
+ }
+ idempotency_key = _event_id(envelope.payload) or envelope.trace_id
+ retry_delay_s = self._retry_initial_s
+ for attempt in range(1, self._max_attempts + 1):
+ try:
+ response = await self._client.post(
+ self._path,
+ json=body,
+ headers={"Idempotency-Key": idempotency_key},
+ )
+ except asyncio.CancelledError:
+ raise
+ except Exception as exc:
+ retryable = _retryable_exception(exc)
+ if attempt >= self._max_attempts or not retryable:
+ if self._failure_mode == "log_and_drop" and retryable:
+ self._log_dropped_report(
+ envelope,
+ input_port=input_port,
+ attempts=attempt,
+ error=exc,
+ )
+ return
+ raise
+ _LOGGER.warning(
+ "HTTP report retry node=%s path=%s attempt=%s/%s error=%s",
+ self._node_id,
+ self._path,
+ attempt,
+ self._max_attempts,
+ exc,
+ )
+ await asyncio.sleep(retry_delay_s)
+ retry_delay_s = min(
+ self._retry_max_s,
+ max(retry_delay_s * 2.0, self._retry_initial_s),
+ )
+ continue
+
+ if (
+ response.status_code in self._retry_statuses
+ and attempt < self._max_attempts
+ ):
+ _LOGGER.warning(
+ "HTTP report retry node=%s path=%s status=%s attempt=%s/%s",
+ self._node_id,
+ self._path,
+ response.status_code,
+ attempt,
+ self._max_attempts,
+ )
+ await asyncio.sleep(retry_delay_s)
+ retry_delay_s = min(
+ self._retry_max_s,
+ max(retry_delay_s * 2.0, self._retry_initial_s),
+ )
+ continue
+ # Status errors outside the explicit retry allowlist are never
+ # retried, even though httpx represents them as HTTP exceptions.
+ try:
+ response.raise_for_status()
+ except asyncio.CancelledError:
+ raise
+ except Exception as exc:
+ if self._failure_mode == "log_and_drop":
+ self._log_dropped_report(
+ envelope,
+ input_port=input_port,
+ attempts=attempt,
+ error=exc,
+ status_code=response.status_code,
+ )
+ return
+ raise
+ return
+
+ def _log_dropped_report(
+ self,
+ envelope: Envelope[Any],
+ *,
+ input_port: str,
+ attempts: int,
+ error: Exception,
+ status_code: int | None = None,
+ ) -> None:
+ _LOGGER.warning(
+ "HTTP report dropped node=%s endpoint=%s path=%s input_port=%s "
+ "failure_mode=log_and_drop action=drop attempts=%s status=%s error_type=%s "
+ "error=%s trace_id=%s",
+ self._node_id,
+ self._endpoint_id,
+ self._path,
+ input_port,
+ attempts,
+ status_code if status_code is not None else "none",
+ type(error).__name__,
+ error,
+ envelope.trace_id,
+ )
+
+
+def _jsonable(value: Any) -> Any:
+ if value is None or isinstance(value, (str, int, float, bool)):
+ return value
+ if isinstance(value, EncodedImage):
+ return {
+ "media_type": value.media_type,
+ "width": value.width,
+ "height": value.height,
+ "encoding": "base64",
+ "data": base64.b64encode(value.data).decode("ascii"),
+ }
+ if isinstance(value, bytes):
+ return {"encoding": "base64", "data": base64.b64encode(value).decode("ascii")}
+ if isinstance(value, Enum):
+ return value.value
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
+ return {
+ field.name: _jsonable(getattr(value, field.name))
+ for field in dataclasses.fields(value)
+ if not field.metadata.get("transient", False)
+ }
+ if isinstance(value, Mapping):
+ return {str(key): _jsonable(item) for key, item in value.items()}
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
+ return [_jsonable(item) for item in value]
+ raise TypeError(f"value is not JSON serializable: {type(value).__name__}")
+
+
+def _event_id(payload: Any) -> str | None:
+ value = getattr(payload, "event_id", None)
+ return value if isinstance(value, str) and value else None
+
+
+def _retryable_exception(exc: Exception) -> bool:
+ return isinstance(exc, (OSError, TimeoutError)) or type(exc).__module__.startswith(
+ "httpx"
+ )
+
+
+def _positive_int(params: Mapping[str, Any], name: str, *, default: int) -> int:
+ value = params.get(name, default)
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
+ raise ValueError(f"{name} must be a positive integer")
+ return value
+
+
+def _non_negative_float(
+ params: Mapping[str, Any], name: str, *, default: float
+) -> float:
+ value = params.get(name, default)
+ if isinstance(value, bool):
+ raise ValueError(f"{name} must be a non-negative number")
+ try:
+ parsed = float(value)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"{name} must be a non-negative number") from exc
+ if not math.isfinite(parsed) or parsed < 0:
+ raise ValueError(f"{name} must be a non-negative number")
+ return parsed
+
+
+__all__ = ["PlatformHttpJsonSink"]
diff --git a/src/cmvr_edge_ai/contracts/__init__.py b/src/cmvr_edge_ai/contracts/__init__.py
new file mode 100644
index 0000000..6c0c5da
--- /dev/null
+++ b/src/cmvr_edge_ai/contracts/__init__.py
@@ -0,0 +1,27 @@
+"""Stable internal schemas for protocol-independent AI nodes."""
+
+from .control import ApprovedRobotCommand, RobotCommand
+from .events import (
+ BoundingBox,
+ ChatTurn,
+ Detection,
+ DetectionAlert,
+ DetectionResult,
+ TextEvent,
+)
+from .media import AudioChunk, CameraIntrinsics, EncodedImage, ImageFrame
+
+__all__ = [
+ "AudioChunk",
+ "ApprovedRobotCommand",
+ "BoundingBox",
+ "CameraIntrinsics",
+ "ChatTurn",
+ "Detection",
+ "DetectionAlert",
+ "DetectionResult",
+ "EncodedImage",
+ "ImageFrame",
+ "RobotCommand",
+ "TextEvent",
+]
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..eec5234
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..361f19e
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-310.pyc b/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-310.pyc
new file mode 100644
index 0000000..f552f76
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-312.pyc b/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-312.pyc
new file mode 100644
index 0000000..7c63e00
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/control.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-310.pyc b/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-310.pyc
new file mode 100644
index 0000000..cd09979
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-312.pyc b/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-312.pyc
new file mode 100644
index 0000000..9fa6dd7
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/events.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-310.pyc b/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-310.pyc
new file mode 100644
index 0000000..1d90821
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-312.pyc b/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-312.pyc
new file mode 100644
index 0000000..4911ffe
Binary files /dev/null and b/src/cmvr_edge_ai/contracts/__pycache__/media.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/contracts/control.py b/src/cmvr_edge_ai/contracts/control.py
new file mode 100644
index 0000000..f4c9971
--- /dev/null
+++ b/src/cmvr_edge_ai/contracts/control.py
@@ -0,0 +1,47 @@
+"""Transport-neutral, expiring robot commands."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from types import MappingProxyType
+from typing import Any, Mapping
+from uuid import uuid4
+from time import time_ns
+
+
+@dataclass(frozen=True, slots=True)
+class RobotCommand:
+ device_id: str
+ action: str
+ valid_until_ns: int
+ sequence: int
+ arguments: Mapping[str, Any] = field(default_factory=dict)
+ command_id: str = field(default_factory=lambda: str(uuid4()))
+
+ def __post_init__(self) -> None:
+ if not self.device_id:
+ raise ValueError("device_id must not be empty")
+ if not self.action:
+ raise ValueError("action must not be empty")
+ if self.valid_until_ns <= 0:
+ raise ValueError("valid_until_ns must be positive Unix time")
+ if self.sequence < 0:
+ raise ValueError("sequence must not be negative")
+ if not self.command_id:
+ raise ValueError("command_id must not be empty")
+ object.__setattr__(self, "arguments", MappingProxyType(dict(self.arguments)))
+
+
+@dataclass(frozen=True, slots=True)
+class ApprovedRobotCommand:
+ """A command that has passed the trusted safety-gate boundary."""
+
+ command: RobotCommand
+ approved_by: str
+ approved_at_ns: int = field(default_factory=time_ns)
+
+ def __post_init__(self) -> None:
+ if not self.approved_by:
+ raise ValueError("approved_by must not be empty")
+ if self.approved_at_ns <= 0:
+ raise ValueError("approved_at_ns must be positive Unix time")
diff --git a/src/cmvr_edge_ai/contracts/events.py b/src/cmvr_edge_ai/contracts/events.py
new file mode 100644
index 0000000..a0717f8
--- /dev/null
+++ b/src/cmvr_edge_ai/contracts/events.py
@@ -0,0 +1,84 @@
+"""AI result and conversational event contracts."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any
+
+from .media import EncodedImage, ImageFrame
+
+
+@dataclass(frozen=True, slots=True)
+class BoundingBox:
+ x_min: float
+ y_min: float
+ x_max: float
+ y_max: float
+
+
+@dataclass(frozen=True, slots=True)
+class Detection:
+ label: str
+ confidence: float
+ box: BoundingBox
+ track_id: str | None = None
+
+
+@dataclass(frozen=True, slots=True)
+class DetectionResult:
+ detections: tuple[Detection, ...]
+ model_id: str
+ inference_ms: float
+ # Human-readable registration metadata. It is optional for compatibility
+ # with existing third-party producers of DetectionResult/v1.
+ model_name: str = ""
+ # Internal, zero-copy frame reference for downstream evidence generation.
+ # It must never be serialized by a generic platform connector.
+ source_frame: ImageFrame | None = field(
+ default=None,
+ repr=False,
+ compare=False,
+ metadata={"transient": True},
+ )
+
+
+@dataclass(frozen=True, slots=True)
+class DetectionAlert:
+ """A temporal detection rule that is ready for a downstream interface.
+
+ A DetectionResult describes one inferred frame. DetectionAlert is emitted
+ only after a stateful rule has observed enough distinct frames inside its
+ configured time window. Keeping the two contracts separate lets raw
+ detections be recorded or inspected without accidentally calling an alert
+ endpoint for every frame.
+ """
+
+ event_id: str
+ rule_id: str
+ model_id: str
+ model_name: str
+ labels: tuple[str, ...]
+ scope: str
+ scope_id: str
+ hit_count: int
+ window_ms: float
+ first_seen_ns: int
+ last_seen_ns: int
+ triggered_at_ns: int
+ max_confidence: float
+ detections: tuple[Detection, ...]
+ image: EncodedImage | None = None
+
+
+@dataclass(frozen=True, slots=True)
+class TextEvent:
+ text: str
+ role: str = "assistant"
+ final: bool = True
+
+
+@dataclass(frozen=True, slots=True)
+class ChatTurn:
+ session_id: str
+ user_text: str
+ history: tuple[dict[str, Any], ...] = ()
diff --git a/src/cmvr_edge_ai/contracts/media.py b/src/cmvr_edge_ai/contracts/media.py
new file mode 100644
index 0000000..11d6897
--- /dev/null
+++ b/src/cmvr_edge_ai/contracts/media.py
@@ -0,0 +1,75 @@
+"""Protocol-neutral media contracts used inside pipelines."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+
+@dataclass(frozen=True, slots=True)
+class CameraIntrinsics:
+ fx: float
+ fy: float
+ cx: float
+ cy: float
+ coefficients: tuple[float, ...] = ()
+
+
+@dataclass(frozen=True, slots=True)
+class ImageFrame:
+ """An encoded packet or decoded image buffer.
+
+ ``codec`` is ``"none"`` for decoded/raw frames. Encoded video packets
+ retain key-frame metadata so a stateful decoder can be used downstream.
+ """
+
+ data: bytes
+ width: int
+ height: int
+ pixel_format: str
+ codec: str = "none"
+ is_key_frame: bool = False
+ intrinsics: CameraIntrinsics | None = None
+
+ @property
+ def is_encoded(self) -> bool:
+ return self.codec.lower() not in {"", "none", "raw"}
+
+
+@dataclass(frozen=True, slots=True)
+class EncodedImage:
+ """A compressed image ready to cross a platform boundary."""
+
+ data: bytes = field(repr=False)
+ media_type: str
+ width: int
+ height: int
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.data, bytes) or not self.data:
+ raise ValueError("encoded image data must be non-empty bytes")
+ if not isinstance(self.media_type, str) or not self.media_type.strip():
+ raise ValueError("encoded image media_type must be a non-empty string")
+ if (
+ isinstance(self.width, bool)
+ or not isinstance(self.width, int)
+ or self.width < 1
+ ):
+ raise ValueError("encoded image width must be a positive integer")
+ if (
+ isinstance(self.height, bool)
+ or not isinstance(self.height, int)
+ or self.height < 1
+ ):
+ raise ValueError("encoded image height must be a positive integer")
+
+
+@dataclass(frozen=True, slots=True)
+class AudioChunk:
+ """A contiguous audio chunk independent of its transport protocol."""
+
+ data: bytes
+ sample_rate: int
+ channels: int
+ sample_format: str
+ codec: str = "pcm"
+ discontinuity: bool = False
diff --git a/src/cmvr_edge_ai/core/__init__.py b/src/cmvr_edge_ai/core/__init__.py
new file mode 100644
index 0000000..271cd75
--- /dev/null
+++ b/src/cmvr_edge_ai/core/__init__.py
@@ -0,0 +1,62 @@
+"""Public building blocks for configuration-driven edge AI pipelines."""
+
+from .component import (
+ DEFAULT_INPUT_PORT,
+ DEFAULT_OUTPUT_PORT,
+ Component,
+ ComponentContext,
+ Emission,
+ HealthReport,
+ Operator,
+ OperatorResult,
+ OutputItem,
+ Sink,
+ Source,
+)
+from .edge_queue import (
+ EdgeQueue,
+ EdgeQueueClosed,
+ EdgeQueueError,
+ EdgeQueueFull,
+ OverflowPolicy,
+ PutResult,
+ QueueStats,
+)
+from .envelope import Envelope
+from .runtime import (
+ Edge,
+ GraphValidationError,
+ NodeExecutionError,
+ PipelineRuntime,
+ PipelineRuntimeError,
+ RuntimeState,
+)
+
+
+__all__ = [
+ "Component",
+ "ComponentContext",
+ "DEFAULT_INPUT_PORT",
+ "DEFAULT_OUTPUT_PORT",
+ "Edge",
+ "EdgeQueue",
+ "EdgeQueueClosed",
+ "EdgeQueueError",
+ "EdgeQueueFull",
+ "Emission",
+ "Envelope",
+ "GraphValidationError",
+ "HealthReport",
+ "NodeExecutionError",
+ "Operator",
+ "OperatorResult",
+ "OutputItem",
+ "OverflowPolicy",
+ "PipelineRuntime",
+ "PipelineRuntimeError",
+ "PutResult",
+ "QueueStats",
+ "RuntimeState",
+ "Sink",
+ "Source",
+]
diff --git a/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..1d42b6b
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5c9c28b
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/component.cpython-310.pyc b/src/cmvr_edge_ai/core/__pycache__/component.cpython-310.pyc
new file mode 100644
index 0000000..3403073
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/component.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/component.cpython-312.pyc b/src/cmvr_edge_ai/core/__pycache__/component.cpython-312.pyc
new file mode 100644
index 0000000..f564e53
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/component.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-310.pyc b/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-310.pyc
new file mode 100644
index 0000000..89b6801
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-312.pyc b/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-312.pyc
new file mode 100644
index 0000000..ca2acbd
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/edge_queue.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-310.pyc b/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-310.pyc
new file mode 100644
index 0000000..cc8d1c7
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-312.pyc b/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-312.pyc
new file mode 100644
index 0000000..eb8fc12
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/envelope.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-310.pyc b/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-310.pyc
new file mode 100644
index 0000000..b5f319c
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-312.pyc b/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-312.pyc
new file mode 100644
index 0000000..849dca5
Binary files /dev/null and b/src/cmvr_edge_ai/core/__pycache__/runtime.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/core/component.py b/src/cmvr_edge_ai/core/component.py
new file mode 100644
index 0000000..ba90752
--- /dev/null
+++ b/src/cmvr_edge_ai/core/component.py
@@ -0,0 +1,196 @@
+"""Minimal asynchronous component contracts for pipeline nodes."""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import AsyncIterable, AsyncIterator, Iterable, Mapping
+from dataclasses import dataclass, field
+from types import MappingProxyType
+from typing import Any, TypeAlias
+
+import asyncio
+
+from .envelope import Envelope
+
+
+DEFAULT_INPUT_PORT = "input"
+DEFAULT_OUTPUT_PORT = "output"
+
+
+@dataclass(frozen=True, slots=True)
+class Emission:
+ """One envelope emitted on a named output port."""
+
+ port: str
+ envelope: Envelope[Any]
+
+ def __post_init__(self) -> None:
+ if not self.port:
+ raise ValueError("emission port must not be empty")
+ if not isinstance(self.envelope, Envelope):
+ raise TypeError("emission envelope must be an Envelope")
+
+
+OutputItem: TypeAlias = Envelope[Any] | Emission
+OperatorResult: TypeAlias = (
+ OutputItem | Iterable[OutputItem] | AsyncIterable[OutputItem] | None
+)
+
+
+@dataclass(frozen=True, slots=True)
+class ComponentContext:
+ """Runtime information made available during component setup."""
+
+ pipeline_id: str
+ node_id: str
+ shutdown_event: asyncio.Event
+ metadata: Mapping[str, Any] = field(default_factory=dict)
+
+ def __post_init__(self) -> None:
+ if not self.pipeline_id:
+ raise ValueError("pipeline_id must not be empty")
+ if not self.node_id:
+ raise ValueError("node_id must not be empty")
+ object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
+
+
+@dataclass(frozen=True, slots=True)
+class HealthReport:
+ """Small transport-neutral health response for supervisors."""
+
+ healthy: bool = True
+ detail: str = ""
+
+
+class Component(ABC):
+ """Base lifecycle shared by sources, operators and sinks.
+
+ Hooks are intentionally no-ops by default. Implementations should acquire
+ expensive resources in ``setup`` or ``start`` and release them in ``stop``.
+ The runtime calls each hook at most once per start/stop cycle.
+ """
+
+ async def setup(self, context: ComponentContext) -> None:
+ """Validate configuration and allocate resources."""
+
+ async def start(self) -> None:
+ """Start background work after every node has completed setup."""
+
+ async def stop(self) -> None:
+ """Release resources; implementations should make this idempotent."""
+
+ async def health(self) -> HealthReport:
+ """Return the component's current health."""
+
+ return HealthReport()
+
+
+class Source(Component, ABC):
+ """A component that produces envelopes from an external or synthetic input."""
+
+ @abstractmethod
+ def messages(self) -> AsyncIterator[OutputItem]:
+ """Return an async iterator of envelopes or named emissions.
+
+ Yielding an :class:`Envelope` uses :data:`DEFAULT_OUTPUT_PORT`.
+ Implementations normally define this method as an async generator.
+ """
+
+ raise NotImplementedError
+
+
+class Operator(Component, ABC):
+ """A component that transforms an envelope and may emit zero or more results."""
+
+ @abstractmethod
+ async def process(
+ self,
+ envelope: Envelope[Any],
+ input_port: str = DEFAULT_INPUT_PORT,
+ ) -> OperatorResult:
+ """Process one envelope received on ``input_port``.
+
+ A plain envelope is emitted on :data:`DEFAULT_OUTPUT_PORT`; use
+ :class:`Emission` for a different output port. Iterables and async
+ iterables are supported for zero-to-many transforms.
+ """
+
+ raise NotImplementedError
+
+
+class Sink(Component, ABC):
+ """A terminal component that sends messages to an external destination."""
+
+ @abstractmethod
+ async def consume(
+ self,
+ envelope: Envelope[Any],
+ input_port: str = DEFAULT_INPUT_PORT,
+ ) -> None:
+ """Consume one envelope received on ``input_port``."""
+
+ raise NotImplementedError
+
+
+async def iter_emissions(
+ result: OperatorResult,
+ *,
+ default_port: str = DEFAULT_OUTPUT_PORT,
+) -> AsyncIterator[Emission]:
+ """Normalize a component result into named emissions.
+
+ The helper performs strict element validation so a component accidentally
+ returning raw bytes or a model object fails near the offending node instead
+ of much later in a connector.
+ """
+
+ if not default_port:
+ raise ValueError("default_port must not be empty")
+
+ if result is None:
+ return
+ if isinstance(result, Envelope):
+ yield Emission(default_port, result)
+ return
+ if isinstance(result, Emission):
+ yield result
+ return
+
+ if isinstance(result, AsyncIterable):
+ async for item in result:
+ yield _as_emission(item, default_port)
+ return
+
+ if isinstance(result, Iterable):
+ for item in result:
+ yield _as_emission(item, default_port)
+ return
+
+ raise TypeError(
+ "component output must be an Envelope, Emission, iterable, "
+ "async iterable, or None"
+ )
+
+
+def _as_emission(item: OutputItem, default_port: str) -> Emission:
+ if isinstance(item, Emission):
+ return item
+ if isinstance(item, Envelope):
+ return Emission(default_port, item)
+ raise TypeError("component output iterable contains an invalid item")
+
+
+__all__ = [
+ "Component",
+ "ComponentContext",
+ "DEFAULT_INPUT_PORT",
+ "DEFAULT_OUTPUT_PORT",
+ "Emission",
+ "HealthReport",
+ "Operator",
+ "OperatorResult",
+ "OutputItem",
+ "Sink",
+ "Source",
+ "iter_emissions",
+]
diff --git a/src/cmvr_edge_ai/core/edge_queue.py b/src/cmvr_edge_ai/core/edge_queue.py
new file mode 100644
index 0000000..f285995
--- /dev/null
+++ b/src/cmvr_edge_ai/core/edge_queue.py
@@ -0,0 +1,232 @@
+"""Bounded per-edge queues with explicit overload behaviour."""
+
+from __future__ import annotations
+
+from collections import deque
+from collections.abc import AsyncIterator
+from dataclasses import dataclass
+from enum import Enum
+from typing import Deque, Generic, TypeVar
+
+import asyncio
+
+
+ItemT = TypeVar("ItemT")
+
+
+class OverflowPolicy(str, Enum):
+ """Action taken when a producer reaches an edge's capacity."""
+
+ BLOCK = "block"
+ DROP_OLDEST = "drop_oldest"
+ DROP_NEWEST = "drop_newest"
+ REJECT = "reject"
+
+
+class PutResult(str, Enum):
+ """Non-exceptional outcome of :meth:`EdgeQueue.put`."""
+
+ ENQUEUED = "enqueued"
+ DROPPED_OLDEST = "dropped_oldest"
+ DROPPED_NEWEST = "dropped_newest"
+
+
+class EdgeQueueError(RuntimeError):
+ """Base class for edge queue failures."""
+
+
+class EdgeQueueClosed(EdgeQueueError):
+ """Raised when reading a drained closed queue or writing a closed queue."""
+
+
+class EdgeQueueFull(EdgeQueueError):
+ """Raised when a full queue uses the ``reject`` overflow policy."""
+
+
+@dataclass(frozen=True, slots=True)
+class QueueStats:
+ """Point-in-time counters for one edge queue."""
+
+ capacity: int
+ current_size: int
+ high_watermark: int
+ enqueued: int
+ dequeued: int
+ dropped_oldest: int
+ dropped_newest: int
+ rejected: int
+ blocked_puts: int
+ discarded_on_close: int
+ closed: bool
+
+
+class EdgeQueue(Generic[ItemT]):
+ """A small asyncio queue whose overload policy is never implicit.
+
+ Unlike :class:`asyncio.Queue`, closing wakes both blocked producers and
+ consumers. A normal close is drainable: existing items are returned before
+ :class:`EdgeQueueClosed` is raised. ``close(discard=True)`` is available for
+ forced shutdown.
+ """
+
+ def __init__(
+ self,
+ capacity: int,
+ overflow: OverflowPolicy | str = OverflowPolicy.BLOCK,
+ *,
+ name: str = "",
+ ) -> None:
+ if capacity < 1:
+ raise ValueError("edge queue capacity must be at least 1")
+ try:
+ parsed_overflow = OverflowPolicy(overflow)
+ except ValueError as exc:
+ choices = ", ".join(policy.value for policy in OverflowPolicy)
+ raise ValueError(
+ f"unknown overflow policy {overflow!r}; use {choices}"
+ ) from exc
+
+ self._capacity = capacity
+ self._overflow = parsed_overflow
+ self._name = name
+ self._items: Deque[ItemT] = deque()
+ self._condition = asyncio.Condition()
+ self._closed = False
+
+ self._high_watermark = 0
+ self._enqueued = 0
+ self._dequeued = 0
+ self._dropped_oldest = 0
+ self._dropped_newest = 0
+ self._rejected = 0
+ self._blocked_puts = 0
+ self._discarded_on_close = 0
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ @property
+ def capacity(self) -> int:
+ return self._capacity
+
+ @property
+ def overflow(self) -> OverflowPolicy:
+ return self._overflow
+
+ @property
+ def closed(self) -> bool:
+ return self._closed
+
+ def qsize(self) -> int:
+ return len(self._items)
+
+ def empty(self) -> bool:
+ return not self._items
+
+ def full(self) -> bool:
+ return len(self._items) >= self._capacity
+
+ def stats(self) -> QueueStats:
+ """Return a consistent-enough event-loop snapshot of queue counters."""
+
+ return QueueStats(
+ capacity=self._capacity,
+ current_size=len(self._items),
+ high_watermark=self._high_watermark,
+ enqueued=self._enqueued,
+ dequeued=self._dequeued,
+ dropped_oldest=self._dropped_oldest,
+ dropped_newest=self._dropped_newest,
+ rejected=self._rejected,
+ blocked_puts=self._blocked_puts,
+ discarded_on_close=self._discarded_on_close,
+ closed=self._closed,
+ )
+
+ async def put(self, item: ItemT) -> PutResult:
+ """Put ``item`` according to this queue's overflow policy."""
+
+ async with self._condition:
+ self._raise_if_closed()
+
+ result = PutResult.ENQUEUED
+ if self._overflow is OverflowPolicy.BLOCK:
+ if self.full():
+ self._blocked_puts += 1
+ while self.full() and not self._closed:
+ await self._condition.wait()
+ self._raise_if_closed()
+ elif self.full() and self._overflow is OverflowPolicy.DROP_OLDEST:
+ self._items.popleft()
+ self._dropped_oldest += 1
+ result = PutResult.DROPPED_OLDEST
+ elif self.full() and self._overflow is OverflowPolicy.DROP_NEWEST:
+ self._dropped_newest += 1
+ return PutResult.DROPPED_NEWEST
+ elif self.full() and self._overflow is OverflowPolicy.REJECT:
+ self._rejected += 1
+ queue_name = f" {self._name!r}" if self._name else ""
+ raise EdgeQueueFull(f"edge queue{queue_name} is full")
+
+ self._items.append(item)
+ self._enqueued += 1
+ self._high_watermark = max(self._high_watermark, len(self._items))
+ self._condition.notify_all()
+ return result
+
+ async def get(self) -> ItemT:
+ """Get the next item, waiting until data arrives or the queue closes."""
+
+ async with self._condition:
+ while not self._items and not self._closed:
+ await self._condition.wait()
+ if not self._items:
+ raise EdgeQueueClosed(self._closed_message())
+
+ item = self._items.popleft()
+ self._dequeued += 1
+ self._condition.notify_all()
+ return item
+
+ async def close(self, *, discard: bool = False) -> None:
+ """Close the queue and wake all waiters.
+
+ The method is idempotent. By default queued items remain available to
+ consumers. With ``discard=True`` they are removed immediately and
+ accounted for in ``discarded_on_close``.
+ """
+
+ async with self._condition:
+ if discard and self._items:
+ self._discarded_on_close += len(self._items)
+ self._items.clear()
+ self._closed = True
+ self._condition.notify_all()
+
+ async def __aiter__(self) -> AsyncIterator[ItemT]:
+ while True:
+ try:
+ yield await self.get()
+ except EdgeQueueClosed:
+ return
+
+ def _raise_if_closed(self) -> None:
+ if self._closed:
+ raise EdgeQueueClosed(self._closed_message())
+
+ def _closed_message(self) -> str:
+ if self._name:
+ return f"edge queue {self._name!r} is closed"
+ return "edge queue is closed"
+
+
+__all__ = [
+ "EdgeQueue",
+ "EdgeQueueClosed",
+ "EdgeQueueError",
+ "EdgeQueueFull",
+ "OverflowPolicy",
+ "PutResult",
+ "QueueStats",
+]
diff --git a/src/cmvr_edge_ai/core/envelope.py b/src/cmvr_edge_ai/core/envelope.py
new file mode 100644
index 0000000..fb6741c
--- /dev/null
+++ b/src/cmvr_edge_ai/core/envelope.py
@@ -0,0 +1,139 @@
+"""Transport-neutral messages used inside an edge-ai pipeline.
+
+The envelope is deliberately independent from protobuf, HTTP and model-specific
+types. Connectors translate their wire representation at the edge of the
+pipeline and operators only exchange :class:`Envelope` instances.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from time import time_ns
+from types import MappingProxyType
+from typing import Any, Generic, Mapping, TypeVar
+from uuid import uuid4
+
+
+PayloadT = TypeVar("PayloadT")
+NewPayloadT = TypeVar("NewPayloadT")
+
+
+def _new_trace_id() -> str:
+ return uuid4().hex
+
+
+@dataclass(frozen=True, slots=True)
+class Envelope(Generic[PayloadT]):
+ """An immutable metadata wrapper around one pipeline payload.
+
+ ``Envelope`` immutability is shallow: the wrapper and its attributes cannot
+ be changed, but callers remain responsible for treating a mutable payload
+ (for example a NumPy array) as read-only after publishing it. This permits
+ zero-copy fan-out without imposing a dependency on a particular array or
+ shared-memory implementation.
+
+ All timestamps use Unix time in nanoseconds so timestamps received from
+ remote processes can be compared. ``deadline_ns`` is optional; runtimes do
+ not drop expired messages automatically because expiry policy is a business
+ decision best implemented by an operator or sink.
+ """
+
+ payload: PayloadT
+ schema_name: str = "opaque"
+ schema_version: int = 1
+ source_id: str = ""
+ sequence: int = 0
+ captured_at_ns: int | None = None
+ received_at_ns: int = field(default_factory=time_ns)
+ deadline_ns: int | None = None
+ trace_id: str = field(default_factory=_new_trace_id)
+ session_id: str | None = None
+ attributes: Mapping[str, Any] = field(default_factory=dict)
+
+ def __post_init__(self) -> None:
+ if not self.schema_name:
+ raise ValueError("schema_name must not be empty")
+ if self.schema_version < 1:
+ raise ValueError("schema_version must be at least 1")
+ if self.sequence < 0:
+ raise ValueError("sequence must not be negative")
+ if not self.trace_id:
+ raise ValueError("trace_id must not be empty")
+ if self.received_at_ns < 0:
+ raise ValueError("received_at_ns must not be negative")
+ if self.captured_at_ns is not None and self.captured_at_ns < 0:
+ raise ValueError("captured_at_ns must not be negative")
+ if self.deadline_ns is not None and self.deadline_ns < 0:
+ raise ValueError("deadline_ns must not be negative")
+
+ copied_attributes = dict(self.attributes)
+ if any(not isinstance(key, str) for key in copied_attributes):
+ raise TypeError("attribute keys must be strings")
+ object.__setattr__(
+ self,
+ "attributes",
+ MappingProxyType(copied_attributes),
+ )
+
+ @property
+ def schema(self) -> str:
+ """Return the compact schema identifier, for example ``ImageFrame/v1``."""
+
+ return f"{self.schema_name}/v{self.schema_version}"
+
+ def is_expired(self, now_ns: int | None = None) -> bool:
+ """Return whether the optional deadline has passed."""
+
+ if self.deadline_ns is None:
+ return False
+ return (time_ns() if now_ns is None else now_ns) >= self.deadline_ns
+
+ def with_payload(
+ self,
+ payload: NewPayloadT,
+ *,
+ schema_name: str | None = None,
+ schema_version: int | None = None,
+ source_id: str | None = None,
+ ) -> Envelope[NewPayloadT]:
+ """Create a derived envelope while retaining correlation metadata."""
+
+ return Envelope(
+ payload=payload,
+ schema_name=self.schema_name if schema_name is None else schema_name,
+ schema_version=(
+ self.schema_version if schema_version is None else schema_version
+ ),
+ source_id=self.source_id if source_id is None else source_id,
+ sequence=self.sequence,
+ captured_at_ns=self.captured_at_ns,
+ # Keep the ingress timestamp so end-to-end latency remains
+ # observable across an arbitrary number of operators.
+ received_at_ns=self.received_at_ns,
+ deadline_ns=self.deadline_ns,
+ trace_id=self.trace_id,
+ session_id=self.session_id,
+ attributes=self.attributes,
+ )
+
+ def with_attributes(self, **attributes: Any) -> Envelope[PayloadT]:
+ """Create a copy with additional or replaced read-only attributes."""
+
+ merged = dict(self.attributes)
+ merged.update(attributes)
+ return Envelope(
+ payload=self.payload,
+ schema_name=self.schema_name,
+ schema_version=self.schema_version,
+ source_id=self.source_id,
+ sequence=self.sequence,
+ captured_at_ns=self.captured_at_ns,
+ received_at_ns=self.received_at_ns,
+ deadline_ns=self.deadline_ns,
+ trace_id=self.trace_id,
+ session_id=self.session_id,
+ attributes=merged,
+ )
+
+
+__all__ = ["Envelope", "PayloadT"]
diff --git a/src/cmvr_edge_ai/core/runtime.py b/src/cmvr_edge_ai/core/runtime.py
new file mode 100644
index 0000000..d93e388
--- /dev/null
+++ b/src/cmvr_edge_ai/core/runtime.py
@@ -0,0 +1,667 @@
+"""A lightweight, in-process asynchronous pipeline runtime."""
+
+from __future__ import annotations
+
+from collections import defaultdict, deque
+from collections.abc import AsyncIterable, AsyncIterator, Iterable, Mapping
+from dataclasses import dataclass
+from enum import Enum
+from types import MappingProxyType
+from typing import Any
+
+import asyncio
+import inspect
+
+from .component import (
+ DEFAULT_INPUT_PORT,
+ DEFAULT_OUTPUT_PORT,
+ Component,
+ ComponentContext,
+ Emission,
+ HealthReport,
+ Operator,
+ Sink,
+ Source,
+ iter_emissions,
+)
+from .edge_queue import (
+ EdgeQueue,
+ EdgeQueueClosed,
+ OverflowPolicy,
+ QueueStats,
+)
+from .envelope import Envelope
+
+
+class RuntimeState(str, Enum):
+ CREATED = "created"
+ STARTING = "starting"
+ RUNNING = "running"
+ STOPPING = "stopping"
+ STOPPED = "stopped"
+
+
+class PipelineRuntimeError(RuntimeError):
+ """Base exception raised by the pipeline runtime."""
+
+
+class GraphValidationError(PipelineRuntimeError, ValueError):
+ """Raised when nodes and edges do not form a valid executable DAG."""
+
+
+class NodeExecutionError(PipelineRuntimeError):
+ """Wrap an exception with the id of the component that raised it."""
+
+ def __init__(self, node_id: str, cause: BaseException) -> None:
+ self.node_id = node_id
+ self.cause = cause
+ super().__init__(f"node {node_id!r} failed: {cause}")
+
+
+@dataclass(frozen=True, slots=True)
+class Edge:
+ """Declarative connection between one output port and one input port."""
+
+ source_node: str
+ target_node: str
+ source_port: str = DEFAULT_OUTPUT_PORT
+ target_port: str = DEFAULT_INPUT_PORT
+ capacity: int = 1
+ overflow: OverflowPolicy | str = OverflowPolicy.BLOCK
+ name: str | None = None
+
+ def __post_init__(self) -> None:
+ for field_name in (
+ "source_node",
+ "target_node",
+ "source_port",
+ "target_port",
+ ):
+ value = getattr(self, field_name)
+ if not value or not value.strip():
+ raise ValueError(f"{field_name} must not be empty")
+ if self.capacity < 1:
+ raise ValueError("edge capacity must be at least 1")
+ if self.name is not None and not self.name.strip():
+ raise ValueError("edge name must not be empty")
+ try:
+ object.__setattr__(self, "overflow", OverflowPolicy(self.overflow))
+ except ValueError as exc:
+ choices = ", ".join(policy.value for policy in OverflowPolicy)
+ raise ValueError(
+ f"unknown overflow policy {self.overflow!r}; use {choices}"
+ ) from exc
+
+ @property
+ def resolved_name(self) -> str:
+ return self.name or (
+ f"{self.source_node}.{self.source_port}"
+ f"->{self.target_node}.{self.target_port}"
+ )
+
+
+@dataclass(slots=True)
+class _LiveEdge:
+ spec: Edge
+ queue: EdgeQueue[Envelope[Any]]
+
+
+class PipelineRuntime:
+ """Execute a configuration-compiled DAG in one asyncio event loop.
+
+ The runtime intentionally does not own threads, processes, transports or
+ retry policy. Those belong to components or a future worker supervisor.
+ Every graph edge owns a separate bounded queue, and only emissions matching
+ that edge's ``source_port`` are routed to it.
+
+ Runtime instances are one-shot. Create a new instance to restart a stopped
+ pipeline; this avoids accidentally reusing closed queues or partially torn
+ down model resources.
+ """
+
+ def __init__(
+ self,
+ nodes: Mapping[str, Component],
+ edges: Iterable[Edge],
+ *,
+ pipeline_id: str = "pipeline",
+ shutdown_timeout_s: float = 10.0,
+ metadata: Mapping[str, Any] | None = None,
+ ) -> None:
+ if not pipeline_id or not pipeline_id.strip():
+ raise ValueError("pipeline_id must not be empty")
+ if shutdown_timeout_s < 0:
+ raise ValueError("shutdown_timeout_s must not be negative")
+
+ self._pipeline_id = pipeline_id
+ self._nodes = dict(nodes)
+ self._edge_specs = tuple(edges)
+ self._shutdown_timeout_s = shutdown_timeout_s
+ self._metadata = MappingProxyType(dict(metadata or {}))
+ self._state = RuntimeState.CREATED
+ self._shutdown_event = asyncio.Event()
+ self._stop_lock = asyncio.Lock()
+ self._tasks: dict[str, asyncio.Task[None]] = {}
+ self._setup_nodes: list[str] = []
+ self._stopped_nodes: set[str] = set()
+
+ self._validate_graph()
+ self._topological_order = self._compute_topological_order()
+ self._live_edges = tuple(
+ _LiveEdge(
+ spec=edge,
+ queue=EdgeQueue(
+ edge.capacity,
+ edge.overflow,
+ name=edge.resolved_name,
+ ),
+ )
+ for edge in self._edge_specs
+ )
+ self._outgoing: dict[tuple[str, str], tuple[_LiveEdge, ...]] = (
+ self._index_outgoing()
+ )
+ self._incoming: dict[str, tuple[_LiveEdge, ...]] = self._index_incoming()
+
+ @property
+ def pipeline_id(self) -> str:
+ return self._pipeline_id
+
+ @property
+ def state(self) -> RuntimeState:
+ return self._state
+
+ @property
+ def nodes(self) -> Mapping[str, Component]:
+ return MappingProxyType(self._nodes)
+
+ @property
+ def edges(self) -> tuple[Edge, ...]:
+ return self._edge_specs
+
+ @property
+ def shutdown_event(self) -> asyncio.Event:
+ return self._shutdown_event
+
+ @property
+ def edge_queues(self) -> Mapping[str, EdgeQueue[Envelope[Any]]]:
+ return MappingProxyType(
+ {edge.spec.resolved_name: edge.queue for edge in self._live_edges}
+ )
+
+ def edge_stats(self) -> dict[str, QueueStats]:
+ """Return snapshots keyed by resolved edge name."""
+
+ return {
+ edge.spec.resolved_name: edge.queue.stats() for edge in self._live_edges
+ }
+
+ async def health(self) -> dict[str, HealthReport]:
+ """Collect health from all nodes without failing the whole query."""
+
+ async def check(node_id: str, component: Component) -> tuple[str, HealthReport]:
+ try:
+ report = await asyncio.wait_for(component.health(), timeout=1.0)
+ task = self._tasks.get(node_id)
+ if task is not None and task.done():
+ if task.cancelled():
+ report = HealthReport(False, "runtime task was cancelled")
+ else:
+ failure = task.exception()
+ if failure is not None:
+ report = HealthReport(
+ False, f"runtime task failed: {failure}"
+ )
+ elif self._state is RuntimeState.RUNNING:
+ report = HealthReport(
+ False,
+ "runtime task completed while pipeline is running",
+ )
+ except Exception as exc: # health is diagnostic, not pipeline control
+ report = HealthReport(False, f"health check failed: {exc}")
+ return node_id, report
+
+ reports = await asyncio.gather(
+ *(check(node_id, component) for node_id, component in self._nodes.items())
+ )
+ return dict(reports)
+
+ async def start(self) -> None:
+ """Run lifecycle hooks and start one task per component."""
+
+ async with self._stop_lock:
+ if self._state is not RuntimeState.CREATED:
+ raise PipelineRuntimeError(
+ f"cannot start pipeline in state {self._state.value!r}"
+ )
+ self._state = RuntimeState.STARTING
+
+ try:
+ # Prepare downstream consumers before upstream producers.
+ for node_id in reversed(self._topological_order):
+ component = self._nodes[node_id]
+ context = ComponentContext(
+ pipeline_id=self._pipeline_id,
+ node_id=node_id,
+ shutdown_event=self._shutdown_event,
+ metadata=dict(self._metadata),
+ )
+ self._setup_nodes.append(node_id)
+ await component.setup(context)
+
+ for node_id in reversed(self._topological_order):
+ component = self._nodes[node_id]
+ await component.start()
+ except BaseException:
+ self._shutdown_event.set()
+ deadline = asyncio.get_running_loop().time() + self._shutdown_timeout_s
+ await self._close_all_edges(discard=True)
+ await self._stop_components(deadline=deadline)
+ self._state = RuntimeState.STOPPED
+ raise
+
+ self._tasks = {
+ node_id: asyncio.create_task(
+ self._run_node(node_id, self._nodes[node_id]),
+ name=f"{self._pipeline_id}:{node_id}",
+ )
+ for node_id in reversed(self._topological_order)
+ }
+ self._state = RuntimeState.RUNNING
+
+ async def wait(self) -> None:
+ """Wait for natural completion, propagating the first node failure."""
+
+ if self._state not in (RuntimeState.RUNNING, RuntimeState.STOPPING):
+ if self._state is RuntimeState.STOPPED:
+ return
+ raise PipelineRuntimeError(
+ f"cannot wait for pipeline in state {self._state.value!r}"
+ )
+ try:
+ await asyncio.gather(*self._tasks.values())
+ except asyncio.CancelledError:
+ raise
+ except BaseException:
+ await self.stop(graceful=False)
+ raise
+
+ async def run(self) -> None:
+ """Start, wait, and always execute lifecycle teardown."""
+
+ await self.start()
+ try:
+ await self.wait()
+ finally:
+ await self.stop()
+
+ async def stop(
+ self,
+ *,
+ graceful: bool = True,
+ timeout_s: float | None = None,
+ ) -> None:
+ """Stop sources, optionally drain downstream queues, then tear down.
+
+ Graceful shutdown cancels source tasks first. Their output queues close
+ in ``finally`` blocks, allowing operators and sinks to drain all already
+ accepted messages. If draining exceeds the timeout, remaining tasks
+ are cancelled and queued messages are discarded.
+ """
+
+ if timeout_s is not None and timeout_s < 0:
+ raise ValueError("timeout_s must not be negative")
+
+ async with self._stop_lock:
+ if self._state is RuntimeState.STOPPED:
+ return
+ if self._state is RuntimeState.CREATED:
+ self._state = RuntimeState.STOPPED
+ self._shutdown_event.set()
+ await self._close_all_edges(discard=True)
+ return
+
+ self._state = RuntimeState.STOPPING
+ self._shutdown_event.set()
+ effective_timeout = (
+ self._shutdown_timeout_s if timeout_s is None else timeout_s
+ )
+ deadline = asyncio.get_running_loop().time() + effective_timeout
+
+ if graceful:
+ source_ids = tuple(
+ node_id
+ for node_id, component in self._nodes.items()
+ if isinstance(component, Source)
+ )
+ self._cancel_tasks(source_ids)
+ # Source hooks close external subscriptions promptly. Operators
+ # and sinks remain alive until accepted messages have drained.
+ await self._stop_components(source_ids, deadline=deadline)
+ drained = await self._wait_for_tasks(self._remaining(deadline))
+ if not drained:
+ self._cancel_tasks()
+ await self._close_all_edges(discard=True)
+ await self._stop_components(deadline=deadline)
+ await self._join_tasks(self._remaining(deadline))
+ else:
+ self._cancel_tasks()
+ await self._close_all_edges(discard=True)
+ await self._stop_components(deadline=deadline)
+ await self._join_tasks(self._remaining(deadline))
+
+ await self._stop_components(deadline=deadline)
+ self._tasks.clear()
+ self._state = RuntimeState.STOPPED
+
+ async def _run_node(self, node_id: str, component: Component) -> None:
+ try:
+ if isinstance(component, Source):
+ await self._run_source(node_id, component)
+ elif isinstance(component, Operator):
+ await self._run_operator(node_id, component)
+ elif isinstance(component, Sink):
+ await self._run_sink(node_id, component)
+ else: # guarded by graph validation
+ raise TypeError("unsupported component kind")
+ except asyncio.CancelledError:
+ raise
+ except BaseException as exc:
+ raise NodeExecutionError(node_id, exc) from exc
+ finally:
+ await self._close_outgoing(node_id)
+
+ async def _run_source(self, node_id: str, source: Source) -> None:
+ stream = source.messages()
+ if inspect.isawaitable(stream):
+ # Be tolerant of an ``async def`` factory that returns an iterator.
+ stream = await stream
+ if not isinstance(stream, AsyncIterable):
+ raise TypeError("Source.messages() must return an async iterable")
+
+ async for item in stream:
+ async for emission in iter_emissions(item):
+ await self._route(node_id, emission)
+
+ async def _run_operator(self, node_id: str, operator: Operator) -> None:
+ async for live_edge, envelope in self._receive(node_id):
+ result = await operator.process(
+ envelope,
+ input_port=live_edge.spec.target_port,
+ )
+ async for emission in iter_emissions(result):
+ await self._route(node_id, emission)
+
+ async def _run_sink(self, node_id: str, sink: Sink) -> None:
+ async for live_edge, envelope in self._receive(node_id):
+ await sink.consume(
+ envelope,
+ input_port=live_edge.spec.target_port,
+ )
+
+ async def _route(self, source_node: str, emission: Emission) -> None:
+ edges = self._outgoing.get((source_node, emission.port), ())
+ if not edges:
+ return
+
+ results = await asyncio.gather(
+ *(edge.queue.put(emission.envelope) for edge in edges),
+ return_exceptions=True,
+ )
+ for result in results:
+ if isinstance(result, EdgeQueueClosed) and self._shutdown_event.is_set():
+ continue
+ if isinstance(result, BaseException):
+ raise result
+
+ async def _receive(
+ self,
+ node_id: str,
+ ) -> AsyncIterator[tuple[_LiveEdge, Envelope[Any]]]:
+ incoming = self._incoming.get(node_id, ())
+ pending: dict[asyncio.Task[Envelope[Any]], _LiveEdge] = {
+ asyncio.create_task(
+ edge.queue.get(),
+ name=f"{self._pipeline_id}:{node_id}:read:{edge.spec.resolved_name}",
+ ): edge
+ for edge in incoming
+ }
+
+ try:
+ while pending:
+ done, _ = await asyncio.wait(
+ pending,
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ for task in done:
+ edge = pending.pop(task)
+ try:
+ envelope = task.result()
+ except EdgeQueueClosed:
+ continue
+
+ yield edge, envelope
+
+ if not (edge.queue.closed and edge.queue.empty()):
+ next_read = asyncio.create_task(
+ edge.queue.get(),
+ name=(
+ f"{self._pipeline_id}:{node_id}:read:"
+ f"{edge.spec.resolved_name}"
+ ),
+ )
+ pending[next_read] = edge
+ finally:
+ for task in pending:
+ task.cancel()
+ if pending:
+ await asyncio.gather(*pending, return_exceptions=True)
+
+ async def _close_outgoing(self, node_id: str) -> None:
+ queues = [
+ live_edge.queue
+ for (source, _), edges in self._outgoing.items()
+ if source == node_id
+ for live_edge in edges
+ ]
+ if queues:
+ await asyncio.gather(*(queue.close() for queue in queues))
+
+ async def _close_all_edges(self, *, discard: bool) -> None:
+ if self._live_edges:
+ await asyncio.gather(
+ *(edge.queue.close(discard=discard) for edge in self._live_edges)
+ )
+
+ async def _wait_for_tasks(self, timeout_s: float) -> bool:
+ if not self._tasks:
+ return True
+ _, pending = await asyncio.wait(
+ self._tasks.values(),
+ timeout=timeout_s,
+ return_when=asyncio.ALL_COMPLETED,
+ )
+ return not pending
+
+ def _cancel_tasks(self, node_ids: Iterable[str] | None = None) -> None:
+ selected = self._tasks.keys() if node_ids is None else node_ids
+ for node_id in selected:
+ task = self._tasks.get(node_id)
+ if task is None:
+ continue
+ if not task.done():
+ task.cancel()
+
+ async def _join_tasks(self, timeout_s: float) -> bool:
+ tasks = tuple(self._tasks.values())
+ if not tasks:
+ return True
+ _, pending = await asyncio.wait(
+ tasks,
+ timeout=timeout_s,
+ return_when=asyncio.ALL_COMPLETED,
+ )
+ for task in pending:
+ task.cancel()
+ return not pending
+
+ async def _stop_components(
+ self,
+ node_ids: Iterable[str] | None = None,
+ *,
+ deadline: float | None = None,
+ ) -> None:
+ # setup may allocate resources even when a later start hook fails.
+ selected = None if node_ids is None else set(node_ids)
+ setup_order = list(dict.fromkeys(self._setup_nodes))
+ for node_id in reversed(setup_order):
+ if selected is not None and node_id not in selected:
+ continue
+ if node_id in self._stopped_nodes:
+ continue
+ try:
+ if deadline is None:
+ await self._nodes[node_id].stop()
+ else:
+ remaining = self._remaining(deadline)
+ if remaining <= 0:
+ continue
+ await asyncio.wait_for(
+ self._nodes[node_id].stop(), timeout=remaining
+ )
+ except Exception:
+ # Teardown is best effort so one broken connector cannot leak all
+ # remaining resources. Supervisors should surface node health/logs.
+ pass
+ finally:
+ self._stopped_nodes.add(node_id)
+ if selected is None:
+ self._setup_nodes.clear()
+
+ @staticmethod
+ def _remaining(deadline: float) -> float:
+ return max(0.0, deadline - asyncio.get_running_loop().time())
+
+ def _index_outgoing(self) -> dict[tuple[str, str], tuple[_LiveEdge, ...]]:
+ mutable: dict[tuple[str, str], list[_LiveEdge]] = defaultdict(list)
+ for edge in self._live_edges:
+ mutable[(edge.spec.source_node, edge.spec.source_port)].append(edge)
+ return {key: tuple(value) for key, value in mutable.items()}
+
+ def _index_incoming(self) -> dict[str, tuple[_LiveEdge, ...]]:
+ mutable: dict[str, list[_LiveEdge]] = defaultdict(list)
+ for edge in self._live_edges:
+ mutable[edge.spec.target_node].append(edge)
+ return {key: tuple(value) for key, value in mutable.items()}
+
+ def _validate_graph(self) -> None:
+ if not self._nodes:
+ raise GraphValidationError("pipeline must contain at least one node")
+
+ for node_id, component in self._nodes.items():
+ if not node_id or not node_id.strip():
+ raise GraphValidationError("node id must not be empty")
+ kinds = sum(
+ isinstance(component, kind) for kind in (Source, Operator, Sink)
+ )
+ if kinds != 1:
+ raise GraphValidationError(
+ f"node {node_id!r} must implement exactly one of "
+ "Source, Operator, or Sink"
+ )
+
+ edge_names: set[str] = set()
+ edge_keys: set[tuple[str, str, str, str]] = set()
+ incoming_count: dict[str, int] = defaultdict(int)
+ outgoing_count: dict[str, int] = defaultdict(int)
+ adjacency: dict[str, set[str]] = defaultdict(set)
+ indegree = {node_id: 0 for node_id in self._nodes}
+
+ for edge in self._edge_specs:
+ if edge.source_node not in self._nodes:
+ raise GraphValidationError(
+ f"edge {edge.resolved_name!r} references unknown source node "
+ f"{edge.source_node!r}"
+ )
+ if edge.target_node not in self._nodes:
+ raise GraphValidationError(
+ f"edge {edge.resolved_name!r} references unknown target node "
+ f"{edge.target_node!r}"
+ )
+ if edge.resolved_name in edge_names:
+ raise GraphValidationError(
+ f"duplicate edge name {edge.resolved_name!r}"
+ )
+ edge_names.add(edge.resolved_name)
+
+ edge_key = (
+ edge.source_node,
+ edge.source_port,
+ edge.target_node,
+ edge.target_port,
+ )
+ if edge_key in edge_keys:
+ raise GraphValidationError(f"duplicate edge {edge.resolved_name!r}")
+ edge_keys.add(edge_key)
+ incoming_count[edge.target_node] += 1
+ outgoing_count[edge.source_node] += 1
+
+ if edge.target_node not in adjacency[edge.source_node]:
+ adjacency[edge.source_node].add(edge.target_node)
+ indegree[edge.target_node] += 1
+
+ for node_id, component in self._nodes.items():
+ if isinstance(component, Source) and incoming_count[node_id]:
+ raise GraphValidationError(
+ f"source node {node_id!r} cannot have incoming edges"
+ )
+ if isinstance(component, (Operator, Sink)) and not incoming_count[node_id]:
+ raise GraphValidationError(
+ f"{type(component).__name__.lower()} node {node_id!r} "
+ "must have at least one incoming edge"
+ )
+ if isinstance(component, Sink) and outgoing_count[node_id]:
+ raise GraphValidationError(
+ f"sink node {node_id!r} cannot have outgoing edges"
+ )
+
+ ready = deque(node_id for node_id, degree in indegree.items() if degree == 0)
+ visited = 0
+ while ready:
+ node_id = ready.popleft()
+ visited += 1
+ for target in adjacency[node_id]:
+ indegree[target] -= 1
+ if indegree[target] == 0:
+ ready.append(target)
+ if visited != len(self._nodes):
+ raise GraphValidationError("pipeline graph must be acyclic")
+
+ def _compute_topological_order(self) -> tuple[str, ...]:
+ adjacency: dict[str, set[str]] = defaultdict(set)
+ indegree = {node_id: 0 for node_id in self._nodes}
+ for edge in self._edge_specs:
+ if edge.target_node not in adjacency[edge.source_node]:
+ adjacency[edge.source_node].add(edge.target_node)
+ indegree[edge.target_node] += 1
+
+ ready = deque(node_id for node_id, degree in indegree.items() if degree == 0)
+ ordered: list[str] = []
+ while ready:
+ node_id = ready.popleft()
+ ordered.append(node_id)
+ for target in adjacency[node_id]:
+ indegree[target] -= 1
+ if indegree[target] == 0:
+ ready.append(target)
+ return tuple(ordered)
+
+
+__all__ = [
+ "Edge",
+ "GraphValidationError",
+ "NodeExecutionError",
+ "PipelineRuntime",
+ "PipelineRuntimeError",
+ "RuntimeState",
+]
diff --git a/src/cmvr_edge_ai/detection/__init__.py b/src/cmvr_edge_ai/detection/__init__.py
new file mode 100644
index 0000000..1771d65
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/__init__.py
@@ -0,0 +1,38 @@
+"""Model-independent object detection, decoding and alert rules."""
+
+from .base import DetectionModel
+from .models import (
+ CONSTRUCTION_PPE_LABELS,
+ PPE_6CLASS_LABELS,
+ register_builtin_detection_models,
+)
+from .operator import DetectionOperator, DetectorParams
+from .plugins import register_detection_plugins
+from .registry import (
+ DetectionModelFactory,
+ DetectionModelRegistry,
+ DetectionModelRegistryError,
+ DetectionModelSpec,
+ DuplicateDetectionModelError,
+ UnknownDetectionModelError,
+)
+from .rules import DetectionTriggerOperator
+from .video import VideoDecoderOperator
+
+__all__ = [
+ "CONSTRUCTION_PPE_LABELS",
+ "PPE_6CLASS_LABELS",
+ "DetectionModel",
+ "DetectionModelFactory",
+ "DetectionModelRegistry",
+ "DetectionModelRegistryError",
+ "DetectionModelSpec",
+ "DetectionOperator",
+ "DetectionTriggerOperator",
+ "DetectorParams",
+ "DuplicateDetectionModelError",
+ "UnknownDetectionModelError",
+ "VideoDecoderOperator",
+ "register_builtin_detection_models",
+ "register_detection_plugins",
+]
diff --git a/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..723c5af
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..19c075a
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-310.pyc
new file mode 100644
index 0000000..90a1b35
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-312.pyc
new file mode 100644
index 0000000..ff3b813
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/annotation.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/base.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/base.cpython-310.pyc
new file mode 100644
index 0000000..46a1cf0
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/base.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/base.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..99647f2
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/base.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-310.pyc
new file mode 100644
index 0000000..dbe8258
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-312.pyc
new file mode 100644
index 0000000..e0608fa
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/operator.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-310.pyc
new file mode 100644
index 0000000..1d6af07
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-312.pyc
new file mode 100644
index 0000000..4f571ba
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/plugins.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-310.pyc
new file mode 100644
index 0000000..75d8566
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-312.pyc
new file mode 100644
index 0000000..42bb807
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/registry.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-310.pyc
new file mode 100644
index 0000000..86d3a13
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-312.pyc
new file mode 100644
index 0000000..aec7f37
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/rules.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/video.cpython-310.pyc b/src/cmvr_edge_ai/detection/__pycache__/video.cpython-310.pyc
new file mode 100644
index 0000000..6939ee2
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/video.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/__pycache__/video.cpython-312.pyc b/src/cmvr_edge_ai/detection/__pycache__/video.cpython-312.pyc
new file mode 100644
index 0000000..d529fd1
Binary files /dev/null and b/src/cmvr_edge_ai/detection/__pycache__/video.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/annotation.py b/src/cmvr_edge_ai/detection/annotation.py
new file mode 100644
index 0000000..6be6b3d
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/annotation.py
@@ -0,0 +1,176 @@
+"""On-demand bounding-box rendering for detection alerts."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from importlib import import_module
+from io import BytesIO
+import math
+from typing import Any
+
+from cmvr_edge_ai.contracts import Detection, EncodedImage, ImageFrame
+
+
+class ImageAnnotationDependencyError(RuntimeError):
+ """Raised when the optional image annotation runtime is unavailable."""
+
+
+class ImageAnnotationError(ValueError):
+ """Raised when an input frame cannot be rendered safely."""
+
+
+def ensure_annotation_support() -> None:
+ """Fail early with an actionable message when Pillow is not installed."""
+
+ _pillow_modules()
+
+
+def render_annotated_jpeg(
+ frame: ImageFrame,
+ detections: Sequence[Detection],
+ *,
+ jpeg_quality: int,
+) -> EncodedImage:
+ """Draw valid detection boxes and encode one decoded frame as JPEG."""
+
+ if isinstance(jpeg_quality, bool) or not isinstance(jpeg_quality, int):
+ raise ImageAnnotationError("jpeg_quality must be an integer")
+ if not 1 <= jpeg_quality <= 95:
+ raise ImageAnnotationError("jpeg_quality must be between 1 and 95")
+ _validate_frame(frame)
+ image_module, image_draw_module = _pillow_modules()
+
+ raw_mode = "BGR" if frame.pixel_format.upper() == "BGR8" else "RGB"
+ image = image_module.frombytes(
+ "RGB",
+ (frame.width, frame.height),
+ frame.data,
+ "raw",
+ raw_mode,
+ )
+ draw = image_draw_module.Draw(image)
+ for detection in detections:
+ if not isinstance(detection, Detection):
+ raise ImageAnnotationError(
+ "detections must contain Detection instances"
+ )
+ box = _clipped_box(detection, frame.width, frame.height)
+ if box is None:
+ # One corrupt backend box must not prevent the other evidence boxes
+ # or the structured safety alert from being produced.
+ continue
+ color = _label_color(detection.label)
+ draw.rectangle(box, outline=color, width=3)
+ label = f"{detection.label} {detection.confidence:.2f}"
+ text_box = draw.textbbox((box[0], box[1]), label)
+ text_width = max(1, text_box[2] - text_box[0])
+ text_height = max(1, text_box[3] - text_box[1])
+ text_y = max(0.0, box[1] - text_height - 4)
+ background = (
+ box[0],
+ text_y,
+ min(float(frame.width - 1), box[0] + text_width + 4),
+ min(float(frame.height - 1), text_y + text_height + 4),
+ )
+ draw.rectangle(background, fill=color)
+ draw.text((box[0] + 2, text_y + 2), label, fill=(255, 255, 255))
+
+ output = BytesIO()
+ image.save(output, format="JPEG", quality=jpeg_quality)
+ return EncodedImage(
+ data=output.getvalue(),
+ media_type="image/jpeg",
+ width=frame.width,
+ height=frame.height,
+ )
+
+
+def _pillow_modules() -> tuple[Any, Any]:
+ try:
+ return import_module("PIL.Image"), import_module("PIL.ImageDraw")
+ except ImportError as exc:
+ raise ImageAnnotationDependencyError(
+ "alert images require Pillow; install the 'image' extra "
+ "(uv sync --extra image)"
+ ) from exc
+
+
+def _validate_frame(frame: ImageFrame) -> None:
+ if not isinstance(frame, ImageFrame):
+ raise ImageAnnotationError(
+ f"expected ImageFrame, got {type(frame).__name__}"
+ )
+ if not isinstance(frame.codec, str):
+ raise ImageAnnotationError("frame codec must be a string")
+ if frame.codec.lower() not in {"", "none", "raw"}:
+ raise ImageAnnotationError("alert images require a decoded ImageFrame")
+ if not isinstance(frame.pixel_format, str) or frame.pixel_format.upper() not in {
+ "BGR8",
+ "RGB8",
+ }:
+ raise ImageAnnotationError(
+ "alert images support only packed BGR8 or RGB8 frames"
+ )
+ if (
+ isinstance(frame.width, bool)
+ or not isinstance(frame.width, int)
+ or frame.width < 1
+ or isinstance(frame.height, bool)
+ or not isinstance(frame.height, int)
+ or frame.height < 1
+ ):
+ raise ImageAnnotationError("frame dimensions must be positive integers")
+ expected_size = frame.width * frame.height * 3
+ if not isinstance(frame.data, bytes):
+ raise ImageAnnotationError("packed frame buffer must be bytes")
+ if len(frame.data) != expected_size:
+ raise ImageAnnotationError(
+ f"packed frame buffer has {len(frame.data)} bytes; "
+ f"expected {expected_size}"
+ )
+
+
+def _clipped_box(
+ detection: Detection,
+ width: int,
+ height: int,
+) -> tuple[float, float, float, float] | None:
+ values = (
+ detection.box.x_min,
+ detection.box.y_min,
+ detection.box.x_max,
+ detection.box.y_max,
+ )
+ if any(
+ isinstance(value, bool)
+ or not isinstance(value, (int, float))
+ or not math.isfinite(float(value))
+ for value in values
+ ):
+ return None
+ x_min, y_min, x_max, y_max = (float(value) for value in values)
+ if x_max <= x_min or y_max <= y_min:
+ return None
+ clipped = (
+ min(max(x_min, 0.0), float(width - 1)),
+ min(max(y_min, 0.0), float(height - 1)),
+ min(max(x_max, 0.0), float(width - 1)),
+ min(max(y_max, 0.0), float(height - 1)),
+ )
+ if clipped[2] <= clipped[0] or clipped[3] <= clipped[1]:
+ return None
+ return clipped
+
+
+def _label_color(label: str) -> tuple[int, int, int]:
+ # Stable across processes unlike Python's randomized hash().
+ seed = sum((index + 1) * byte for index, byte in enumerate(label.encode("utf-8")))
+ return (64 + seed % 192, 64 + (seed * 5) % 192, 64 + (seed * 11) % 192)
+
+
+__all__ = [
+ "ImageAnnotationDependencyError",
+ "ImageAnnotationError",
+ "ensure_annotation_support",
+ "render_annotated_jpeg",
+]
diff --git a/src/cmvr_edge_ai/detection/base.py b/src/cmvr_edge_ai/detection/base.py
new file mode 100644
index 0000000..0c3224a
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/base.py
@@ -0,0 +1,50 @@
+"""Backend-neutral abstraction for object-detection models."""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+
+from cmvr_edge_ai.contracts import Detection, ImageFrame
+
+
+class DetectionModel(ABC):
+ """A loaded detection model owned by one pipeline component.
+
+ The interface is deliberately synchronous because most inference SDKs expose
+ blocking APIs. Pipeline operators must offload these methods to the shared
+ thread executor (or to an explicitly owned process worker) instead of calling
+ them directly on the asyncio event loop.
+ """
+
+ @abstractmethod
+ def load(self) -> None:
+ """Load weights and allocate model runtime resources."""
+
+ raise NotImplementedError
+
+ @abstractmethod
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ """Detect selected ``labels`` in one decoded image frame.
+
+ Implementations may use the selected labels to reduce backend
+ post-processing, but callers still validate/filter returned detections at
+ the common operator boundary. ``confidence`` is in the inclusive
+ ``[0, 1]`` range.
+ """
+
+ raise NotImplementedError
+
+ @abstractmethod
+ def close(self) -> None:
+ """Release model resources; implementations should be idempotent."""
+
+ raise NotImplementedError
+
+
+__all__ = ["DetectionModel"]
diff --git a/src/cmvr_edge_ai/detection/models/__init__.py b/src/cmvr_edge_ai/detection/models/__init__.py
new file mode 100644
index 0000000..8118766
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/models/__init__.py
@@ -0,0 +1,17 @@
+"""Built-in detection model adapters and artifact registrations."""
+
+from .ppe_yolo import (
+ CONSTRUCTION_PPE_LABELS,
+ PPE_6CLASS_LABELS,
+ register_builtin_detection_models,
+)
+from .yolo import UltralyticsYoloModel, YoloDependencyError, YoloModelOptions
+
+__all__ = [
+ "CONSTRUCTION_PPE_LABELS",
+ "PPE_6CLASS_LABELS",
+ "UltralyticsYoloModel",
+ "YoloDependencyError",
+ "YoloModelOptions",
+ "register_builtin_detection_models",
+]
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..28e3681
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..af90f71
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-310.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-310.pyc
new file mode 100644
index 0000000..69ef277
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-312.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-312.pyc
new file mode 100644
index 0000000..c5b61de
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/ppe_yolo.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-310.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-310.pyc
new file mode 100644
index 0000000..25421db
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-312.pyc b/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-312.pyc
new file mode 100644
index 0000000..e97719c
Binary files /dev/null and b/src/cmvr_edge_ai/detection/models/__pycache__/yolo.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/detection/models/ppe_yolo.py b/src/cmvr_edge_ai/detection/models/ppe_yolo.py
new file mode 100644
index 0000000..9dc6c02
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/models/ppe_yolo.py
@@ -0,0 +1,96 @@
+"""Registration metadata for the built-in PPE YOLOv8 checkpoints."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any
+
+from ..base import DetectionModel
+from ..registry import DetectionModelRegistry, DetectionModelSpec
+from .yolo import UltralyticsYoloModel
+
+
+CONSTRUCTION_PPE_LABELS = (
+ "Boots",
+ "Ear-Protection",
+ "Glass",
+ "Glove",
+ "Hard_hat",
+ "Mask",
+ "No-Boots",
+ "No-Ear-Protection",
+ "No-Glass",
+ "No-Glove",
+ "No-Helmet",
+ "No-Mask",
+ "No-Vest",
+ "Worker",
+ "Vest",
+ "Circular_Saw",
+ "Fire_Extinguisher",
+ "Fire_prevention_Net",
+ "Welding_Equipment",
+)
+
+PPE_6CLASS_LABELS = (
+ "Gloves",
+ "Vest",
+ "goggles",
+ "helmet",
+ "mask",
+ "safety_shoe",
+)
+
+
+def _create_construction_ppe_model(
+ params: Mapping[str, Any],
+) -> DetectionModel:
+ return UltralyticsYoloModel(
+ params,
+ expected_labels=CONSTRUCTION_PPE_LABELS,
+ )
+
+
+def _create_ppe_6classes_model(
+ params: Mapping[str, Any],
+) -> DetectionModel:
+ return UltralyticsYoloModel(
+ params,
+ expected_labels=PPE_6CLASS_LABELS,
+ )
+
+
+def register_builtin_detection_models(registry: DetectionModelRegistry) -> None:
+ registry.register(
+ DetectionModelSpec(
+ model_id="construction-ppe-yolov8@1",
+ name="Construction PPE YOLOv8s",
+ supported_labels=CONSTRUCTION_PPE_LABELS,
+ factory=_create_construction_ppe_model,
+ backend="ultralytics-yolo",
+ description=(
+ "Construction-site worker and personal protective equipment "
+ "detector with positive and No-* violation labels"
+ ),
+ )
+ )
+ registry.register(
+ DetectionModelSpec(
+ model_id="ppe-6classes-yolov8n@1",
+ name="PPE Detection YOLOv8n (6 Classes)",
+ supported_labels=PPE_6CLASS_LABELS,
+ factory=_create_ppe_6classes_model,
+ backend="ultralytics-yolo",
+ description=(
+ "Lightweight six-class personal protective equipment detector "
+ "for gloves, vests, goggles, helmets, masks and safety shoes"
+ ),
+ )
+ )
+
+
+__all__ = [
+ "CONSTRUCTION_PPE_LABELS",
+ "PPE_6CLASS_LABELS",
+ "register_builtin_detection_models",
+]
diff --git a/src/cmvr_edge_ai/detection/models/yolo.py b/src/cmvr_edge_ai/detection/models/yolo.py
new file mode 100644
index 0000000..1aff00d
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/models/yolo.py
@@ -0,0 +1,209 @@
+"""Ultralytics YOLO adapter for registered detection model artifacts."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from importlib import import_module
+from pathlib import Path
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field, field_validator
+
+from cmvr_edge_ai.contracts import BoundingBox, Detection, ImageFrame
+
+from ..base import DetectionModel
+
+
+class YoloDependencyError(RuntimeError):
+ """Raised when the optional YOLO runtime has not been installed."""
+
+
+class YoloModelOptions(BaseModel):
+ model_config = ConfigDict(extra="forbid", frozen=True)
+
+ weights: str = Field(min_length=1)
+ device: int | str = "cpu"
+ imgsz: int = Field(default=640, ge=32, le=8192)
+ iou: float = Field(default=0.7, ge=0.0, le=1.0)
+ half: bool = False
+ max_det: int = Field(default=300, ge=1, le=100_000)
+ agnostic_nms: bool = False
+
+ @field_validator("weights")
+ @classmethod
+ def normalize_weights(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("weights must not be empty")
+ return value
+
+ @field_validator("device")
+ @classmethod
+ def validate_device(cls, value: int | str) -> int | str:
+ if isinstance(value, int):
+ if value < 0:
+ raise ValueError("device must be non-negative")
+ return value
+ value = value.strip()
+ if not value:
+ raise ValueError("device must not be empty")
+ return value
+
+
+class UltralyticsYoloModel(DetectionModel):
+ """Normalize an Ultralytics detector behind the framework model API."""
+
+ def __init__(
+ self,
+ params: Mapping[str, Any],
+ *,
+ expected_labels: tuple[str, ...],
+ ) -> None:
+ try:
+ self._options = YoloModelOptions.model_validate(dict(params))
+ except Exception as exc:
+ raise ValueError(f"invalid YOLO model options: {exc}") from exc
+ self._expected_labels = expected_labels
+ self._label_to_id = {
+ label: index for index, label in enumerate(self._expected_labels)
+ }
+ self._model: Any = None
+ self._numpy: Any = None
+
+ @property
+ def weights_path(self) -> Path:
+ return Path(self._options.weights).expanduser()
+
+ def load(self) -> None:
+ weights = self.weights_path
+ if not weights.is_file():
+ raise FileNotFoundError(f"YOLO weights do not exist: {weights}")
+ try:
+ np = import_module("numpy")
+ YOLO = getattr(import_module("ultralytics"), "YOLO")
+ except ImportError as exc:
+ raise YoloDependencyError(
+ "YOLO support is not installed; install "
+ "cmvr-edge-ai[yolo]. The PPE checkpoint was created with "
+ "Ultralytics 8.4.31, so validate the deployed 8.x runtime."
+ ) from exc
+
+ # Ultralytics .pt files use the PyTorch checkpoint loader. Deploy only
+ # weights obtained from a trusted build/artifact source.
+ model = YOLO(str(weights), task="detect")
+ actual_labels = _ordered_model_names(model.names)
+ if actual_labels != self._expected_labels:
+ raise ValueError(
+ "YOLO checkpoint labels do not match the registered model: "
+ f"expected {self._expected_labels!r}, got {actual_labels!r}"
+ )
+ self._numpy = np
+ self._model = model
+
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ if self._model is None or self._numpy is None:
+ raise RuntimeError("YOLO model has not been loaded")
+ if frame.is_encoded:
+ raise ValueError("YOLO requires a decoded ImageFrame")
+ pixel_format = frame.pixel_format.strip().upper()
+ if pixel_format not in {"BGR8", "RGB8"}:
+ raise ValueError(
+ f"YOLO adapter supports BGR8 or RGB8, got {frame.pixel_format!r}"
+ )
+ expected_size = frame.width * frame.height * 3
+ if frame.width <= 0 or frame.height <= 0 or len(frame.data) != expected_size:
+ raise ValueError(
+ "decoded frame buffer size does not match width*height*3: "
+ f"got {len(frame.data)}, expected {expected_size}"
+ )
+
+ image = self._numpy.frombuffer(frame.data, dtype=self._numpy.uint8).reshape(
+ (frame.height, frame.width, 3)
+ )
+ if pixel_format == "RGB8":
+ # Ultralytics ndarray inputs follow the OpenCV BGR convention.
+ image = image[:, :, ::-1]
+
+ class_ids = [self._label_to_id[label] for label in labels]
+ results = self._model.predict(
+ source=image,
+ classes=class_ids,
+ conf=confidence,
+ iou=self._options.iou,
+ imgsz=self._options.imgsz,
+ device=self._options.device,
+ half=self._options.half,
+ max_det=self._options.max_det,
+ agnostic_nms=self._options.agnostic_nms,
+ verbose=False,
+ )
+ if not results:
+ return ()
+ boxes = results[0].boxes
+ if boxes is None:
+ return ()
+
+ xyxy_rows = _to_python_list(boxes.xyxy)
+ confidences = _to_python_list(boxes.conf)
+ class_indexes = _to_python_list(boxes.cls)
+ detections: list[Detection] = []
+ selected = frozenset(labels)
+ for coordinates, score, class_index in zip(
+ xyxy_rows, confidences, class_indexes
+ ):
+ index = int(class_index)
+ if not 0 <= index < len(self._expected_labels):
+ raise ValueError(f"YOLO returned an unknown class id: {index}")
+ label = self._expected_labels[index]
+ if label not in selected:
+ continue
+ if len(coordinates) != 4:
+ raise ValueError("YOLO xyxy result must contain four coordinates")
+ detections.append(
+ Detection(
+ label=label,
+ confidence=float(score),
+ box=BoundingBox(*(float(value) for value in coordinates)),
+ )
+ )
+ return tuple(detections)
+
+ def close(self) -> None:
+ self._model = None
+ self._numpy = None
+
+
+def _ordered_model_names(value: Any) -> tuple[str, ...]:
+ if isinstance(value, Mapping):
+ try:
+ indexes = sorted(int(key) for key in value)
+ if indexes != list(range(len(indexes))):
+ raise ValueError("class IDs must be contiguous and start at zero")
+ return tuple(str(value[index]) for index in indexes)
+ except (KeyError, TypeError, ValueError) as exc:
+ raise ValueError(f"invalid YOLO model.names mapping: {value!r}") from exc
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
+ return tuple(str(label) for label in value)
+ raise ValueError(f"invalid YOLO model.names value: {value!r}")
+
+
+def _to_python_list(value: Any) -> list[Any]:
+ if hasattr(value, "detach"):
+ value = value.detach()
+ if hasattr(value, "cpu"):
+ value = value.cpu()
+ if hasattr(value, "tolist"):
+ value = value.tolist()
+ return list(value)
+
+
+__all__ = [
+ "UltralyticsYoloModel",
+ "YoloDependencyError",
+ "YoloModelOptions",
+]
diff --git a/src/cmvr_edge_ai/detection/operator.py b/src/cmvr_edge_ai/detection/operator.py
new file mode 100644
index 0000000..63b1649
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/operator.py
@@ -0,0 +1,374 @@
+"""Model-independent detection pipeline operator."""
+
+from __future__ import annotations
+
+import logging
+import math
+from collections import Counter
+from collections.abc import Mapping, Sequence
+from concurrent.futures import Executor
+from time import monotonic_ns, perf_counter
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
+
+from cmvr_edge_ai.contracts import Detection, DetectionResult, ImageFrame
+from cmvr_edge_ai.core.component import (
+ ComponentContext,
+ Emission,
+ HealthReport,
+ Operator,
+)
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.workers import run_blocking
+
+from .base import DetectionModel
+from .registry import DetectionModelRegistry, DetectionModelSpec
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DetectorParams(BaseModel):
+ """Strict deployment parameters shared by every detection backend."""
+
+ model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
+
+ model: str = Field(min_length=1)
+ detect_labels: tuple[str, ...] | None = None
+ confidence: float = Field(default=0.25, ge=0.0, le=1.0)
+ label_confidence: dict[str, float] = Field(default_factory=dict)
+ model_options: dict[str, Any] = Field(default_factory=dict)
+ max_fps: float | None = Field(default=None, gt=0.0, le=1000.0)
+ attach_frame: bool = Field(default=False, strict=True)
+ inference_log_interval_s: float | None = Field(
+ default=None,
+ gt=0.0,
+ le=3600.0,
+ )
+
+ @field_validator("model")
+ @classmethod
+ def normalize_model(cls, value: str) -> str:
+ value = value.strip()
+ if not value:
+ raise ValueError("model must not be empty")
+ return value
+
+ @field_validator("inference_log_interval_s", mode="before")
+ @classmethod
+ def reject_boolean_log_interval(cls, value: Any) -> Any:
+ if isinstance(value, bool):
+ raise ValueError("inference_log_interval_s must be a number of seconds")
+ return value
+
+ @field_validator("detect_labels")
+ @classmethod
+ def validate_detect_labels(
+ cls, value: tuple[str, ...] | None
+ ) -> tuple[str, ...] | None:
+ if value is None:
+ return None
+ if not value:
+ raise ValueError("detect_labels must not be empty; omit it to detect all")
+ normalized = tuple(label.strip() for label in value)
+ if any(not label for label in normalized):
+ raise ValueError("detect_labels must contain non-empty strings")
+ if len(set(normalized)) != len(normalized):
+ raise ValueError("detect_labels must not contain duplicates")
+ return normalized
+
+ @field_validator("label_confidence")
+ @classmethod
+ def validate_label_confidence(cls, value: dict[str, float]) -> dict[str, float]:
+ normalized: dict[str, float] = {}
+ for label, confidence in value.items():
+ clean_label = label.strip()
+ if not clean_label:
+ raise ValueError("label_confidence keys must not be empty")
+ parsed = float(confidence)
+ if not 0.0 <= parsed <= 1.0:
+ raise ValueError(
+ f"label_confidence.{clean_label} must be between 0 and 1"
+ )
+ normalized[clean_label] = parsed
+ return normalized
+
+ @model_validator(mode="after")
+ def validate_label_threshold_scope(self) -> DetectorParams:
+ if self.detect_labels is not None:
+ unknown = set(self.label_confidence) - set(self.detect_labels)
+ if unknown:
+ raise ValueError(
+ "label_confidence contains labels not selected by detect_labels: "
+ + ", ".join(sorted(unknown))
+ )
+ return self
+
+
+class DetectionOperator(Operator):
+ """Load one registered model and normalize its per-frame detections."""
+
+ def __init__(
+ self,
+ node_id: str,
+ params: Mapping[str, Any],
+ *,
+ model_registry: DetectionModelRegistry,
+ ) -> None:
+ self._node_id = node_id
+ try:
+ self._params = DetectorParams.model_validate(dict(params))
+ except Exception as exc:
+ raise ValueError(f"invalid detection node parameters: {exc}") from exc
+
+ self._spec = model_registry.resolve(self._params.model)
+ selected = (
+ self._spec.supported_labels
+ if self._params.detect_labels is None
+ else self._params.detect_labels
+ )
+ unsupported = set(selected) - set(self._spec.supported_labels)
+ if unsupported:
+ raise ValueError(
+ f"model {self._spec.model_id!r} does not support label(s): "
+ + ", ".join(sorted(unsupported))
+ )
+ invalid_thresholds = set(self._params.label_confidence) - set(
+ self._spec.supported_labels
+ )
+ if invalid_thresholds:
+ raise ValueError(
+ f"model {self._spec.model_id!r} does not support threshold label(s): "
+ + ", ".join(sorted(invalid_thresholds))
+ )
+
+ self._selected_labels = tuple(selected)
+ self._selected_label_set = frozenset(selected)
+ self._minimum_backend_confidence = min(
+ (
+ self._params.label_confidence.get(label, self._params.confidence)
+ for label in self._selected_labels
+ ),
+ default=self._params.confidence,
+ )
+ self._model: DetectionModel = self._spec.factory(self._params.model_options)
+ if not isinstance(self._model, DetectionModel):
+ raise TypeError(
+ f"model factory for {self._spec.model_id!r} returned "
+ f"{type(self._model).__name__}, expected DetectionModel"
+ )
+
+ self._executor: Executor | None = None
+ self._loaded = False
+ self._closed = False
+ self._last_started_ns: int | None = None
+ self._processed = 0
+ self._skipped_rate_limit = 0
+ self._detections = 0
+ self._last_inference_log_ns: int | None = None
+ self._log_window_frames = 0
+ self._log_window_detections = 0
+ self._log_window_inference_ms = 0.0
+ self._log_window_labels: Counter[str] = Counter()
+
+ @property
+ def model_spec(self) -> DetectionModelSpec:
+ return self._spec
+
+ @property
+ def selected_labels(self) -> tuple[str, ...]:
+ return self._selected_labels
+
+ @property
+ def attach_frame(self) -> bool:
+ return self._params.attach_frame
+
+ async def setup(self, context: ComponentContext) -> None:
+ if self._closed:
+ raise RuntimeError("detection operator is already closed")
+ executor = context.metadata.get("thread_executor")
+ if executor is not None and not isinstance(executor, Executor):
+ raise TypeError("component context thread_executor is not an Executor")
+ self._executor = executor
+ load_started = perf_counter()
+ await run_blocking(self._model.load, executor=self._executor)
+ load_ms = (perf_counter() - load_started) * 1000.0
+ self._loaded = True
+ _LOGGER.info(
+ "detection model loaded node=%s model=%s name=%s backend=%s "
+ "labels=%s load_ms=%.2f",
+ self._node_id,
+ self._spec.model_id,
+ self._spec.name,
+ self._spec.backend,
+ ",".join(self._selected_labels),
+ load_ms,
+ )
+
+ async def process(
+ self, envelope: Envelope[Any], input_port: str = "frames"
+ ) -> Emission | None:
+ del input_port
+ if not self._loaded:
+ raise RuntimeError("detection model has not been loaded")
+ frame = envelope.payload
+ if not isinstance(frame, ImageFrame):
+ raise TypeError(
+ f"{self._node_id} expected ImageFrame, got {type(frame).__name__}"
+ )
+ if frame.is_encoded:
+ raise ValueError(
+ f"{self._node_id} requires a decoded ImageFrame; got codec "
+ f"{frame.codec!r}"
+ )
+
+ now = monotonic_ns()
+ if self._params.max_fps is not None and self._last_started_ns is not None:
+ minimum_interval_ns = int(1_000_000_000 / self._params.max_fps)
+ if now - self._last_started_ns < minimum_interval_ns:
+ self._skipped_rate_limit += 1
+ return None
+ self._last_started_ns = now
+
+ started = perf_counter()
+ predicted = await run_blocking(
+ self._model.predict,
+ frame,
+ labels=self._selected_labels,
+ confidence=self._minimum_backend_confidence,
+ executor=self._executor,
+ )
+ inference_ms = (perf_counter() - started) * 1000.0
+ detections = self._filter_detections(predicted)
+ self._processed += 1
+ self._detections += len(detections)
+ self._record_inference_log(
+ envelope,
+ detections,
+ inference_ms=inference_ms,
+ now_ns=now,
+ )
+
+ result = DetectionResult(
+ detections=detections,
+ model_id=self._spec.model_id,
+ inference_ms=inference_ms,
+ model_name=self._spec.name,
+ source_frame=frame if self._params.attach_frame else None,
+ )
+ output = envelope.with_payload(
+ result,
+ schema_name="DetectionResult",
+ schema_version=1,
+ ).with_attributes(
+ detection_model_id=self._spec.model_id,
+ detection_model_name=self._spec.name,
+ )
+ return Emission("detections", output)
+
+ async def stop(self) -> None:
+ if self._closed:
+ return
+ self._closed = True
+ try:
+ # close() is required to be idempotent and is also called after a
+ # partially failed load so native allocations are not leaked.
+ await run_blocking(self._model.close, executor=self._executor)
+ finally:
+ self._loaded = False
+
+ async def health(self) -> HealthReport:
+ return HealthReport(
+ healthy=self._loaded,
+ detail=(
+ f"model={self._spec.model_id} loaded={self._loaded} "
+ f"frames={self._processed} detections={self._detections} "
+ f"rate_skipped={self._skipped_rate_limit}"
+ ),
+ )
+
+ def _record_inference_log(
+ self,
+ envelope: Envelope[Any],
+ detections: tuple[Detection, ...],
+ *,
+ inference_ms: float,
+ now_ns: int,
+ ) -> None:
+ interval_s = self._params.inference_log_interval_s
+ if interval_s is None:
+ return
+
+ self._log_window_frames += 1
+ self._log_window_detections += len(detections)
+ self._log_window_inference_ms += inference_ms
+ self._log_window_labels.update(item.label for item in detections)
+
+ interval_ns = int(interval_s * 1_000_000_000)
+ if (
+ self._last_inference_log_ns is not None
+ and now_ns - self._last_inference_log_ns < interval_ns
+ ):
+ return
+
+ hit_labels = ",".join(
+ f"{label}:{count}"
+ for label, count in sorted(self._log_window_labels.items())
+ ) or "none"
+ average_inference_ms = (
+ self._log_window_inference_ms / self._log_window_frames
+ )
+ _LOGGER.info(
+ "detection inference node=%s model=%s total_frames=%s "
+ "window_frames=%s window_detections=%s hit_labels=%s "
+ "avg_inference_ms=%.2f rate_skipped=%s source=%s sequence=%s",
+ self._node_id,
+ self._spec.model_id,
+ self._processed,
+ self._log_window_frames,
+ self._log_window_detections,
+ hit_labels,
+ average_inference_ms,
+ self._skipped_rate_limit,
+ envelope.source_id,
+ envelope.sequence,
+ )
+ self._last_inference_log_ns = now_ns
+ self._log_window_frames = 0
+ self._log_window_detections = 0
+ self._log_window_inference_ms = 0.0
+ self._log_window_labels.clear()
+
+ def _filter_detections(
+ self, predicted: Sequence[Detection]
+ ) -> tuple[Detection, ...]:
+ normalized: list[Detection] = []
+ for detection in predicted:
+ if not isinstance(detection, Detection):
+ raise TypeError(
+ f"model {self._spec.model_id!r} returned "
+ f"{type(detection).__name__}, expected Detection"
+ )
+ if detection.label not in self._spec.supported_labels:
+ raise ValueError(
+ f"model {self._spec.model_id!r} returned unregistered label "
+ f"{detection.label!r}"
+ )
+ if detection.label not in self._selected_label_set:
+ continue
+ if not math.isfinite(detection.confidence) or not (
+ 0.0 <= detection.confidence <= 1.0
+ ):
+ raise ValueError(
+ f"model returned invalid confidence {detection.confidence!r}"
+ )
+ threshold = self._params.label_confidence.get(
+ detection.label, self._params.confidence
+ )
+ if detection.confidence >= threshold:
+ normalized.append(detection)
+ return tuple(normalized)
+
+
+__all__ = ["DetectionOperator", "DetectorParams"]
diff --git a/src/cmvr_edge_ai/detection/plugins.py b/src/cmvr_edge_ai/detection/plugins.py
new file mode 100644
index 0000000..0a3662c
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/plugins.py
@@ -0,0 +1,62 @@
+"""Pipeline plugin registrations for model-independent detection nodes."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any
+
+from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec
+
+from .operator import DetectionOperator
+from .registry import DetectionModelRegistry
+from .rules import DetectionTriggerOperator
+from .video import VideoDecoderOperator
+
+
+def register_detection_plugins(
+ registry: PluginRegistry,
+ model_registry: DetectionModelRegistry,
+) -> None:
+ def detector_factory(node_id: str, params: Mapping[str, Any]) -> DetectionOperator:
+ return DetectionOperator(
+ node_id,
+ params,
+ model_registry=model_registry,
+ )
+
+ registry.register(
+ PluginSpec(
+ plugin_id="media.video_decoder.pyav@1",
+ kind=PluginKind.OPERATOR,
+ factory=VideoDecoderOperator,
+ inputs={"frames": "ImageFrame/v1"},
+ outputs={"frames": "ImageFrame/v1"},
+ description="Stateful H264/H265 decoder backed by optional PyAV",
+ tags=frozenset({"media", "video", "decoder"}),
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="detection.model@1",
+ kind=PluginKind.OPERATOR,
+ factory=detector_factory,
+ inputs={"frames": "ImageFrame/v1"},
+ outputs={"detections": "DetectionResult/v1"},
+ description="Run a registered object-detection model",
+ tags=frozenset({"ai", "detection", "model"}),
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="detection.repeat_gate@1",
+ kind=PluginKind.OPERATOR,
+ factory=DetectionTriggerOperator,
+ inputs={"detections": "DetectionResult/v1"},
+ outputs={"alerts": "DetectionAlert/v1"},
+ description="Emit alerts after configurable repeated frame hits",
+ tags=frozenset({"detection", "rule", "temporal"}),
+ )
+ )
+
+
+__all__ = ["register_detection_plugins"]
diff --git a/src/cmvr_edge_ai/detection/registry.py b/src/cmvr_edge_ai/detection/registry.py
new file mode 100644
index 0000000..f2bdec3
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/registry.py
@@ -0,0 +1,154 @@
+"""Explicit registry for deployable object-detection model artifacts."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Mapping, Sequence
+from dataclasses import dataclass
+from importlib import metadata
+from typing import Any, cast
+
+from .base import DetectionModel
+
+
+class DetectionModelRegistryError(RuntimeError):
+ """Base class for detection model registration and lookup failures."""
+
+
+class DuplicateDetectionModelError(DetectionModelRegistryError):
+ """Raised when a model identifier is registered more than once."""
+
+
+class UnknownDetectionModelError(DetectionModelRegistryError):
+ """Raised when a requested model identifier has not been registered."""
+
+
+DetectionModelFactory = Callable[[Mapping[str, Any]], DetectionModel]
+
+
+@dataclass(frozen=True, slots=True)
+class DetectionModelSpec:
+ """Static metadata and factory for one versioned model artifact."""
+
+ model_id: str
+ name: str
+ supported_labels: tuple[str, ...]
+ factory: DetectionModelFactory
+ backend: str
+ description: str = ""
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.model_id, str) or "@" not in self.model_id:
+ raise ValueError(
+ "model id must be versioned (for example detector-name@1): "
+ f"{self.model_id}"
+ )
+ identifier, version = self.model_id.rsplit("@", maxsplit=1)
+ if not identifier or not version.isdigit() or int(version) < 1:
+ raise ValueError(
+ "model id must end in a positive numeric version: " f"{self.model_id}"
+ )
+
+ if not isinstance(self.name, str) or not self.name.strip():
+ raise ValueError("model name must be a non-empty string")
+ if not isinstance(self.backend, str) or not self.backend.strip():
+ raise ValueError("model backend must be a non-empty string")
+ if not isinstance(self.description, str):
+ raise TypeError("model description must be a string")
+ if not callable(self.factory):
+ raise TypeError("detection model factory must be callable")
+
+ labels_value = self.supported_labels
+ if not isinstance(labels_value, Sequence) or isinstance(
+ labels_value, (str, bytes, bytearray)
+ ):
+ raise TypeError("supported_labels must be an ordered collection")
+ labels = tuple(labels_value)
+ if not labels:
+ raise ValueError("supported_labels must not be empty")
+
+ invalid = [
+ repr(label)
+ for label in labels
+ if not isinstance(label, str) or not label.strip()
+ ]
+ if invalid:
+ raise ValueError(
+ "supported_labels must contain non-empty strings: " + ", ".join(invalid)
+ )
+ seen: set[str] = set()
+ duplicates: list[str] = []
+ for label in labels:
+ if label in seen and label not in duplicates:
+ duplicates.append(label)
+ seen.add(label)
+ if duplicates:
+ raise ValueError(
+ "supported_labels must be unique; duplicate(s): "
+ + ", ".join(duplicates)
+ )
+
+ object.__setattr__(self, "name", self.name.strip())
+ object.__setattr__(self, "backend", self.backend.strip())
+ object.__setattr__(self, "supported_labels", labels)
+
+
+class DetectionModelRegistry:
+ """In-memory model registry with optional Python entry-point discovery."""
+
+ ENTRY_POINT_GROUP = "cmvr_edge_ai.detection_models"
+
+ def __init__(self) -> None:
+ self._models: dict[str, DetectionModelSpec] = {}
+
+ def register(self, spec: DetectionModelSpec) -> DetectionModelSpec:
+ if not isinstance(spec, DetectionModelSpec):
+ raise TypeError("detection model registry accepts DetectionModelSpec")
+ if spec.model_id in self._models:
+ raise DuplicateDetectionModelError(
+ f"detection model already registered: {spec.model_id}"
+ )
+ self._models[spec.model_id] = spec
+ return spec
+
+ def resolve(self, model_id: str) -> DetectionModelSpec:
+ try:
+ return self._models[model_id]
+ except KeyError as exc:
+ available = ", ".join(sorted(self._models)) or ""
+ raise UnknownDetectionModelError(
+ f"unknown detection model {model_id!r}; available models: {available}"
+ ) from exc
+
+ def specs(self) -> tuple[DetectionModelSpec, ...]:
+ return tuple(self._models[key] for key in sorted(self._models))
+
+ def load_entry_points(self) -> None:
+ """Load model specs or registration callbacks from installed packages."""
+
+ discovered = cast(Any, metadata.entry_points())
+ selected = (
+ discovered.select(group=self.ENTRY_POINT_GROUP)
+ if hasattr(discovered, "select")
+ else discovered.get(self.ENTRY_POINT_GROUP, ())
+ )
+ for entry_point in selected:
+ loaded = entry_point.load()
+ if isinstance(loaded, DetectionModelSpec):
+ self.register(loaded)
+ elif callable(loaded):
+ loaded(self)
+ else:
+ raise DetectionModelRegistryError(
+ f"entry point {entry_point.name!r} must expose "
+ "DetectionModelSpec or a registry callback"
+ )
+
+
+__all__ = [
+ "DetectionModelFactory",
+ "DetectionModelRegistry",
+ "DetectionModelRegistryError",
+ "DetectionModelSpec",
+ "DuplicateDetectionModelError",
+ "UnknownDetectionModelError",
+]
diff --git a/src/cmvr_edge_ai/detection/rules.py b/src/cmvr_edge_ai/detection/rules.py
new file mode 100644
index 0000000..5bf9685
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/rules.py
@@ -0,0 +1,661 @@
+"""Stateful temporal rules for normalized object-detection results."""
+
+from __future__ import annotations
+
+from collections import deque
+from collections.abc import Mapping, Sequence
+from concurrent.futures import Executor
+from dataclasses import dataclass, field, replace
+import logging
+import math
+from typing import Any, Literal, cast
+from uuid import uuid4
+
+from cmvr_edge_ai.contracts import (
+ BoundingBox,
+ Detection,
+ DetectionAlert,
+ DetectionResult,
+ EncodedImage,
+ ImageFrame,
+)
+from cmvr_edge_ai.core import ComponentContext, Emission, Envelope, HealthReport, Operator
+from cmvr_edge_ai.workers import run_blocking
+
+from .annotation import ensure_annotation_support, render_annotated_jpeg
+
+
+TimeSource = Literal["auto", "captured", "received"]
+RuleScope = Literal["source", "track"]
+FrameKey = tuple[str, int]
+StateKey = tuple[str, str, str]
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True, slots=True)
+class DetectionTriggerRule:
+ """Validated deployment rule used by :class:`DetectionTriggerOperator`."""
+
+ rule_id: str
+ labels: tuple[str, ...]
+ min_hits: int
+ window_ms: float
+ cooldown_ms: float
+ min_confidence: float
+ scope: RuleScope
+
+ @property
+ def window_ns(self) -> int:
+ return int(self.window_ms * 1_000_000)
+
+ @property
+ def cooldown_ns(self) -> int:
+ return int(self.cooldown_ms * 1_000_000)
+
+
+@dataclass(frozen=True, slots=True)
+class AlertImageOptions:
+ enabled: bool = False
+ jpeg_quality: int = 85
+
+
+@dataclass(frozen=True, slots=True)
+class _Hit:
+ timestamp_ns: int
+ labels: tuple[str, ...]
+ max_confidence: float
+
+
+@dataclass(slots=True)
+class _TriggerState:
+ hits: deque[_Hit] = field(default_factory=deque)
+ # Kept independently from ``hits`` so replaying the threshold frame cannot
+ # immediately retrigger a zero-cooldown rule after its hit window resets.
+ seen_frames: dict[FrameKey, int] = field(default_factory=dict)
+ cooldown_until_ns: int | None = None
+ last_observed_ns: int | None = None
+
+
+class DetectionTriggerOperator(Operator):
+ """Emit alerts after distinct frames satisfy temporal detection rules.
+
+ A hit is one matching *frame* for one rule and scope, regardless of how
+ many matching boxes that frame contains. Source-scoped rules aggregate a
+ camera stream. Track-scoped rules maintain independent windows per
+ ``track_id`` and ignore detections for which no track ID is available.
+
+ Once a rule triggers, its hit window is cleared. Frames observed during
+ cooldown are deliberately not accumulated; a fresh ``min_hits`` must be
+ observed after cooldown expires. Recent frame identities remain cached
+ for one window to prevent duplicate/replayed envelopes from being counted.
+ """
+
+ _PARAM_KEYS = frozenset({"rules", "time_source", "alert_image"})
+ _RULE_KEYS = frozenset(
+ {
+ "id",
+ "labels",
+ "min_hits",
+ "window_ms",
+ "cooldown_ms",
+ "min_confidence",
+ "scope",
+ }
+ )
+
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ if not isinstance(params, Mapping):
+ raise TypeError("detection trigger params must be a mapping")
+ unknown = set(params) - self._PARAM_KEYS
+ if unknown:
+ raise ValueError(
+ "unknown detection trigger parameter(s): "
+ + ", ".join(sorted(str(item) for item in unknown))
+ )
+
+ self._node_id = node_id
+ self._time_source = _parse_time_source(params.get("time_source", "auto"))
+ self._alert_image = _parse_alert_image(params.get("alert_image"))
+ self._rules = _parse_rules(params.get("rules"))
+ self._rules_by_id = {rule.rule_id: rule for rule in self._rules}
+ self._states: dict[StateKey, _TriggerState] = {}
+ self._executor: Executor | None = None
+
+ self._processed_frames = 0
+ self._alerts_emitted = 0
+ self._cooldown_suppressed = 0
+ self._duplicate_frames = 0
+ self._stale_frames = 0
+ self._untracked_detections = 0
+ self._invalid_detections = 0
+ self._alert_images_encoded = 0
+ self._alert_images_attached = 0
+ self._alert_image_missing_frames = 0
+ self._alert_image_failures = 0
+
+ @property
+ def rules(self) -> tuple[DetectionTriggerRule, ...]:
+ """Return the immutable, validated rules (primarily for diagnostics)."""
+
+ return self._rules
+
+ @property
+ def alert_image_enabled(self) -> bool:
+ return self._alert_image.enabled
+
+ async def setup(self, context: ComponentContext) -> None:
+ executor = context.metadata.get("thread_executor")
+ if executor is not None and not isinstance(executor, Executor):
+ raise TypeError("component context thread_executor is not an Executor")
+ self._executor = executor
+ if self._alert_image.enabled:
+ # Configuration errors should be discovered before camera traffic
+ # starts, while individual corrupt frames remain fail-soft below.
+ ensure_annotation_support()
+
+ async def process(
+ self,
+ envelope: Envelope[Any],
+ input_port: str = "input",
+ ) -> tuple[Emission, ...] | None:
+ del input_port
+ result = envelope.payload
+ if not isinstance(result, DetectionResult):
+ raise TypeError(
+ f"{self._node_id} expected DetectionResult, "
+ f"got {type(result).__name__}"
+ )
+
+ timestamp_ns = self._timestamp_for(envelope)
+ self._processed_frames += 1
+ frame_key = (envelope.source_id, envelope.sequence)
+ emissions: list[Emission] = []
+ detections, invalid_count = _wire_safe_detections(result.detections)
+ if invalid_count:
+ self._invalid_detections += invalid_count
+ _LOGGER.warning(
+ "ignored invalid detections node=%s source=%s sequence=%s count=%s",
+ self._node_id,
+ envelope.source_id,
+ envelope.sequence,
+ invalid_count,
+ )
+
+ for rule in self._rules:
+ matching = _matching_detections(detections, rule)
+ if not matching:
+ continue
+
+ if rule.scope == "source":
+ emission = self._observe(
+ rule=rule,
+ scope_id=envelope.source_id,
+ source_id=envelope.source_id,
+ frame_key=frame_key,
+ timestamp_ns=timestamp_ns,
+ detections=matching,
+ result=result,
+ envelope=envelope,
+ )
+ if emission is not None:
+ emissions.append(emission)
+ continue
+
+ by_track: dict[str, list[Detection]] = {}
+ for detection in matching:
+ if detection.track_id is None or not str(detection.track_id):
+ self._untracked_detections += 1
+ continue
+ by_track.setdefault(str(detection.track_id), []).append(detection)
+ for track_id, track_detections in by_track.items():
+ emission = self._observe(
+ rule=rule,
+ scope_id=track_id,
+ source_id=envelope.source_id,
+ frame_key=frame_key,
+ timestamp_ns=timestamp_ns,
+ detections=tuple(track_detections),
+ result=result,
+ envelope=envelope,
+ )
+ if emission is not None:
+ emissions.append(emission)
+
+ self._prune_inactive_states(timestamp_ns)
+ if emissions and self._alert_image.enabled:
+ emissions = await self._attach_alert_image(
+ emissions,
+ result.source_frame,
+ envelope,
+ )
+ return tuple(emissions) if emissions else None
+
+ async def stop(self) -> None:
+ self._executor = None
+
+ async def health(self) -> HealthReport:
+ return HealthReport(
+ not (
+ self._invalid_detections > 0
+ or (
+ self._alert_image.enabled
+ and (
+ self._alert_image_missing_frames > 0
+ or self._alert_image_failures > 0
+ )
+ )
+ ),
+ " ".join(
+ (
+ f"processed_frames={self._processed_frames}",
+ f"alerts_emitted={self._alerts_emitted}",
+ f"cooldown_suppressed={self._cooldown_suppressed}",
+ f"duplicate_frames={self._duplicate_frames}",
+ f"stale_frames={self._stale_frames}",
+ f"untracked_detections={self._untracked_detections}",
+ f"invalid_detections={self._invalid_detections}",
+ f"alert_images_encoded={self._alert_images_encoded}",
+ f"alert_images_attached={self._alert_images_attached}",
+ f"alert_image_missing_frames={self._alert_image_missing_frames}",
+ f"alert_image_failures={self._alert_image_failures}",
+ f"active_states={len(self._states)}",
+ )
+ ),
+ )
+
+ async def _attach_alert_image(
+ self,
+ emissions: list[Emission],
+ frame: ImageFrame | None,
+ source_envelope: Envelope[Any],
+ ) -> list[Emission]:
+ if frame is None:
+ self._alert_image_missing_frames += 1
+ _LOGGER.warning(
+ "detection alert image omitted node=%s source=%s sequence=%s: "
+ "DetectionResult has no source_frame",
+ self._node_id,
+ source_envelope.source_id,
+ source_envelope.sequence,
+ )
+ return emissions
+
+ # Multiple rules/tracks can trigger from the same threshold frame. A
+ # single union image bounds edge CPU and memory while retaining every
+ # box relevant to at least one emitted alert.
+ unique_detections: list[Detection] = []
+ for emission in emissions:
+ alert = emission.envelope.payload
+ if not isinstance(alert, DetectionAlert):
+ continue
+ for detection in alert.detections:
+ if detection not in unique_detections:
+ unique_detections.append(detection)
+
+ try:
+ image = await run_blocking(
+ render_annotated_jpeg,
+ frame,
+ tuple(unique_detections),
+ jpeg_quality=self._alert_image.jpeg_quality,
+ executor=self._executor,
+ )
+ if not isinstance(image, EncodedImage):
+ raise TypeError(
+ "alert image renderer returned "
+ f"{type(image).__name__}, expected EncodedImage"
+ )
+ except Exception:
+ self._alert_image_failures += 1
+ _LOGGER.warning(
+ "detection alert image rendering failed node=%s source=%s "
+ "sequence=%s; sending structured alert without image",
+ self._node_id,
+ source_envelope.source_id,
+ source_envelope.sequence,
+ exc_info=True,
+ )
+ return emissions
+
+ updated: list[Emission] = []
+ for emission in emissions:
+ alert = emission.envelope.payload
+ if not isinstance(alert, DetectionAlert):
+ updated.append(emission)
+ continue
+ updated.append(
+ Emission(
+ emission.port,
+ emission.envelope.with_payload(replace(alert, image=image)),
+ )
+ )
+ self._alert_images_encoded += 1
+ self._alert_images_attached += len(updated)
+ return updated
+
+ def _timestamp_for(self, envelope: Envelope[Any]) -> int:
+ if self._time_source == "captured":
+ if envelope.captured_at_ns is None:
+ raise ValueError(
+ f"{self._node_id} requires captured_at_ns when "
+ "time_source='captured'"
+ )
+ return envelope.captured_at_ns
+ if self._time_source == "received":
+ return envelope.received_at_ns
+ return (
+ envelope.captured_at_ns
+ if envelope.captured_at_ns is not None
+ else envelope.received_at_ns
+ )
+
+ def _observe(
+ self,
+ *,
+ rule: DetectionTriggerRule,
+ scope_id: str,
+ source_id: str,
+ frame_key: FrameKey,
+ timestamp_ns: int,
+ detections: tuple[Detection, ...],
+ result: DetectionResult,
+ envelope: Envelope[Any],
+ ) -> Emission | None:
+ state_key = (rule.rule_id, source_id, scope_id)
+ state = self._states.setdefault(state_key, _TriggerState())
+
+ if state.last_observed_ns is not None and timestamp_ns < state.last_observed_ns:
+ self._stale_frames += 1
+ return None
+ state.last_observed_ns = timestamp_ns
+ self._evict_old_entries(state, rule, timestamp_ns)
+
+ if frame_key in state.seen_frames:
+ self._duplicate_frames += 1
+ return None
+ state.seen_frames[frame_key] = timestamp_ns
+
+ if (
+ state.cooldown_until_ns is not None
+ and timestamp_ns < state.cooldown_until_ns
+ ):
+ self._cooldown_suppressed += 1
+ return None
+ if state.cooldown_until_ns is not None:
+ state.cooldown_until_ns = None
+ state.hits.clear()
+
+ present_labels = {detection.label for detection in detections}
+ hit_labels = tuple(label for label in rule.labels if label in present_labels)
+ hit = _Hit(
+ timestamp_ns=timestamp_ns,
+ labels=hit_labels,
+ max_confidence=max(detection.confidence for detection in detections),
+ )
+ state.hits.append(hit)
+ if len(state.hits) < rule.min_hits:
+ return None
+
+ hits = tuple(state.hits)
+ labels_seen = {label for item in hits for label in item.labels}
+ alert = DetectionAlert(
+ event_id=uuid4().hex,
+ rule_id=rule.rule_id,
+ model_id=result.model_id,
+ model_name=result.model_name or result.model_id,
+ labels=tuple(label for label in rule.labels if label in labels_seen),
+ scope=rule.scope,
+ scope_id=scope_id,
+ hit_count=len(hits),
+ window_ms=rule.window_ms,
+ first_seen_ns=hits[0].timestamp_ns,
+ last_seen_ns=hits[-1].timestamp_ns,
+ triggered_at_ns=timestamp_ns,
+ max_confidence=max(item.max_confidence for item in hits),
+ # Only boxes from the threshold frame are attached. Retaining all
+ # boxes from every hit would multiply memory and HTTP payload size.
+ detections=detections,
+ )
+ state.hits.clear()
+ state.cooldown_until_ns = timestamp_ns + rule.cooldown_ns
+ self._alerts_emitted += 1
+ return Emission(
+ "alerts",
+ envelope.with_payload(
+ alert,
+ schema_name="DetectionAlert",
+ schema_version=1,
+ ),
+ )
+
+ @staticmethod
+ def _evict_old_entries(
+ state: _TriggerState,
+ rule: DetectionTriggerRule,
+ timestamp_ns: int,
+ ) -> None:
+ cutoff_ns = timestamp_ns - rule.window_ns
+ # The window is inclusive at its lower boundary: an observation exactly
+ # ``window_ms`` old remains eligible.
+ while state.hits and state.hits[0].timestamp_ns < cutoff_ns:
+ state.hits.popleft()
+ expired_frames = [
+ key for key, seen_ns in state.seen_frames.items() if seen_ns < cutoff_ns
+ ]
+ for key in expired_frames:
+ del state.seen_frames[key]
+
+ def _prune_inactive_states(self, timestamp_ns: int) -> None:
+ expired: list[StateKey] = []
+ for key, state in self._states.items():
+ rule = self._rules_by_id[key[0]]
+ if state.last_observed_ns is None or timestamp_ns < state.last_observed_ns:
+ continue
+ self._evict_old_entries(state, rule, timestamp_ns)
+ if (
+ state.cooldown_until_ns is not None
+ and timestamp_ns >= state.cooldown_until_ns
+ ):
+ state.cooldown_until_ns = None
+ if (
+ not state.hits
+ and not state.seen_frames
+ and state.cooldown_until_ns is None
+ and timestamp_ns - state.last_observed_ns > rule.window_ns
+ ):
+ expired.append(key)
+ for key in expired:
+ del self._states[key]
+
+
+def _parse_time_source(value: Any) -> TimeSource:
+ if not isinstance(value, str) or value not in {"auto", "captured", "received"}:
+ raise ValueError("time_source must be one of: auto, captured, received")
+ return cast(TimeSource, value)
+
+
+def _parse_alert_image(value: Any) -> AlertImageOptions:
+ if value is None:
+ return AlertImageOptions()
+ if not isinstance(value, Mapping):
+ raise ValueError("alert_image must be a mapping")
+ unknown = set(value) - {"enabled", "jpeg_quality"}
+ if unknown:
+ raise ValueError(
+ "alert_image has unknown field(s): "
+ + ", ".join(sorted(str(item) for item in unknown))
+ )
+ enabled = value.get("enabled", False)
+ if not isinstance(enabled, bool):
+ raise ValueError("alert_image.enabled must be a boolean")
+ jpeg_quality = _strict_int(
+ value.get("jpeg_quality", 85), "alert_image.jpeg_quality"
+ )
+ if not 1 <= jpeg_quality <= 95:
+ raise ValueError("alert_image.jpeg_quality must be between 1 and 95")
+ return AlertImageOptions(enabled=enabled, jpeg_quality=jpeg_quality)
+
+
+def _parse_rules(value: Any) -> tuple[DetectionTriggerRule, ...]:
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
+ raise ValueError("rules must be a non-empty list")
+ if not value:
+ raise ValueError("rules must be a non-empty list")
+
+ parsed: list[DetectionTriggerRule] = []
+ seen_ids: set[str] = set()
+ for index, raw_rule in enumerate(value):
+ path = f"rules[{index}]"
+ if not isinstance(raw_rule, Mapping):
+ raise ValueError(f"{path} must be a mapping")
+ unknown = set(raw_rule) - DetectionTriggerOperator._RULE_KEYS
+ if unknown:
+ raise ValueError(
+ f"{path} has unknown field(s): "
+ + ", ".join(sorted(str(item) for item in unknown))
+ )
+
+ rule_id = _non_empty_string(raw_rule.get("id"), f"{path}.id")
+ if rule_id in seen_ids:
+ raise ValueError(f"duplicate detection rule id: {rule_id}")
+ seen_ids.add(rule_id)
+
+ labels_value = raw_rule.get("labels")
+ if not isinstance(labels_value, Sequence) or isinstance(
+ labels_value, (str, bytes)
+ ):
+ raise ValueError(f"{path}.labels must be a non-empty list")
+ labels = tuple(
+ _non_empty_string(label, f"{path}.labels[{label_index}]")
+ for label_index, label in enumerate(labels_value)
+ )
+ if not labels:
+ raise ValueError(f"{path}.labels must be a non-empty list")
+ if len(set(labels)) != len(labels):
+ raise ValueError(f"{path}.labels must not contain duplicates")
+
+ min_hits = _strict_int(raw_rule.get("min_hits"), f"{path}.min_hits")
+ if min_hits < 1:
+ raise ValueError(f"{path}.min_hits must be at least 1")
+ window_ms = _finite_number(raw_rule.get("window_ms"), f"{path}.window_ms")
+ if window_ms <= 0 or int(window_ms * 1_000_000) < 1:
+ raise ValueError(f"{path}.window_ms must be positive")
+ cooldown_ms = _finite_number(
+ raw_rule.get("cooldown_ms", 0.0), f"{path}.cooldown_ms"
+ )
+ if cooldown_ms < 0:
+ raise ValueError(f"{path}.cooldown_ms must not be negative")
+ min_confidence = _finite_number(
+ raw_rule.get("min_confidence", 0.0), f"{path}.min_confidence"
+ )
+ if not 0.0 <= min_confidence <= 1.0:
+ raise ValueError(f"{path}.min_confidence must be between 0 and 1")
+ scope = raw_rule.get("scope", "source")
+ if not isinstance(scope, str) or scope not in {"source", "track"}:
+ raise ValueError(f"{path}.scope must be 'source' or 'track'")
+
+ parsed.append(
+ DetectionTriggerRule(
+ rule_id=rule_id,
+ labels=labels,
+ min_hits=min_hits,
+ window_ms=window_ms,
+ cooldown_ms=cooldown_ms,
+ min_confidence=min_confidence,
+ scope=cast(RuleScope, scope),
+ )
+ )
+ return tuple(parsed)
+
+
+def _matching_detections(
+ detections: tuple[Detection, ...], rule: DetectionTriggerRule
+) -> tuple[Detection, ...]:
+ labels = set(rule.labels)
+ matching: list[Detection] = []
+ for detection in detections:
+ if not isinstance(detection, Detection):
+ raise TypeError(
+ "DetectionResult.detections must contain Detection instances"
+ )
+ if not math.isfinite(detection.confidence):
+ raise ValueError("detection confidence must be finite")
+ if detection.label in labels and detection.confidence >= rule.min_confidence:
+ matching.append(detection)
+ return tuple(matching)
+
+
+def _wire_safe_detections(
+ detections: tuple[Detection, ...],
+) -> tuple[tuple[Detection, ...], int]:
+ """Drop malformed items that could make an entire JSON alert unsendable."""
+
+ valid: list[Detection] = []
+ invalid_count = 0
+ for detection in detections:
+ if not isinstance(detection, Detection):
+ raise TypeError(
+ "DetectionResult.detections must contain Detection instances"
+ )
+ if not _wire_safe_detection(detection):
+ invalid_count += 1
+ continue
+ valid.append(detection)
+ return tuple(valid), invalid_count
+
+
+def _wire_safe_detection(detection: Detection) -> bool:
+ if not isinstance(detection.label, str) or not detection.label:
+ return False
+ if (
+ isinstance(detection.confidence, bool)
+ or not isinstance(detection.confidence, (int, float))
+ or not math.isfinite(float(detection.confidence))
+ or not 0.0 <= float(detection.confidence) <= 1.0
+ ):
+ return False
+ if detection.track_id is not None and not isinstance(detection.track_id, str):
+ return False
+ if not isinstance(detection.box, BoundingBox):
+ return False
+ coordinates = (
+ detection.box.x_min,
+ detection.box.y_min,
+ detection.box.x_max,
+ detection.box.y_max,
+ )
+ if any(
+ isinstance(value, bool)
+ or not isinstance(value, (int, float))
+ or not math.isfinite(float(value))
+ for value in coordinates
+ ):
+ return False
+ x_min, y_min, x_max, y_max = (float(value) for value in coordinates)
+ return x_max > x_min and y_max > y_min
+
+
+def _non_empty_string(value: Any, path: str) -> str:
+ if not isinstance(value, str) or not value.strip():
+ raise ValueError(f"{path} must be a non-empty string")
+ return value.strip()
+
+
+def _strict_int(value: Any, path: str) -> int:
+ if isinstance(value, bool) or not isinstance(value, int):
+ raise ValueError(f"{path} must be an integer")
+ return value
+
+
+def _finite_number(value: Any, path: str) -> float:
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
+ raise ValueError(f"{path} must be a number")
+ parsed = float(value)
+ if not math.isfinite(parsed):
+ raise ValueError(f"{path} must be finite")
+ return parsed
+
+
+__all__ = ["AlertImageOptions", "DetectionTriggerOperator", "DetectionTriggerRule"]
diff --git a/src/cmvr_edge_ai/detection/video.py b/src/cmvr_edge_ai/detection/video.py
new file mode 100644
index 0000000..29976c8
--- /dev/null
+++ b/src/cmvr_edge_ai/detection/video.py
@@ -0,0 +1,319 @@
+"""Stateful decoding of encoded camera packets into raw image frames.
+
+The framework keeps the concrete FFmpeg dependency optional. Importing this
+module therefore never imports PyAV; the default backend does so lazily when
+the first encoded key frame is received. Tests and alternative deployments
+can inject another backend through ``backend_factory``.
+"""
+
+from __future__ import annotations
+
+import threading
+from collections.abc import Callable, Iterable, Mapping
+from concurrent.futures import Executor
+from dataclasses import dataclass
+from importlib import import_module
+from typing import Any, Protocol
+
+from cmvr_edge_ai.contracts import ImageFrame
+from cmvr_edge_ai.core.component import (
+ ComponentContext,
+ Emission,
+ HealthReport,
+ Operator,
+)
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.workers.thread import run_blocking
+
+
+class VideoDecodeError(RuntimeError):
+ """A recoverable malformed/discontinuous encoded-video packet."""
+
+
+@dataclass(frozen=True, slots=True)
+class DecodedVideoFrame:
+ """One packed image produced by a decoder backend."""
+
+ data: bytes
+ width: int
+ height: int
+ pixel_format: str = "BGR8"
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.data, bytes):
+ raise TypeError("decoded video frame data must be bytes")
+ if self.width <= 0 or self.height <= 0:
+ raise ValueError("decoded video frame dimensions must be positive")
+ if not isinstance(self.pixel_format, str) or not self.pixel_format.strip():
+ raise ValueError("decoded video frame pixel_format must not be empty")
+
+
+class VideoDecoderBackend(Protocol):
+ """Synchronous stateful decoder owned by one operator instance."""
+
+ def decode(self, data: bytes) -> Iterable[DecodedVideoFrame]:
+ """Consume one encoded packet and return zero or more complete images."""
+
+ def close(self) -> None:
+ """Release native decoder resources; implementations must be idempotent."""
+
+
+VideoDecoderBackendFactory = Callable[[str], VideoDecoderBackend]
+
+
+class _PyAvDecoderBackend:
+ """Default Annex-B H.264/H.265 decoder backed by optional PyAV."""
+
+ def __init__(self, codec: str) -> None:
+ try:
+ av = import_module("av")
+ except ImportError as exc:
+ raise RuntimeError(
+ "PyAV is required to decode encoded video; install the optional "
+ "video dependencies (for example: pip install -e '.[video]')"
+ ) from exc
+
+ self._av = av
+ self._codec: Any = av.CodecContext.create(codec, "r")
+
+ def decode(self, data: bytes) -> tuple[DecodedVideoFrame, ...]:
+ if self._codec is None:
+ raise RuntimeError("video decoder backend is closed")
+ try:
+ frames = self._codec.decode(self._av.Packet(data))
+ except Exception as exc:
+ ffmpeg_error = getattr(
+ getattr(self._av, "error", None), "FFmpegError", None
+ )
+ if isinstance(ffmpeg_error, type) and isinstance(exc, ffmpeg_error):
+ raise VideoDecodeError(str(exc)) from exc
+ raise
+
+ decoded: list[DecodedVideoFrame] = []
+ for frame in frames:
+ image = frame.to_ndarray(format="bgr24")
+ decoded.append(
+ DecodedVideoFrame(
+ data=image.tobytes(order="C"),
+ width=int(frame.width),
+ height=int(frame.height),
+ pixel_format="BGR8",
+ )
+ )
+ return tuple(decoded)
+
+ def close(self) -> None:
+ # PyAV CodecContext does not expose a stable explicit close API across
+ # supported versions. Dropping the final reference releases it.
+ self._codec = None
+
+
+class VideoDecoderOperator(Operator):
+ """Decode one encoded stream while recovering at key-frame boundaries.
+
+ A decoder context is valid only for one source/session/codec/dimension
+ identity and a continuous sequence. Any identity change or sequence gap
+ closes that context and suppresses packets until the next key frame. Raw
+ frames are emitted unchanged and never import the optional decoder library.
+ """
+
+ def __init__(
+ self,
+ node_id: str,
+ params: Mapping[str, Any],
+ *,
+ backend_factory: VideoDecoderBackendFactory | None = None,
+ ) -> None:
+ if not isinstance(node_id, str) or not node_id.strip():
+ raise ValueError("video decoder node_id must not be empty")
+ if params:
+ unknown = ", ".join(sorted(str(key) for key in params))
+ raise ValueError(f"video decoder does not support parameters: {unknown}")
+
+ self._node_id = node_id
+ self._backend_factory = backend_factory or _PyAvDecoderBackend
+ self._backend: VideoDecoderBackend | None = None
+ self._backend_codec: str | None = None
+ self._backend_lock = threading.Lock()
+ self._executor: Executor | None = None
+ self._stream_identity: tuple[str, str | None, str, int, int] | None = None
+ self._last_sequence: int | None = None
+ self._waiting_for_keyframe = True
+ self._decoded_frames = 0
+ self._dropped_until_keyframe = 0
+ self._resets = 0
+ self._decode_errors = 0
+
+ async def setup(self, context: ComponentContext) -> None:
+ executor = context.metadata.get("thread_executor")
+ if executor is not None and not isinstance(executor, Executor):
+ raise TypeError("component context thread_executor must be an Executor")
+ self._executor = executor
+
+ async def process(
+ self,
+ envelope: Envelope[Any],
+ input_port: str = "input",
+ ) -> Emission | tuple[Emission, ...] | None:
+ del input_port
+ payload = envelope.payload
+ if not isinstance(payload, ImageFrame):
+ raise TypeError(
+ f"{self._node_id} expected ImageFrame, got " f"{type(payload).__name__}"
+ )
+
+ codec = _normalize_codec(payload.codec)
+ if codec is None:
+ if self._stream_identity is not None or self._backend is not None:
+ await self._reset_stream()
+ return Emission("frames", envelope)
+
+ if payload.width <= 0 or payload.height <= 0:
+ raise ValueError("encoded image dimensions must be positive")
+ if not payload.data:
+ raise ValueError("encoded image data must not be empty")
+
+ identity = (
+ envelope.source_id,
+ envelope.session_id,
+ codec,
+ payload.width,
+ payload.height,
+ )
+ identity_changed = self._stream_identity != identity
+ sequence_gap = (
+ not identity_changed
+ and self._last_sequence is not None
+ and envelope.sequence != self._last_sequence + 1
+ )
+ if identity_changed or sequence_gap:
+ await self._reset_stream(identity=identity)
+
+ self._stream_identity = identity
+ self._last_sequence = envelope.sequence
+ if self._waiting_for_keyframe and not payload.is_key_frame:
+ self._dropped_until_keyframe += 1
+ return None
+
+ try:
+ frames = await run_blocking(
+ self._decode_sync,
+ codec,
+ payload.data,
+ executor=self._executor,
+ )
+ except VideoDecodeError:
+ self._decode_errors += 1
+ await self._reset_stream(identity=identity)
+ self._last_sequence = envelope.sequence
+ return None
+
+ # A successfully accepted key frame establishes decoder continuity even
+ # when FFmpeg buffers it and returns no image until a later packet.
+ self._waiting_for_keyframe = False
+ emissions: list[Emission] = []
+ for frame in frames:
+ decoded = ImageFrame(
+ data=frame.data,
+ width=frame.width,
+ height=frame.height,
+ pixel_format=frame.pixel_format,
+ codec="none",
+ is_key_frame=False,
+ intrinsics=payload.intrinsics,
+ )
+ emissions.append(
+ Emission(
+ "frames",
+ envelope.with_payload(
+ decoded,
+ schema_name="ImageFrame",
+ schema_version=1,
+ ),
+ )
+ )
+ self._decoded_frames += len(emissions)
+ return tuple(emissions) if emissions else None
+
+ async def stop(self) -> None:
+ await self._reset_stream(count_reset=False)
+ self._executor = None
+
+ async def health(self) -> HealthReport:
+ return HealthReport(
+ True,
+ "decoded_frames={} dropped_until_keyframe={} resets={} "
+ "decode_errors={} waiting_for_keyframe={}".format(
+ self._decoded_frames,
+ self._dropped_until_keyframe,
+ self._resets,
+ self._decode_errors,
+ self._waiting_for_keyframe,
+ ),
+ )
+
+ async def _reset_stream(
+ self,
+ *,
+ identity: tuple[str, str | None, str, int, int] | None = None,
+ count_reset: bool = True,
+ ) -> None:
+ had_state = self._stream_identity is not None or self._backend is not None
+ if had_state:
+ # Always synchronize with the backend lock once a stream exists.
+ # If an awaited executor call was cancelled while native decoding
+ # continued, checking only ``self._backend`` here could race before
+ # the worker publishes the newly-created backend and leak it.
+ await run_blocking(self._close_backend_sync, executor=self._executor)
+ self._stream_identity = identity
+ self._last_sequence = None
+ self._waiting_for_keyframe = True
+ if count_reset and had_state:
+ self._resets += 1
+
+ def _decode_sync(
+ self,
+ codec: str,
+ data: bytes,
+ ) -> tuple[DecodedVideoFrame, ...]:
+ with self._backend_lock:
+ if self._backend is None:
+ self._backend = self._backend_factory(codec)
+ self._backend_codec = codec
+ elif self._backend_codec != codec:
+ # The async state machine normally closes it first. Retain a
+ # defensive guard in case this helper is reused independently.
+ self._backend.close()
+ self._backend = self._backend_factory(codec)
+ self._backend_codec = codec
+ return tuple(self._backend.decode(data))
+
+ def _close_backend_sync(self) -> None:
+ with self._backend_lock:
+ backend = self._backend
+ self._backend = None
+ self._backend_codec = None
+ if backend is not None:
+ backend.close()
+
+
+def _normalize_codec(value: str) -> str | None:
+ if not isinstance(value, str):
+ raise TypeError("image codec must be a string")
+ codec = value.strip().lower().lstrip(".")
+ if codec in {"", "none", "raw"}:
+ return None
+ if codec in {"h264", "avc", "avc1"}:
+ return "h264"
+ if codec in {"h265", "hevc", "hev1", "hvc1"}:
+ return "hevc"
+ raise ValueError(f"unsupported encoded video codec: {value!r}")
+
+
+__all__ = [
+ "DecodedVideoFrame",
+ "VideoDecodeError",
+ "VideoDecoderBackend",
+ "VideoDecoderBackendFactory",
+ "VideoDecoderOperator",
+]
diff --git a/src/cmvr_edge_ai/observability/__init__.py b/src/cmvr_edge_ai/observability/__init__.py
new file mode 100644
index 0000000..10eaf99
--- /dev/null
+++ b/src/cmvr_edge_ai/observability/__init__.py
@@ -0,0 +1,5 @@
+"""Health, logging and metrics helpers."""
+
+from .logging import JsonFormatter, configure_logging
+
+__all__ = ["JsonFormatter", "configure_logging"]
diff --git a/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..74c8884
Binary files /dev/null and b/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..9d3d2ef
Binary files /dev/null and b/src/cmvr_edge_ai/observability/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-310.pyc b/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-310.pyc
new file mode 100644
index 0000000..91ad56d
Binary files /dev/null and b/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-312.pyc b/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-312.pyc
new file mode 100644
index 0000000..9d80bbd
Binary files /dev/null and b/src/cmvr_edge_ai/observability/__pycache__/logging.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/observability/logging.py b/src/cmvr_edge_ai/observability/logging.py
new file mode 100644
index 0000000..de06e0d
--- /dev/null
+++ b/src/cmvr_edge_ai/observability/logging.py
@@ -0,0 +1,36 @@
+"""Low-overhead text or JSON logging setup."""
+
+from __future__ import annotations
+
+import json
+import logging
+from datetime import datetime, timezone
+from typing import Any
+
+
+class JsonFormatter(logging.Formatter):
+ def format(self, record: logging.LogRecord) -> str:
+ payload: dict[str, Any] = {
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "level": record.levelname,
+ "logger": record.name,
+ "message": record.getMessage(),
+ }
+ if record.exc_info:
+ payload["exception"] = self.formatException(record.exc_info)
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
+
+
+def configure_logging(level: str = "INFO", log_format: str = "text") -> None:
+ handler = logging.StreamHandler()
+ if log_format == "json":
+ handler.setFormatter(JsonFormatter())
+ elif log_format == "text":
+ handler.setFormatter(
+ logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
+ )
+ else:
+ raise ValueError("log_format must be 'text' or 'json'")
+ logging.basicConfig(
+ level=getattr(logging, level.upper()), handlers=[handler], force=True
+ )
diff --git a/src/cmvr_edge_ai/plugins/__init__.py b/src/cmvr_edge_ai/plugins/__init__.py
new file mode 100644
index 0000000..8542898
--- /dev/null
+++ b/src/cmvr_edge_ai/plugins/__init__.py
@@ -0,0 +1,23 @@
+"""Plugin discovery and registration."""
+
+from .registry import (
+ DuplicatePluginError,
+ PluginError,
+ PluginKind,
+ PluginRegistry,
+ PluginSpec,
+ UnknownPluginError,
+ schemas_compatible,
+)
+from .builtin import register_builtin_plugins
+
+__all__ = [
+ "DuplicatePluginError",
+ "PluginError",
+ "PluginKind",
+ "PluginRegistry",
+ "PluginSpec",
+ "UnknownPluginError",
+ "schemas_compatible",
+ "register_builtin_plugins",
+]
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..7666180
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..14d417c
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-310.pyc b/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-310.pyc
new file mode 100644
index 0000000..ae13b9a
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-312.pyc b/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-312.pyc
new file mode 100644
index 0000000..65a69c1
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/builtin.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-310.pyc b/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-310.pyc
new file mode 100644
index 0000000..78c2a8f
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-312.pyc b/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-312.pyc
new file mode 100644
index 0000000..392a6b2
Binary files /dev/null and b/src/cmvr_edge_ai/plugins/__pycache__/registry.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/plugins/builtin.py b/src/cmvr_edge_ai/plugins/builtin.py
new file mode 100644
index 0000000..497c1c0
--- /dev/null
+++ b/src/cmvr_edge_ai/plugins/builtin.py
@@ -0,0 +1,296 @@
+"""Small built-in plugins used for smoke tests and control boundaries."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+import math
+from collections.abc import Mapping
+from time import monotonic_ns, time_ns
+from typing import Any
+
+from cmvr_edge_ai.contracts import ApprovedRobotCommand, RobotCommand
+from cmvr_edge_ai.core.component import Emission, HealthReport, Operator, Sink, Source
+from cmvr_edge_ai.core.envelope import Envelope
+
+from .registry import PluginKind, PluginRegistry, PluginSpec
+
+
+class SequenceSource(Source):
+ """Finite synthetic source useful for deployment and runtime smoke tests."""
+
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ self._node_id = node_id
+ items = params.get("items", ())
+ if isinstance(items, (str, bytes, bytearray, Mapping)):
+ raise ValueError("items must be a sequence, not a scalar or mapping")
+ try:
+ self._items = tuple(items)
+ except TypeError as exc:
+ raise ValueError("items must be an iterable sequence") from exc
+ self._interval_s = float(params.get("interval_s", 0.0))
+ self._schema_name = str(params.get("schema_name", "opaque"))
+ self._schema_version = int(params.get("schema_version", 1))
+ if self._interval_s < 0:
+ raise ValueError("interval_s must not be negative")
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ for sequence, payload in enumerate(self._items):
+ if self._interval_s:
+ await asyncio.sleep(self._interval_s)
+ yield Emission(
+ "output",
+ Envelope(
+ payload=payload,
+ schema_name=self._schema_name,
+ schema_version=self._schema_version,
+ source_id=self._node_id,
+ sequence=sequence,
+ ),
+ )
+
+
+class PassthroughOperator(Operator):
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ del node_id
+ del params
+
+ async def process(
+ self, envelope: Envelope[Any], input_port: str = "input"
+ ) -> Emission:
+ del input_port
+ return Emission("output", envelope)
+
+
+class LogSink(Sink):
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ logger_name = str(params.get("logger", "cmvr_edge_ai.sink"))
+ self._logger = logging.getLogger(logger_name)
+ self._node_id = node_id
+ self._level = getattr(logging, str(params.get("level", "INFO")).upper())
+
+ async def consume(self, envelope: Envelope[Any], input_port: str = "input") -> None:
+ self._logger.log(
+ self._level,
+ "pipeline output node=%s port=%s schema=%s trace_id=%s payload=%r",
+ self._node_id,
+ input_port,
+ envelope.schema,
+ envelope.trace_id,
+ envelope.payload,
+ )
+
+
+class CommandSafetyGate(Operator):
+ """Fail-closed validation for transport-neutral robot commands."""
+
+ def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
+ self._node_id = node_id
+ self._allowed_actions = frozenset(
+ str(action) for action in params.get("allowed_actions", ())
+ )
+ self._allowed_devices = frozenset(
+ str(device) for device in params.get("allowed_devices", ())
+ )
+ self._min_interval_ns = int(float(params.get("min_interval_ms", 0.0)) * 1e6)
+ self._max_ttl_ns = int(float(params.get("max_ttl_ms", 5000.0)) * 1e6)
+ self._argument_limits = _parse_argument_limits(
+ params.get("argument_limits", {})
+ )
+ required_limits = params.get("require_limits_for", ())
+ self._require_limits_for = frozenset(
+ str(action) for action in required_limits
+ ) | {"set_velocity"}
+ self._enforce_sequence = _strict_bool(
+ params, "enforce_monotonic_sequence", default=True
+ )
+ self._last_command_ns: dict[tuple[str, str], int] = {}
+ self._last_sequence: dict[tuple[str, str], int] = {}
+ self._rejected_count = 0
+ self._logger = logging.getLogger(__name__)
+ if not self._allowed_actions:
+ raise ValueError("safety gate requires a non-empty allowed_actions list")
+ if self._min_interval_ns < 0:
+ raise ValueError("min_interval_ms must not be negative")
+ if self._max_ttl_ns <= 0:
+ raise ValueError("max_ttl_ms must be positive")
+ missing_limits = (self._allowed_actions & self._require_limits_for) - set(
+ self._argument_limits
+ )
+ if missing_limits:
+ raise ValueError(
+ "safety gate requires argument_limits for action(s): "
+ + ", ".join(sorted(missing_limits))
+ )
+ if "set_velocity" in self._allowed_actions:
+ velocity_fields = set(self._argument_limits["set_velocity"])
+ missing_fields = {"vx", "vy", "wz"} - velocity_fields
+ if missing_fields:
+ raise ValueError(
+ "argument_limits.set_velocity must define vx, vy and wz; "
+ f"missing: {', '.join(sorted(missing_fields))}"
+ )
+
+ async def process(
+ self, envelope: Envelope[Any], input_port: str = "input"
+ ) -> Emission | None:
+ del input_port
+ command = envelope.payload
+ if not isinstance(command, RobotCommand):
+ raise TypeError(
+ f"{self._node_id} expected RobotCommand, got {type(command).__name__}"
+ )
+ now_wall_ns = time_ns()
+ if envelope.is_expired(now_wall_ns) or command.valid_until_ns <= now_wall_ns:
+ return self._reject(command, "expired")
+ if command.valid_until_ns - now_wall_ns > self._max_ttl_ns:
+ return self._reject(command, "TTL exceeds safety-gate maximum")
+ if command.action not in self._allowed_actions:
+ return self._reject(command, "action is not allowlisted")
+ if self._allowed_devices and command.device_id not in self._allowed_devices:
+ return self._reject(command, "device is not allowlisted")
+
+ for argument, (minimum, maximum) in self._argument_limits.get(
+ command.action, {}
+ ).items():
+ if argument not in command.arguments:
+ return self._reject(
+ command, f"required argument {argument!r} is missing"
+ )
+ try:
+ value = float(command.arguments[argument])
+ except (TypeError, ValueError):
+ return self._reject(command, f"argument {argument!r} is not numeric")
+ if not math.isfinite(value) or not minimum <= value <= maximum:
+ return self._reject(
+ command,
+ f"argument {argument!r}={value!r} is outside "
+ f"[{minimum}, {maximum}]",
+ )
+
+ key = (command.device_id, command.action)
+ previous_sequence = self._last_sequence.get(key)
+ if (
+ self._enforce_sequence
+ and previous_sequence is not None
+ and command.sequence <= previous_sequence
+ ):
+ return self._reject(command, "sequence is stale or duplicated")
+ self._last_sequence[key] = command.sequence
+ now = monotonic_ns()
+ previous = self._last_command_ns.get(key)
+ if previous is not None and now - previous < self._min_interval_ns:
+ return self._reject(command, "rate limit exceeded")
+ self._last_command_ns[key] = now
+ approved = ApprovedRobotCommand(command=command, approved_by=self._node_id)
+ return Emission(
+ "commands",
+ envelope.with_payload(
+ approved,
+ schema_name="ApprovedRobotCommand",
+ schema_version=1,
+ source_id=self._node_id,
+ ),
+ )
+
+ async def health(self) -> HealthReport:
+ return HealthReport(True, f"rejected_commands={self._rejected_count}")
+
+ def _reject(self, command: RobotCommand, reason: str) -> Emission | None:
+ self._rejected_count += 1
+ self._logger.warning(
+ "robot command rejected gate=%s command_id=%s device=%s action=%s "
+ "reason=%s",
+ self._node_id,
+ command.command_id,
+ command.device_id,
+ command.action,
+ reason,
+ )
+ return None
+
+
+def _parse_argument_limits(value: Any) -> dict[str, dict[str, tuple[float, float]]]:
+ if not isinstance(value, Mapping):
+ raise ValueError("argument_limits must be a mapping")
+ parsed: dict[str, dict[str, tuple[float, float]]] = {}
+ for action, arguments in value.items():
+ if not isinstance(arguments, Mapping):
+ raise ValueError(f"argument_limits.{action} must be a mapping")
+ parsed_arguments: dict[str, tuple[float, float]] = {}
+ for name, bounds in arguments.items():
+ if (
+ not isinstance(bounds, Mapping)
+ or "min" not in bounds
+ or "max" not in bounds
+ ):
+ raise ValueError(
+ f"argument_limits.{action}.{name} requires min and max"
+ )
+ minimum = float(bounds["min"])
+ maximum = float(bounds["max"])
+ if not math.isfinite(minimum) or not math.isfinite(maximum):
+ raise ValueError("argument limits must be finite")
+ if minimum > maximum:
+ raise ValueError(f"argument_limits.{action}.{name} min exceeds max")
+ parsed_arguments[str(name)] = (minimum, maximum)
+ parsed[str(action)] = parsed_arguments
+ return parsed
+
+
+def _strict_bool(params: Mapping[str, Any], name: str, *, default: bool) -> bool:
+ value = params.get(name, default)
+ if type(value) is not bool:
+ raise ValueError(f"{name} must be a boolean, not {value!r}")
+ return value
+
+
+def register_builtin_plugins(registry: PluginRegistry) -> None:
+ registry.register(
+ PluginSpec(
+ plugin_id="core.sequence_source@1",
+ kind=PluginKind.SOURCE,
+ factory=SequenceSource,
+ outputs={"output": "*"},
+ description="Finite synthetic source for smoke tests",
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="core.passthrough@1",
+ kind=PluginKind.OPERATOR,
+ factory=PassthroughOperator,
+ inputs={"input": "*"},
+ outputs={"output": "*"},
+ description="Protocol-neutral pass-through operator",
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="core.log_sink@1",
+ kind=PluginKind.SINK,
+ factory=LogSink,
+ inputs={"input": "*"},
+ description="Structured log sink",
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="safety.robot_command_gate@1",
+ kind=PluginKind.OPERATOR,
+ factory=CommandSafetyGate,
+ inputs={"input": "RobotCommand/v1"},
+ outputs={"commands": "ApprovedRobotCommand/v1"},
+ description="Fail-closed allowlist and rate gate for robot commands",
+ tags=frozenset({"safety_gate"}),
+ )
+ )
+
+
+__all__ = [
+ "CommandSafetyGate",
+ "LogSink",
+ "PassthroughOperator",
+ "SequenceSource",
+ "register_builtin_plugins",
+]
diff --git a/src/cmvr_edge_ai/plugins/registry.py b/src/cmvr_edge_ai/plugins/registry.py
new file mode 100644
index 0000000..5a28a63
--- /dev/null
+++ b/src/cmvr_edge_ai/plugins/registry.py
@@ -0,0 +1,166 @@
+"""Explicit plugin registry used by the configuration compiler.
+
+Plugins are registered under stable, versioned identifiers. Runtime
+configuration never imports an arbitrary ``module:object`` path.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Mapping
+from dataclasses import dataclass, field
+from enum import Enum
+from importlib import metadata
+from types import MappingProxyType
+from typing import Any, cast
+
+
+class PluginError(RuntimeError):
+ """Base class for plugin registration and lookup failures."""
+
+
+class DuplicatePluginError(PluginError):
+ """Raised when a plugin identifier is registered twice."""
+
+
+class UnknownPluginError(PluginError):
+ """Raised when configuration references an unknown plugin."""
+
+
+class PluginKind(str, Enum):
+ SOURCE = "source"
+ OPERATOR = "operator"
+ SINK = "sink"
+
+
+PluginFactory = Callable[[str, Mapping[str, Any]], Any]
+
+
+@dataclass(frozen=True, slots=True)
+class PluginSpec:
+ """Static metadata and factory for a pipeline component."""
+
+ plugin_id: str
+ kind: PluginKind
+ factory: PluginFactory
+ inputs: Mapping[str, str] = field(default_factory=dict)
+ outputs: Mapping[str, str] = field(default_factory=dict)
+ description: str = ""
+ tags: frozenset[str] = field(default_factory=frozenset)
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.plugin_id, str) or "@" not in self.plugin_id:
+ raise ValueError(
+ f"plugin id must be versioned (for example name@1): {self.plugin_id}"
+ )
+ name, version = self.plugin_id.rsplit("@", maxsplit=1)
+ if not name or not version.isdigit() or int(version) < 1:
+ raise ValueError(
+ f"plugin id must end in a positive numeric version: {self.plugin_id}"
+ )
+ try:
+ parsed_kind = PluginKind(self.kind)
+ except ValueError as exc:
+ raise ValueError(f"unknown plugin kind: {self.kind!r}") from exc
+ if not callable(self.factory):
+ raise TypeError("plugin factory must be callable")
+
+ inputs = _validated_ports("input", self.inputs)
+ outputs = _validated_ports("output", self.outputs)
+ tags = frozenset(self.tags)
+ if any(not isinstance(tag, str) or not tag for tag in tags):
+ raise ValueError("plugin tags must be non-empty strings")
+
+ object.__setattr__(self, "kind", parsed_kind)
+ object.__setattr__(self, "inputs", MappingProxyType(inputs))
+ object.__setattr__(self, "outputs", MappingProxyType(outputs))
+ object.__setattr__(self, "tags", tags)
+ if parsed_kind is PluginKind.SOURCE and inputs:
+ raise ValueError("source plugins cannot declare input ports")
+ if parsed_kind is PluginKind.SINK and outputs:
+ raise ValueError("sink plugins cannot declare output ports")
+
+
+class PluginRegistry:
+ """In-memory registry with optional Python entry-point discovery."""
+
+ ENTRY_POINT_GROUP = "cmvr_edge_ai.plugins"
+
+ def __init__(self) -> None:
+ self._plugins: dict[str, PluginSpec] = {}
+
+ def register(self, spec: PluginSpec) -> PluginSpec:
+ if spec.plugin_id in self._plugins:
+ raise DuplicatePluginError(f"plugin already registered: {spec.plugin_id}")
+ self._plugins[spec.plugin_id] = spec
+ return spec
+
+ def resolve(self, plugin_id: str) -> PluginSpec:
+ try:
+ return self._plugins[plugin_id]
+ except KeyError as exc:
+ available = ", ".join(sorted(self._plugins)) or ""
+ raise UnknownPluginError(
+ f"unknown plugin {plugin_id!r}; available plugins: {available}"
+ ) from exc
+
+ def create(
+ self, plugin_id: str, node_id: str, params: Mapping[str, Any] | None = None
+ ) -> Any:
+ return self.resolve(plugin_id).factory(node_id, params or {})
+
+ def specs(self) -> tuple[PluginSpec, ...]:
+ return tuple(self._plugins[key] for key in sorted(self._plugins))
+
+ def load_entry_points(self) -> None:
+ """Load plugin registration callbacks from installed distributions.
+
+ An entry point may expose either a ``PluginSpec`` or a callable taking
+ this registry. Discovery remains explicit and restricted to the
+ dedicated entry-point group.
+ """
+
+ discovered = cast(Any, metadata.entry_points())
+ selected = (
+ discovered.select(group=self.ENTRY_POINT_GROUP)
+ if hasattr(discovered, "select")
+ else discovered.get(self.ENTRY_POINT_GROUP, ())
+ )
+ for entry_point in selected:
+ loaded = entry_point.load()
+ if isinstance(loaded, PluginSpec):
+ self.register(loaded)
+ elif callable(loaded):
+ loaded(self)
+ else:
+ raise PluginError(
+ f"entry point {entry_point.name!r} must expose PluginSpec "
+ "or a registry callback"
+ )
+
+
+def schemas_compatible(source_schema: str, target_schema: str) -> bool:
+ """Return whether two declared port schemas can be connected."""
+
+ return (
+ source_schema == target_schema or source_schema == "*" or target_schema == "*"
+ )
+
+
+def _validated_ports(direction: str, ports: Mapping[str, str]) -> dict[str, str]:
+ if not isinstance(ports, Mapping):
+ raise TypeError(f"plugin {direction} ports must be a mapping")
+ copied = dict(ports)
+ invalid = sorted(
+ repr(name)
+ for name, schema in copied.items()
+ if not isinstance(name, str)
+ or not name
+ or not isinstance(schema, str)
+ or not schema
+ )
+ if invalid:
+ raise ValueError(
+ f"plugin {direction} ports require non-empty string names and schemas: "
+ + ", ".join(invalid)
+ )
+ return copied
diff --git a/src/cmvr_edge_ai/transports/__init__.py b/src/cmvr_edge_ai/transports/__init__.py
new file mode 100644
index 0000000..c5b8c0b
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/__init__.py
@@ -0,0 +1,5 @@
+"""Protocol-native transport helpers used by boundary connectors."""
+
+from .base import ManagedTransport
+
+__all__ = ["ManagedTransport"]
diff --git a/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..3d809ff
Binary files /dev/null and b/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..6760d78
Binary files /dev/null and b/src/cmvr_edge_ai/transports/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/__pycache__/base.cpython-310.pyc b/src/cmvr_edge_ai/transports/__pycache__/base.cpython-310.pyc
new file mode 100644
index 0000000..72fc6be
Binary files /dev/null and b/src/cmvr_edge_ai/transports/__pycache__/base.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/__pycache__/base.cpython-312.pyc b/src/cmvr_edge_ai/transports/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..0d9621b
Binary files /dev/null and b/src/cmvr_edge_ai/transports/__pycache__/base.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/base.py b/src/cmvr_edge_ai/transports/base.py
new file mode 100644
index 0000000..5395d18
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/base.py
@@ -0,0 +1,15 @@
+"""Shared transport lifecycle only.
+
+The framework intentionally does not force gRPC streams, HTTP requests and
+QUIC datagrams behind one fake ``send`` method. Connectors expose the common
+Source/Operator/Sink contracts while transport details stay protocol-native.
+"""
+
+from __future__ import annotations
+
+from typing import Protocol
+
+
+class ManagedTransport(Protocol):
+ async def close(self) -> None:
+ """Release connections and protocol resources."""
diff --git a/src/cmvr_edge_ai/transports/grpc/__init__.py b/src/cmvr_edge_ai/transports/grpc/__init__.py
new file mode 100644
index 0000000..63053af
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/grpc/__init__.py
@@ -0,0 +1,16 @@
+"""gRPC transport utilities."""
+
+from .channel import (
+ GrpcChannelPool,
+ GrpcChannelSettings,
+ GrpcDependencyError,
+)
+from .bidi import BidiRequestStream, BidiRequestStreamClosed
+
+__all__ = [
+ "BidiRequestStream",
+ "BidiRequestStreamClosed",
+ "GrpcChannelPool",
+ "GrpcChannelSettings",
+ "GrpcDependencyError",
+]
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..57006d3
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..772516e
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-310.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-310.pyc
new file mode 100644
index 0000000..3352d2a
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-312.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-312.pyc
new file mode 100644
index 0000000..581c3ab
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/bidi.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-310.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-310.pyc
new file mode 100644
index 0000000..8b4e133
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-312.pyc b/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-312.pyc
new file mode 100644
index 0000000..f1746e9
Binary files /dev/null and b/src/cmvr_edge_ai/transports/grpc/__pycache__/channel.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/grpc/bidi.py b/src/cmvr_edge_ai/transports/grpc/bidi.py
new file mode 100644
index 0000000..c707701
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/grpc/bidi.py
@@ -0,0 +1,68 @@
+"""Bounded request iterator for true bidirectional gRPC connectors."""
+
+from __future__ import annotations
+
+from typing import AsyncIterator, Generic, TypeVar
+
+from cmvr_edge_ai.core.edge_queue import (
+ EdgeQueue,
+ EdgeQueueClosed,
+ OverflowPolicy,
+ QueueStats,
+)
+
+
+RequestT = TypeVar("RequestT")
+
+
+class BidiRequestStreamClosed(RuntimeError):
+ """Raised when a producer writes after or during stream shutdown."""
+
+
+class BidiRequestStream(Generic[RequestT]):
+ """An async request iterator with explicit backpressure and close."""
+
+ def __init__(self, capacity: int = 8) -> None:
+ if capacity < 1:
+ raise ValueError("capacity must be at least 1")
+ self._queue: EdgeQueue[RequestT] = EdgeQueue(
+ capacity,
+ OverflowPolicy.BLOCK,
+ name="grpc-bidi-request-stream",
+ )
+ self._consumer_started = False
+
+ @property
+ def closed(self) -> bool:
+ return self._queue.closed
+
+ def stats(self) -> QueueStats:
+ return self._queue.stats()
+
+ async def send(self, request: RequestT) -> None:
+ try:
+ await self._queue.put(request)
+ except EdgeQueueClosed as exc:
+ raise BidiRequestStreamClosed(
+ "bidirectional request stream is closed"
+ ) from exc
+
+ async def close(self) -> None:
+ # A normal close drains already accepted requests, while atomically
+ # waking every blocked sender so none can report a false success.
+ await self._queue.close()
+
+ async def __aiter__(self) -> AsyncIterator[RequestT]:
+ if self._consumer_started:
+ raise RuntimeError("BidiRequestStream supports exactly one consumer")
+ self._consumer_started = True
+ try:
+ async for item in self._queue:
+ yield item
+ finally:
+ # If gRPC cancels or abandons the iterator, no queued request can be
+ # delivered. Discard it and wake blocked producers immediately.
+ await self._queue.close(discard=True)
+
+
+__all__ = ["BidiRequestStream", "BidiRequestStreamClosed"]
diff --git a/src/cmvr_edge_ai/transports/grpc/channel.py b/src/cmvr_edge_ai/transports/grpc/channel.py
new file mode 100644
index 0000000..6f390c8
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/grpc/channel.py
@@ -0,0 +1,79 @@
+"""Async gRPC channel reuse with a lazy optional dependency."""
+
+from __future__ import annotations
+
+import asyncio
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+
+class GrpcDependencyError(RuntimeError):
+ """Raised when a gRPC connector is used without the grpc extra."""
+
+
+@dataclass(frozen=True, slots=True)
+class GrpcChannelSettings:
+ target: str
+ tls: bool = False
+ root_certificates: Path | None = None
+ options: tuple[tuple[str, int | str], ...] = field(default_factory=tuple)
+
+
+class GrpcChannelPool:
+ """Create one ``grpc.aio.Channel`` per named endpoint.
+
+ The pool must be created and used from a single asyncio event-loop thread,
+ matching the threading requirements of ``grpc.aio`` objects.
+ """
+
+ def __init__(self) -> None:
+ self._channels: dict[str, Any] = {}
+ self._settings: dict[str, GrpcChannelSettings] = {}
+ self._lock = asyncio.Lock()
+
+ async def get(self, endpoint_id: str, settings: GrpcChannelSettings) -> Any:
+ async with self._lock:
+ existing = self._channels.get(endpoint_id)
+ if existing is not None:
+ if self._settings[endpoint_id] != settings:
+ raise ValueError(
+ f"gRPC endpoint {endpoint_id!r} requested with conflicting settings"
+ )
+ return existing
+
+ try:
+ import grpc
+ except ImportError as exc:
+ raise GrpcDependencyError(
+ "gRPC support is not installed; install cmvr-edge-ai[grpc]"
+ ) from exc
+
+ if settings.tls:
+ roots = (
+ settings.root_certificates.read_bytes()
+ if settings.root_certificates is not None
+ else None
+ )
+ credentials = grpc.ssl_channel_credentials(root_certificates=roots)
+ channel = grpc.aio.secure_channel(
+ settings.target, credentials, options=settings.options
+ )
+ else:
+ channel = grpc.aio.insecure_channel(
+ settings.target, options=settings.options
+ )
+
+ self._channels[endpoint_id] = channel
+ self._settings[endpoint_id] = settings
+ return channel
+
+ async def close(self, grace_s: float = 1.0) -> None:
+ async with self._lock:
+ channels = tuple(self._channels.values())
+ self._channels.clear()
+ self._settings.clear()
+ await asyncio.gather(
+ *(channel.close(grace=grace_s) for channel in channels),
+ return_exceptions=True,
+ )
diff --git a/src/cmvr_edge_ai/transports/http/__init__.py b/src/cmvr_edge_ai/transports/http/__init__.py
new file mode 100644
index 0000000..15b569a
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/http/__init__.py
@@ -0,0 +1,5 @@
+"""HTTP transport utilities."""
+
+from .client import HttpClientPool, HttpClientSettings, HttpDependencyError
+
+__all__ = ["HttpClientPool", "HttpClientSettings", "HttpDependencyError"]
diff --git a/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..b50e5d9
Binary files /dev/null and b/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..85c9c5c
Binary files /dev/null and b/src/cmvr_edge_ai/transports/http/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-310.pyc b/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-310.pyc
new file mode 100644
index 0000000..2cac9f8
Binary files /dev/null and b/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-312.pyc b/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-312.pyc
new file mode 100644
index 0000000..20a568b
Binary files /dev/null and b/src/cmvr_edge_ai/transports/http/__pycache__/client.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/http/client.py b/src/cmvr_edge_ai/transports/http/client.py
new file mode 100644
index 0000000..a151ef4
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/http/client.py
@@ -0,0 +1,62 @@
+"""Shared asynchronous HTTP clients, imported only when configured."""
+
+from __future__ import annotations
+
+import asyncio
+from dataclasses import dataclass, field
+from typing import Any
+
+
+class HttpDependencyError(RuntimeError):
+ """Raised when an HTTP connector is used without the HTTP extra."""
+
+
+@dataclass(frozen=True, slots=True)
+class HttpClientSettings:
+ base_url: str
+ timeout_s: float = 5.0
+ headers: tuple[tuple[str, str], ...] = field(default_factory=tuple)
+ verify_tls: bool = True
+
+
+class HttpClientPool:
+ def __init__(self) -> None:
+ self._clients: dict[str, Any] = {}
+ self._settings: dict[str, HttpClientSettings] = {}
+ self._lock = asyncio.Lock()
+
+ async def get(self, endpoint_id: str, settings: HttpClientSettings) -> Any:
+ async with self._lock:
+ existing = self._clients.get(endpoint_id)
+ if existing is not None:
+ if self._settings[endpoint_id] != settings:
+ raise ValueError(
+ f"HTTP endpoint {endpoint_id!r} requested with conflicting settings"
+ )
+ return existing
+
+ try:
+ import httpx
+ except ImportError as exc:
+ raise HttpDependencyError(
+ "HTTP support is not installed; install cmvr-edge-ai[http]"
+ ) from exc
+
+ client = httpx.AsyncClient(
+ base_url=settings.base_url,
+ timeout=settings.timeout_s,
+ headers=dict(settings.headers),
+ verify=settings.verify_tls,
+ )
+ self._clients[endpoint_id] = client
+ self._settings[endpoint_id] = settings
+ return client
+
+ async def close(self) -> None:
+ async with self._lock:
+ clients = tuple(self._clients.values())
+ self._clients.clear()
+ self._settings.clear()
+ await asyncio.gather(
+ *(client.aclose() for client in clients), return_exceptions=True
+ )
diff --git a/src/cmvr_edge_ai/transports/quic/__init__.py b/src/cmvr_edge_ai/transports/quic/__init__.py
new file mode 100644
index 0000000..462bad7
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/quic/__init__.py
@@ -0,0 +1 @@
+"""Reserved package for future QUIC stream and datagram connectors."""
diff --git a/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..d0f850e
Binary files /dev/null and b/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..6510f4f
Binary files /dev/null and b/src/cmvr_edge_ai/transports/quic/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/transports/udp/__init__.py b/src/cmvr_edge_ai/transports/udp/__init__.py
new file mode 100644
index 0000000..7b5677b
--- /dev/null
+++ b/src/cmvr_edge_ai/transports/udp/__init__.py
@@ -0,0 +1,5 @@
+"""Reserved package for typed UDP connectors.
+
+UDP datagrams will be exposed through Source/Sink plugins rather than a
+protocol-erasing transport API.
+"""
diff --git a/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..fcecc59
Binary files /dev/null and b/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..e572369
Binary files /dev/null and b/src/cmvr_edge_ai/transports/udp/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__init__.py b/src/cmvr_edge_ai/workers/__init__.py
new file mode 100644
index 0000000..3508bdf
--- /dev/null
+++ b/src/cmvr_edge_ai/workers/__init__.py
@@ -0,0 +1,11 @@
+"""Explicit, resource-bounded execution helpers for plugin authors."""
+
+from .process import PersistentProcessWorker, ProcessWorkerError, RemoteWorkerError
+from .thread import run_blocking
+
+__all__ = [
+ "PersistentProcessWorker",
+ "ProcessWorkerError",
+ "RemoteWorkerError",
+ "run_blocking",
+]
diff --git a/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-310.pyc b/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..7aac0c3
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-312.pyc b/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1a79865
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__pycache__/process.cpython-310.pyc b/src/cmvr_edge_ai/workers/__pycache__/process.cpython-310.pyc
new file mode 100644
index 0000000..bd2f084
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/process.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__pycache__/process.cpython-312.pyc b/src/cmvr_edge_ai/workers/__pycache__/process.cpython-312.pyc
new file mode 100644
index 0000000..d116b9c
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/process.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-310.pyc b/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-310.pyc
new file mode 100644
index 0000000..21c5483
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-310.pyc differ
diff --git a/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-312.pyc b/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-312.pyc
new file mode 100644
index 0000000..4f4e1c0
Binary files /dev/null and b/src/cmvr_edge_ai/workers/__pycache__/thread.cpython-312.pyc differ
diff --git a/src/cmvr_edge_ai/workers/process.py b/src/cmvr_edge_ai/workers/process.py
new file mode 100644
index 0000000..c30b1ed
--- /dev/null
+++ b/src/cmvr_edge_ai/workers/process.py
@@ -0,0 +1,211 @@
+"""Small, bounded, persistent subprocess worker for CPU/model plugins.
+
+The worker is intentionally not wired automatically from YAML. A model plugin
+owns one worker for its whole lifecycle, loads its model once in the child, and
+submits small payloads or shared-memory references. Large decoded frames
+should not be copied through these queues.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import multiprocessing as mp
+import queue
+import traceback
+from collections.abc import Callable
+from typing import Any
+from uuid import uuid4
+from time import monotonic
+
+
+WorkerHandler = Callable[[Any], Any]
+WorkerFactory = Callable[[], WorkerHandler]
+_STOP = "__cmvr_edge_ai_worker_stop__"
+
+
+class ProcessWorkerError(RuntimeError):
+ pass
+
+
+class RemoteWorkerError(ProcessWorkerError):
+ def __init__(self, remote_type: str, message: str, remote_traceback: str) -> None:
+ self.remote_type = remote_type
+ self.remote_traceback = remote_traceback
+ super().__init__(f"remote {remote_type}: {message}\n{remote_traceback}")
+
+
+class PersistentProcessWorker:
+ """One long-lived subprocess with bounded request and result queues."""
+
+ def __init__(
+ self,
+ factory: WorkerFactory,
+ *,
+ name: str = "model-worker",
+ start_method: str = "spawn",
+ queue_capacity: int = 1,
+ ) -> None:
+ if queue_capacity < 1:
+ raise ValueError("queue_capacity must be at least 1")
+ self._factory = factory
+ self._name = name
+ self._context: Any = mp.get_context(start_method)
+ self._requests = self._context.Queue(maxsize=queue_capacity)
+ self._results = self._context.Queue(maxsize=queue_capacity)
+ self._process: mp.Process | None = None
+ self._submit_lock = asyncio.Lock()
+ self._closed = False
+ self._poisoned_reason: str | None = None
+
+ @property
+ def is_alive(self) -> bool:
+ return self._process is not None and self._process.is_alive()
+
+ @property
+ def poisoned(self) -> bool:
+ """Whether an in-flight timeout/cancellation made correlation unsafe."""
+
+ return self._poisoned_reason is not None
+
+ async def start(self) -> None:
+ if self._closed:
+ raise ProcessWorkerError("worker is one-shot and has already been stopped")
+ if self._process is not None:
+ raise ProcessWorkerError("worker has already been started")
+ process = self._context.Process(
+ target=_worker_main,
+ args=(self._factory, self._requests, self._results),
+ name=self._name,
+ )
+ process.start()
+ self._process = process
+
+ async def submit(self, payload: Any, *, timeout_s: float | None = None) -> Any:
+ if not self.is_alive:
+ raise ProcessWorkerError("worker is not running")
+ if self._poisoned_reason is not None:
+ raise ProcessWorkerError(
+ "worker is poisoned by an indeterminate prior request; stop and "
+ f"recreate it ({self._poisoned_reason})"
+ )
+ if timeout_s is not None and timeout_s <= 0:
+ raise ValueError("timeout_s must be positive")
+ request_id = uuid4().hex
+ async with self._submit_lock:
+ if self._poisoned_reason is not None:
+ raise ProcessWorkerError(
+ "worker is poisoned by an indeterminate prior request; stop "
+ f"and recreate it ({self._poisoned_reason})"
+ )
+ deadline = monotonic() + timeout_s if timeout_s is not None else None
+ request_may_have_been_sent = False
+ try:
+ # Cancellation can race the backing queue thread, so once the
+ # put begins we conservatively treat its delivery as uncertain.
+ request_may_have_been_sent = True
+ await self._put_request((request_id, payload), deadline)
+ response = await self._get_response(deadline)
+ except BaseException as exc:
+ if request_may_have_been_sent:
+ self._poisoned_reason = type(exc).__name__
+ raise
+
+ response_id, success, value = response
+ if response_id != request_id:
+ self._poisoned_reason = "response correlation mismatch"
+ raise ProcessWorkerError(
+ f"worker response correlation mismatch: {response_id} != {request_id}"
+ )
+ if success:
+ return value
+ remote_type, message, remote_traceback = value
+ raise RemoteWorkerError(remote_type, message, remote_traceback)
+
+ async def _put_request(self, request: Any, deadline: float | None) -> None:
+ while True:
+ if not self.is_alive:
+ raise ProcessWorkerError("worker exited before accepting request")
+ timeout = _poll_timeout(deadline)
+ try:
+ await asyncio.to_thread(self._requests.put, request, True, timeout)
+ return
+ except queue.Full:
+ if deadline is not None and monotonic() >= deadline:
+ raise TimeoutError("worker request timed out")
+
+ async def _get_response(self, deadline: float | None) -> Any:
+ while True:
+ if not self.is_alive:
+ raise ProcessWorkerError("worker exited before returning a response")
+ timeout = _poll_timeout(deadline)
+ try:
+ return await asyncio.to_thread(self._results.get, True, timeout)
+ except queue.Empty:
+ if deadline is not None and monotonic() >= deadline:
+ raise TimeoutError("worker response timed out")
+
+ async def stop(self, *, timeout_s: float = 5.0) -> None:
+ if timeout_s <= 0:
+ raise ValueError("timeout_s must be positive")
+ process = self._process
+ if process is None:
+ return
+ if process.is_alive():
+ try:
+ await asyncio.to_thread(
+ self._requests.put, _STOP, True, min(timeout_s, 1.0)
+ )
+ except queue.Full:
+ pass
+ await asyncio.to_thread(process.join, timeout_s)
+ if process.is_alive():
+ process.terminate()
+ await asyncio.to_thread(process.join, 1.0)
+ self._process = None
+ self._requests.close()
+ self._results.close()
+ self._closed = True
+
+
+def _worker_main(factory: WorkerFactory, requests: Any, results: Any) -> None:
+ try:
+ handler = factory()
+ except BaseException as exc:
+ # There is no request id yet, so the parent observes process death.
+ traceback.print_exception(exc)
+ return
+
+ while True:
+ item = requests.get()
+ if item == _STOP:
+ return
+ request_id, payload = item
+ try:
+ result = handler(payload)
+ results.put((request_id, True, result))
+ except BaseException as exc:
+ results.put(
+ (
+ request_id,
+ False,
+ (type(exc).__name__, str(exc), traceback.format_exc()),
+ )
+ )
+
+
+def _poll_timeout(deadline: float | None) -> float:
+ if deadline is None:
+ return 0.2
+ remaining = deadline - monotonic()
+ if remaining <= 0:
+ return 0.0
+ return min(remaining, 0.2)
+
+
+__all__ = [
+ "PersistentProcessWorker",
+ "ProcessWorkerError",
+ "RemoteWorkerError",
+ "WorkerFactory",
+ "WorkerHandler",
+]
diff --git a/src/cmvr_edge_ai/workers/thread.py b/src/cmvr_edge_ai/workers/thread.py
new file mode 100644
index 0000000..6ae775c
--- /dev/null
+++ b/src/cmvr_edge_ai/workers/thread.py
@@ -0,0 +1,32 @@
+"""Bounded thread offload helper using the application's shared executor."""
+
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Callable
+from concurrent.futures import Executor
+from functools import partial
+from typing import Any, TypeVar
+
+
+ResultT = TypeVar("ResultT")
+
+
+async def run_blocking(
+ function: Callable[..., ResultT],
+ *args: Any,
+ executor: Executor | None = None,
+ **kwargs: Any,
+) -> ResultT:
+ """Run blocking native/SDK code without starving the event loop.
+
+ Plugins should pass the bounded ``thread_executor`` from ComponentContext
+ metadata. Leaving it unset deliberately uses the host loop's executor
+ without replacing or taking ownership of that executor.
+ """
+
+ call = partial(function, *args, **kwargs)
+ return await asyncio.get_running_loop().run_in_executor(executor, call)
+
+
+__all__ = ["run_blocking"]
diff --git a/talk_server/README.md b/talk_server/README.md
new file mode 100644
index 0000000..2cb2f6b
--- /dev/null
+++ b/talk_server/README.md
@@ -0,0 +1,15 @@
+# Talk pipeline
+
+`pipeline.yaml` is runnable with a simulated audio chunk so the framework can
+be tested before the cmvr-es microphone stream proto is available.
+
+When the bidirectional audio RPC lands, replace the synthetic source with the
+typed `cmvr.grpc.microphone_audio_stream@1` connector and expand the chain to:
+
+```text
+AudioChunk -> VAD -> ASR -> dialogue -> TTS -> speaker stream
+```
+
+The internal `AudioChunk/v1` contract and port/QoS model are already independent
+of the final protobuf message names.
+
diff --git a/talk_server/pipeline.yaml b/talk_server/pipeline.yaml
new file mode 100644
index 0000000..0f65f2f
--- /dev/null
+++ b/talk_server/pipeline.yaml
@@ -0,0 +1,42 @@
+api_version: cmvr.edge.ai/v1
+
+runtime:
+ thread_workers: 3
+ shutdown_timeout_s: 8
+
+pipelines:
+ talk_smoke:
+ enabled: true
+ nodes:
+ audio_stream_placeholder:
+ # Replace with cmvr.grpc.microphone_audio_stream@1 when its proto lands.
+ uses: core.sequence_source@1
+ with:
+ items:
+ - simulated-audio-chunk
+ schema_name: AudioChunk
+ schema_version: 1
+
+ dialogue_placeholder:
+ # The real chain will be VAD -> ASR -> dialogue -> TTS.
+ uses: core.passthrough@1
+
+ output:
+ uses: core.log_sink@1
+ with:
+ logger: cmvr_edge_ai.talk
+
+ edges:
+ - from: audio_stream_placeholder.output
+ to: dialogue_placeholder.input
+ qos:
+ profile: audio_contiguous
+ capacity: 16
+ overflow: block
+
+ - from: dialogue_placeholder.output
+ to: output.input
+ qos:
+ profile: request
+ capacity: 8
+ overflow: block
diff --git a/tests/__pycache__/conftest.cpython-310.pyc b/tests/__pycache__/conftest.cpython-310.pyc
new file mode 100644
index 0000000..68c5b34
Binary files /dev/null and b/tests/__pycache__/conftest.cpython-310.pyc differ
diff --git a/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..e5f30ae
Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/__pycache__/conftest.cpython-312.pyc b/tests/__pycache__/conftest.cpython-312.pyc
new file mode 100644
index 0000000..65b4cc7
Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312.pyc differ
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..4ded8f2
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,13 @@
+"""Test bootstrap for the repository's ``src`` layout."""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
+SOURCE_ROOT = PROJECT_ROOT / "src"
+
+if str(SOURCE_ROOT) not in sys.path:
+ sys.path.insert(0, str(SOURCE_ROOT))
diff --git a/tests/integration/__pycache__/test_application_cli.cpython-310.pyc b/tests/integration/__pycache__/test_application_cli.cpython-310.pyc
new file mode 100644
index 0000000..0d184ea
Binary files /dev/null and b/tests/integration/__pycache__/test_application_cli.cpython-310.pyc differ
diff --git a/tests/integration/__pycache__/test_application_cli.cpython-312-pytest-7.4.4.pyc b/tests/integration/__pycache__/test_application_cli.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..6d6689e
Binary files /dev/null and b/tests/integration/__pycache__/test_application_cli.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/integration/__pycache__/test_application_cli.cpython-312.pyc b/tests/integration/__pycache__/test_application_cli.cpython-312.pyc
new file mode 100644
index 0000000..625501c
Binary files /dev/null and b/tests/integration/__pycache__/test_application_cli.cpython-312.pyc differ
diff --git a/tests/integration/__pycache__/test_detection_pipeline.cpython-310.pyc b/tests/integration/__pycache__/test_detection_pipeline.cpython-310.pyc
new file mode 100644
index 0000000..a759bda
Binary files /dev/null and b/tests/integration/__pycache__/test_detection_pipeline.cpython-310.pyc differ
diff --git a/tests/integration/__pycache__/test_detection_pipeline.cpython-312-pytest-7.4.4.pyc b/tests/integration/__pycache__/test_detection_pipeline.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..71abac3
Binary files /dev/null and b/tests/integration/__pycache__/test_detection_pipeline.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/integration/__pycache__/test_detection_pipeline.cpython-312.pyc b/tests/integration/__pycache__/test_detection_pipeline.cpython-312.pyc
new file mode 100644
index 0000000..5c7bc30
Binary files /dev/null and b/tests/integration/__pycache__/test_detection_pipeline.cpython-312.pyc differ
diff --git a/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312-pytest-7.4.4.pyc b/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..0002559
Binary files /dev/null and b/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312.pyc b/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312.pyc
new file mode 100644
index 0000000..f87e365
Binary files /dev/null and b/tests/integration/__pycache__/test_dual_detection_pipeline.cpython-312.pyc differ
diff --git a/tests/integration/__pycache__/test_process_worker.cpython-310.pyc b/tests/integration/__pycache__/test_process_worker.cpython-310.pyc
new file mode 100644
index 0000000..003ee57
Binary files /dev/null and b/tests/integration/__pycache__/test_process_worker.cpython-310.pyc differ
diff --git a/tests/integration/__pycache__/test_process_worker.cpython-312-pytest-7.4.4.pyc b/tests/integration/__pycache__/test_process_worker.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..ddb0121
Binary files /dev/null and b/tests/integration/__pycache__/test_process_worker.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/integration/__pycache__/test_process_worker.cpython-312.pyc b/tests/integration/__pycache__/test_process_worker.cpython-312.pyc
new file mode 100644
index 0000000..61dd317
Binary files /dev/null and b/tests/integration/__pycache__/test_process_worker.cpython-312.pyc differ
diff --git a/tests/integration/__pycache__/test_runtime.cpython-310.pyc b/tests/integration/__pycache__/test_runtime.cpython-310.pyc
new file mode 100644
index 0000000..95a32e9
Binary files /dev/null and b/tests/integration/__pycache__/test_runtime.cpython-310.pyc differ
diff --git a/tests/integration/__pycache__/test_runtime.cpython-312-pytest-7.4.4.pyc b/tests/integration/__pycache__/test_runtime.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..e19efaa
Binary files /dev/null and b/tests/integration/__pycache__/test_runtime.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/integration/__pycache__/test_runtime.cpython-312.pyc b/tests/integration/__pycache__/test_runtime.cpython-312.pyc
new file mode 100644
index 0000000..fb877a3
Binary files /dev/null and b/tests/integration/__pycache__/test_runtime.cpython-312.pyc differ
diff --git a/tests/integration/test_application_cli.py b/tests/integration/test_application_cli.py
new file mode 100644
index 0000000..0194d6b
--- /dev/null
+++ b/tests/integration/test_application_cli.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+
+from cmvr_edge_ai.application import (
+ ApplicationState,
+ EdgeAIApplication,
+ create_default_registry,
+ validate_application,
+)
+from cmvr_edge_ai.cli import main
+from cmvr_edge_ai.config import load_config
+
+
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+SMOKE_CONFIG = PROJECT_ROOT / "configs" / "smoke.yaml"
+DETECTION_CONFIG = PROJECT_ROOT / "detect_server" / "pipeline.yaml"
+
+
+def test_application_runs_the_finite_smoke_pipeline() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ loop = asyncio.get_running_loop()
+ host_executor = ThreadPoolExecutor(max_workers=1)
+ loop.set_default_executor(host_executor)
+ application = EdgeAIApplication(
+ load_config(SMOKE_CONFIG),
+ create_default_registry(discover_entry_points=False),
+ )
+ try:
+ await application.start()
+ running_health = await application.health()
+ await application.wait()
+ await application.stop()
+ # The application owns only its bounded executor. Stopping it must
+ # not poison an embedding event loop's pre-existing executor.
+ host_executor_result = await asyncio.to_thread(lambda: "still-usable")
+ return application, running_health, host_executor_result
+ finally:
+ # Python 3.12 Runner shuts its default executor down from another
+ # helper thread. Explicit cleanup keeps this regression deterministic
+ # in restricted containers where that helper cannot wake the loop.
+ host_executor.shutdown(wait=True, cancel_futures=True)
+ loop._default_executor = None # type: ignore[attr-defined]
+
+ application, running_health, host_executor_result = asyncio.run(exercise())
+
+ assert running_health["state"] == "running"
+ assert set(running_health["pipelines"]) == {"smoke"}
+ assert application.pipelines == ("smoke",)
+ assert application.state is ApplicationState.STOPPED
+ assert host_executor_result == "still-usable"
+
+
+def test_cli_validates_and_runs_smoke_config(
+ capsys, monkeypatch
+) -> None: # type: ignore[no-untyped-def]
+ # ``configure_logging(force=True)`` intentionally owns process logging in
+ # production. Avoid leaking that global CLI side effect into later tests.
+ monkeypatch.setattr("cmvr_edge_ai.cli.configure_logging", lambda *_: None)
+ assert main(["validate", "--config", str(SMOKE_CONFIG)]) == 0
+ validation_output = capsys.readouterr()
+ assert "configuration is valid; pipelines: smoke" in validation_output.out
+ assert validation_output.err == ""
+
+ assert main(["run", "--config", str(SMOKE_CONFIG), "--log-level", "WARNING"]) == 0
+ run_output = capsys.readouterr()
+ assert run_output.err == ""
+
+
+def test_cli_lists_versioned_builtin_plugins(capsys) -> None: # type: ignore[no-untyped-def]
+ assert main(["plugins"]) == 0
+
+ output = capsys.readouterr().out
+ assert "core.sequence_source@1\tsource" in output
+ assert "detection.model@1\toperator" in output
+ assert "media.video_decoder.pyav@1\toperator" in output
+ assert "safety.robot_command_gate@1\toperator" in output
+
+
+def test_cli_lists_detection_model_metadata(capsys) -> None: # type: ignore[no-untyped-def]
+ assert main(["models"]) == 0
+
+ output = capsys.readouterr().out
+ assert (
+ "construction-ppe-yolov8@1\tConstruction PPE YOLOv8s\t" "ultralytics-yolo"
+ ) in output
+ assert "No-Helmet" in output
+ assert "No-Vest" in output
+ assert (
+ "ppe-6classes-yolov8n@1\tPPE Detection YOLOv8n (6 Classes)\t"
+ "ultralytics-yolo"
+ ) in output
+ assert "Gloves,Vest,goggles,helmet,mask,safety_shoe" in output
+
+
+def test_real_detection_config_compiles_without_loading_optional_runtimes() -> None:
+ config = load_config(DETECTION_CONFIG)
+ compiled = validate_application(
+ config,
+ create_default_registry(discover_entry_points=False),
+ )
+
+ assert len(compiled) == 1
+ assert (
+ config.pipelines["detection"].nodes["platform"].params["failure_mode"]
+ == "log_and_drop"
+ )
+ assert set(compiled[0].plugin_specs) == {
+ "camera",
+ "decoder",
+ "detector",
+ "repeat_gate",
+ "platform",
+ }
diff --git a/tests/integration/test_detection_pipeline.py b/tests/integration/test_detection_pipeline.py
new file mode 100644
index 0000000..4e24398
--- /dev/null
+++ b/tests/integration/test_detection_pipeline.py
@@ -0,0 +1,223 @@
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Mapping, Sequence
+from concurrent.futures import ThreadPoolExecutor
+from io import BytesIO
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.application import create_default_registry
+from cmvr_edge_ai.compiler import compile_pipeline
+from cmvr_edge_ai.config import load_config_data
+from cmvr_edge_ai.contracts import (
+ BoundingBox,
+ Detection,
+ DetectionAlert,
+ ImageFrame,
+)
+from cmvr_edge_ai.core import Envelope, Sink
+from cmvr_edge_ai.detection import (
+ DetectionModel,
+ DetectionModelRegistry,
+ DetectionModelSpec,
+)
+from cmvr_edge_ai.plugins import PluginKind, PluginSpec
+
+
+class _AlwaysViolationModel(DetectionModel):
+ def __init__(self) -> None:
+ self.load_calls = 0
+ self.predict_calls = 0
+ self.close_calls = 0
+
+ def load(self) -> None:
+ self.load_calls += 1
+
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ del frame, confidence
+ self.predict_calls += 1
+ if "No-Helmet" not in labels:
+ return ()
+ return (
+ Detection(
+ "No-Helmet",
+ 0.95,
+ BoundingBox(1.0, 2.0, 20.0, 40.0),
+ ),
+ )
+
+ def close(self) -> None:
+ self.close_calls += 1
+
+
+class _CollectAlertSink(Sink):
+ def __init__(self) -> None:
+ self.alerts: list[Envelope[Any]] = []
+
+ async def consume(
+ self, envelope: Envelope[Any], input_port: str = "alerts"
+ ) -> None:
+ del input_port
+ self.alerts.append(envelope)
+
+
+async def _keep_event_loop_responsive() -> None:
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def test_decoded_frames_flow_through_registered_model_and_repeat_gate() -> None:
+ pillow_image = pytest.importorskip("PIL.Image")
+ model = _AlwaysViolationModel()
+ models = DetectionModelRegistry()
+ models.register(
+ DetectionModelSpec(
+ model_id="fake-ppe@1",
+ name="Fake PPE",
+ supported_labels=("No-Helmet",),
+ factory=lambda options: model,
+ backend="fake",
+ )
+ )
+ registry = create_default_registry(
+ discover_entry_points=False,
+ model_registry=models,
+ )
+ sink = _CollectAlertSink()
+
+ def sink_factory(node_id: str, params: Mapping[str, Any]) -> Sink:
+ del node_id, params
+ return sink
+
+ registry.register(
+ PluginSpec(
+ plugin_id="test.alert_sink@1",
+ kind=PluginKind.SINK,
+ factory=sink_factory,
+ inputs={"alerts": "DetectionAlert/v1"},
+ )
+ )
+ width, height = 64, 48
+ frames = (
+ ImageFrame(bytes((0, 0, 255)) * (width * height), width, height, "BGR8"),
+ ImageFrame(bytes((0, 255, 0)) * (width * height), width, height, "BGR8"),
+ ImageFrame(bytes((255, 0, 0)) * (width * height), width, height, "BGR8"),
+ )
+ config = load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "ppe": {
+ "nodes": {
+ "frames": {
+ "uses": "core.sequence_source@1",
+ "with": {
+ "items": list(frames),
+ "schema_name": "ImageFrame",
+ },
+ },
+ "detector": {
+ "uses": "detection.model@1",
+ "with": {
+ "model": "fake-ppe@1",
+ "detect_labels": ["No-Helmet"],
+ "attach_frame": True,
+ },
+ },
+ "gate": {
+ "uses": "detection.repeat_gate@1",
+ "with": {
+ "time_source": "received",
+ "alert_image": {
+ "enabled": True,
+ "jpeg_quality": 85,
+ },
+ "rules": [
+ {
+ "id": "no-helmet",
+ "labels": ["No-Helmet"],
+ "min_hits": 2,
+ "window_ms": 10_000,
+ "cooldown_ms": 30_000,
+ }
+ ],
+ },
+ },
+ "sink": {"uses": "test.alert_sink@1"},
+ },
+ "edges": [
+ {"from": "frames.output", "to": "detector.frames"},
+ {
+ "from": "detector.detections",
+ "to": "gate.detections",
+ },
+ {"from": "gate.alerts", "to": "sink.alerts"},
+ ],
+ }
+ },
+ }
+ )
+
+ gate_health_details: list[str] = []
+
+ async def scenario() -> None:
+ loop = asyncio.get_running_loop()
+ executor = ThreadPoolExecutor(max_workers=2)
+ loop.set_default_executor(executor)
+ ticker = asyncio.create_task(_keep_event_loop_responsive())
+ try:
+ compiled = compile_pipeline(
+ config,
+ "ppe",
+ registry,
+ metadata={"thread_executor": executor},
+ )
+ await compiled.runtime.run()
+ gate_health = await compiled.runtime.nodes["gate"].health()
+ gate_health_details.append(gate_health.detail)
+ finally:
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ executor.shutdown(wait=True, cancel_futures=True)
+ loop._default_executor = None # type: ignore[attr-defined]
+
+ asyncio.run(scenario())
+
+ assert model.load_calls == 1
+ assert model.predict_calls == 3
+ assert model.close_calls == 1
+ assert len(sink.alerts) == 1
+ alert = sink.alerts[0].payload
+ assert isinstance(alert, DetectionAlert)
+ assert alert.rule_id == "no-helmet"
+ assert alert.hit_count == 2
+ assert alert.labels == ("No-Helmet",)
+ assert sink.alerts[0].sequence == 1
+ assert alert.image is not None
+ assert alert.image.media_type == "image/jpeg"
+ assert (alert.image.width, alert.image.height) == (width, height)
+ assert alert.image.data.startswith(b"\xff\xd8")
+
+ with pillow_image.open(BytesIO(alert.image.data)) as decoded:
+ decoded.load()
+ assert decoded.format == "JPEG"
+ assert decoded.mode == "RGB"
+ assert decoded.size == (width, height)
+ # This pixel lies outside the annotation and proves that the evidence
+ # image came from sequence 1 (green), the frame that reached min_hits.
+ red, green, blue = decoded.getpixel((width - 1, height - 1))
+ assert green > 200
+ assert red < 40
+ assert blue < 40
+
+ assert gate_health_details
+ assert "processed_frames=3" in gate_health_details[0]
+ assert "alerts_emitted=1" in gate_health_details[0]
+ assert "alert_images_encoded=1" in gate_health_details[0]
diff --git a/tests/integration/test_dual_detection_pipeline.py b/tests/integration/test_dual_detection_pipeline.py
new file mode 100644
index 0000000..39d7511
--- /dev/null
+++ b/tests/integration/test_dual_detection_pipeline.py
@@ -0,0 +1,349 @@
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Mapping, Sequence
+from concurrent.futures import ThreadPoolExecutor
+from typing import Any
+
+from cmvr_edge_ai.application import (
+ create_default_model_registry,
+ create_default_registry,
+)
+from cmvr_edge_ai.compiler import compile_pipeline
+from cmvr_edge_ai.config import load_config_data
+from cmvr_edge_ai.contracts import (
+ BoundingBox,
+ Detection,
+ DetectionAlert,
+ DetectionResult,
+ ImageFrame,
+)
+from cmvr_edge_ai.core import Envelope, Sink
+from cmvr_edge_ai.detection import (
+ DetectionModel,
+ DetectionModelRegistry,
+ DetectionModelSpec,
+)
+from cmvr_edge_ai.plugins import PluginKind, PluginSpec
+
+
+SIX_CLASS_MODEL_ID = "ppe-6classes-yolov8n@1"
+SIX_CLASS_LABELS = (
+ "Gloves",
+ "Vest",
+ "goggles",
+ "helmet",
+ "mask",
+ "safety_shoe",
+)
+
+
+class _FixedDetectionModel(DetectionModel):
+ def __init__(self, label: str) -> None:
+ self.label = label
+ self.load_calls = 0
+ self.close_calls = 0
+ self.frames: list[ImageFrame] = []
+ self.selected_labels: list[tuple[str, ...]] = []
+
+ def load(self) -> None:
+ self.load_calls += 1
+
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ del confidence
+ self.frames.append(frame)
+ self.selected_labels.append(labels)
+ return (
+ Detection(
+ label=self.label,
+ confidence=0.95,
+ box=BoundingBox(1.0, 2.0, 20.0, 30.0),
+ ),
+ )
+
+ def close(self) -> None:
+ self.close_calls += 1
+
+
+class _CollectSink(Sink):
+ def __init__(self) -> None:
+ self.envelopes: list[Envelope[Any]] = []
+ self.input_ports: list[str] = []
+
+ async def consume(
+ self,
+ envelope: Envelope[Any],
+ input_port: str = "input",
+ ) -> None:
+ self.envelopes.append(envelope)
+ self.input_ports.append(input_port)
+
+
+async def _keep_event_loop_responsive() -> None:
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def test_default_registry_contains_six_class_ppe_model_contract() -> None:
+ registry = create_default_model_registry(discover_entry_points=False)
+
+ spec = registry.resolve(SIX_CLASS_MODEL_ID)
+
+ assert spec.model_id == SIX_CLASS_MODEL_ID
+ assert spec.supported_labels == SIX_CLASS_LABELS
+
+
+def test_decoder_fans_out_to_alert_and_raw_detection_branches() -> None:
+ ppe_model = _FixedDetectionModel("No-Helmet")
+ six_class_model = _FixedDetectionModel("helmet")
+ models = DetectionModelRegistry()
+ models.register(
+ DetectionModelSpec(
+ model_id="construction-ppe-yolov8@1",
+ name="Test Construction PPE",
+ supported_labels=("No-Helmet",),
+ factory=lambda options: ppe_model,
+ backend="fake",
+ )
+ )
+ models.register(
+ DetectionModelSpec(
+ model_id=SIX_CLASS_MODEL_ID,
+ name="Test Six-Class PPE",
+ supported_labels=SIX_CLASS_LABELS,
+ factory=lambda options: six_class_model,
+ backend="fake",
+ )
+ )
+ registry = create_default_registry(
+ discover_entry_points=False,
+ model_registry=models,
+ )
+ alert_sink = _CollectSink()
+ raw_sink = _CollectSink()
+ registry.register(
+ PluginSpec(
+ plugin_id="test.dual_alert_sink@1",
+ kind=PluginKind.SINK,
+ factory=lambda node_id, params: alert_sink,
+ inputs={"alerts": "DetectionAlert/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ plugin_id="test.dual_raw_detection_sink@1",
+ kind=PluginKind.SINK,
+ factory=lambda node_id, params: raw_sink,
+ inputs={"detections": "DetectionResult/v1"},
+ )
+ )
+ width, height = 4, 3
+ frames = (
+ ImageFrame(bytes((0, 0, 255)) * (width * height), width, height, "BGR8"),
+ ImageFrame(bytes((0, 255, 0)) * (width * height), width, height, "BGR8"),
+ )
+ config = load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "dual_detection": {
+ "nodes": {
+ "source": {
+ "uses": "core.sequence_source@1",
+ "with": {
+ "items": list(frames),
+ "schema_name": "ImageFrame",
+ },
+ },
+ "decoder": {"uses": "media.video_decoder.pyav@1"},
+ "ppe_detector": {
+ "uses": "detection.model@1",
+ "with": {
+ "model": "construction-ppe-yolov8@1",
+ "detect_labels": ["No-Helmet"],
+ },
+ },
+ "ppe_gate": {
+ "uses": "detection.repeat_gate@1",
+ "with": {
+ "time_source": "received",
+ "rules": [
+ {
+ "id": "no-helmet",
+ "labels": ["No-Helmet"],
+ "min_hits": 2,
+ "window_ms": 60_000,
+ "cooldown_ms": 60_000,
+ "scope": "source",
+ }
+ ],
+ },
+ },
+ "alert_sink": {"uses": "test.dual_alert_sink@1"},
+ "six_class_detector": {
+ "uses": "detection.model@1",
+ "with": {
+ "model": SIX_CLASS_MODEL_ID,
+ "detect_labels": list(SIX_CLASS_LABELS),
+ "attach_frame": False,
+ },
+ },
+ "raw_sink": {
+ "uses": "test.dual_raw_detection_sink@1"
+ },
+ },
+ "edges": [
+ {
+ "from": "source.output",
+ "to": "decoder.frames",
+ "qos": {
+ "profile": "video_contiguous",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ {
+ "from": "decoder.frames",
+ "to": "ppe_detector.frames",
+ "qos": {
+ "profile": "telemetry",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ {
+ "from": "decoder.frames",
+ "to": "six_class_detector.frames",
+ "qos": {
+ "profile": "telemetry",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ {
+ "from": "ppe_detector.detections",
+ "to": "ppe_gate.detections",
+ "qos": {
+ "profile": "telemetry",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ {
+ "from": "ppe_gate.alerts",
+ "to": "alert_sink.alerts",
+ "qos": {
+ "profile": "telemetry",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ {
+ "from": "six_class_detector.detections",
+ "to": "raw_sink.detections",
+ "qos": {
+ "profile": "telemetry",
+ "capacity": 4,
+ "overflow": "block",
+ },
+ },
+ ],
+ }
+ },
+ }
+ )
+
+ async def scenario(): # type: ignore[no-untyped-def]
+ loop = asyncio.get_running_loop()
+ executor = ThreadPoolExecutor(max_workers=2)
+ loop.set_default_executor(executor)
+ ticker = asyncio.create_task(_keep_event_loop_responsive())
+ try:
+ compiled = compile_pipeline(
+ config,
+ "dual_detection",
+ registry,
+ metadata={"thread_executor": executor},
+ )
+ await compiled.runtime.run()
+ return compiled
+ finally:
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ executor.shutdown(wait=True, cancel_futures=True)
+ loop._default_executor = None # type: ignore[attr-defined]
+
+ compiled = asyncio.run(scenario())
+
+ assert compiled.plugin_specs["ppe_gate"].outputs == {
+ "alerts": "DetectionAlert/v1"
+ }
+ assert compiled.plugin_specs["six_class_detector"].outputs == {
+ "detections": "DetectionResult/v1"
+ }
+
+ assert ppe_model.load_calls == ppe_model.close_calls == 1
+ assert six_class_model.load_calls == six_class_model.close_calls == 1
+ assert len(ppe_model.frames) == len(six_class_model.frames) == len(frames)
+ assert all(
+ ppe_frame is six_class_frame
+ for ppe_frame, six_class_frame in zip(
+ ppe_model.frames,
+ six_class_model.frames,
+ )
+ )
+ assert ppe_model.selected_labels == [("No-Helmet",)] * len(frames)
+ assert six_class_model.selected_labels == [SIX_CLASS_LABELS] * len(frames)
+
+ assert len(alert_sink.envelopes) == 1
+ alert_envelope = alert_sink.envelopes[0]
+ assert alert_sink.input_ports == ["alerts"]
+ assert alert_envelope.schema == "DetectionAlert/v1"
+ assert alert_envelope.source_id == "source"
+ assert alert_envelope.sequence == 1
+ assert isinstance(alert_envelope.payload, DetectionAlert)
+ assert alert_envelope.payload.model_id == "construction-ppe-yolov8@1"
+ assert alert_envelope.payload.rule_id == "no-helmet"
+
+ assert raw_sink.input_ports == ["detections", "detections"]
+ assert [envelope.schema for envelope in raw_sink.envelopes] == [
+ "DetectionResult/v1",
+ "DetectionResult/v1",
+ ]
+ assert [envelope.source_id for envelope in raw_sink.envelopes] == [
+ "source",
+ "source",
+ ]
+ assert [envelope.sequence for envelope in raw_sink.envelopes] == [0, 1]
+ assert all(
+ isinstance(envelope.payload, DetectionResult)
+ for envelope in raw_sink.envelopes
+ )
+ assert all(
+ envelope.payload.model_id == SIX_CLASS_MODEL_ID
+ for envelope in raw_sink.envelopes
+ )
+ assert all(
+ envelope.payload.source_frame is None
+ for envelope in raw_sink.envelopes
+ )
+ assert alert_envelope.trace_id == raw_sink.envelopes[1].trace_id
+
+ stats = compiled.runtime.edge_stats()
+ ppe_fanout = next(
+ value
+ for name, value in stats.items()
+ if name.endswith("decoder.frames->ppe_detector.frames")
+ )
+ six_class_fanout = next(
+ value
+ for name, value in stats.items()
+ if name.endswith("decoder.frames->six_class_detector.frames")
+ )
+ assert ppe_fanout.enqueued == len(frames)
+ assert six_class_fanout.enqueued == len(frames)
diff --git a/tests/integration/test_process_worker.py b/tests/integration/test_process_worker.py
new file mode 100644
index 0000000..6cd9683
--- /dev/null
+++ b/tests/integration/test_process_worker.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+from time import sleep
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.workers import (
+ PersistentProcessWorker,
+ ProcessWorkerError,
+ RemoteWorkerError,
+)
+
+
+def _worker_handler(payload: Any) -> Any:
+ if payload == "explode":
+ raise RuntimeError("intentional remote failure")
+ if payload == "slow":
+ sleep(0.3)
+ return "late-result"
+ return payload * 2
+
+
+def _worker_factory(): # type: ignore[no-untyped-def]
+ """Top-level factory so the spawn start method can import and pickle it."""
+
+ return _worker_handler
+
+
+async def _keep_restricted_event_loop_responsive() -> None:
+ """Provide wakeups where sandboxed self-pipe notifications are unavailable."""
+
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def test_spawn_worker_returns_results_and_surfaces_remote_errors() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ loop = asyncio.get_running_loop()
+ host_executor = ThreadPoolExecutor(max_workers=2)
+ loop.set_default_executor(host_executor)
+ ticker = asyncio.create_task(_keep_restricted_event_loop_responsive())
+ worker = PersistentProcessWorker(
+ _worker_factory,
+ start_method="spawn",
+ queue_capacity=1,
+ )
+ try:
+ await worker.start()
+ assert await worker.submit(21, timeout_s=10) == 42
+ with pytest.raises(RemoteWorkerError) as captured:
+ await worker.submit("explode", timeout_s=10)
+ assert captured.value.remote_type == "RuntimeError"
+ assert "intentional remote failure" in str(captured.value)
+ finally:
+ await worker.stop(timeout_s=5)
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ host_executor.shutdown(wait=True, cancel_futures=True)
+ loop._default_executor = None # type: ignore[attr-defined]
+ return worker
+
+ worker = asyncio.run(exercise())
+
+ assert worker.is_alive is False
+
+
+def test_timed_out_request_poisons_worker_and_rejects_later_submits() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ loop = asyncio.get_running_loop()
+ host_executor = ThreadPoolExecutor(max_workers=2)
+ loop.set_default_executor(host_executor)
+ ticker = asyncio.create_task(_keep_restricted_event_loop_responsive())
+ worker = PersistentProcessWorker(
+ _worker_factory,
+ start_method="spawn",
+ queue_capacity=1,
+ )
+ try:
+ await worker.start()
+ with pytest.raises(TimeoutError, match="timed out"):
+ await worker.submit("slow", timeout_s=0.05)
+ assert worker.is_alive is True
+ with pytest.raises(ProcessWorkerError, match="worker is poisoned"):
+ await worker.submit(22, timeout_s=1)
+ finally:
+ await worker.stop(timeout_s=2)
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ host_executor.shutdown(wait=True, cancel_futures=True)
+ loop._default_executor = None # type: ignore[attr-defined]
+ return worker
+
+ worker = asyncio.run(exercise())
+
+ assert worker.is_alive is False
diff --git a/tests/integration/test_runtime.py b/tests/integration/test_runtime.py
new file mode 100644
index 0000000..098df83
--- /dev/null
+++ b/tests/integration/test_runtime.py
@@ -0,0 +1,235 @@
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+from cmvr_edge_ai.core import (
+ ComponentContext,
+ Edge,
+ Envelope,
+ PipelineRuntime,
+ RuntimeState,
+ Sink,
+ Source,
+)
+
+
+class ListSource(Source):
+ def __init__(self, items: list[str]) -> None:
+ self.items = items
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ for sequence, item in enumerate(self.items):
+ yield Envelope(item, source_id="source", sequence=sequence)
+
+
+class CollectSink(Sink):
+ def __init__(self, delay_s: float = 0.0) -> None:
+ self.delay_s = delay_s
+ self.envelopes: list[Envelope[Any]] = []
+ self.input_ports: list[str] = []
+ self.stop_calls = 0
+
+ async def consume(self, envelope: Envelope[Any], input_port: str = "input") -> None:
+ if self.delay_s:
+ await asyncio.sleep(self.delay_s)
+ self.envelopes.append(envelope)
+ self.input_ports.append(input_port)
+
+ async def stop(self) -> None:
+ self.stop_calls += 1
+
+
+class BurstThenWaitSource(Source):
+ def __init__(self, items: list[str]) -> None:
+ self.items = items
+ self.produced = asyncio.Event()
+ self.release = asyncio.Event()
+ self.stop_calls = 0
+
+ async def setup(self, context: ComponentContext) -> None:
+ self.shutdown_event = context.shutdown_event
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ for sequence, item in enumerate(self.items):
+ yield Envelope(item, source_id="burst", sequence=sequence)
+ self.produced.set()
+ await self.release.wait()
+
+ async def stop(self) -> None:
+ self.stop_calls += 1
+
+
+class SetupBlockingSource(Source):
+ def __init__(
+ self,
+ setup_entered: asyncio.Event,
+ allow_setup: asyncio.Event,
+ run_forever: asyncio.Event,
+ ) -> None:
+ self.setup_entered = setup_entered
+ self.allow_setup = allow_setup
+ self.run_forever = run_forever
+ self.stop_calls = 0
+
+ async def setup(self, context: ComponentContext) -> None:
+ del context
+ self.setup_entered.set()
+ await self.allow_setup.wait()
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ await self.run_forever.wait()
+ if False:
+ yield Envelope("unreachable")
+
+ async def stop(self) -> None:
+ self.stop_calls += 1
+
+
+class BlockingStopSource(Source):
+ def __init__(
+ self,
+ run_forever: asyncio.Event,
+ stop_entered: asyncio.Event,
+ stop_cancelled: asyncio.Event,
+ ) -> None:
+ self.run_forever = run_forever
+ self.stop_entered = stop_entered
+ self.stop_cancelled = stop_cancelled
+
+ async def messages(self): # type: ignore[no-untyped-def]
+ await self.run_forever.wait()
+ if False:
+ yield Envelope("unreachable")
+
+ async def stop(self) -> None:
+ self.stop_entered.set()
+ try:
+ await asyncio.Event().wait()
+ except asyncio.CancelledError:
+ self.stop_cancelled.set()
+ raise
+
+
+def test_runtime_fan_out_delivers_each_envelope_to_each_edge() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ source = ListSource(["one", "two", "three"])
+ left = CollectSink()
+ right = CollectSink()
+ runtime = PipelineRuntime(
+ {"source": source, "left": left, "right": right},
+ [
+ Edge("source", "left", capacity=2, name="left-edge"),
+ Edge("source", "right", capacity=2, name="right-edge"),
+ ],
+ pipeline_id="fanout",
+ )
+ await runtime.run()
+ return runtime, left, right
+
+ runtime, left, right = asyncio.run(exercise())
+
+ assert runtime.state is RuntimeState.STOPPED
+ assert [item.payload for item in left.envelopes] == ["one", "two", "three"]
+ assert [item.payload for item in right.envelopes] == ["one", "two", "three"]
+ assert all(
+ left_item is right_item
+ for left_item, right_item in zip(left.envelopes, right.envelopes)
+ )
+ assert left.input_ports == ["input"] * 3
+ assert set(runtime.edge_stats()) == {"left-edge", "right-edge"}
+
+
+def test_graceful_stop_drains_already_accepted_messages_and_stops_once() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ source = BurstThenWaitSource(["one", "two", "three"])
+ sink = CollectSink(delay_s=0.01)
+ runtime = PipelineRuntime(
+ {"source": source, "sink": sink},
+ [Edge("source", "sink", capacity=3, name="drain-edge")],
+ pipeline_id="graceful",
+ shutdown_timeout_s=1,
+ )
+
+ await runtime.start()
+ await asyncio.wait_for(source.produced.wait(), timeout=1)
+ await runtime.stop(graceful=True)
+ return source, sink, runtime
+
+ source, sink, runtime = asyncio.run(exercise())
+
+ assert [item.payload for item in sink.envelopes] == ["one", "two", "three"]
+ assert source.stop_calls == 1
+ assert sink.stop_calls == 1
+ assert runtime.state is RuntimeState.STOPPED
+ stats = runtime.edge_stats()["drain-edge"]
+ assert stats.enqueued == stats.dequeued == 3
+ assert stats.discarded_on_close == 0
+ assert stats.closed is True
+
+
+def test_stop_racing_start_waits_for_startup_then_tears_down_once() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ setup_entered = asyncio.Event()
+ allow_setup = asyncio.Event()
+ source = SetupBlockingSource(
+ setup_entered,
+ allow_setup,
+ asyncio.Event(),
+ )
+ sink = CollectSink()
+ runtime = PipelineRuntime(
+ {"source": source, "sink": sink},
+ [Edge("source", "sink")],
+ pipeline_id="start-stop-race",
+ )
+
+ start_task = asyncio.create_task(runtime.start())
+ await asyncio.wait_for(setup_entered.wait(), timeout=1)
+ stop_task = asyncio.create_task(runtime.stop(graceful=False, timeout_s=1))
+ await asyncio.sleep(0)
+ assert runtime.state is RuntimeState.STARTING
+ assert stop_task.done() is False
+
+ allow_setup.set()
+ await asyncio.wait_for(asyncio.gather(start_task, stop_task), timeout=1)
+ return runtime, source, sink
+
+ runtime, source, sink = asyncio.run(exercise())
+
+ assert runtime.state is RuntimeState.STOPPED
+ assert runtime.shutdown_event.is_set()
+ assert source.stop_calls == 1
+ assert sink.stop_calls == 1
+
+
+def test_blocking_component_stop_is_bounded_by_shutdown_timeout() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ stop_entered = asyncio.Event()
+ stop_cancelled = asyncio.Event()
+ source = BlockingStopSource(
+ asyncio.Event(),
+ stop_entered,
+ stop_cancelled,
+ )
+ sink = CollectSink()
+ runtime = PipelineRuntime(
+ {"source": source, "sink": sink},
+ [Edge("source", "sink")],
+ pipeline_id="bounded-stop",
+ )
+ await runtime.start()
+
+ loop = asyncio.get_running_loop()
+ started_at = loop.time()
+ await runtime.stop(graceful=False, timeout_s=0.05)
+ elapsed = loop.time() - started_at
+ await asyncio.sleep(0)
+ return runtime, stop_entered, stop_cancelled, elapsed
+
+ runtime, stop_entered, stop_cancelled, elapsed = asyncio.run(exercise())
+
+ assert stop_entered.is_set()
+ assert stop_cancelled.is_set()
+ assert elapsed < 0.5
+ assert runtime.state is RuntimeState.STOPPED
diff --git a/tests/unit/__pycache__/test_agv_connector.cpython-310.pyc b/tests/unit/__pycache__/test_agv_connector.cpython-310.pyc
new file mode 100644
index 0000000..5e58ff1
Binary files /dev/null and b/tests/unit/__pycache__/test_agv_connector.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_agv_connector.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_agv_connector.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..5310bd1
Binary files /dev/null and b/tests/unit/__pycache__/test_agv_connector.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_agv_connector.cpython-312.pyc b/tests/unit/__pycache__/test_agv_connector.cpython-312.pyc
new file mode 100644
index 0000000..fb7b923
Binary files /dev/null and b/tests/unit/__pycache__/test_agv_connector.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_bidi_stream.cpython-310.pyc b/tests/unit/__pycache__/test_bidi_stream.cpython-310.pyc
new file mode 100644
index 0000000..3e005d2
Binary files /dev/null and b/tests/unit/__pycache__/test_bidi_stream.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_bidi_stream.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_bidi_stream.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..2818914
Binary files /dev/null and b/tests/unit/__pycache__/test_bidi_stream.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_bidi_stream.cpython-312.pyc b/tests/unit/__pycache__/test_bidi_stream.cpython-312.pyc
new file mode 100644
index 0000000..3388cd5
Binary files /dev/null and b/tests/unit/__pycache__/test_bidi_stream.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_builtin_plugins.cpython-310.pyc b/tests/unit/__pycache__/test_builtin_plugins.cpython-310.pyc
new file mode 100644
index 0000000..9298218
Binary files /dev/null and b/tests/unit/__pycache__/test_builtin_plugins.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_builtin_plugins.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_builtin_plugins.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..11ee504
Binary files /dev/null and b/tests/unit/__pycache__/test_builtin_plugins.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_builtin_plugins.cpython-312.pyc b/tests/unit/__pycache__/test_builtin_plugins.cpython-312.pyc
new file mode 100644
index 0000000..ed5d0db
Binary files /dev/null and b/tests/unit/__pycache__/test_builtin_plugins.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_camera_connector.cpython-310.pyc b/tests/unit/__pycache__/test_camera_connector.cpython-310.pyc
new file mode 100644
index 0000000..9b312e7
Binary files /dev/null and b/tests/unit/__pycache__/test_camera_connector.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_camera_connector.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_camera_connector.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..4d49ea2
Binary files /dev/null and b/tests/unit/__pycache__/test_camera_connector.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_camera_connector.cpython-312.pyc b/tests/unit/__pycache__/test_camera_connector.cpython-312.pyc
new file mode 100644
index 0000000..22da0f3
Binary files /dev/null and b/tests/unit/__pycache__/test_camera_connector.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_compiler.cpython-310.pyc b/tests/unit/__pycache__/test_compiler.cpython-310.pyc
new file mode 100644
index 0000000..b037d58
Binary files /dev/null and b/tests/unit/__pycache__/test_compiler.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_compiler.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_compiler.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..1ddd4d5
Binary files /dev/null and b/tests/unit/__pycache__/test_compiler.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_compiler.cpython-312.pyc b/tests/unit/__pycache__/test_compiler.cpython-312.pyc
new file mode 100644
index 0000000..c6b7087
Binary files /dev/null and b/tests/unit/__pycache__/test_compiler.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_config.cpython-310.pyc b/tests/unit/__pycache__/test_config.cpython-310.pyc
new file mode 100644
index 0000000..baa5b86
Binary files /dev/null and b/tests/unit/__pycache__/test_config.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_config.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_config.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..64adfef
Binary files /dev/null and b/tests/unit/__pycache__/test_config.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_config.cpython-312.pyc b/tests/unit/__pycache__/test_config.cpython-312.pyc
new file mode 100644
index 0000000..579c03a
Binary files /dev/null and b/tests/unit/__pycache__/test_config.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_annotation.cpython-310.pyc b/tests/unit/__pycache__/test_detection_annotation.cpython-310.pyc
new file mode 100644
index 0000000..9be4a73
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_annotation.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_annotation.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_detection_annotation.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..7e44e1d
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_annotation.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_annotation.cpython-312.pyc b/tests/unit/__pycache__/test_detection_annotation.cpython-312.pyc
new file mode 100644
index 0000000..3f42023
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_annotation.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_compile.cpython-310.pyc b/tests/unit/__pycache__/test_detection_compile.cpython-310.pyc
new file mode 100644
index 0000000..ed7645f
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_compile.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_compile.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_detection_compile.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..3377a95
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_compile.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_compile.cpython-312.pyc b/tests/unit/__pycache__/test_detection_compile.cpython-312.pyc
new file mode 100644
index 0000000..ebfb71e
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_compile.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_operator.cpython-310.pyc b/tests/unit/__pycache__/test_detection_operator.cpython-310.pyc
new file mode 100644
index 0000000..4b77faf
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_operator.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_operator.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_detection_operator.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..c4dac3f
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_operator.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_operator.cpython-312.pyc b/tests/unit/__pycache__/test_detection_operator.cpython-312.pyc
new file mode 100644
index 0000000..145b892
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_operator.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_registry.cpython-310.pyc b/tests/unit/__pycache__/test_detection_registry.cpython-310.pyc
new file mode 100644
index 0000000..5040f89
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_registry.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_registry.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_detection_registry.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..d21dab4
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_registry.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_registry.cpython-312.pyc b/tests/unit/__pycache__/test_detection_registry.cpython-312.pyc
new file mode 100644
index 0000000..0c7ff3f
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_registry.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_trigger.cpython-310.pyc b/tests/unit/__pycache__/test_detection_trigger.cpython-310.pyc
new file mode 100644
index 0000000..9b3a253
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_trigger.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_trigger.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_detection_trigger.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..7c82595
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_trigger.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_detection_trigger.cpython-312.pyc b/tests/unit/__pycache__/test_detection_trigger.cpython-312.pyc
new file mode 100644
index 0000000..81b97e9
Binary files /dev/null and b/tests/unit/__pycache__/test_detection_trigger.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_edge_queue.cpython-310.pyc b/tests/unit/__pycache__/test_edge_queue.cpython-310.pyc
new file mode 100644
index 0000000..eb810c7
Binary files /dev/null and b/tests/unit/__pycache__/test_edge_queue.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_edge_queue.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_edge_queue.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..8c70fde
Binary files /dev/null and b/tests/unit/__pycache__/test_edge_queue.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_edge_queue.cpython-312.pyc b/tests/unit/__pycache__/test_edge_queue.cpython-312.pyc
new file mode 100644
index 0000000..05a674f
Binary files /dev/null and b/tests/unit/__pycache__/test_edge_queue.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_http_json_sink.cpython-310.pyc b/tests/unit/__pycache__/test_http_json_sink.cpython-310.pyc
new file mode 100644
index 0000000..edf798c
Binary files /dev/null and b/tests/unit/__pycache__/test_http_json_sink.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_http_json_sink.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_http_json_sink.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..166a2dd
Binary files /dev/null and b/tests/unit/__pycache__/test_http_json_sink.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_http_json_sink.cpython-312.pyc b/tests/unit/__pycache__/test_http_json_sink.cpython-312.pyc
new file mode 100644
index 0000000..9dd3b12
Binary files /dev/null and b/tests/unit/__pycache__/test_http_json_sink.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_safety_gate.cpython-310.pyc b/tests/unit/__pycache__/test_safety_gate.cpython-310.pyc
new file mode 100644
index 0000000..aea0571
Binary files /dev/null and b/tests/unit/__pycache__/test_safety_gate.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_safety_gate.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_safety_gate.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..d562a43
Binary files /dev/null and b/tests/unit/__pycache__/test_safety_gate.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_safety_gate.cpython-312.pyc b/tests/unit/__pycache__/test_safety_gate.cpython-312.pyc
new file mode 100644
index 0000000..27624e1
Binary files /dev/null and b/tests/unit/__pycache__/test_safety_gate.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_video_decoder.cpython-310.pyc b/tests/unit/__pycache__/test_video_decoder.cpython-310.pyc
new file mode 100644
index 0000000..83dafcd
Binary files /dev/null and b/tests/unit/__pycache__/test_video_decoder.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_video_decoder.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_video_decoder.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..b334af0
Binary files /dev/null and b/tests/unit/__pycache__/test_video_decoder.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_video_decoder.cpython-312.pyc b/tests/unit/__pycache__/test_video_decoder.cpython-312.pyc
new file mode 100644
index 0000000..0f6c891
Binary files /dev/null and b/tests/unit/__pycache__/test_video_decoder.cpython-312.pyc differ
diff --git a/tests/unit/__pycache__/test_yolo_model.cpython-310.pyc b/tests/unit/__pycache__/test_yolo_model.cpython-310.pyc
new file mode 100644
index 0000000..d229f10
Binary files /dev/null and b/tests/unit/__pycache__/test_yolo_model.cpython-310.pyc differ
diff --git a/tests/unit/__pycache__/test_yolo_model.cpython-312-pytest-7.4.4.pyc b/tests/unit/__pycache__/test_yolo_model.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..d923ab6
Binary files /dev/null and b/tests/unit/__pycache__/test_yolo_model.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/unit/__pycache__/test_yolo_model.cpython-312.pyc b/tests/unit/__pycache__/test_yolo_model.cpython-312.pyc
new file mode 100644
index 0000000..abbc439
Binary files /dev/null and b/tests/unit/__pycache__/test_yolo_model.cpython-312.pyc differ
diff --git a/tests/unit/test_agv_connector.py b/tests/unit/test_agv_connector.py
new file mode 100644
index 0000000..ff28921
--- /dev/null
+++ b/tests/unit/test_agv_connector.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import pytest
+
+from cmvr_edge_ai.connectors.cmvr_es import CmvrAgvCommandSink
+
+
+VELOCITY_LIMITS = {
+ "vx": {"min": -0.5, "max": 0.5},
+ "vy": {"min": -0.3, "max": 0.3},
+ "wz": {"min": -1.0, "max": 1.0},
+}
+
+
+def test_agv_sink_requires_a_fixed_device() -> None:
+ with pytest.raises(ValueError, match="fixed, non-empty device_id"):
+ CmvrAgvCommandSink("agv", {"endpoint": "cmvr_es"})
+
+
+def test_unsafe_velocity_override_requires_a_real_boolean() -> None:
+ with pytest.raises(ValueError, match="must be a boolean"):
+ CmvrAgvCommandSink(
+ "agv",
+ {
+ "endpoint": "cmvr_es",
+ "device_id": "src1100",
+ # Environment expansion deliberately leaves plugin params as
+ # strings. The dangerous override must not use bool("false").
+ "unsafe_allow_unleased_velocity": "false",
+ "velocity_limits": VELOCITY_LIMITS,
+ },
+ )
+
+
+def test_unsafe_velocity_override_requires_sink_side_limits() -> None:
+ with pytest.raises(ValueError, match="requires explicit velocity_limits"):
+ CmvrAgvCommandSink(
+ "agv",
+ {
+ "endpoint": "cmvr_es",
+ "device_id": "src1100",
+ "unsafe_allow_unleased_velocity": True,
+ },
+ )
diff --git a/tests/unit/test_bidi_stream.py b/tests/unit/test_bidi_stream.py
new file mode 100644
index 0000000..e37d857
--- /dev/null
+++ b/tests/unit/test_bidi_stream.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import asyncio
+
+import pytest
+
+from cmvr_edge_ai.transports.grpc import (
+ BidiRequestStream,
+ BidiRequestStreamClosed,
+)
+
+
+def test_close_wakes_blocked_sender_without_accepting_its_request() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ stream = BidiRequestStream[str](capacity=1)
+ await stream.send("accepted")
+ blocked_sender = asyncio.create_task(stream.send("blocked"))
+ await asyncio.sleep(0)
+ assert blocked_sender.done() is False
+
+ await stream.close()
+ with pytest.raises(BidiRequestStreamClosed, match="closed"):
+ await blocked_sender
+ drained = [request async for request in stream]
+ return drained, stream.stats()
+
+ drained, stats = asyncio.run(exercise())
+
+ assert drained == ["accepted"]
+ assert stats.enqueued == 1
+ assert stats.blocked_puts == 1
+ assert stats.closed is True
+
+
+def test_bidirectional_request_stream_allows_exactly_one_consumer() -> None:
+ async def exercise() -> None:
+ stream = BidiRequestStream[str]()
+ await stream.send("first")
+ first_consumer = stream.__aiter__()
+ assert await anext(first_consumer) == "first"
+
+ second_consumer = stream.__aiter__()
+ with pytest.raises(RuntimeError, match="exactly one consumer"):
+ await anext(second_consumer)
+
+ await stream.close()
+ with pytest.raises(StopAsyncIteration):
+ await anext(first_consumer)
+
+ asyncio.run(exercise())
diff --git a/tests/unit/test_builtin_plugins.py b/tests/unit/test_builtin_plugins.py
new file mode 100644
index 0000000..d6922c3
--- /dev/null
+++ b/tests/unit/test_builtin_plugins.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+import asyncio
+
+from cmvr_edge_ai.plugins import PluginRegistry, register_builtin_plugins
+
+
+def test_builtin_source_cannot_emit_an_undeclared_dynamic_port() -> None:
+ registry = PluginRegistry()
+ register_builtin_plugins(registry)
+ source = registry.create(
+ "core.sequence_source@1",
+ "source",
+ {"items": ["message"], "output_port": "undeclared"},
+ )
+
+ async def collect(): # type: ignore[no-untyped-def]
+ return [emission async for emission in source.messages()]
+
+ emissions = asyncio.run(collect())
+ declared_ports = registry.resolve("core.sequence_source@1").outputs
+
+ assert [emission.port for emission in emissions] == ["output"]
+ assert all(emission.port in declared_ports for emission in emissions)
diff --git a/tests/unit/test_camera_connector.py b/tests/unit/test_camera_connector.py
new file mode 100644
index 0000000..bbc1fdb
--- /dev/null
+++ b/tests/unit/test_camera_connector.py
@@ -0,0 +1,576 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import deque
+from types import SimpleNamespace
+from typing import Any
+
+import pytest
+
+import cmvr_edge_ai.connectors.cmvr_es.camera as camera_module
+from cmvr_edge_ai.connectors.cmvr_es import (
+ CmvrCameraRgbStreamSource,
+ CmvrServiceError,
+)
+from cmvr_edge_ai.contracts import ImageFrame
+from cmvr_edge_ai.core import Emission
+
+
+class _FakeCall:
+ def __init__(self, *events: Any) -> None:
+ self._events = deque(events)
+ self.cancel_count = 0
+
+ def __aiter__(self) -> _FakeCall:
+ return self
+
+ async def __anext__(self) -> Any:
+ if not self._events:
+ raise StopAsyncIteration
+ event = self._events.popleft()
+ if isinstance(event, BaseException):
+ raise event
+ return event
+
+ def cancel(self) -> bool:
+ self.cancel_count += 1
+ return True
+
+
+class _BlockingCall:
+ def __init__(self) -> None:
+ self.entered = asyncio.Event()
+ self._released = asyncio.Event()
+ self.cancel_count = 0
+
+ def __aiter__(self) -> _BlockingCall:
+ return self
+
+ async def __anext__(self) -> Any:
+ self.entered.set()
+ await self._released.wait()
+ raise asyncio.CancelledError
+
+ def cancel(self) -> bool:
+ self.cancel_count += 1
+ self._released.set()
+ return True
+
+
+class _ImmediateRetryEvent(asyncio.Event):
+ """Exercise reconnect paths without waiting for wall-clock backoff."""
+
+ async def wait(self) -> bool:
+ if self.is_set():
+ return True
+ raise TimeoutError
+
+
+class _OneProgressTickEvent:
+ """Cause exactly one heartbeat tick, then terminate its logging loop."""
+
+ def __init__(self) -> None:
+ self._done = False
+
+ def is_set(self) -> bool:
+ return self._done
+
+ async def wait(self) -> bool:
+ self._done = True
+ raise TimeoutError
+
+
+class _FakeStub:
+ def __init__(
+ self,
+ *calls: Any,
+ start_feedbacks: tuple[Any, ...] = (),
+ ) -> None:
+ self._calls = deque(calls)
+ self._start_feedbacks = deque(start_feedbacks)
+ self.events: list[str] = []
+ self.start_requests: list[Any] = []
+ self.start_timeouts: list[float | None] = []
+ self.start_count = 0
+ self.open_count = 0
+
+ async def StartCamera(
+ self,
+ request: Any,
+ *,
+ timeout: float | None = None,
+ ) -> Any:
+ self.events.append("start")
+ self.start_requests.append(request)
+ self.start_timeouts.append(timeout)
+ self.start_count += 1
+ if not self._start_feedbacks:
+ return _start_feedback()
+ feedback = self._start_feedbacks.popleft()
+ if isinstance(feedback, BaseException):
+ raise feedback
+ return feedback
+
+ def GetRGBImageStream(self, request_stream: Any) -> Any:
+ # The connector owns request-stream lifetime. This fake only exercises
+ # response and reconnect behavior, so protobuf request objects are not
+ # imported or inspected.
+ del request_stream
+ self.events.append("stream")
+ self.open_count += 1
+ if not self._calls:
+ raise AssertionError("camera connector opened an unexpected stream")
+ return self._calls.popleft()
+
+
+def _start_feedback(
+ *,
+ success: bool = True,
+ error_message: str = "",
+) -> Any:
+ return SimpleNamespace(
+ header=SimpleNamespace(
+ success=success,
+ error_message=error_message,
+ )
+ )
+
+
+def _response(
+ remote_sequence: int,
+ *,
+ data: bytes = b"encoded-frame",
+ codec: str = "H264",
+ key_frame: bool = False,
+ width: int = 640,
+ height: int = 480,
+ seconds: int = 0,
+ nanos: int = 0,
+ success: bool = True,
+ error_message: str = "",
+) -> Any:
+ return SimpleNamespace(
+ header=SimpleNamespace(
+ success=success,
+ error_message=error_message,
+ timestamp=SimpleNamespace(seconds=seconds, nanos=nanos),
+ ),
+ color_frame=SimpleNamespace(
+ data=data,
+ width=width,
+ height=height,
+ codec=codec,
+ is_key_frame=key_frame,
+ ),
+ intrinsics=SimpleNamespace(
+ fx=100.0,
+ fy=101.0,
+ cx=50.0,
+ cy=51.0,
+ coeffs=(0.1, 0.2, 0.3, 0.4, 0.5),
+ ),
+ seq_no=remote_sequence,
+ )
+
+
+def _source(stub: Any, **params: Any) -> CmvrCameraRgbStreamSource:
+ configured = {
+ "endpoint": "cmvr_es",
+ "device_id": "camera-1",
+ "pixel_format": "BGR8",
+ "reconnect_initial_s": 0.001,
+ "reconnect_max_s": 0.002,
+ **params,
+ }
+ source = CmvrCameraRgbStreamSource("camera", configured)
+ # setup() is deliberately bypassed: these unit tests exercise stream state
+ # without importing generated protobuf bindings or constructing gRPC objects.
+ source._stub = stub # type: ignore[attr-defined]
+ source._start_request = object() # type: ignore[attr-defined]
+ source._stream_request = object() # type: ignore[attr-defined]
+ source._shutdown_event = asyncio.Event() # type: ignore[attr-defined]
+ source._rpc_timeout_s = 2.5 # type: ignore[attr-defined]
+ return source
+
+
+def test_reconnect_uses_local_monotonic_sequence_and_new_session_id(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ session_ids = iter(("session-a", "session-b"))
+ monkeypatch.setattr(
+ camera_module,
+ "uuid4",
+ lambda: SimpleNamespace(hex=next(session_ids)),
+ )
+ first_call = _FakeCall(
+ _response(
+ 900,
+ data=b"first",
+ key_frame=True,
+ seconds=12,
+ nanos=34,
+ )
+ )
+ second_call = _FakeCall(_response(3, data=b"second"))
+
+ stub = _FakeStub(first_call, second_call)
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ source = _source(stub)
+ stream = source.messages()
+ first = await asyncio.wait_for(anext(stream), timeout=1)
+ second = await asyncio.wait_for(anext(stream), timeout=1)
+ await stream.aclose()
+ return first, second
+
+ first, second = asyncio.run(exercise())
+
+ assert isinstance(first, Emission)
+ assert isinstance(second, Emission)
+ assert first.port == second.port == "frames"
+ assert [first.envelope.sequence, second.envelope.sequence] == [0, 1]
+ assert [first.envelope.session_id, second.envelope.session_id] == [
+ "session-a",
+ "session-b",
+ ]
+ assert [
+ first.envelope.attributes["remote_sequence"],
+ second.envelope.attributes["remote_sequence"],
+ ] == [900, 3]
+ assert first.envelope.attributes["stream_session_id"] == "session-a"
+ assert second.envelope.attributes["stream_session_id"] == "session-b"
+ assert first.envelope.source_id == second.envelope.source_id == "camera-1"
+ assert first.envelope.captured_at_ns == 12_000_000_034
+ assert second.envelope.captured_at_ns is None
+ assert isinstance(first.envelope.payload, ImageFrame)
+ assert first.envelope.payload.data == b"first"
+ assert first.envelope.payload.codec == "H264"
+ assert first.envelope.payload.is_key_frame is True
+ assert first.envelope.payload.pixel_format == "BGR8"
+ assert first.envelope.payload.intrinsics is not None
+ assert first.envelope.payload.intrinsics.coefficients == (
+ 0.1,
+ 0.2,
+ 0.3,
+ 0.4,
+ 0.5,
+ )
+ assert first_call.cancel_count == 1
+ assert second_call.cancel_count == 1
+ assert stub.events == ["start", "stream", "start", "stream"]
+ assert stub.start_count == stub.open_count == 2
+ assert stub.start_timeouts == [2.5, 2.5]
+ assert all(request is not None for request in stub.start_requests)
+
+
+def test_transport_failure_reconnects_to_a_new_call() -> None:
+ disconnected = _FakeCall(ConnectionError("temporary disconnect"))
+ recovered = _FakeCall(_response(77, data=b"recovered", key_frame=True))
+ stub = _FakeStub(disconnected, recovered)
+
+ async def exercise() -> Emission:
+ source = _source(stub)
+ stream = source.messages()
+ result = await asyncio.wait_for(anext(stream), timeout=1)
+ await stream.aclose()
+ assert isinstance(result, Emission)
+ return result
+
+ emission = asyncio.run(exercise())
+
+ assert stub.open_count == 2
+ assert stub.start_count == 2
+ assert stub.events == ["start", "stream", "start", "stream"]
+ assert disconnected.cancel_count == 1
+ assert recovered.cancel_count == 1
+ assert emission.envelope.sequence == 0
+ assert emission.envelope.attributes["remote_sequence"] == 77
+ assert emission.envelope.payload.data == b"recovered"
+
+
+def test_start_camera_rejection_prevents_stream_open() -> None:
+ stub = _FakeStub(
+ _FakeCall(_response(1)),
+ start_feedbacks=(
+ _start_feedback(
+ success=False,
+ error_message="camera is unavailable",
+ ),
+ ),
+ )
+
+ async def exercise() -> None:
+ source = _source(stub, reconnect=False)
+ with pytest.raises(CmvrServiceError, match="camera is unavailable"):
+ await asyncio.wait_for(anext(source.messages()), timeout=1)
+
+ asyncio.run(exercise())
+
+ assert stub.events == ["start"]
+ assert stub.start_count == 1
+ assert stub.start_timeouts == [2.5]
+ assert stub.open_count == 0
+
+
+def test_start_camera_transport_failure_is_retried_before_stream_open() -> None:
+ recovered = _FakeCall(_response(5, data=b"after-start-retry"))
+ stub = _FakeStub(
+ recovered,
+ start_feedbacks=(
+ ConnectionError("StartCamera unavailable"),
+ _start_feedback(),
+ ),
+ )
+
+ async def exercise() -> Emission:
+ source = _source(stub)
+ emission = await asyncio.wait_for(anext(source.messages()), timeout=1)
+ await source.stop()
+ assert isinstance(emission, Emission)
+ return emission
+
+ emission = asyncio.run(exercise())
+
+ assert stub.events == ["start", "start", "stream"]
+ assert stub.start_count == 2
+ assert stub.start_timeouts == [2.5, 2.5]
+ assert stub.open_count == 1
+ assert emission.envelope.payload.data == b"after-start-retry"
+
+
+def test_camera_stream_logs_the_complete_lifecycle(
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ now_ns = 0
+
+ def advancing_clock() -> int:
+ nonlocal now_ns
+ now_ns += 2_000_000_000
+ return now_ns
+
+ elapsed_s = 0.0
+
+ def advancing_timer() -> float:
+ nonlocal elapsed_s
+ elapsed_s += 0.05
+ return elapsed_s
+
+ monkeypatch.setattr(camera_module, "monotonic_ns", advancing_clock)
+ monkeypatch.setattr(camera_module, "perf_counter", advancing_timer)
+ first_session = _FakeCall(
+ _response(10, data=b"first", key_frame=True),
+ _response(11, data=b"second"),
+ ConnectionError("stream interrupted"),
+ )
+ recovered_session = _FakeCall(_response(12, data=b"recovered"))
+ stub = _FakeStub(first_session, recovered_session)
+
+ async def exercise() -> None:
+ source = _source(stub, stream_log_interval_s=1)
+ source._shutdown_event = _ImmediateRetryEvent() # type: ignore[attr-defined]
+ stream = source.messages()
+ await anext(stream)
+ await anext(stream)
+ await anext(stream)
+ await source.stop()
+ await stream.aclose()
+
+ with caplog.at_level(logging.INFO, logger=camera_module.__name__):
+ asyncio.run(exercise())
+
+ messages = [record.getMessage() for record in caplog.records]
+ expected_phrases = (
+ "camera start requested",
+ "camera start succeeded",
+ "camera stream opening",
+ "camera stream first frame",
+ "camera stream disconnected",
+ "camera source stopping",
+ "camera source stopped",
+ )
+ for phrase in expected_phrases:
+ assert any(phrase in message for message in messages), messages
+
+ assert sum("camera start requested" in message for message in messages) == 2
+ assert sum("camera start succeeded" in message for message in messages) == 2
+ assert sum("camera stream opening" in message for message in messages) == 2
+ first_frame_messages = [
+ message for message in messages if "camera stream first frame" in message
+ ]
+ assert len(first_frame_messages) == 2
+ assert "remote_sequence=10" in first_frame_messages[0]
+ assert "remote_sequence=12" in first_frame_messages[1]
+
+
+def test_camera_stream_progress_log_reports_window_statistics(
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setattr(camera_module, "monotonic_ns", lambda: 3_000_000_000)
+ stats = camera_module._StreamStats(
+ session_id="session-progress",
+ opened_ns=0,
+ window_started_ns=1_000_000_000,
+ frames=3,
+ received_bytes=600,
+ key_frames=1,
+ window_frames=2,
+ window_bytes=500,
+ window_key_frames=1,
+ last_frame_ns=2_500_000_000,
+ last_remote_sequence=42,
+ codec="H264",
+ width=640,
+ height=480,
+ )
+ source = _source(_FakeStub(), stream_log_interval_s=1)
+
+ async def exercise() -> None:
+ await source._log_stream_progress( # type: ignore[attr-defined]
+ stats,
+ _OneProgressTickEvent(), # type: ignore[arg-type]
+ )
+
+ with caplog.at_level(logging.INFO, logger=camera_module.__name__):
+ asyncio.run(exercise())
+
+ messages = [
+ record.getMessage()
+ for record in caplog.records
+ if "camera stream progress" in record.getMessage()
+ ]
+ assert len(messages) == 1
+ message = messages[0]
+ assert "session=session-progress" in message
+ assert "total_frames=3" in message
+ assert "window_frames=2" in message
+ assert "window_bytes=500" in message
+ assert "last_remote_sequence=42" in message
+ assert "codec=H264" in message
+ assert "width=640" in message
+ assert "height=480" in message
+
+
+def test_reconnect_false_propagates_the_disconnect() -> None:
+ failed_call = _FakeCall(ConnectionError("camera cable disconnected"))
+ stub = _FakeStub(failed_call)
+
+ async def exercise() -> None:
+ source = _source(stub, reconnect=False)
+ with pytest.raises(ConnectionError, match="camera cable disconnected"):
+ await asyncio.wait_for(anext(source.messages()), timeout=1)
+
+ asyncio.run(exercise())
+
+ assert stub.open_count == 1
+ assert stub.start_count == 1
+ assert stub.events == ["start", "stream"]
+ assert failed_call.cancel_count == 1
+
+
+def test_max_reconnect_attempts_raises_after_the_allowed_retries() -> None:
+ first_failure = _FakeCall(ConnectionError("first disconnect"))
+ second_failure = _FakeCall(ConnectionError("second disconnect"))
+ stub = _FakeStub(first_failure, second_failure)
+
+ async def exercise() -> None:
+ source = _source(stub, max_reconnect_attempts=1)
+ with pytest.raises(
+ CmvrServiceError,
+ match="exceeded max_reconnect_attempts",
+ ):
+ await asyncio.wait_for(anext(source.messages()), timeout=1)
+
+ asyncio.run(exercise())
+
+ assert stub.open_count == 2
+ assert stub.start_count == 2
+ assert stub.events == ["start", "stream", "start", "stream"]
+ assert first_failure.cancel_count == 1
+ assert second_failure.cancel_count == 1
+
+
+def test_stop_cancels_the_active_stream_call() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ call = _BlockingCall()
+ stub = _FakeStub(call)
+ source = _source(stub)
+ stream = source.messages()
+ pending = asyncio.create_task(anext(stream))
+ await asyncio.wait_for(call.entered.wait(), timeout=1)
+
+ await source.stop()
+ with pytest.raises(asyncio.CancelledError):
+ await asyncio.wait_for(pending, timeout=1)
+ await stream.aclose()
+ return call, source, stub
+
+ call, source, stub = asyncio.run(exercise())
+
+ assert call.cancel_count == 1
+ assert stub.events == ["start", "stream"]
+ assert source._call is None # type: ignore[attr-defined]
+
+
+@pytest.mark.parametrize(
+ ("params", "message"),
+ [
+ ({"typo": True}, "unknown camera source parameter"),
+ ({"device_id": None}, "non-empty device_id"),
+ ({"device_id": " "}, "non-empty device_id"),
+ ({"device_id": 1}, "non-empty device_id"),
+ ({"pixel_format": ""}, "pixel_format must be a non-empty string"),
+ ({"pixel_format": 1}, "pixel_format must be a non-empty string"),
+ ({"reconnect": "true"}, "reconnect must be a boolean"),
+ ({"reconnect": 1}, "reconnect must be a boolean"),
+ ({"reconnect_initial_s": True}, "must be a positive number"),
+ ({"reconnect_initial_s": 0}, "must be a positive number"),
+ ({"reconnect_initial_s": -1}, "must be a positive number"),
+ ({"reconnect_initial_s": float("nan")}, "must be a positive number"),
+ ({"reconnect_initial_s": float("inf")}, "must be a positive number"),
+ ({"reconnect_max_s": 0}, "must be a positive number"),
+ ({"reconnect_max_s": float("nan")}, "must be a positive number"),
+ ({"stream_log_interval_s": True}, "must be a positive number"),
+ ({"stream_log_interval_s": 0}, "must be a positive number"),
+ ({"stream_log_interval_s": -1}, "must be a positive number"),
+ ({"stream_log_interval_s": float("nan")}, "must be a positive number"),
+ ({"stream_log_interval_s": float("inf")}, "must be a positive number"),
+ (
+ {"reconnect_initial_s": 2, "reconnect_max_s": 1},
+ "reconnect_max_s must be >= reconnect_initial_s",
+ ),
+ ({"max_reconnect_attempts": True}, "must be an integer or null"),
+ ({"max_reconnect_attempts": 1.5}, "must be an integer or null"),
+ ({"max_reconnect_attempts": "1"}, "must be an integer or null"),
+ ({"max_reconnect_attempts": -1}, "must not be negative"),
+ ],
+)
+def test_camera_reconnect_parameters_are_strictly_validated(
+ params: dict[str, Any],
+ message: str,
+) -> None:
+ configured: dict[str, Any] = {"device_id": "camera-1"}
+ configured.update(params)
+
+ with pytest.raises(ValueError, match=message):
+ CmvrCameraRgbStreamSource("camera", configured)
+
+
+def test_numeric_strings_are_accepted_for_environment_expanded_delays() -> None:
+ source = CmvrCameraRgbStreamSource(
+ "camera",
+ {
+ "device_id": "camera-1",
+ "reconnect_initial_s": "0.25",
+ "reconnect_max_s": "2.5",
+ "max_reconnect_attempts": 0,
+ "stream_log_interval_s": "12.5",
+ },
+ )
+
+ assert source._reconnect_initial_s == 0.25 # type: ignore[attr-defined]
+ assert source._reconnect_max_s == 2.5 # type: ignore[attr-defined]
+ assert source._max_reconnect_attempts == 0 # type: ignore[attr-defined]
+ assert source._stream_log_interval_s == 12.5 # type: ignore[attr-defined]
diff --git a/tests/unit/test_compiler.py b/tests/unit/test_compiler.py
new file mode 100644
index 0000000..59759af
--- /dev/null
+++ b/tests/unit/test_compiler.py
@@ -0,0 +1,372 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.compiler import PipelineCompileError, compile_pipeline
+from cmvr_edge_ai.config import load_config_data
+from cmvr_edge_ai.core import Envelope, Operator, Sink, Source
+from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec
+
+
+class EmptySource(Source):
+ async def messages(self): # type: ignore[no-untyped-def]
+ if False:
+ yield Envelope("unused")
+
+
+class IdentityOperator(Operator):
+ async def process(
+ self, envelope: Envelope[Any], input_port: str = "input"
+ ) -> Envelope[Any]:
+ del input_port
+ return envelope
+
+
+class NullSink(Sink):
+ async def consume(self, envelope: Envelope[Any], input_port: str = "input") -> None:
+ del envelope, input_port
+
+
+def _source_factory(node_id: str, params: Mapping[str, Any]) -> EmptySource:
+ del node_id, params
+ return EmptySource()
+
+
+def _operator_factory(node_id: str, params: Mapping[str, Any]) -> IdentityOperator:
+ del node_id, params
+ return IdentityOperator()
+
+
+def _sink_factory(node_id: str, params: Mapping[str, Any]) -> NullSink:
+ del node_id, params
+ return NullSink()
+
+
+def _registry() -> PluginRegistry:
+ registry = PluginRegistry()
+ registry.register(
+ PluginSpec(
+ "test.text_source@1",
+ PluginKind.SOURCE,
+ _source_factory,
+ outputs={"events": "TextEvent/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.text_operator@1",
+ PluginKind.OPERATOR,
+ _operator_factory,
+ inputs={"input": "TextEvent/v1"},
+ outputs={"events": "TextEvent/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.audio_sink@1",
+ PluginKind.SINK,
+ _sink_factory,
+ inputs={"input": "AudioChunk/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.any_sink@1",
+ PluginKind.SINK,
+ _sink_factory,
+ inputs={"input": "*"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.command_source@1",
+ PluginKind.SOURCE,
+ _source_factory,
+ outputs={"commands": "RobotCommand/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.approved_source@1",
+ PluginKind.SOURCE,
+ _source_factory,
+ outputs={"commands": "ApprovedRobotCommand/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.safety_gate@1",
+ PluginKind.OPERATOR,
+ _operator_factory,
+ inputs={"input": "RobotCommand/v1"},
+ outputs={"commands": "ApprovedRobotCommand/v1"},
+ tags=frozenset({"safety_gate"}),
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.approved_operator@1",
+ PluginKind.OPERATOR,
+ _operator_factory,
+ inputs={"input": "ApprovedRobotCommand/v1"},
+ outputs={"commands": "ApprovedRobotCommand/v1"},
+ )
+ )
+ registry.register(
+ PluginSpec(
+ "test.actuator@1",
+ PluginKind.SINK,
+ _sink_factory,
+ inputs={"commands": "ApprovedRobotCommand/v1"},
+ tags=frozenset({"actuator"}),
+ )
+ )
+ return registry
+
+
+def _config(nodes: dict[str, dict[str, Any]], edges: list[dict[str, Any]]):
+ return load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "test": {
+ "nodes": nodes,
+ "edges": edges,
+ }
+ },
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ ("edge", "message"),
+ [
+ (
+ {"from": "source.missing", "to": "sink.input"},
+ "unknown output port",
+ ),
+ (
+ {"from": "source.events", "to": "sink.missing"},
+ "unknown input port",
+ ),
+ ],
+)
+def test_compile_rejects_unknown_plugin_ports(
+ edge: dict[str, str], message: str
+) -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.text_source@1"},
+ "sink": {"uses": "test.any_sink@1"},
+ },
+ [edge],
+ )
+
+ with pytest.raises(PipelineCompileError, match=message):
+ compile_pipeline(config, "test", _registry())
+
+
+def test_compile_rejects_incompatible_schemas() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.text_source@1"},
+ "sink": {"uses": "test.audio_sink@1"},
+ },
+ [{"from": "source.events", "to": "sink.input"}],
+ )
+
+ with pytest.raises(PipelineCompileError, match="schema mismatch"):
+ compile_pipeline(config, "test", _registry())
+
+
+def test_compile_accepts_wildcard_schema() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.text_source@1"},
+ "sink": {"uses": "test.any_sink@1"},
+ },
+ [{"from": "source.events", "to": "sink.input"}],
+ )
+
+ compiled = compile_pipeline(config, "test", _registry())
+
+ assert compiled.pipeline_id == "test"
+ assert compiled.runtime.edges[0].source_port == "events"
+
+
+def test_video_contiguous_qos_requires_lossless_overflow() -> None:
+ nodes = {
+ "source": {"uses": "test.text_source@1"},
+ "operator": {"uses": "test.text_operator@1"},
+ }
+ accepted = _config(
+ nodes,
+ [
+ {
+ "from": "source.events",
+ "to": "operator.input",
+ "qos": {
+ "profile": "video_contiguous",
+ "capacity": 8,
+ "overflow": "block",
+ },
+ }
+ ],
+ )
+
+ compiled = compile_pipeline(accepted, "test", _registry())
+ assert compiled.runtime.edges[0].capacity == 8
+
+ rejected = _config(
+ nodes,
+ [
+ {
+ "from": "source.events",
+ "to": "operator.input",
+ "qos": {
+ "profile": "video_contiguous",
+ "capacity": 2,
+ "overflow": "drop_oldest",
+ },
+ }
+ ],
+ )
+ with pytest.raises(PipelineCompileError, match="does not allow"):
+ compile_pipeline(rejected, "test", _registry())
+
+
+def test_compile_rejects_direct_path_into_actuator() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.approved_source@1"},
+ "actuator": {"uses": "test.actuator@1"},
+ },
+ [{"from": "source.commands", "to": "actuator.commands"}],
+ )
+
+ with pytest.raises(PipelineCompileError, match="must pass through a safety gate"):
+ compile_pipeline(config, "test", _registry())
+
+
+def test_compile_accepts_actuator_only_after_safety_gate() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.command_source@1"},
+ "gate": {"uses": "test.safety_gate@1"},
+ "actuator": {"uses": "test.actuator@1"},
+ },
+ [
+ {"from": "source.commands", "to": "gate.input"},
+ {"from": "gate.commands", "to": "actuator.commands"},
+ ],
+ )
+
+ compiled = compile_pipeline(config, "test", _registry())
+
+ assert set(compiled.plugin_specs) == {"source", "gate", "actuator"}
+
+
+def test_compile_rejects_mixed_safe_and_unsafe_actuator_paths() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.command_source@1"},
+ "bypass": {"uses": "test.approved_source@1"},
+ "gate": {"uses": "test.safety_gate@1"},
+ "actuator": {"uses": "test.actuator@1"},
+ },
+ [
+ {"from": "source.commands", "to": "gate.input"},
+ {"from": "gate.commands", "to": "actuator.commands"},
+ {"from": "bypass.commands", "to": "actuator.commands"},
+ ],
+ )
+
+ with pytest.raises(PipelineCompileError, match="unsafe actuator"):
+ compile_pipeline(config, "test", _registry())
+
+
+def test_compile_rejects_operator_between_gate_and_actuator() -> None:
+ config = _config(
+ {
+ "source": {"uses": "test.command_source@1"},
+ "gate": {"uses": "test.safety_gate@1"},
+ "transform": {"uses": "test.approved_operator@1"},
+ "actuator": {"uses": "test.actuator@1"},
+ },
+ [
+ {"from": "source.commands", "to": "gate.input"},
+ {"from": "gate.commands", "to": "transform.input"},
+ {"from": "transform.commands", "to": "actuator.commands"},
+ ],
+ )
+
+ with pytest.raises(PipelineCompileError, match="safety gate directly"):
+ compile_pipeline(config, "test", _registry())
+
+
+@pytest.mark.parametrize(
+ ("node_options", "message"),
+ [
+ ({"execution": {"mode": "thread"}}, "execution settings not implemented"),
+ ({"resources": {"memory_mb": 256}}, "resource limits"),
+ ],
+)
+def test_compile_rejects_unenforced_node_settings(
+ node_options: dict[str, Any], message: str
+) -> None:
+ config = _config(
+ {
+ "source": {
+ "uses": "test.text_source@1",
+ **node_options,
+ }
+ },
+ [],
+ )
+
+ with pytest.raises(PipelineCompileError, match=message):
+ compile_pipeline(config, "test", _registry())
+
+
+@pytest.mark.parametrize(
+ ("field", "value"),
+ [
+ ("max_processes", 2),
+ ("process_start_method", "forkserver"),
+ ("health_bind", "127.0.0.1:8080"),
+ ("reserved_memory_mb", 256),
+ ],
+)
+def test_compile_rejects_reserved_runtime_fields(field: str, value: Any) -> None:
+ config = load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "runtime": {field: value},
+ "pipelines": {
+ "test": {"nodes": {"source": {"uses": "test.text_source@1"}}}
+ },
+ }
+ )
+
+ with pytest.raises(PipelineCompileError, match="reserved but not enforced"):
+ compile_pipeline(config, "test", _registry())
+
+
+def test_compile_rejects_pipeline_priority_without_scheduler() -> None:
+ config = load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "test": {
+ "priority": "high",
+ "nodes": {"source": {"uses": "test.text_source@1"}},
+ }
+ },
+ }
+ )
+
+ with pytest.raises(PipelineCompileError, match="no priority scheduler"):
+ compile_pipeline(config, "test", _registry())
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
new file mode 100644
index 0000000..962c2c9
--- /dev/null
+++ b/tests/unit/test_config.py
@@ -0,0 +1,145 @@
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from cmvr_edge_ai.config import (
+ MissingEnvironmentVariable,
+ load_config_data,
+ load_config_text,
+)
+
+
+def test_load_config_expands_environment_at_every_level() -> None:
+ config = load_config_text(
+ """
+api_version: cmvr.edge.ai/v1
+runtime:
+ thread_workers: ${EDGE_THREADS:-2}
+endpoints:
+ robot:
+ transport: GRPC
+ target: env://CMVR_TARGET
+ platform:
+ transport: http
+ base_url: ${PLATFORM_URL:-http://127.0.0.1:8080}
+pipelines:
+ detection:
+ nodes:
+ source:
+ uses: core.sequence_source@1
+ with:
+ items: [env://SAMPLE_ITEM]
+""",
+ environ={
+ "CMVR_TARGET": "192.168.0.10:50052",
+ "EDGE_THREADS": "3",
+ "SAMPLE_ITEM": "frame-1",
+ },
+ )
+
+ assert config.runtime.thread_workers == 3
+ assert config.endpoints["robot"].transport == "grpc"
+ assert config.endpoints["robot"].target == "192.168.0.10:50052"
+ assert config.endpoints["platform"].base_url == "http://127.0.0.1:8080"
+ assert config.pipelines["detection"].nodes["source"].params["items"] == ["frame-1"]
+
+
+def test_load_config_rejects_missing_required_environment_variable() -> None:
+ with pytest.raises(MissingEnvironmentVariable, match="CMVR_TARGET"):
+ load_config_text(
+ """
+api_version: cmvr.edge.ai/v1
+endpoints:
+ robot:
+ transport: grpc
+ target: ${CMVR_TARGET}
+pipelines:
+ smoke:
+ nodes:
+ source:
+ uses: core.sequence_source@1
+""",
+ environ={},
+ )
+
+
+def test_config_rejects_edges_to_unknown_nodes() -> None:
+ with pytest.raises(ValidationError, match="references unknown node"):
+ load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "invalid": {
+ "nodes": {
+ "source": {"uses": "test.source@1"},
+ },
+ "edges": [{"from": "source.output", "to": "missing.input"}],
+ }
+ },
+ }
+ )
+
+
+def test_config_rejects_cycles_before_runtime_construction() -> None:
+ with pytest.raises(ValidationError, match="must be acyclic"):
+ load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "cyclic": {
+ "nodes": {
+ "first": {"uses": "test.operator@1"},
+ "second": {"uses": "test.operator@1"},
+ },
+ "edges": [
+ {"from": "first.output", "to": "second.input"},
+ {"from": "second.output", "to": "first.input"},
+ ],
+ }
+ },
+ }
+ )
+
+
+def test_config_models_forbid_unknown_fields() -> None:
+ with pytest.raises(ValidationError, match="extra_forbidden"):
+ load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "unexpected": True,
+ "pipelines": {
+ "smoke": {"nodes": {"source": {"uses": "test.source@1"}}}
+ },
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ ("endpoint", "message"),
+ [
+ ({"transport": "grpc"}, "gRPC endpoint requires target"),
+ ({"transport": "http"}, "HTTP endpoint requires base_url"),
+ (
+ {
+ "transport": "http",
+ "base_url": "https://platform.example",
+ "tls": False,
+ },
+ "HTTP endpoint must not set tls",
+ ),
+ ],
+)
+def test_builtin_endpoints_reject_incomplete_or_ambiguous_addresses(
+ endpoint: dict[str, object], message: str
+) -> None:
+ with pytest.raises(ValidationError, match=message):
+ load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "endpoints": {"external": endpoint},
+ "pipelines": {
+ "smoke": {"nodes": {"source": {"uses": "test.source@1"}}}
+ },
+ }
+ )
diff --git a/tests/unit/test_detection_annotation.py b/tests/unit/test_detection_annotation.py
new file mode 100644
index 0000000..e96a1d5
--- /dev/null
+++ b/tests/unit/test_detection_annotation.py
@@ -0,0 +1,169 @@
+from __future__ import annotations
+
+from io import BytesIO
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.contracts import BoundingBox, Detection, EncodedImage, ImageFrame
+from cmvr_edge_ai.detection.annotation import render_annotated_jpeg
+
+
+Image = pytest.importorskip("PIL.Image")
+ImageDraw = pytest.importorskip("PIL.ImageDraw")
+
+
+def _solid_frame(
+ *,
+ pixel_format: str,
+ rgb: tuple[int, int, int],
+ width: int = 64,
+ height: int = 48,
+) -> ImageFrame:
+ stored_pixel = rgb if pixel_format == "RGB8" else tuple(reversed(rgb))
+ return ImageFrame(
+ data=bytes(stored_pixel) * width * height,
+ width=width,
+ height=height,
+ pixel_format=pixel_format,
+ )
+
+
+def _detection(
+ label: str,
+ box: BoundingBox,
+ *,
+ confidence: float = 0.93,
+) -> Detection:
+ return Detection(label=label, confidence=confidence, box=box)
+
+
+def _record_drawing_calls(
+ monkeypatch: pytest.MonkeyPatch,
+) -> tuple[list[tuple[float, float, float, float]], list[str]]:
+ rectangle_calls: list[tuple[float, float, float, float]] = []
+ text_calls: list[str] = []
+ original_rectangle = ImageDraw.ImageDraw.rectangle
+ original_text = ImageDraw.ImageDraw.text
+
+ def recording_rectangle(
+ draw: ImageDraw.ImageDraw,
+ xy: Any,
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ coordinates = tuple(xy)
+ if len(coordinates) == 2 and all(
+ isinstance(point, (tuple, list)) for point in coordinates
+ ):
+ coordinates = tuple(
+ coordinate for point in coordinates for coordinate in point
+ )
+ if len(coordinates) == 4:
+ rectangle_calls.append(tuple(float(value) for value in coordinates))
+ original_rectangle(draw, xy, *args, **kwargs)
+
+ def recording_text(
+ draw: ImageDraw.ImageDraw,
+ xy: Any,
+ text: str,
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ text_calls.append(text)
+ original_text(draw, xy, text, *args, **kwargs)
+
+ monkeypatch.setattr(ImageDraw.ImageDraw, "rectangle", recording_rectangle)
+ monkeypatch.setattr(ImageDraw.ImageDraw, "text", recording_text)
+ return rectangle_calls, text_calls
+
+
+@pytest.mark.parametrize("pixel_format", ["BGR8", "RGB8"])
+def test_rendered_alert_image_preserves_color_and_is_a_decodable_jpeg(
+ pixel_format: str,
+) -> None:
+ expected_rgb = (24, 96, 184)
+ frame = _solid_frame(pixel_format=pixel_format, rgb=expected_rgb)
+ detection = _detection(
+ "No-Helmet",
+ BoundingBox(x_min=8.0, y_min=8.0, x_max=32.0, y_max=32.0),
+ )
+
+ encoded = render_annotated_jpeg(frame, (detection,), jpeg_quality=90)
+
+ assert isinstance(encoded, EncodedImage)
+ assert encoded.media_type == "image/jpeg"
+ assert (encoded.width, encoded.height) == (frame.width, frame.height)
+ assert encoded.data.startswith(b"\xff\xd8")
+ assert encoded.data.endswith(b"\xff\xd9")
+
+ with Image.open(BytesIO(encoded.data)) as decoded:
+ assert decoded.format == "JPEG"
+ assert decoded.size == (frame.width, frame.height)
+ rgb_image = decoded.convert("RGB")
+
+ # Sample well away from the box and its label. JPEG is lossy, so compare
+ # with a tolerance while still catching an RGB/BGR channel swap.
+ assert rgb_image.getpixel((frame.width - 3, frame.height - 3)) == pytest.approx(
+ expected_rgb,
+ abs=12,
+ )
+
+
+def test_renderer_draws_each_detection_box_and_label(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ rectangle_calls, text_calls = _record_drawing_calls(monkeypatch)
+
+ frame = _solid_frame(pixel_format="BGR8", rgb=(0, 0, 0))
+ detections = (
+ _detection(
+ "No-Helmet",
+ BoundingBox(x_min=5.0, y_min=10.0, x_max=30.0, y_max=35.0),
+ ),
+ _detection(
+ "No-Vest",
+ BoundingBox(x_min=34.0, y_min=12.0, x_max=58.0, y_max=40.0),
+ confidence=0.81,
+ ),
+ )
+
+ render_annotated_jpeg(frame, detections, jpeg_quality=85)
+
+ assert (5.0, 10.0, 30.0, 35.0) in rectangle_calls
+ assert (34.0, 12.0, 58.0, 40.0) in rectangle_calls
+ assert any("No-Helmet" in text for text in text_calls)
+ assert any("No-Vest" in text for text in text_calls)
+
+
+def test_renderer_clips_out_of_bounds_box_and_skips_invalid_boxes(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ rectangle_calls, text_calls = _record_drawing_calls(monkeypatch)
+ frame = _solid_frame(pixel_format="RGB8", rgb=(0, 0, 0))
+ detections = (
+ _detection(
+ "Clipped",
+ BoundingBox(x_min=-5.0, y_min=-10.0, x_max=80.0, y_max=60.0),
+ ),
+ _detection(
+ "Reversed",
+ BoundingBox(x_min=30.0, y_min=30.0, x_max=10.0, y_max=10.0),
+ ),
+ _detection(
+ "Outside",
+ BoundingBox(x_min=70.0, y_min=5.0, x_max=90.0, y_max=20.0),
+ ),
+ _detection(
+ "Not-Finite",
+ BoundingBox(x_min=float("nan"), y_min=1.0, x_max=10.0, y_max=10.0),
+ ),
+ )
+
+ render_annotated_jpeg(frame, detections, jpeg_quality=85)
+
+ assert (0.0, 0.0, 63.0, 47.0) in rectangle_calls
+ assert any("Clipped" in text for text in text_calls)
+ assert all("Reversed" not in text for text in text_calls)
+ assert all("Outside" not in text for text in text_calls)
+ assert all("Not-Finite" not in text for text in text_calls)
diff --git a/tests/unit/test_detection_compile.py b/tests/unit/test_detection_compile.py
new file mode 100644
index 0000000..38cbe15
--- /dev/null
+++ b/tests/unit/test_detection_compile.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import pytest
+
+from cmvr_edge_ai.application import create_default_registry
+from cmvr_edge_ai.compiler import PipelineCompileError, compile_pipeline
+from cmvr_edge_ai.config import load_config_data
+
+
+def _config(
+ rule_label: str,
+ *,
+ attach_frame: bool = False,
+ alert_image: bool = False,
+): # type: ignore[no-untyped-def]
+ return load_config_data(
+ {
+ "api_version": "cmvr.edge.ai/v1",
+ "pipelines": {
+ "detection": {
+ "nodes": {
+ "source": {
+ "uses": "core.sequence_source@1",
+ "with": {
+ "items": [],
+ "schema_name": "ImageFrame",
+ },
+ },
+ "detector": {
+ "uses": "detection.model@1",
+ "with": {
+ "model": "construction-ppe-yolov8@1",
+ "detect_labels": ["No-Helmet"],
+ "attach_frame": attach_frame,
+ "model_options": {"weights": "/not-loaded.pt"},
+ },
+ },
+ "gate": {
+ "uses": "detection.repeat_gate@1",
+ "with": {
+ "alert_image": {"enabled": alert_image},
+ "rules": [
+ {
+ "id": "ppe-rule",
+ "labels": [rule_label],
+ "min_hits": 2,
+ "window_ms": 1000,
+ }
+ ]
+ },
+ },
+ },
+ "edges": [
+ {"from": "source.output", "to": "detector.frames"},
+ {
+ "from": "detector.detections",
+ "to": "gate.detections",
+ },
+ ],
+ }
+ },
+ }
+ )
+
+
+def test_compile_accepts_rule_label_selected_by_detector() -> None:
+ compiled = compile_pipeline(
+ _config("No-Helmet"),
+ "detection",
+ create_default_registry(discover_entry_points=False),
+ )
+
+ assert set(compiled.plugin_specs) == {"source", "detector", "gate"}
+
+
+def test_compile_rejects_rule_label_ignored_by_detector() -> None:
+ with pytest.raises(PipelineCompileError, match="No-Vest"):
+ compile_pipeline(
+ _config("No-Vest"),
+ "detection",
+ create_default_registry(discover_entry_points=False),
+ )
+
+
+def test_compile_accepts_alert_images_when_detector_attaches_frames() -> None:
+ compiled = compile_pipeline(
+ _config("No-Helmet", attach_frame=True, alert_image=True),
+ "detection",
+ create_default_registry(discover_entry_points=False),
+ )
+
+ assert compiled.runtime.nodes["detector"].attach_frame is True
+ assert compiled.runtime.nodes["gate"].alert_image_enabled is True
+
+
+def test_compile_rejects_alert_images_without_attached_frames() -> None:
+ with pytest.raises(
+ PipelineCompileError,
+ match="enables alert_image.*attach_frame=true",
+ ):
+ compile_pipeline(
+ _config("No-Helmet", attach_frame=False, alert_image=True),
+ "detection",
+ create_default_registry(discover_entry_points=False),
+ )
diff --git a/tests/unit/test_detection_operator.py b/tests/unit/test_detection_operator.py
new file mode 100644
index 0000000..f7d88f6
--- /dev/null
+++ b/tests/unit/test_detection_operator.py
@@ -0,0 +1,442 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+import threading
+from collections.abc import Awaitable, Callable, Mapping, Sequence
+from concurrent.futures import ThreadPoolExecutor
+from typing import Any, TypeVar
+
+import pytest
+
+from cmvr_edge_ai.contracts import BoundingBox, Detection, DetectionResult, ImageFrame
+from cmvr_edge_ai.core.component import ComponentContext, Emission
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.detection.base import DetectionModel
+from cmvr_edge_ai.detection.operator import DetectionOperator
+from cmvr_edge_ai.detection.registry import (
+ DetectionModelRegistry,
+ DetectionModelSpec,
+ UnknownDetectionModelError,
+)
+
+
+ResultT = TypeVar("ResultT")
+
+
+class RecordingDetectionModel(DetectionModel):
+ def __init__(self, detections: Sequence[Detection] = ()) -> None:
+ self.detections = tuple(detections)
+ self.load_calls = 0
+ self.predict_calls = 0
+ self.close_calls = 0
+ self.predict_arguments: list[tuple[ImageFrame, tuple[str, ...], float]] = []
+ self.call_threads: dict[str, list[int]] = {
+ "load": [],
+ "predict": [],
+ "close": [],
+ }
+
+ def load(self) -> None:
+ self.load_calls += 1
+ self.call_threads["load"].append(threading.get_ident())
+
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ self.predict_calls += 1
+ self.predict_arguments.append((frame, labels, confidence))
+ self.call_threads["predict"].append(threading.get_ident())
+ return self.detections
+
+ def close(self) -> None:
+ self.close_calls += 1
+ self.call_threads["close"].append(threading.get_ident())
+
+
+def _detection(label: str, confidence: float) -> Detection:
+ return Detection(
+ label=label,
+ confidence=confidence,
+ box=BoundingBox(x_min=1.0, y_min=2.0, x_max=11.0, y_max=22.0),
+ )
+
+
+def _frame(*, codec: str = "none") -> ImageFrame:
+ return ImageFrame(
+ data=bytes(range(12)),
+ width=2,
+ height=2,
+ pixel_format="BGR8",
+ codec=codec,
+ is_key_frame=codec != "none",
+ )
+
+
+def _envelope(frame: ImageFrame | None = None) -> Envelope[ImageFrame]:
+ return Envelope(
+ payload=_frame() if frame is None else frame,
+ schema_name="ImageFrame",
+ schema_version=1,
+ source_id="camera-right",
+ sequence=42,
+ captured_at_ns=1_234_000_000,
+ received_at_ns=1_235_000_000,
+ deadline_ns=1_500_000_000,
+ trace_id="trace-detection-42",
+ session_id="camera-session",
+ attributes={"site": "park-a"},
+ )
+
+
+def _registry(
+ model: RecordingDetectionModel,
+ *,
+ labels: tuple[str, ...] = ("Worker", "No-Helmet", "No-Vest"),
+ factory_options: list[dict[str, Any]] | None = None,
+) -> DetectionModelRegistry:
+ def factory(options: Mapping[str, Any]) -> DetectionModel:
+ if factory_options is not None:
+ factory_options.append(dict(options))
+ return model
+
+ registry = DetectionModelRegistry()
+ registry.register(
+ DetectionModelSpec(
+ model_id="construction-ppe@1",
+ name="Construction PPE",
+ supported_labels=labels,
+ factory=factory,
+ backend="fake",
+ )
+ )
+ return registry
+
+
+def _context(executor: ThreadPoolExecutor) -> ComponentContext:
+ return ComponentContext(
+ pipeline_id="detection-test",
+ node_id="detector",
+ shutdown_event=asyncio.Event(),
+ metadata={"thread_executor": executor},
+ )
+
+
+async def _keep_restricted_event_loop_responsive() -> None:
+ """Provide wakeups where sandboxed self-pipe notifications are unavailable."""
+
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def _run_with_thread_executor(
+ exercise: Callable[[ThreadPoolExecutor], Awaitable[ResultT]],
+) -> ResultT:
+ async def runner() -> ResultT:
+ loop = asyncio.get_running_loop()
+ executor = ThreadPoolExecutor(max_workers=1)
+ loop.set_default_executor(executor)
+ ticker = asyncio.create_task(_keep_restricted_event_loop_responsive())
+ try:
+ return await exercise(executor)
+ finally:
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ executor.shutdown(wait=True, cancel_futures=True)
+ # asyncio.run() otherwise tries to shut down the same executor again.
+ loop._default_executor = None # type: ignore[attr-defined]
+
+ return asyncio.run(runner())
+
+
+def test_operator_loads_once_predicts_and_closes_idempotently() -> None:
+ model = RecordingDetectionModel([_detection("Worker", 0.9)])
+ options_seen: list[dict[str, Any]] = []
+ operator = DetectionOperator(
+ "detector",
+ {
+ "model": "construction-ppe@1",
+ "model_options": {"weights": "/models/ppe.pt", "device": "cpu"},
+ },
+ model_registry=_registry(model, factory_options=options_seen),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ result = await operator.process(_envelope())
+ await operator.stop()
+ await operator.stop()
+
+ assert isinstance(result, Emission)
+ assert model.load_calls == 1
+ assert model.predict_calls == 1
+ assert model.close_calls == 1
+ assert options_seen == [{"weights": "/models/ppe.pt", "device": "cpu"}]
+
+ _run_with_thread_executor(scenario)
+
+
+def test_operator_filters_selected_labels_and_per_label_confidence() -> None:
+ selected_worker = _detection("Worker", 0.50)
+ selected_violation = _detection("No-Helmet", 0.80)
+ model = RecordingDetectionModel(
+ [
+ _detection("Worker", 0.49),
+ selected_worker,
+ _detection("No-Helmet", 0.79),
+ selected_violation,
+ _detection("No-Vest", 0.99),
+ ]
+ )
+ operator = DetectionOperator(
+ "detector",
+ {
+ "model": "construction-ppe@1",
+ "detect_labels": ["Worker", "No-Helmet"],
+ "confidence": 0.50,
+ "label_confidence": {"No-Helmet": 0.80},
+ },
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ emission = await operator.process(_envelope())
+ await operator.stop()
+
+ assert isinstance(emission, Emission)
+ payload = emission.envelope.payload
+ assert isinstance(payload, DetectionResult)
+ assert payload.detections == (selected_worker, selected_violation)
+ assert operator.selected_labels == ("Worker", "No-Helmet")
+ assert model.predict_arguments[0][1:] == (("Worker", "No-Helmet"), 0.50)
+
+ _run_with_thread_executor(scenario)
+
+
+def test_operator_rejects_unknown_model_during_construction() -> None:
+ with pytest.raises(UnknownDetectionModelError, match="missing@1"):
+ DetectionOperator(
+ "detector",
+ {"model": "missing@1"},
+ model_registry=DetectionModelRegistry(),
+ )
+
+
+@pytest.mark.parametrize(
+ "params",
+ [
+ {
+ "model": "construction-ppe@1",
+ "detect_labels": ["Unknown-Label"],
+ },
+ {
+ "model": "construction-ppe@1",
+ "label_confidence": {"Unknown-Label": 0.9},
+ },
+ ],
+)
+def test_operator_rejects_unknown_labels_during_construction(
+ params: dict[str, Any],
+) -> None:
+ with pytest.raises(ValueError, match="Unknown-Label"):
+ DetectionOperator(
+ "detector",
+ params,
+ model_registry=_registry(RecordingDetectionModel()),
+ )
+
+
+def test_operator_rejects_encoded_frame_without_calling_model() -> None:
+ model = RecordingDetectionModel()
+ operator = DetectionOperator(
+ "detector",
+ {"model": "construction-ppe@1"},
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ with pytest.raises(ValueError, match="decoded ImageFrame"):
+ await operator.process(_envelope(_frame(codec="h264")))
+ await operator.stop()
+
+ assert model.predict_calls == 0
+
+ _run_with_thread_executor(scenario)
+
+
+def test_operator_emits_result_metadata_and_preserves_envelope_correlation() -> None:
+ source = _envelope()
+ expected_detection = _detection("No-Helmet", 0.93)
+ model = RecordingDetectionModel([expected_detection])
+ operator = DetectionOperator(
+ "detector",
+ {"model": "construction-ppe@1"},
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ emission = await operator.process(source)
+ await operator.stop()
+
+ assert isinstance(emission, Emission)
+ assert emission.port == "detections"
+ output = emission.envelope
+ assert output.schema == "DetectionResult/v1"
+ assert isinstance(output.payload, DetectionResult)
+ assert output.payload.detections == (expected_detection,)
+ assert output.payload.model_id == "construction-ppe@1"
+ assert output.payload.model_name == "Construction PPE"
+ assert output.payload.inference_ms >= 0.0
+ assert output.payload.source_frame is None
+ assert operator.attach_frame is False
+ assert output.source_id == source.source_id
+ assert output.sequence == source.sequence
+ assert output.captured_at_ns == source.captured_at_ns
+ assert output.received_at_ns == source.received_at_ns
+ assert output.deadline_ns == source.deadline_ns
+ assert output.trace_id == source.trace_id
+ assert output.session_id == source.session_id
+ assert output.attributes == {
+ "site": "park-a",
+ "detection_model_id": "construction-ppe@1",
+ "detection_model_name": "Construction PPE",
+ }
+
+ _run_with_thread_executor(scenario)
+
+
+def test_operator_attach_frame_keeps_the_decoded_frame_by_reference() -> None:
+ source = _envelope()
+ model = RecordingDetectionModel([_detection("No-Helmet", 0.93)])
+ operator = DetectionOperator(
+ "detector",
+ {"model": "construction-ppe@1", "attach_frame": True},
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ emission = await operator.process(source)
+ await operator.stop()
+
+ assert isinstance(emission, Emission)
+ assert isinstance(emission.envelope.payload, DetectionResult)
+ assert emission.envelope.payload.source_frame is source.payload
+ assert operator.attach_frame is True
+
+ _run_with_thread_executor(scenario)
+
+
+def test_blocking_model_methods_run_on_the_supplied_thread_pool() -> None:
+ event_loop_thread = threading.get_ident()
+ model = RecordingDetectionModel()
+ operator = DetectionOperator(
+ "detector",
+ {"model": "construction-ppe@1"},
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ await operator.process(_envelope())
+ await operator.stop()
+
+ assert all(len(thread_ids) == 1 for thread_ids in model.call_threads.values())
+ worker_threads = {
+ thread_id
+ for thread_ids in model.call_threads.values()
+ for thread_id in thread_ids
+ }
+ assert len(worker_threads) == 1
+ assert event_loop_thread not in worker_threads
+
+ _run_with_thread_executor(scenario)
+
+
+def test_max_fps_skips_frames_without_invoking_model_again() -> None:
+ model = RecordingDetectionModel()
+ operator = DetectionOperator(
+ "detector",
+ {"model": "construction-ppe@1", "max_fps": 0.001},
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ first = await operator.process(_envelope())
+ second = await operator.process(_envelope())
+ await operator.stop()
+
+ assert isinstance(first, Emission)
+ assert second is None
+ assert model.predict_calls == 1
+
+ _run_with_thread_executor(scenario)
+
+
+def test_operator_logs_model_load_and_configured_inference_heartbeat(
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ timestamps = iter((0, 1_000_000_000, 6_000_000_000))
+ monkeypatch.setattr(
+ "cmvr_edge_ai.detection.operator.monotonic_ns",
+ lambda: next(timestamps),
+ )
+ model = RecordingDetectionModel([_detection("No-Helmet", 0.93)])
+ operator = DetectionOperator(
+ "detector",
+ {
+ "model": "construction-ppe@1",
+ "detect_labels": ["No-Helmet"],
+ "inference_log_interval_s": 5,
+ },
+ model_registry=_registry(model),
+ )
+
+ async def scenario(executor: ThreadPoolExecutor) -> None:
+ await operator.setup(_context(executor))
+ await operator.process(_envelope())
+ await operator.process(_envelope())
+ await operator.process(_envelope())
+ await operator.stop()
+
+ with caplog.at_level(logging.INFO, logger="cmvr_edge_ai.detection.operator"):
+ _run_with_thread_executor(scenario)
+
+ messages = [record.getMessage() for record in caplog.records]
+ assert any(
+ "detection model loaded node=detector model=construction-ppe@1" in message
+ for message in messages
+ )
+ inference_messages = [
+ message for message in messages if message.startswith("detection inference ")
+ ]
+ assert len(inference_messages) == 2
+ assert "total_frames=1" in inference_messages[0]
+ assert "window_frames=1" in inference_messages[0]
+ assert "window_detections=1" in inference_messages[0]
+ assert "hit_labels=No-Helmet:1" in inference_messages[0]
+ assert "total_frames=3" in inference_messages[1]
+ assert "window_frames=2" in inference_messages[1]
+ assert "window_detections=2" in inference_messages[1]
+ assert "hit_labels=No-Helmet:2" in inference_messages[1]
+
+
+@pytest.mark.parametrize("value", [0, -1, 3600.1, True])
+def test_operator_rejects_invalid_inference_log_interval(value: Any) -> None:
+ with pytest.raises(ValueError, match="inference_log_interval_s"):
+ DetectionOperator(
+ "detector",
+ {
+ "model": "construction-ppe@1",
+ "inference_log_interval_s": value,
+ },
+ model_registry=_registry(RecordingDetectionModel()),
+ )
diff --git a/tests/unit/test_detection_registry.py b/tests/unit/test_detection_registry.py
new file mode 100644
index 0000000..5ad32a5
--- /dev/null
+++ b/tests/unit/test_detection_registry.py
@@ -0,0 +1,177 @@
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.contracts import Detection, ImageFrame
+from cmvr_edge_ai.detection.base import DetectionModel
+from cmvr_edge_ai.detection.registry import (
+ DetectionModelRegistry,
+ DetectionModelRegistryError,
+ DetectionModelSpec,
+ DuplicateDetectionModelError,
+ UnknownDetectionModelError,
+)
+
+
+class FakeDetectionModel(DetectionModel):
+ def load(self) -> None:
+ pass
+
+ def predict(
+ self,
+ frame: ImageFrame,
+ labels: tuple[str, ...],
+ confidence: float,
+ ) -> Sequence[Detection]:
+ del frame, labels, confidence
+ return ()
+
+ def close(self) -> None:
+ pass
+
+
+def _factory(params: Mapping[str, Any]) -> DetectionModel:
+ del params
+ return FakeDetectionModel()
+
+
+def _spec(model_id: str = "ppe-yolo@1") -> DetectionModelSpec:
+ return DetectionModelSpec(
+ model_id=model_id,
+ name="Construction PPE",
+ supported_labels=("Worker", "No-Helmet", "No-Vest"),
+ factory=_factory,
+ backend="ultralytics",
+ description="PPE violation detector",
+ )
+
+
+def test_detection_model_spec_preserves_order_and_copies_labels() -> None:
+ labels = ["Worker", "No-Helmet", "No-Vest"]
+ spec = DetectionModelSpec(
+ model_id="ppe@2",
+ name=" PPE detector ",
+ supported_labels=labels, # type: ignore[arg-type]
+ factory=_factory,
+ backend=" yolo ",
+ )
+ labels.append("No-Glove")
+
+ assert spec.name == "PPE detector"
+ assert spec.backend == "yolo"
+ assert spec.supported_labels == ("Worker", "No-Helmet", "No-Vest")
+
+
+@pytest.mark.parametrize("model_id", ["ppe", "@1", "ppe@0", "ppe@-1", "ppe@v1"])
+def test_detection_model_spec_requires_positive_version(model_id: str) -> None:
+ with pytest.raises(ValueError, match="model id"):
+ DetectionModelSpec(
+ model_id=model_id,
+ name="PPE",
+ supported_labels=("Worker",),
+ factory=_factory,
+ backend="fake",
+ )
+
+
+@pytest.mark.parametrize(
+ ("overrides", "exception", "message"),
+ [
+ ({"name": " "}, ValueError, "model name"),
+ ({"backend": ""}, ValueError, "model backend"),
+ ({"supported_labels": ()}, ValueError, "must not be empty"),
+ ({"supported_labels": ("Worker", "")}, ValueError, "non-empty strings"),
+ (
+ {"supported_labels": ("Worker", "Worker")},
+ ValueError,
+ "must be unique",
+ ),
+ ({"supported_labels": "Worker"}, TypeError, "ordered collection"),
+ ({"supported_labels": {"Worker"}}, TypeError, "ordered collection"),
+ ({"factory": None}, TypeError, "factory must be callable"),
+ ],
+)
+def test_detection_model_spec_rejects_invalid_metadata(
+ overrides: dict[str, Any], exception: type[Exception], message: str
+) -> None:
+ values: dict[str, Any] = {
+ "model_id": "ppe@1",
+ "name": "PPE",
+ "supported_labels": ("Worker",),
+ "factory": _factory,
+ "backend": "fake",
+ }
+ values.update(overrides)
+
+ with pytest.raises(exception, match=message):
+ DetectionModelSpec(**values)
+
+
+def test_registry_registers_resolves_and_sorts_specs() -> None:
+ registry = DetectionModelRegistry()
+ second = registry.register(_spec("z-model@1"))
+ first = registry.register(_spec("a-model@2"))
+
+ assert registry.resolve("z-model@1") is second
+ assert registry.specs() == (first, second)
+
+
+def test_registry_rejects_duplicate_and_reports_available_models() -> None:
+ registry = DetectionModelRegistry()
+ registry.register(_spec("ppe@1"))
+
+ with pytest.raises(DuplicateDetectionModelError, match="ppe@1"):
+ registry.register(_spec("ppe@1"))
+ with pytest.raises(UnknownDetectionModelError, match=r"available models: ppe@1"):
+ registry.resolve("missing@1")
+
+
+@dataclass
+class _EntryPoint:
+ name: str
+ value: Any
+
+ def load(self) -> Any:
+ return self.value
+
+
+class _EntryPoints(list[_EntryPoint]):
+ def select(self, *, group: str) -> _EntryPoints:
+ assert group == DetectionModelRegistry.ENTRY_POINT_GROUP
+ return self
+
+
+def test_registry_discovers_specs_and_registration_callbacks(monkeypatch) -> None: # type: ignore[no-untyped-def]
+ def register_callback(registry: DetectionModelRegistry) -> None:
+ registry.register(_spec("callback@1"))
+
+ entry_points = _EntryPoints(
+ [
+ _EntryPoint("spec", _spec("spec@1")),
+ _EntryPoint("callback", register_callback),
+ ]
+ )
+ monkeypatch.setattr(
+ "cmvr_edge_ai.detection.registry.metadata.entry_points",
+ lambda: entry_points,
+ )
+ registry = DetectionModelRegistry()
+
+ registry.load_entry_points()
+
+ assert [spec.model_id for spec in registry.specs()] == ["callback@1", "spec@1"]
+
+
+def test_registry_rejects_invalid_entry_point(monkeypatch) -> None: # type: ignore[no-untyped-def]
+ entry_points = _EntryPoints([_EntryPoint("invalid", object())])
+ monkeypatch.setattr(
+ "cmvr_edge_ai.detection.registry.metadata.entry_points",
+ lambda: entry_points,
+ )
+
+ with pytest.raises(DetectionModelRegistryError, match="invalid"):
+ DetectionModelRegistry().load_entry_points()
diff --git a/tests/unit/test_detection_trigger.py b/tests/unit/test_detection_trigger.py
new file mode 100644
index 0000000..8b7a6be
--- /dev/null
+++ b/tests/unit/test_detection_trigger.py
@@ -0,0 +1,692 @@
+from __future__ import annotations
+
+import asyncio
+from dataclasses import asdict
+import json
+import threading
+from concurrent.futures import ThreadPoolExecutor
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.contracts.events import (
+ BoundingBox,
+ Detection,
+ DetectionAlert,
+ DetectionResult,
+)
+from cmvr_edge_ai.contracts.media import EncodedImage, ImageFrame
+from cmvr_edge_ai.core import ComponentContext, Emission, Envelope
+import cmvr_edge_ai.detection.rules as detection_rules
+from cmvr_edge_ai.detection.annotation import ImageAnnotationDependencyError
+from cmvr_edge_ai.detection.rules import DetectionTriggerOperator
+
+
+NS_PER_MS = 1_000_000
+
+
+def _frame(value: int = 0) -> ImageFrame:
+ return ImageFrame(bytes([value]) * 4 * 4 * 3, 4, 4, "BGR8")
+
+
+async def _keep_restricted_event_loop_responsive() -> None:
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def _detection(
+ label: str = "No-Helmet",
+ confidence: float = 0.9,
+ *,
+ track_id: str | None = None,
+) -> Detection:
+ return Detection(
+ label=label,
+ confidence=confidence,
+ box=BoundingBox(1.0, 2.0, 30.0, 40.0),
+ track_id=track_id,
+ )
+
+
+def _envelope(
+ sequence: int,
+ at_ms: float,
+ *detections: Detection,
+ source_id: str = "camera-a",
+ captured: bool = False,
+ trace_id: str | None = None,
+ frame: ImageFrame | None = None,
+) -> Envelope[DetectionResult]:
+ timestamp_ns = int(at_ms * NS_PER_MS)
+ kwargs: dict[str, Any] = {
+ "payload": DetectionResult(
+ detections=tuple(detections),
+ model_id="construction-ppe-yolov8@1",
+ inference_ms=12.5,
+ model_name="Construction PPE YOLOv8",
+ source_frame=frame,
+ ),
+ "schema_name": "DetectionResult",
+ "schema_version": 1,
+ "source_id": source_id,
+ "sequence": sequence,
+ "received_at_ns": timestamp_ns,
+ "captured_at_ns": timestamp_ns if captured else None,
+ }
+ if trace_id is not None:
+ kwargs["trace_id"] = trace_id
+ return Envelope(**kwargs)
+
+
+def _operator(
+ *,
+ labels: list[str] | None = None,
+ min_hits: int = 3,
+ window_ms: float = 1_000,
+ cooldown_ms: float = 0,
+ min_confidence: float = 0.5,
+ scope: str = "source",
+ time_source: str = "received",
+ alert_image: dict[str, Any] | None = None,
+) -> DetectionTriggerOperator:
+ params: dict[str, Any] = {
+ "time_source": time_source,
+ "rules": [
+ {
+ "id": "missing-ppe",
+ "labels": labels or ["No-Helmet"],
+ "min_hits": min_hits,
+ "window_ms": window_ms,
+ "cooldown_ms": cooldown_ms,
+ "min_confidence": min_confidence,
+ "scope": scope,
+ }
+ ],
+ }
+ if alert_image is not None:
+ params["alert_image"] = alert_image
+ return DetectionTriggerOperator("trigger", params)
+
+
+def _process(
+ operator: DetectionTriggerOperator, envelope: Envelope[Any]
+) -> tuple[Emission, ...] | None:
+ return asyncio.run(operator.process(envelope))
+
+
+def _single_alert(
+ result: tuple[Emission, ...] | None
+) -> tuple[Emission, DetectionAlert]:
+ assert result is not None
+ assert len(result) == 1
+ emission = result[0]
+ assert isinstance(emission.envelope.payload, DetectionAlert)
+ return emission, emission.envelope.payload
+
+
+@pytest.mark.parametrize(
+ ("params", "message"),
+ [
+ ({"rules": [], "unexpected": True}, "unknown detection trigger"),
+ ({"rules": []}, "non-empty list"),
+ (
+ {"rules": [], "alert_image": "enabled"},
+ "alert_image must be a mapping",
+ ),
+ (
+ {"rules": [], "alert_image": {"unexpected": True}},
+ "alert_image has unknown field",
+ ),
+ (
+ {"rules": [], "alert_image": {"enabled": "true"}},
+ "alert_image.enabled must be a boolean",
+ ),
+ (
+ {"rules": [], "alert_image": {"jpeg_quality": True}},
+ "alert_image.jpeg_quality must be an integer",
+ ),
+ (
+ {"rules": [], "alert_image": {"jpeg_quality": 0}},
+ "alert_image.jpeg_quality must be between 1 and 95",
+ ),
+ (
+ {"rules": [], "alert_image": {"jpeg_quality": 96}},
+ "alert_image.jpeg_quality must be between 1 and 95",
+ ),
+ (
+ {
+ "rules": [
+ {
+ "id": "r",
+ "labels": ["No-Helmet"],
+ "min_hits": 2,
+ "window_ms": 100,
+ "typo": True,
+ }
+ ]
+ },
+ "unknown field",
+ ),
+ (
+ {
+ "rules": [
+ {
+ "id": "r",
+ "labels": ["No-Helmet", "No-Helmet"],
+ "min_hits": 2,
+ "window_ms": 100,
+ }
+ ]
+ },
+ "duplicates",
+ ),
+ (
+ {
+ "rules": [
+ {
+ "id": "r",
+ "labels": ["No-Helmet"],
+ "min_hits": True,
+ "window_ms": 100,
+ }
+ ]
+ },
+ "must be an integer",
+ ),
+ (
+ {
+ "time_source": "wall",
+ "rules": [
+ {
+ "id": "r",
+ "labels": ["No-Helmet"],
+ "min_hits": 2,
+ "window_ms": 100,
+ }
+ ],
+ },
+ "time_source",
+ ),
+ ],
+)
+def test_trigger_configuration_is_strict(params: dict[str, Any], message: str) -> None:
+ with pytest.raises(ValueError, match=message):
+ DetectionTriggerOperator("trigger", params)
+
+
+def test_source_rule_counts_distinct_frames_not_boxes_and_preserves_metadata() -> None:
+ operator = _operator(min_hits=3, window_ms=1_000, min_confidence=0.7)
+
+ first = _envelope(
+ 1,
+ 0,
+ _detection(confidence=0.8),
+ _detection(confidence=0.91),
+ _detection("No-Vest", confidence=0.99),
+ )
+ assert _process(operator, first) is None
+ # Replaying a frame with the same source and sequence must not create a hit.
+ assert _process(operator, _envelope(1, 100, _detection(confidence=0.99))) is None
+ # Below-threshold detections do not count.
+ assert _process(operator, _envelope(2, 500, _detection(confidence=0.69))) is None
+ assert _process(operator, _envelope(2, 500, _detection(confidence=0.85))) is None
+
+ threshold = _envelope(
+ 3,
+ 1_000,
+ _detection(confidence=0.95),
+ trace_id="threshold-frame",
+ )
+ emission, alert = _single_alert(_process(operator, threshold))
+
+ assert emission.port == "alerts"
+ assert emission.envelope.schema == "DetectionAlert/v1"
+ assert emission.envelope.trace_id == "threshold-frame"
+ assert emission.envelope.source_id == "camera-a"
+ assert alert.rule_id == "missing-ppe"
+ assert alert.model_id == "construction-ppe-yolov8@1"
+ assert alert.model_name == "Construction PPE YOLOv8"
+ assert alert.labels == ("No-Helmet",)
+ assert alert.scope == "source"
+ assert alert.scope_id == "camera-a"
+ assert alert.hit_count == 3
+ assert alert.window_ms == 1_000
+ assert alert.first_seen_ns == 0
+ assert alert.last_seen_ns == 1_000 * NS_PER_MS
+ assert alert.triggered_at_ns == 1_000 * NS_PER_MS
+ assert alert.max_confidence == pytest.approx(0.95)
+ assert alert.detections == threshold.payload.detections
+ assert alert.event_id
+
+
+def test_invalid_detections_are_counted_and_never_reach_an_alert_payload() -> None:
+ operator = _operator(min_hits=1)
+ invalid_nan = Detection(
+ "No-Helmet",
+ 0.99,
+ BoundingBox(float("nan"), 2.0, 30.0, 40.0),
+ )
+ invalid_reversed = Detection(
+ "No-Helmet",
+ 0.98,
+ BoundingBox(30.0, 40.0, 1.0, 2.0),
+ )
+ valid = _detection(confidence=0.95)
+
+ assert _process(operator, _envelope(1, 0, invalid_nan, invalid_reversed)) is None
+ _, alert = _single_alert(
+ _process(
+ operator,
+ _envelope(2, 10, invalid_nan, invalid_reversed, valid),
+ )
+ )
+
+ assert alert.detections == (valid,)
+ # The remaining structured alert can be emitted by a strict JSON client.
+ json.dumps(asdict(alert), allow_nan=False)
+ health = asyncio.run(operator.health())
+ assert health.healthy is False
+ assert "invalid_detections=4" in health.detail
+
+
+def test_window_lower_boundary_is_inclusive_and_older_hits_expire() -> None:
+ inclusive = _operator(min_hits=2, window_ms=1_000)
+ assert _process(inclusive, _envelope(1, 0, _detection())) is None
+ _, alert = _single_alert(_process(inclusive, _envelope(2, 1_000, _detection())))
+ assert alert.first_seen_ns == 0
+
+ expired = _operator(min_hits=2, window_ms=1_000)
+ assert _process(expired, _envelope(1, 0, _detection())) is None
+ assert _process(expired, _envelope(2, 1_001, _detection())) is None
+
+
+def test_cooldown_does_not_accumulate_hits_and_requires_a_fresh_window() -> None:
+ operator = _operator(min_hits=2, window_ms=1_000, cooldown_ms=500)
+ assert _process(operator, _envelope(1, 0, _detection())) is None
+ _single_alert(_process(operator, _envelope(2, 100, _detection())))
+
+ # The threshold frame itself remains deduplicated even after the hit deque
+ # was reset, and new frames during cooldown are ignored rather than queued.
+ assert _process(operator, _envelope(2, 100, _detection())) is None
+ assert _process(operator, _envelope(3, 200, _detection())) is None
+ assert _process(operator, _envelope(4, 599, _detection())) is None
+ assert _process(operator, _envelope(5, 600, _detection())) is None
+ _, alert = _single_alert(_process(operator, _envelope(6, 700, _detection())))
+ assert alert.first_seen_ns == 600 * NS_PER_MS
+ assert alert.last_seen_ns == 700 * NS_PER_MS
+
+ health = asyncio.run(operator.health())
+ assert health.healthy is True
+ assert "alerts_emitted=2" in health.detail
+ assert "cooldown_suppressed=2" in health.detail
+ assert "duplicate_frames=1" in health.detail
+
+
+def test_source_state_is_isolated_per_camera() -> None:
+ operator = _operator(min_hits=2)
+ assert (
+ _process(operator, _envelope(1, 0, _detection(), source_id="camera-a")) is None
+ )
+ assert (
+ _process(operator, _envelope(1, 10, _detection(), source_id="camera-b")) is None
+ )
+
+ _, alert = _single_alert(
+ _process(operator, _envelope(2, 20, _detection(), source_id="camera-a"))
+ )
+ assert alert.scope_id == "camera-a"
+
+
+def test_track_rules_are_isolated_and_can_emit_multiple_alerts_per_frame() -> None:
+ operator = _operator(min_hits=2, scope="track")
+ first = _envelope(
+ 1,
+ 0,
+ _detection(track_id="worker-1"),
+ _detection(confidence=0.95, track_id="worker-1"),
+ _detection(track_id="worker-2"),
+ _detection(track_id=None),
+ )
+ assert _process(operator, first) is None
+
+ result = _process(
+ operator,
+ _envelope(
+ 2,
+ 100,
+ _detection(track_id="worker-1"),
+ _detection(track_id="worker-2"),
+ ),
+ )
+ assert result is not None
+ assert {item.envelope.payload.scope_id for item in result} == {
+ "worker-1",
+ "worker-2",
+ }
+ assert all(item.envelope.payload.hit_count == 2 for item in result)
+ assert all(item.envelope.payload.scope == "track" for item in result)
+ assert "untracked_detections=1" in asyncio.run(operator.health()).detail
+
+
+def test_multiple_rules_can_trigger_from_one_result() -> None:
+ operator = DetectionTriggerOperator(
+ "trigger",
+ {
+ "time_source": "received",
+ "rules": [
+ {
+ "id": "helmet",
+ "labels": ["No-Helmet"],
+ "min_hits": 1,
+ "window_ms": 100,
+ "cooldown_ms": 0,
+ "min_confidence": 0.5,
+ "scope": "source",
+ },
+ {
+ "id": "vest",
+ "labels": ["No-Vest"],
+ "min_hits": 1,
+ "window_ms": 100,
+ "cooldown_ms": 0,
+ "min_confidence": 0.5,
+ "scope": "source",
+ },
+ ],
+ },
+ )
+ result = _process(
+ operator,
+ _envelope(1, 0, _detection("No-Helmet"), _detection("No-Vest")),
+ )
+
+ assert result is not None
+ assert [item.envelope.payload.rule_id for item in result] == ["helmet", "vest"]
+
+
+def test_alert_image_is_encoded_once_only_when_the_threshold_frame_triggers(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ render_calls: list[
+ tuple[int, ImageFrame, tuple[Detection, ...], int]
+ ] = []
+ encoded = EncodedImage(b"jpeg", "image/jpeg", 4, 4)
+
+ def render(
+ frame: ImageFrame,
+ detections: tuple[Detection, ...],
+ *,
+ jpeg_quality: int,
+ ) -> EncodedImage:
+ render_calls.append(
+ (threading.get_ident(), frame, tuple(detections), jpeg_quality)
+ )
+ return encoded
+
+ monkeypatch.setattr(detection_rules, "ensure_annotation_support", lambda: None)
+ monkeypatch.setattr(detection_rules, "render_annotated_jpeg", render)
+ operator = _operator(
+ min_hits=2,
+ alert_image={"enabled": True, "jpeg_quality": 73},
+ )
+ empty_frame = _frame(1)
+ first_hit_frame = _frame(2)
+ threshold_frame = _frame(3)
+ threshold_detection = _detection(confidence=0.96)
+
+ async def scenario() -> tuple[Emission, ...] | None:
+ event_loop_thread = threading.get_ident()
+ executor = ThreadPoolExecutor(max_workers=1)
+ ticker = asyncio.create_task(_keep_restricted_event_loop_responsive())
+ try:
+ await operator.setup(
+ ComponentContext(
+ pipeline_id="detection",
+ node_id="trigger",
+ shutdown_event=asyncio.Event(),
+ metadata={"thread_executor": executor},
+ )
+ )
+ assert (
+ await operator.process(_envelope(1, 0, frame=empty_frame)) is None
+ )
+ assert (
+ await operator.process(
+ _envelope(2, 100, _detection(), frame=first_hit_frame)
+ )
+ is None
+ )
+ assert render_calls == []
+ result = await operator.process(
+ _envelope(
+ 3,
+ 200,
+ threshold_detection,
+ frame=threshold_frame,
+ )
+ )
+ assert render_calls[0][0] != event_loop_thread
+ return result
+ finally:
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ await operator.stop()
+ executor.shutdown(wait=True, cancel_futures=True)
+
+ result = asyncio.run(scenario())
+ _, alert = _single_alert(result)
+
+ assert len(render_calls) == 1
+ _, rendered_frame, rendered_detections, quality = render_calls[0]
+ assert rendered_frame is threshold_frame
+ assert rendered_detections == (threshold_detection,)
+ assert quality == 73
+ assert alert.image is encoded
+ health = asyncio.run(operator.health())
+ assert health.healthy is True
+ assert "alert_images_encoded=1" in health.detail
+ assert "alert_images_attached=1" in health.detail
+
+
+def test_multiple_alerts_from_one_frame_share_one_union_image(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ helmet = _detection("No-Helmet", confidence=0.91)
+ vest = _detection("No-Vest", confidence=0.88)
+ threshold_frame = _frame(4)
+ encoded = EncodedImage(b"shared-jpeg", "image/jpeg", 4, 4)
+ render_calls: list[tuple[ImageFrame, tuple[Detection, ...]]] = []
+
+ def render(
+ frame: ImageFrame,
+ detections: tuple[Detection, ...],
+ *,
+ jpeg_quality: int,
+ ) -> EncodedImage:
+ assert jpeg_quality == 85
+ render_calls.append((frame, tuple(detections)))
+ return encoded
+
+ async def run_inline(
+ function: Any,
+ *args: Any,
+ executor: Any = None,
+ **kwargs: Any,
+ ) -> Any:
+ del executor
+ return function(*args, **kwargs)
+
+ monkeypatch.setattr(detection_rules, "render_annotated_jpeg", render)
+ monkeypatch.setattr(detection_rules, "run_blocking", run_inline)
+ operator = DetectionTriggerOperator(
+ "trigger",
+ {
+ "time_source": "received",
+ "alert_image": {"enabled": True},
+ "rules": [
+ {
+ "id": "helmet",
+ "labels": ["No-Helmet"],
+ "min_hits": 1,
+ "window_ms": 100,
+ },
+ {
+ "id": "vest",
+ "labels": ["No-Vest"],
+ "min_hits": 1,
+ "window_ms": 100,
+ },
+ ],
+ },
+ )
+
+ result = _process(
+ operator,
+ _envelope(1, 0, helmet, vest, frame=threshold_frame),
+ )
+
+ assert result is not None
+ assert render_calls == [(threshold_frame, (helmet, vest))]
+ assert [item.envelope.payload.rule_id for item in result] == ["helmet", "vest"]
+ assert all(item.envelope.payload.image is encoded for item in result)
+ health = asyncio.run(operator.health())
+ assert "alert_images_encoded=1" in health.detail
+ assert "alert_images_attached=2" in health.detail
+
+
+def test_missing_threshold_frame_is_fail_soft_and_commits_cooldown() -> None:
+ operator = _operator(
+ min_hits=1,
+ cooldown_ms=500,
+ alert_image={"enabled": True},
+ )
+
+ _, alert = _single_alert(_process(operator, _envelope(1, 0, _detection())))
+ assert alert.image is None
+ # The alert state is committed even though its evidence image is missing.
+ assert _process(operator, _envelope(1, 100, _detection())) is None
+ assert _process(operator, _envelope(2, 100, _detection())) is None
+
+ health = asyncio.run(operator.health())
+ assert health.healthy is False
+ assert "alerts_emitted=1" in health.detail
+ assert "duplicate_frames=1" in health.detail
+ assert "cooldown_suppressed=1" in health.detail
+ assert "alert_image_missing_frames=1" in health.detail
+ assert "alert_images_encoded=0" in health.detail
+
+
+def test_renderer_failure_is_fail_soft_and_commits_cooldown(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ render_calls = 0
+
+ def fail_render(*args: Any, **kwargs: Any) -> EncodedImage:
+ nonlocal render_calls
+ render_calls += 1
+ raise ValueError("bad packed frame")
+
+ async def run_inline(
+ function: Any,
+ *args: Any,
+ executor: Any = None,
+ **kwargs: Any,
+ ) -> Any:
+ del executor
+ return function(*args, **kwargs)
+
+ monkeypatch.setattr(detection_rules, "render_annotated_jpeg", fail_render)
+ monkeypatch.setattr(detection_rules, "run_blocking", run_inline)
+ operator = _operator(
+ min_hits=1,
+ cooldown_ms=500,
+ alert_image={"enabled": True},
+ )
+ frame = _frame(5)
+
+ _, alert = _single_alert(
+ _process(operator, _envelope(1, 0, _detection(), frame=frame))
+ )
+ assert alert.image is None
+ assert render_calls == 1
+ assert _process(operator, _envelope(1, 100, _detection(), frame=frame)) is None
+ assert _process(operator, _envelope(2, 100, _detection(), frame=frame)) is None
+
+ health = asyncio.run(operator.health())
+ assert health.healthy is False
+ assert "alerts_emitted=1" in health.detail
+ assert "duplicate_frames=1" in health.detail
+ assert "cooldown_suppressed=1" in health.detail
+ assert "alert_image_failures=1" in health.detail
+ assert "alert_images_encoded=0" in health.detail
+
+
+def test_pillow_dependency_is_checked_only_when_alert_images_are_enabled(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ checks = 0
+
+ def missing_pillow() -> None:
+ nonlocal checks
+ checks += 1
+ raise ImageAnnotationDependencyError("install the 'image' extra")
+
+ monkeypatch.setattr(detection_rules, "ensure_annotation_support", missing_pillow)
+ context = ComponentContext(
+ pipeline_id="detection",
+ node_id="trigger",
+ shutdown_event=asyncio.Event(),
+ )
+
+ asyncio.run(_operator(min_hits=1).setup(context))
+ assert checks == 0
+
+ enabled = _operator(
+ min_hits=1,
+ alert_image={"enabled": True},
+ )
+ with pytest.raises(ImageAnnotationDependencyError, match="image.*extra"):
+ asyncio.run(enabled.setup(context))
+ assert checks == 1
+
+
+def test_time_source_is_explicit_and_captured_requires_a_timestamp() -> None:
+ captured = _operator(min_hits=2, window_ms=10, time_source="captured")
+ with pytest.raises(ValueError, match="requires captured_at_ns"):
+ _process(captured, _envelope(1, 0, _detection()))
+
+ auto = _operator(min_hits=2, window_ms=10, time_source="auto")
+ first = _envelope(1, 0, _detection(), captured=True)
+ second = Envelope(
+ payload=first.payload,
+ schema_name="DetectionResult",
+ source_id="camera-a",
+ sequence=2,
+ captured_at_ns=20 * NS_PER_MS,
+ received_at_ns=5 * NS_PER_MS,
+ )
+ assert _process(auto, first) is None
+ # auto prefers capture time, so the first hit is outside this 10 ms window.
+ assert _process(auto, second) is None
+
+ received = _operator(min_hits=2, window_ms=10, time_source="received")
+ assert _process(received, first) is None
+ _single_alert(_process(received, second))
+
+
+def test_wrong_payload_and_out_of_order_frames_fail_closed() -> None:
+ operator = _operator(min_hits=2)
+ with pytest.raises(TypeError, match="expected DetectionResult"):
+ _process(
+ operator,
+ Envelope(
+ "not-a-detection",
+ source_id="camera-a",
+ sequence=1,
+ received_at_ns=0,
+ ),
+ )
+
+ assert _process(operator, _envelope(1, 100, _detection())) is None
+ assert _process(operator, _envelope(2, 99, _detection())) is None
+ assert "stale_frames=1" in asyncio.run(operator.health()).detail
diff --git a/tests/unit/test_edge_queue.py b/tests/unit/test_edge_queue.py
new file mode 100644
index 0000000..560c6cb
--- /dev/null
+++ b/tests/unit/test_edge_queue.py
@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+import asyncio
+
+import pytest
+
+from cmvr_edge_ai.core import (
+ EdgeQueue,
+ EdgeQueueClosed,
+ EdgeQueueFull,
+ OverflowPolicy,
+ PutResult,
+)
+
+
+def test_drop_oldest_keeps_the_freshest_items_and_updates_stats() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ queue = EdgeQueue[int](2, OverflowPolicy.DROP_OLDEST, name="camera")
+ assert await queue.put(1) is PutResult.ENQUEUED
+ assert await queue.put(2) is PutResult.ENQUEUED
+ assert await queue.put(3) is PutResult.DROPPED_OLDEST
+ assert [await queue.get(), await queue.get()] == [2, 3]
+ await queue.close()
+ with pytest.raises(EdgeQueueClosed, match="camera"):
+ await queue.get()
+ return queue.stats()
+
+ stats = asyncio.run(exercise())
+
+ assert stats.enqueued == 3
+ assert stats.dequeued == 2
+ assert stats.dropped_oldest == 1
+ assert stats.high_watermark == 2
+ assert stats.closed is True
+
+
+def test_drop_newest_preserves_queued_items() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ queue = EdgeQueue[str](2, "drop_newest")
+ await queue.put("first")
+ await queue.put("second")
+ result = await queue.put("third")
+ items = [await queue.get(), await queue.get()]
+ return result, items, queue.stats()
+
+ result, items, stats = asyncio.run(exercise())
+
+ assert result is PutResult.DROPPED_NEWEST
+ assert items == ["first", "second"]
+ assert stats.enqueued == 2
+ assert stats.dropped_newest == 1
+
+
+def test_reject_policy_raises_without_mutating_contents() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ queue = EdgeQueue[str](1, "reject", name="control")
+ await queue.put("accepted")
+ with pytest.raises(EdgeQueueFull, match="control"):
+ await queue.put("rejected")
+ return await queue.get(), queue.stats()
+
+ item, stats = asyncio.run(exercise())
+
+ assert item == "accepted"
+ assert stats.enqueued == 1
+ assert stats.rejected == 1
+
+
+def test_block_policy_releases_producer_after_consumer_makes_space() -> None:
+ async def exercise(): # type: ignore[no-untyped-def]
+ queue = EdgeQueue[str](1, "block")
+ await queue.put("first")
+ pending_put = asyncio.create_task(queue.put("second"))
+ await asyncio.sleep(0)
+ assert pending_put.done() is False
+
+ first = await queue.get()
+ put_result = await asyncio.wait_for(pending_put, timeout=1)
+ second = await queue.get()
+ return first, second, put_result, queue.stats()
+
+ first, second, put_result, stats = asyncio.run(exercise())
+
+ assert (first, second) == ("first", "second")
+ assert put_result is PutResult.ENQUEUED
+ assert stats.blocked_puts == 1
diff --git a/tests/unit/test_http_json_sink.py b/tests/unit/test_http_json_sink.py
new file mode 100644
index 0000000..a7079d6
--- /dev/null
+++ b/tests/unit/test_http_json_sink.py
@@ -0,0 +1,505 @@
+from __future__ import annotations
+
+import asyncio
+import base64
+from copy import deepcopy
+from dataclasses import dataclass
+import json
+import logging
+from typing import Any
+
+import pytest
+
+import cmvr_edge_ai.connectors.platform.http_json as http_json
+from cmvr_edge_ai.connectors.platform.http_json import PlatformHttpJsonSink
+from cmvr_edge_ai.contracts import (
+ BoundingBox,
+ Detection,
+ DetectionAlert,
+ DetectionResult,
+ EncodedImage,
+ ImageFrame,
+)
+from cmvr_edge_ai.core.component import ComponentContext
+from cmvr_edge_ai.core.envelope import Envelope
+from cmvr_edge_ai.transports.http import HttpClientPool, HttpClientSettings
+
+
+class FakeHttpStatusError(RuntimeError):
+ def __init__(self, status_code: int) -> None:
+ self.status_code = status_code
+ super().__init__(f"HTTP status {status_code}")
+
+
+@dataclass
+class FakeResponse:
+ status_code: int
+
+ def raise_for_status(self) -> None:
+ if self.status_code >= 400:
+ raise FakeHttpStatusError(self.status_code)
+
+
+class FakeHttpClient:
+ def __init__(self, outcomes: list[FakeResponse | BaseException]) -> None:
+ self._outcomes = list(outcomes)
+ self.calls: list[dict[str, Any]] = []
+
+ async def post(
+ self,
+ path: str,
+ *,
+ json: dict[str, Any],
+ headers: dict[str, str],
+ ) -> FakeResponse:
+ self.calls.append(
+ {
+ "path": path,
+ "json": deepcopy(json),
+ "body_id": id(json),
+ "headers": dict(headers),
+ }
+ )
+ if not self._outcomes:
+ raise AssertionError("fake HTTP client has no configured outcome")
+ outcome = self._outcomes.pop(0)
+ if isinstance(outcome, BaseException):
+ raise outcome
+ return outcome
+
+
+class FakeHttpClientPool(HttpClientPool):
+ def __init__(self, client: FakeHttpClient) -> None:
+ self.client = client
+ self.get_calls: list[tuple[str, HttpClientSettings]] = []
+
+ async def get(
+ self, endpoint_id: str, settings: HttpClientSettings
+ ) -> FakeHttpClient:
+ self.get_calls.append((endpoint_id, settings))
+ return self.client
+
+
+def _alert(
+ event_id: str = "ppe-event-123",
+ *,
+ image: EncodedImage | None = None,
+) -> DetectionAlert:
+ detection = Detection(
+ label="No-Helmet",
+ confidence=0.94,
+ box=BoundingBox(x_min=10.0, y_min=20.0, x_max=100.0, y_max=200.0),
+ )
+ return DetectionAlert(
+ event_id=event_id,
+ rule_id="no-helmet-rule",
+ model_id="construction-ppe@1",
+ model_name="Construction PPE",
+ labels=("No-Helmet",),
+ scope="source",
+ scope_id="right-camera",
+ hit_count=3,
+ window_ms=2_000.0,
+ first_seen_ns=1_000_000_000,
+ last_seen_ns=2_000_000_000,
+ triggered_at_ns=2_000_000_000,
+ max_confidence=0.94,
+ detections=(detection,),
+ image=image,
+ )
+
+
+def _envelope(payload: Any, *, trace_id: str = "trace-fallback-456") -> Envelope[Any]:
+ return Envelope(
+ payload=payload,
+ schema_name=(
+ "DetectionAlert" if isinstance(payload, DetectionAlert) else "opaque"
+ ),
+ schema_version=1,
+ source_id="right-camera",
+ sequence=17,
+ captured_at_ns=2_000_000_000,
+ received_at_ns=2_010_000_000,
+ trace_id=trace_id,
+ attributes={"site": "park-a"},
+ )
+
+
+async def _setup_sink(
+ client: FakeHttpClient,
+ **params: Any,
+) -> PlatformHttpJsonSink:
+ sink = PlatformHttpJsonSink(
+ "platform",
+ {
+ "endpoint": "platform",
+ "path": "/v1/detection-alerts",
+ **params,
+ },
+ )
+ pool = FakeHttpClientPool(client)
+ context = ComponentContext(
+ pipeline_id="detection",
+ node_id="platform",
+ shutdown_event=asyncio.Event(),
+ metadata={
+ "endpoints": {
+ "platform": {
+ "base_url": "http://platform.invalid",
+ "timeout_s": 1.0,
+ "metadata": {},
+ "options": {},
+ }
+ },
+ "http_client_pool": pool,
+ },
+ )
+ await sink.setup(context)
+ assert pool.get_calls[0][0] == "platform"
+ return sink
+
+
+def test_detection_alert_event_id_is_the_idempotency_key() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([FakeResponse(200)])
+ sink = await _setup_sink(client, max_attempts=1)
+
+ await sink.consume(_envelope(_alert()), input_port="alerts")
+
+ assert len(client.calls) == 1
+ call = client.calls[0]
+ assert call["headers"] == {"Idempotency-Key": "ppe-event-123"}
+ assert call["path"] == "/v1/detection-alerts"
+ assert call["json"]["input_port"] == "alerts"
+ assert call["json"]["payload"]["event_id"] == "ppe-event-123"
+ assert call["json"]["payload"]["image"] is None
+
+ asyncio.run(scenario())
+
+
+def test_detection_alert_image_has_a_flat_base64_wire_shape() -> None:
+ async def scenario() -> None:
+ jpeg = b"\xff\xd8\xff\xe0annotated-image\xff\xd9"
+ image = EncodedImage(
+ data=jpeg,
+ media_type="image/jpeg",
+ width=1280,
+ height=720,
+ )
+ client = FakeHttpClient([FakeResponse(200)])
+ sink = await _setup_sink(client, max_attempts=1)
+
+ await sink.consume(_envelope(_alert(image=image)), input_port="alerts")
+
+ body = client.calls[0]["json"]
+ payload = body["payload"]
+ assert payload["image"] == {
+ "media_type": "image/jpeg",
+ "width": 1280,
+ "height": 720,
+ "encoding": "base64",
+ "data": base64.b64encode(jpeg).decode("ascii"),
+ }
+ assert (
+ base64.b64decode(payload["image"]["data"], validate=True) == jpeg
+ )
+ assert payload["detections"][0] == {
+ "label": "No-Helmet",
+ "confidence": 0.94,
+ "box": {
+ "x_min": 10.0,
+ "y_min": 20.0,
+ "x_max": 100.0,
+ "y_max": 200.0,
+ },
+ "track_id": None,
+ }
+ # The fake receives the pre-serialization dict; make sure a real JSON
+ # encoder can consume the exact body without any remaining bytes.
+ json.dumps(body)
+
+ asyncio.run(scenario())
+
+
+def test_transient_detection_source_frame_is_never_serialized() -> None:
+ async def scenario() -> None:
+ raw_frame = ImageFrame(bytes(range(12)), 2, 2, "BGR8")
+ result = DetectionResult(
+ detections=(
+ Detection(
+ "No-Helmet",
+ 0.9,
+ BoundingBox(1.0, 0.0, 2.0, 2.0),
+ ),
+ ),
+ model_id="construction-ppe@1",
+ inference_ms=3.5,
+ source_frame=raw_frame,
+ )
+ client = FakeHttpClient([FakeResponse(200)])
+ sink = await _setup_sink(client, max_attempts=1)
+
+ await sink.consume(_envelope(result), input_port="detections")
+
+ payload = client.calls[0]["json"]["payload"]
+ assert "source_frame" not in payload
+ assert payload["detections"][0]["label"] == "No-Helmet"
+ assert base64.b64encode(raw_frame.data).decode("ascii") not in json.dumps(
+ client.calls[0]["json"]
+ )
+
+ asyncio.run(scenario())
+
+
+def test_payload_without_event_id_falls_back_to_envelope_trace_id() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([FakeResponse(204)])
+ sink = await _setup_sink(client, max_attempts=1)
+
+ await sink.consume(_envelope({"status": "ok"}))
+
+ assert client.calls[0]["headers"] == {"Idempotency-Key": "trace-fallback-456"}
+
+ asyncio.run(scenario())
+
+
+@pytest.mark.parametrize(
+ "outcomes",
+ [
+ [FakeResponse(503), FakeResponse(503), FakeResponse(200)],
+ [OSError("offline"), TimeoutError("timed out"), FakeResponse(200)],
+ ],
+ ids=("retryable-status", "network-errors"),
+)
+def test_retryable_failures_retry_to_max_attempts_with_an_unchanged_body(
+ outcomes: list[FakeResponse | BaseException],
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ async def scenario() -> None:
+ encode_calls = 0
+ original_b64encode = http_json.base64.b64encode
+
+ def recording_b64encode(value: bytes) -> bytes:
+ nonlocal encode_calls
+ encode_calls += 1
+ return original_b64encode(value)
+
+ monkeypatch.setattr(http_json.base64, "b64encode", recording_b64encode)
+ client = FakeHttpClient(outcomes)
+ sink = await _setup_sink(
+ client,
+ max_attempts=3,
+ retry_initial_s=0,
+ retry_max_s=0,
+ )
+ envelope = _envelope(
+ _alert(image=EncodedImage(b"jpeg", "image/jpeg", 2, 2))
+ )
+
+ await sink.consume(envelope)
+
+ assert len(client.calls) == 3
+ first_body = client.calls[0]["json"]
+ assert all(call["json"] == first_body for call in client.calls)
+ assert len({call["body_id"] for call in client.calls}) == 1
+ assert all(
+ call["headers"] == {"Idempotency-Key": "ppe-event-123"}
+ for call in client.calls
+ )
+ assert encode_calls == 1
+
+ asyncio.run(scenario())
+
+
+def test_final_retryable_non_success_response_is_raised() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([FakeResponse(503), FakeResponse(503)])
+ sink = await _setup_sink(
+ client,
+ max_attempts=2,
+ retry_initial_s=0,
+ retry_max_s=0,
+ )
+
+ with pytest.raises(FakeHttpStatusError, match="503"):
+ await sink.consume(_envelope(_alert()))
+
+ assert len(client.calls) == 2
+
+ asyncio.run(scenario())
+
+
+@pytest.mark.parametrize(
+ "outcome",
+ [FakeResponse(400), ValueError("invalid request body")],
+ ids=("non-retryable-status", "non-retryable-exception"),
+)
+def test_non_retryable_failure_is_raised_without_retry(
+ outcome: FakeResponse | BaseException,
+) -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([outcome, FakeResponse(200)])
+ sink = await _setup_sink(
+ client,
+ max_attempts=3,
+ retry_initial_s=0,
+ retry_max_s=0,
+ )
+
+ with pytest.raises((FakeHttpStatusError, ValueError)):
+ await sink.consume(_envelope(_alert()))
+
+ assert len(client.calls) == 1
+
+ asyncio.run(scenario())
+
+
+@pytest.mark.parametrize(
+ ("outcomes", "expected_attempts", "expected_status"),
+ [
+ ([OSError("offline"), TimeoutError("timed out")], 2, "none"),
+ ([FakeResponse(503), FakeResponse(503)], 2, "503"),
+ ([FakeResponse(400)], 1, "400"),
+ ],
+ ids=(
+ "network-errors",
+ "retryable-status",
+ "non-retryable-status",
+ ),
+)
+def test_log_and_drop_warns_without_failing_the_sink(
+ outcomes: list[FakeResponse | BaseException],
+ expected_attempts: int,
+ expected_status: str,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient(outcomes)
+ sink = await _setup_sink(
+ client,
+ failure_mode="log_and_drop",
+ max_attempts=2,
+ retry_initial_s=0,
+ retry_max_s=0,
+ )
+
+ await sink.consume(_envelope(_alert()), input_port="alerts")
+
+ assert len(client.calls) == expected_attempts
+
+ with caplog.at_level(logging.WARNING, logger=http_json.__name__):
+ asyncio.run(scenario())
+
+ dropped = [
+ record.getMessage()
+ for record in caplog.records
+ if "HTTP report dropped" in record.getMessage()
+ ]
+ assert len(dropped) == 1
+ assert "failure_mode=log_and_drop" in dropped[0]
+ assert "action=drop" in dropped[0]
+ assert f"attempts={expected_attempts}" in dropped[0]
+ assert f"status={expected_status}" in dropped[0]
+ assert "trace_id=trace-fallback-456" in dropped[0]
+
+
+def test_log_and_drop_never_swallows_cancellation() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([asyncio.CancelledError()])
+ sink = await _setup_sink(
+ client,
+ failure_mode="log_and_drop",
+ max_attempts=1,
+ )
+
+ with pytest.raises(asyncio.CancelledError):
+ await sink.consume(_envelope(_alert()))
+
+ asyncio.run(scenario())
+
+
+def test_log_and_drop_continues_with_the_next_envelope() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([OSError("offline"), FakeResponse(204)])
+ sink = await _setup_sink(
+ client,
+ failure_mode="log_and_drop",
+ max_attempts=1,
+ )
+
+ await sink.consume(_envelope(_alert("dropped-event")))
+ await sink.consume(_envelope(_alert("delivered-event")))
+
+ assert len(client.calls) == 2
+ assert client.calls[0]["headers"] == {
+ "Idempotency-Key": "dropped-event"
+ }
+ assert client.calls[1]["headers"] == {
+ "Idempotency-Key": "delivered-event"
+ }
+
+ asyncio.run(scenario())
+
+
+def test_log_and_drop_does_not_swallow_an_unknown_client_error() -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([ValueError("client programming error")])
+ sink = await _setup_sink(
+ client,
+ failure_mode="log_and_drop",
+ max_attempts=3,
+ )
+
+ with pytest.raises(ValueError, match="client programming error"):
+ await sink.consume(_envelope(_alert()))
+
+ assert len(client.calls) == 1
+
+ asyncio.run(scenario())
+
+
+def test_log_and_drop_success_does_not_emit_a_dropped_warning(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ async def scenario() -> None:
+ client = FakeHttpClient([FakeResponse(204)])
+ sink = await _setup_sink(
+ client,
+ failure_mode="log_and_drop",
+ max_attempts=1,
+ )
+ await sink.consume(_envelope(_alert()))
+
+ with caplog.at_level(logging.WARNING, logger=http_json.__name__):
+ asyncio.run(scenario())
+
+ assert not any(
+ "HTTP report dropped" in record.getMessage() for record in caplog.records
+ )
+
+
+@pytest.mark.parametrize(
+ ("params", "message"),
+ [
+ ({"max_attempts": 0}, "max_attempts must be a positive integer"),
+ ({"max_attempts": True}, "max_attempts must be a positive integer"),
+ ({"retry_initial_s": -0.1}, "retry_initial_s must be a non-negative"),
+ ({"retry_max_s": False}, "retry_max_s must be a non-negative"),
+ (
+ {"retry_initial_s": 2.0, "retry_max_s": 1.0},
+ "retry_max_s must be >= retry_initial_s",
+ ),
+ ({"retry_statuses": "503"}, "retry_statuses must be a sequence"),
+ ({"retry_statuses": [99]}, "must be between 100 and 599"),
+ ({"retry_statuses": [600]}, "must be between 100 and 599"),
+ ({"retry_statuses": ["not-a-status"]}, "must be a sequence"),
+ ({"failure_mode": "warn"}, "must be 'raise' or 'log_and_drop'"),
+ ({"failure_mode": ""}, "must be 'raise' or 'log_and_drop'"),
+ ({"failure_mode": None}, "must be 'raise' or 'log_and_drop'"),
+ ({"failure_mode": True}, "must be 'raise' or 'log_and_drop'"),
+ ({"failure_mode": []}, "must be 'raise' or 'log_and_drop'"),
+ ],
+)
+def test_retry_configuration_is_validated(params: dict[str, Any], message: str) -> None:
+ with pytest.raises(ValueError, match=message):
+ PlatformHttpJsonSink("platform", {"endpoint": "platform", **params})
diff --git a/tests/unit/test_safety_gate.py b/tests/unit/test_safety_gate.py
new file mode 100644
index 0000000..64c21af
--- /dev/null
+++ b/tests/unit/test_safety_gate.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Mapping
+from time import time_ns
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.contracts import ApprovedRobotCommand, RobotCommand
+from cmvr_edge_ai.core import Envelope
+from cmvr_edge_ai.plugins.builtin import CommandSafetyGate
+
+
+def _envelope(
+ action: str,
+ *,
+ valid_until_ns: int,
+ sequence: int = 1,
+ arguments: Mapping[str, Any] | None = None,
+) -> Envelope[RobotCommand]:
+ return Envelope(
+ RobotCommand(
+ device_id="src1100",
+ action=action,
+ valid_until_ns=valid_until_ns,
+ sequence=sequence,
+ arguments={} if arguments is None else arguments,
+ ),
+ schema_name="RobotCommand",
+ )
+
+
+def test_command_safety_gate_is_fail_closed() -> None:
+ gate = CommandSafetyGate(
+ "gate",
+ {
+ "allowed_actions": ["emergency_stop"],
+ "allowed_devices": ["src1100"],
+ },
+ )
+ future = time_ns() + 5_000_000_000
+
+ command_envelope = _envelope("emergency_stop", valid_until_ns=future)
+ accepted = asyncio.run(gate.process(command_envelope))
+ expired = asyncio.run(
+ gate.process(_envelope("emergency_stop", valid_until_ns=time_ns() - 1))
+ )
+ unauthorized = asyncio.run(
+ gate.process(_envelope("set_velocity", valid_until_ns=future))
+ )
+
+ assert accepted is not None
+ assert accepted.port == "commands"
+ assert accepted.envelope.schema == "ApprovedRobotCommand/v1"
+ assert isinstance(accepted.envelope.payload, ApprovedRobotCommand)
+ assert accepted.envelope.payload.command is command_envelope.payload
+ assert accepted.envelope.payload.approved_by == "gate"
+ assert accepted.envelope.trace_id == command_envelope.trace_id
+ assert expired is None
+ assert unauthorized is None
+ assert asyncio.run(gate.health()).detail == "rejected_commands=2"
+
+
+def test_velocity_gate_requires_limits_and_rejects_replays() -> None:
+ with pytest.raises(ValueError, match="requires argument_limits"):
+ CommandSafetyGate("gate", {"allowed_actions": ["set_velocity"]})
+
+ gate = CommandSafetyGate(
+ "gate",
+ {
+ "allowed_actions": ["set_velocity"],
+ "allowed_devices": ["src1100"],
+ "argument_limits": {
+ "set_velocity": {
+ "vx": {"min": -1.0, "max": 1.0},
+ "vy": {"min": -1.0, "max": 1.0},
+ "wz": {"min": -2.0, "max": 2.0},
+ }
+ },
+ },
+ )
+ future = time_ns() + 5_000_000_000
+ accepted = _envelope(
+ "set_velocity",
+ valid_until_ns=future,
+ sequence=10,
+ arguments={"vx": 0.5, "vy": 0.0, "wz": 0.25},
+ )
+
+ approved = asyncio.run(gate.process(accepted))
+ assert approved is not None
+ assert isinstance(approved.envelope.payload, ApprovedRobotCommand)
+ assert asyncio.run(gate.process(accepted)) is None
+ outside_limits = asyncio.run(
+ gate.process(
+ _envelope(
+ "set_velocity",
+ valid_until_ns=future,
+ sequence=11,
+ arguments={"vx": 1.5, "vy": 0.0, "wz": 0.0},
+ )
+ )
+ )
+ assert outside_limits is None
+ assert asyncio.run(gate.health()).detail == "rejected_commands=2"
diff --git a/tests/unit/test_video_decoder.py b/tests/unit/test_video_decoder.py
new file mode 100644
index 0000000..17b9416
--- /dev/null
+++ b/tests/unit/test_video_decoder.py
@@ -0,0 +1,445 @@
+from __future__ import annotations
+
+import asyncio
+import threading
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass, field
+from typing import Any
+
+import pytest
+
+import cmvr_edge_ai.detection.video as video_module
+from cmvr_edge_ai.contracts import CameraIntrinsics, ImageFrame
+from cmvr_edge_ai.core import ComponentContext, Emission, Envelope
+from cmvr_edge_ai.detection.video import (
+ DecodedVideoFrame,
+ VideoDecodeError,
+ VideoDecoderOperator,
+)
+
+
+@dataclass
+class _BackendLog:
+ created_codecs: list[str] = field(default_factory=list)
+ decoded_packets: list[bytes] = field(default_factory=list)
+ worker_threads: list[int] = field(default_factory=list)
+ close_count: int = 0
+
+
+class _FakeBackend:
+ def __init__(self, codec: str, log: _BackendLog) -> None:
+ self._log = log
+ self._closed = False
+ log.created_codecs.append(codec)
+ log.worker_threads.append(threading.get_ident())
+
+ def decode(self, data: bytes) -> tuple[DecodedVideoFrame, ...]:
+ if self._closed:
+ raise RuntimeError("fake decoder is closed")
+ self._log.decoded_packets.append(data)
+ self._log.worker_threads.append(threading.get_ident())
+ if data == b"decode-error":
+ raise VideoDecodeError("synthetic packet discontinuity")
+ if data == b"buffered":
+ return ()
+ if data == b"two-frames":
+ return (
+ DecodedVideoFrame(b"first", 2, 2),
+ DecodedVideoFrame(b"second", 3, 2),
+ )
+ return (DecodedVideoFrame(b"decoded:" + data, 2, 2),)
+
+ def close(self) -> None:
+ if not self._closed:
+ self._closed = True
+ self._log.close_count += 1
+ self._log.worker_threads.append(threading.get_ident())
+
+
+def _factory(log: _BackendLog): # type: ignore[no-untyped-def]
+ return lambda codec: _FakeBackend(codec, log)
+
+
+def _image(
+ *,
+ codec: str = "H264",
+ key_frame: bool = False,
+ width: int = 640,
+ height: int = 480,
+ data: bytes = b"packet",
+ intrinsics: CameraIntrinsics | None = None,
+) -> ImageFrame:
+ return ImageFrame(
+ data=data,
+ width=width,
+ height=height,
+ pixel_format="encoded",
+ codec=codec,
+ is_key_frame=key_frame,
+ intrinsics=intrinsics,
+ )
+
+
+def _envelope(
+ image: Any,
+ *,
+ source_id: str = "camera-1",
+ session_id: str | None = "session-1",
+ sequence: int = 0,
+) -> Envelope[Any]:
+ return Envelope(
+ payload=image,
+ schema_name="ImageFrame",
+ schema_version=1,
+ source_id=source_id,
+ session_id=session_id,
+ sequence=sequence,
+ captured_at_ns=123,
+ received_at_ns=456,
+ deadline_ns=999,
+ trace_id="trace-1",
+ attributes={"transport": "test"},
+ )
+
+
+async def _setup(operator: VideoDecoderOperator, executor: Any = None) -> None:
+ metadata = {} if executor is None else {"thread_executor": executor}
+ await operator.setup(
+ ComponentContext(
+ pipeline_id="test-pipeline",
+ node_id="decoder",
+ shutdown_event=asyncio.Event(),
+ metadata=metadata,
+ )
+ )
+
+
+async def _keep_restricted_event_loop_responsive() -> None:
+ """Provide wakeups where sandboxed self-pipe notifications are unavailable."""
+
+ while True:
+ await asyncio.sleep(0.01)
+
+
+def _run_with_ticker(coroutine: Any) -> Any:
+ async def runner() -> Any:
+ loop = asyncio.get_running_loop()
+ host_executor = ThreadPoolExecutor(max_workers=2)
+ loop.set_default_executor(host_executor)
+ ticker = asyncio.create_task(_keep_restricted_event_loop_responsive())
+ try:
+ return await coroutine
+ finally:
+ ticker.cancel()
+ await asyncio.gather(ticker, return_exceptions=True)
+ host_executor.shutdown(wait=True, cancel_futures=True)
+ # Avoid a second shutdown by asyncio.run() after ownership above.
+ loop._default_executor = None # type: ignore[attr-defined]
+
+ return asyncio.run(runner())
+
+
+def test_raw_frame_is_passed_through_without_loading_a_backend() -> None:
+ factory_calls: list[str] = []
+
+ def forbidden_factory(codec: str): # type: ignore[no-untyped-def]
+ factory_calls.append(codec)
+ raise AssertionError("raw frames must not construct a decoder")
+
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=forbidden_factory)
+ envelope = _envelope(
+ ImageFrame(
+ data=b"raw-bgr",
+ width=2,
+ height=2,
+ pixel_format="BGR8",
+ codec="none",
+ )
+ )
+
+ async def exercise() -> Emission:
+ await _setup(operator)
+ result = await operator.process(envelope)
+ await operator.stop()
+ assert isinstance(result, Emission)
+ return result
+
+ emission = _run_with_ticker(exercise())
+
+ assert emission.port == "frames"
+ assert emission.envelope is envelope
+ assert factory_calls == []
+
+
+def test_waits_for_keyframe_decodes_off_loop_and_preserves_envelope_metadata() -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+ intrinsics = CameraIntrinsics(100.0, 101.0, 50.0, 51.0)
+ event_loop_thread = threading.get_ident()
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ await _setup(operator, executor)
+ dropped = await operator.process(
+ _envelope(_image(key_frame=False), sequence=10)
+ )
+ key_envelope = _envelope(
+ _image(
+ codec=".H264",
+ key_frame=True,
+ data=b"key",
+ intrinsics=intrinsics,
+ ),
+ sequence=11,
+ )
+ decoded = await operator.process(key_envelope)
+ health = await operator.health()
+ await operator.stop()
+ return dropped, key_envelope, decoded, health
+
+ dropped, original, result, health = _run_with_ticker(exercise())
+
+ assert dropped is None
+ assert isinstance(result, tuple) and len(result) == 1
+ emission = result[0]
+ assert emission.port == "frames"
+ assert isinstance(emission.envelope.payload, ImageFrame)
+ assert emission.envelope.payload == ImageFrame(
+ data=b"decoded:key",
+ width=2,
+ height=2,
+ pixel_format="BGR8",
+ codec="none",
+ is_key_frame=False,
+ intrinsics=intrinsics,
+ )
+ assert emission.envelope.source_id == original.source_id
+ assert emission.envelope.session_id == original.session_id
+ assert emission.envelope.sequence == original.sequence
+ assert emission.envelope.trace_id == original.trace_id
+ assert emission.envelope.captured_at_ns == original.captured_at_ns
+ assert emission.envelope.received_at_ns == original.received_at_ns
+ assert emission.envelope.attributes == original.attributes
+ assert log.created_codecs == ["h264"]
+ assert log.decoded_packets == [b"key"]
+ assert log.close_count == 1
+ assert log.worker_threads
+ assert all(thread_id != event_loop_thread for thread_id in log.worker_threads)
+ assert "decoded_frames=1" in health.detail
+ assert "dropped_until_keyframe=1" in health.detail
+
+
+@pytest.mark.parametrize(
+ ("change", "expected_codec"),
+ [
+ ({"source_id": "camera-2"}, "h264"),
+ ({"session_id": "session-2"}, "h264"),
+ ({"codec": "H265"}, "hevc"),
+ ({"width": 800}, "h264"),
+ ({"height": 600}, "h264"),
+ ({"sequence": 4}, "h264"),
+ ],
+)
+def test_stream_identity_change_or_sequence_gap_resets_and_waits_for_keyframe(
+ change: dict[str, Any],
+ expected_codec: str,
+) -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ await _setup(operator)
+ first = await operator.process(
+ _envelope(_image(key_frame=True, data=b"initial"), sequence=1)
+ )
+
+ source_id = str(change.get("source_id", "camera-1"))
+ session_id = change.get("session_id", "session-1")
+ sequence = int(change.get("sequence", 2))
+ codec = str(change.get("codec", "H264"))
+ width = int(change.get("width", 640))
+ height = int(change.get("height", 480))
+ changed_image = _image(
+ codec=codec,
+ key_frame=False,
+ width=width,
+ height=height,
+ data=b"must-drop",
+ )
+ dropped = await operator.process(
+ _envelope(
+ changed_image,
+ source_id=source_id,
+ session_id=session_id,
+ sequence=sequence,
+ )
+ )
+ recovered = await operator.process(
+ _envelope(
+ _image(
+ codec=codec,
+ key_frame=True,
+ width=width,
+ height=height,
+ data=b"recovered",
+ ),
+ source_id=source_id,
+ session_id=session_id,
+ sequence=sequence + 1,
+ )
+ )
+ await operator.stop()
+ return first, dropped, recovered
+
+ first, dropped, recovered = _run_with_ticker(exercise())
+
+ assert isinstance(first, tuple)
+ assert dropped is None
+ assert isinstance(recovered, tuple)
+ assert log.created_codecs == ["h264", expected_codec]
+ assert log.decoded_packets == [b"initial", b"recovered"]
+ assert log.close_count == 2
+
+
+def test_recoverable_decode_error_resets_and_suppresses_until_next_keyframe() -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ await _setup(operator)
+ failed = await operator.process(
+ _envelope(
+ _image(key_frame=True, data=b"decode-error"),
+ sequence=1,
+ )
+ )
+ suppressed = await operator.process(
+ _envelope(_image(key_frame=False, data=b"p-frame"), sequence=2)
+ )
+ recovered = await operator.process(
+ _envelope(_image(key_frame=True, data=b"new-key"), sequence=3)
+ )
+ health = await operator.health()
+ await operator.stop()
+ await operator.stop()
+ return failed, suppressed, recovered, health
+
+ failed, suppressed, recovered, health = _run_with_ticker(exercise())
+
+ assert failed is None
+ assert suppressed is None
+ assert isinstance(recovered, tuple)
+ assert log.created_codecs == ["h264", "h264"]
+ assert log.decoded_packets == [b"decode-error", b"new-key"]
+ assert log.close_count == 2
+ assert "decode_errors=1" in health.detail
+ assert "resets=1" in health.detail
+
+
+def test_backend_can_emit_zero_or_multiple_decoded_frames() -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ await _setup(operator)
+ buffered = await operator.process(
+ _envelope(_image(key_frame=True, data=b"buffered"), sequence=1)
+ )
+ multiple = await operator.process(
+ _envelope(_image(key_frame=False, data=b"two-frames"), sequence=2)
+ )
+ await operator.stop()
+ return buffered, multiple
+
+ buffered, multiple = _run_with_ticker(exercise())
+
+ assert buffered is None
+ assert isinstance(multiple, tuple) and len(multiple) == 2
+ assert [item.envelope.payload.data for item in multiple] == [b"first", b"second"]
+
+
+def test_raw_transition_closes_an_existing_encoded_decoder() -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+ raw_envelope = _envelope(
+ ImageFrame(b"raw", 1, 1, "BGR8", codec="raw"),
+ sequence=2,
+ )
+
+ async def exercise(): # type: ignore[no-untyped-def]
+ await _setup(operator)
+ await operator.process(
+ _envelope(_image(key_frame=True, data=b"key"), sequence=1)
+ )
+ raw = await operator.process(raw_envelope)
+ await operator.stop()
+ return raw
+
+ result = _run_with_ticker(exercise())
+
+ assert isinstance(result, Emission)
+ assert result.envelope is raw_envelope
+ assert log.close_count == 1
+
+
+def test_rejects_wrong_payload_unsupported_codec_and_invalid_dimensions() -> None:
+ log = _BackendLog()
+ operator = VideoDecoderOperator("decoder", {}, backend_factory=_factory(log))
+
+ async def exercise() -> None:
+ with pytest.raises(TypeError, match="expected ImageFrame"):
+ await operator.process(_envelope("not-an-image"))
+ with pytest.raises(ValueError, match="unsupported encoded video codec"):
+ await operator.process(
+ _envelope(_image(codec="vp9", key_frame=True), sequence=1)
+ )
+ with pytest.raises(ValueError, match="dimensions must be positive"):
+ await operator.process(
+ _envelope(_image(width=0, key_frame=True), sequence=1)
+ )
+
+ _run_with_ticker(exercise())
+ assert log.created_codecs == []
+
+
+def test_default_backend_imports_pyav_only_for_an_encoded_keyframe(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ imported: list[str] = []
+
+ def missing_import(name: str): # type: ignore[no-untyped-def]
+ imported.append(name)
+ raise ImportError("synthetic missing dependency")
+
+ monkeypatch.setattr(video_module, "import_module", missing_import)
+ operator = VideoDecoderOperator("decoder", {})
+ raw = _envelope(ImageFrame(b"raw", 1, 1, "BGR8", codec="none"))
+
+ async def exercise() -> None:
+ assert isinstance(await operator.process(raw), Emission)
+ assert imported == []
+ assert (
+ await operator.process(_envelope(_image(key_frame=False), sequence=1))
+ is None
+ )
+ assert imported == []
+ with pytest.raises(RuntimeError, match="optional video dependencies"):
+ await operator.process(_envelope(_image(key_frame=True), sequence=2))
+
+ _run_with_ticker(exercise())
+ assert imported == ["av"]
+
+
+def test_setup_rejects_a_non_executor_thread_executor() -> None:
+ operator = VideoDecoderOperator("decoder", {})
+
+ async def exercise() -> None:
+ with pytest.raises(TypeError, match="must be an Executor"):
+ await _setup(operator, object())
+
+ _run_with_ticker(exercise())
+
+
+def test_constructor_rejects_unknown_parameters() -> None:
+ with pytest.raises(ValueError, match="does not support parameters: typo"):
+ VideoDecoderOperator("decoder", {"typo": True})
diff --git a/tests/unit/test_yolo_model.py b/tests/unit/test_yolo_model.py
new file mode 100644
index 0000000..7c77a68
--- /dev/null
+++ b/tests/unit/test_yolo_model.py
@@ -0,0 +1,378 @@
+from __future__ import annotations
+
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any
+
+import pytest
+
+from cmvr_edge_ai.contracts import BoundingBox, Detection, ImageFrame
+from cmvr_edge_ai.detection.models import (
+ PPE_6CLASS_LABELS,
+ register_builtin_detection_models,
+)
+from cmvr_edge_ai.detection.models import yolo as yolo_module
+from cmvr_edge_ai.detection.models.yolo import (
+ UltralyticsYoloModel,
+ YoloDependencyError,
+)
+from cmvr_edge_ai.detection.registry import DetectionModelRegistry
+
+
+class _FakeArray:
+ def __init__(self, data: bytes, *, channel_reversed: bool = False) -> None:
+ self.data = data
+ self.channel_reversed = channel_reversed
+ self.shape: tuple[int, ...] | None = None
+
+ def reshape(self, shape: tuple[int, ...]) -> _FakeArray:
+ self.shape = shape
+ return self
+
+ def __getitem__(self, key: object) -> _FakeArray:
+ assert key == (slice(None), slice(None), slice(None, None, -1))
+ reversed_array = _FakeArray(self.data, channel_reversed=True)
+ reversed_array.shape = self.shape
+ return reversed_array
+
+
+class _FakeNumpy:
+ uint8 = object()
+
+ def __init__(self) -> None:
+ self.frombuffer_calls: list[tuple[bytes, object]] = []
+
+ def frombuffer(self, data: bytes, *, dtype: object) -> _FakeArray:
+ self.frombuffer_calls.append((data, dtype))
+ return _FakeArray(data)
+
+
+class _FakeTensor:
+ """Exercise the detach/cpu/tolist path used by real torch tensors."""
+
+ def __init__(self, value: list[Any]) -> None:
+ self.value = value
+ self.detached = False
+ self.on_cpu = False
+
+ def detach(self) -> _FakeTensor:
+ self.detached = True
+ return self
+
+ def cpu(self) -> _FakeTensor:
+ self.on_cpu = True
+ return self
+
+ def tolist(self) -> list[Any]:
+ return self.value
+
+
+class _FakeBoxes:
+ def __init__(
+ self,
+ xyxy: list[list[float]],
+ confidence: list[float],
+ classes: list[float],
+ ) -> None:
+ self.xyxy = _FakeTensor(xyxy)
+ self.conf = _FakeTensor(confidence)
+ self.cls = _FakeTensor(classes)
+
+
+def _weights(tmp_path: Path) -> Path:
+ path = tmp_path / "best.pt"
+ path.write_bytes(b"test-only-placeholder")
+ return path
+
+
+def _install_fake_runtime(
+ monkeypatch: pytest.MonkeyPatch,
+ *,
+ names: object,
+ results: list[object] | None = None,
+) -> tuple[_FakeNumpy, list[Any]]:
+ numpy = _FakeNumpy()
+ created: list[Any] = []
+
+ class FakeYOLO:
+ def __init__(self, weights: str, *, task: str) -> None:
+ self.weights = weights
+ self.task = task
+ self.names = names
+ self.predict_calls: list[dict[str, Any]] = []
+ created.append(self)
+
+ def predict(self, **kwargs: Any) -> list[object]:
+ self.predict_calls.append(kwargs)
+ return [] if results is None else results
+
+ ultralytics = SimpleNamespace(YOLO=FakeYOLO)
+
+ def fake_import_module(name: str) -> object:
+ if name == "numpy":
+ return numpy
+ if name == "ultralytics":
+ return ultralytics
+ raise AssertionError(f"unexpected lazy import: {name}")
+
+ monkeypatch.setattr(yolo_module, "import_module", fake_import_module)
+ return numpy, created
+
+
+def _frame(
+ *,
+ pixel_format: str = "BGR8",
+ codec: str = "none",
+ data: bytes = bytes(range(12)),
+) -> ImageFrame:
+ return ImageFrame(
+ data=data,
+ width=2,
+ height=2,
+ pixel_format=pixel_format,
+ codec=codec,
+ )
+
+
+def test_yolo_dependency_is_loaded_lazily(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ calls: list[str] = []
+ fake_numpy = _FakeNumpy()
+
+ def missing_ultralytics(name: str) -> object:
+ calls.append(name)
+ if name == "numpy":
+ return fake_numpy
+ raise ImportError("ultralytics is intentionally unavailable")
+
+ monkeypatch.setattr(yolo_module, "import_module", missing_ultralytics)
+ model = UltralyticsYoloModel(
+ {"weights": str(_weights(tmp_path))},
+ expected_labels=("No-Helmet",),
+ )
+
+ assert calls == []
+ with pytest.raises(YoloDependencyError, match=r"cmvr-edge-ai\[yolo\]"):
+ model.load()
+ assert calls == ["numpy", "ultralytics"]
+
+
+def test_yolo_load_requires_checkpoint_labels_to_match_registration_exactly(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ weights = _weights(tmp_path)
+ _, created = _install_fake_runtime(
+ monkeypatch,
+ names={0: "Vest", 1: "No-Helmet"},
+ )
+ model = UltralyticsYoloModel(
+ {"weights": str(weights)},
+ expected_labels=("No-Helmet", "Vest"),
+ )
+
+ with pytest.raises(ValueError, match="checkpoint labels do not match"):
+ model.load()
+ assert len(created) == 1
+ assert created[0].weights == str(weights)
+ assert created[0].task == "detect"
+
+ # A complete, ordered mapping is accepted; comparison is not set-based.
+ _, accepted = _install_fake_runtime(
+ monkeypatch,
+ names={0: "No-Helmet", 1: "Vest"},
+ )
+ matching = UltralyticsYoloModel(
+ {"weights": str(weights)},
+ expected_labels=("No-Helmet", "Vest"),
+ )
+ matching.load()
+ assert len(accepted) == 1
+
+
+def test_ppe_6classes_builtin_registration_preserves_checkpoint_label_order() -> None:
+ registry = DetectionModelRegistry()
+ register_builtin_detection_models(registry)
+
+ spec = registry.resolve("ppe-6classes-yolov8n@1")
+
+ assert spec.name == "PPE Detection YOLOv8n (6 Classes)"
+ assert spec.backend == "ultralytics-yolo"
+ assert spec.supported_labels == (
+ "Gloves",
+ "Vest",
+ "goggles",
+ "helmet",
+ "mask",
+ "safety_shoe",
+ )
+ assert spec.supported_labels == PPE_6CLASS_LABELS
+
+
+def test_ppe_6classes_factory_enforces_checkpoint_label_order(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ registry = DetectionModelRegistry()
+ register_builtin_detection_models(registry)
+ spec = registry.resolve("ppe-6classes-yolov8n@1")
+ weights = _weights(tmp_path)
+ model = spec.factory({"weights": str(weights)})
+ _install_fake_runtime(
+ monkeypatch,
+ names={
+ 0: "Vest",
+ 1: "Gloves",
+ 2: "goggles",
+ 3: "helmet",
+ 4: "mask",
+ 5: "safety_shoe",
+ },
+ )
+
+ assert isinstance(model, UltralyticsYoloModel)
+ with pytest.raises(ValueError, match="checkpoint labels do not match"):
+ model.load()
+
+ _, created = _install_fake_runtime(
+ monkeypatch,
+ names=dict(enumerate(PPE_6CLASS_LABELS)),
+ )
+ matching = spec.factory({"weights": str(weights)})
+ matching.load()
+
+ assert isinstance(matching, UltralyticsYoloModel)
+ assert len(created) == 1
+ assert created[0].weights == str(weights)
+ assert created[0].task == "detect"
+
+
+@pytest.mark.parametrize(
+ ("frame", "message"),
+ [
+ (_frame(codec="h264"), "decoded ImageFrame"),
+ (_frame(pixel_format="GRAY8"), "supports BGR8 or RGB8"),
+ (_frame(data=b"too-short"), r"width\*height\*3"),
+ ],
+)
+def test_yolo_rejects_encoded_unsupported_or_malformed_buffers(
+ tmp_path: Path,
+ monkeypatch: pytest.MonkeyPatch,
+ frame: ImageFrame,
+ message: str,
+) -> None:
+ _install_fake_runtime(monkeypatch, names=("No-Helmet",))
+ model = UltralyticsYoloModel(
+ {"weights": str(_weights(tmp_path))},
+ expected_labels=("No-Helmet",),
+ )
+ model.load()
+
+ with pytest.raises(ValueError, match=message):
+ model.predict(frame, ("No-Helmet",), 0.5)
+
+
+def test_yolo_converts_rgb_to_bgr_but_keeps_bgr_buffer_order(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ numpy, created = _install_fake_runtime(monkeypatch, names=("No-Helmet",))
+ model = UltralyticsYoloModel(
+ {"weights": str(_weights(tmp_path))},
+ expected_labels=("No-Helmet",),
+ )
+ model.load()
+
+ model.predict(_frame(pixel_format=" BGR8 "), ("No-Helmet",), 0.5)
+ bgr_source = created[0].predict_calls[-1]["source"]
+ assert isinstance(bgr_source, _FakeArray)
+ assert bgr_source.shape == (2, 2, 3)
+ assert bgr_source.channel_reversed is False
+
+ model.predict(_frame(pixel_format="RGB8"), ("No-Helmet",), 0.5)
+ rgb_source = created[0].predict_calls[-1]["source"]
+ assert isinstance(rgb_source, _FakeArray)
+ assert rgb_source.shape == (2, 2, 3)
+ assert rgb_source.channel_reversed is True
+ assert numpy.frombuffer_calls == [
+ (bytes(range(12)), numpy.uint8),
+ (bytes(range(12)), numpy.uint8),
+ ]
+
+
+def test_yolo_predict_forwards_options_and_normalizes_boxes(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ boxes = _FakeBoxes(
+ xyxy=[
+ [1.0, 2.0, 30.0, 40.0],
+ [5.5, 6.5, 50.5, 60.5],
+ [0.0, 0.0, 1.0, 1.0],
+ ],
+ confidence=[0.91, 0.82, 0.99],
+ classes=[1.0, 2.0, 0.0],
+ )
+ _, created = _install_fake_runtime(
+ monkeypatch,
+ names={0: "Worker", 1: "No-Helmet", 2: "No-Vest"},
+ results=[SimpleNamespace(boxes=boxes)],
+ )
+ model = UltralyticsYoloModel(
+ {
+ "weights": str(_weights(tmp_path)),
+ "device": "cuda:0",
+ "imgsz": 320,
+ "iou": 0.45,
+ "half": True,
+ "max_det": 42,
+ "agnostic_nms": True,
+ },
+ expected_labels=("Worker", "No-Helmet", "No-Vest"),
+ )
+ model.load()
+
+ detections = model.predict(
+ _frame(),
+ ("No-Helmet", "No-Vest"),
+ 0.73,
+ )
+
+ call = created[0].predict_calls[-1]
+ assert call["classes"] == [1, 2]
+ assert call["conf"] == pytest.approx(0.73)
+ assert call["iou"] == pytest.approx(0.45)
+ assert call["imgsz"] == 320
+ assert call["device"] == "cuda:0"
+ assert call["half"] is True
+ assert call["max_det"] == 42
+ assert call["agnostic_nms"] is True
+ assert call["verbose"] is False
+ assert detections == (
+ Detection(
+ label="No-Helmet",
+ confidence=0.91,
+ box=BoundingBox(1.0, 2.0, 30.0, 40.0),
+ ),
+ Detection(
+ label="No-Vest",
+ confidence=0.82,
+ box=BoundingBox(5.5, 6.5, 50.5, 60.5),
+ ),
+ )
+ assert boxes.xyxy.detached and boxes.xyxy.on_cpu
+ assert boxes.conf.detached and boxes.conf.on_cpu
+ assert boxes.cls.detached and boxes.cls.on_cpu
+
+
+def test_yolo_close_is_idempotent_and_requires_reload_before_predict(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ _install_fake_runtime(monkeypatch, names=("No-Helmet",))
+ model = UltralyticsYoloModel(
+ {"weights": str(_weights(tmp_path))},
+ expected_labels=("No-Helmet",),
+ )
+ model.load()
+ model.close()
+ model.close()
+
+ with pytest.raises(RuntimeError, match="has not been loaded"):
+ model.predict(_frame(), ("No-Helmet",), 0.5)
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..a554b26
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1845 @@
+version = 1
+revision = 3
+requires-python = ">=3.10, <3.13"
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "extra != 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+required-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+]
+conflicts = [[
+ { package = "cmvr-edge-ai", extra = "yolo" },
+ { package = "cmvr-edge-ai", extra = "yolo-cpu" },
+]]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "idna" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
+[[package]]
+name = "av"
+version = "16.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/51/2217a9249409d2e88e16e3f16f7c0def9fd3e7ffc4238b2ec211f9935bdb/av-16.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:2395748b0c34fe3a150a1721e4f3d4487b939520991b13e7b36f8926b3b12295", size = 26942590, upload-time = "2026-01-09T20:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/cd/a7070f4febc76a327c38808e01e2ff6b94531fe0b321af54ea3915165338/av-16.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:72d7ac832710a158eeb7a93242370aa024a7646516291c562ee7f14a7ea881fd", size = 21507910, upload-time = "2026-01-09T20:18:02.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/30/ec812418cd9b297f0238fe20eb0747d8a8b68d82c5f73c56fe519a274143/av-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6cbac833092e66b6b0ac4d81ab077970b8ca874951e9c3974d41d922aaa653ed", size = 38738309, upload-time = "2026-01-09T20:18:04.701Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b8/6c5795bf1f05f45c5261f8bce6154e0e5e86b158a6676650ddd77c28805e/av-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:eb990672d97c18f99c02f31c8d5750236f770ffe354b5a52c5f4d16c5e65f619", size = 40293006, upload-time = "2026-01-09T20:18:07.238Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/44/5e183bcb9333fc3372ee6e683be8b0c9b515a506894b2d32ff465430c074/av-16.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05ad70933ac3b8ef896a820ea64b33b6cca91a5fac5259cb9ba7fa010435be15", size = 40123516, upload-time = "2026-01-09T20:18:09.955Z" },
+ { url = "https://files.pythonhosted.org/packages/12/1d/b5346d582a3c3d958b4d26a2cc63ce607233582d956121eb20d2bbe55c2e/av-16.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d831a1062a3c47520bf99de6ec682bd1d64a40dfa958e5457bb613c5270e7ce3", size = 41463289, upload-time = "2026-01-09T20:18:12.459Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/31/acc946c0545f72b8d0d74584cb2a0ade9b7dfe2190af3ef9aa52a2e3c0b1/av-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:358ab910fef3c5a806c55176f2b27e5663b33c4d0a692dafeb049c6ed71f8aff", size = 31754959, upload-time = "2026-01-09T20:18:14.718Z" },
+ { url = "https://files.pythonhosted.org/packages/48/d0/b71b65d1b36520dcb8291a2307d98b7fc12329a45614a303ff92ada4d723/av-16.1.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:e88ad64ee9d2b9c4c5d891f16c22ae78e725188b8926eb88187538d9dd0b232f", size = 26927747, upload-time = "2026-01-09T20:18:16.976Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/79/720a5a6ccdee06eafa211b945b0a450e3a0b8fc3d12922f0f3c454d870d2/av-16.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cb296073fa6935724de72593800ba86ae49ed48af03960a4aee34f8a611f442b", size = 21492232, upload-time = "2026-01-09T20:18:19.266Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/4f/a1ba8d922f2f6d1a3d52419463ef26dd6c4d43ee364164a71b424b5ae204/av-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:720edd4d25aa73723c1532bb0597806d7b9af5ee34fc02358782c358cfe2f879", size = 39291737, upload-time = "2026-01-09T20:18:21.513Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/31/fc62b9fe8738d2693e18d99f040b219e26e8df894c10d065f27c6b4f07e3/av-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c7f2bc703d0df260a1fdf4de4253c7f5500ca9fc57772ea241b0cb241bcf972e", size = 40846822, upload-time = "2026-01-09T20:18:24.275Z" },
+ { url = "https://files.pythonhosted.org/packages/53/10/ab446583dbce730000e8e6beec6ec3c2753e628c7f78f334a35cad0317f4/av-16.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d69c393809babada7d54964d56099e4b30a3e1f8b5736ca5e27bd7be0e0f3c83", size = 40675604, upload-time = "2026-01-09T20:18:26.866Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d7/1003be685277005f6d63fd9e64904ee222fe1f7a0ea70af313468bb597db/av-16.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:441892be28582356d53f282873c5a951592daaf71642c7f20165e3ddcb0b4c63", size = 42015955, upload-time = "2026-01-09T20:18:29.461Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/4a/fa2a38ee9306bf4579f556f94ecbc757520652eb91294d2a99c7cf7623b9/av-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:273a3e32de64819e4a1cd96341824299fe06f70c46f2288b5dc4173944f0fd62", size = 31750339, upload-time = "2026-01-09T20:18:32.249Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" },
+ { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" },
+ { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.6.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" },
+ { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" },
+ { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" },
+ { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" },
+ { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" },
+ { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" },
+ { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" },
+ { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" },
+ { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
+ { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
+ { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
+ { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
+]
+
+[[package]]
+name = "cmvr-edge-ai"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "pydantic" },
+ { name = "pyyaml" },
+]
+
+[package.optional-dependencies]
+grpc = [
+ { name = "grpcio" },
+ { name = "protobuf" },
+]
+http = [
+ { name = "httpx" },
+]
+image = [
+ { name = "pillow" },
+]
+video = [
+ { name = "av" },
+]
+yolo = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "ultralytics" },
+]
+yolo-cpu = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torch", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torch", version = "2.7.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torchvision", version = "0.22.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torchvision", version = "0.22.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "ultralytics" },
+]
+
+[package.dev-dependencies]
+codegen = [
+ { name = "grpcio-tools" },
+]
+dev = [
+ { name = "grpcio-tools" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+test = [
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "av", marker = "extra == 'video'", specifier = ">=12,<17" },
+ { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.76,<2" },
+ { name = "httpx", marker = "extra == 'http'", specifier = ">=0.27,<1" },
+ { name = "numpy", marker = "extra == 'yolo'", specifier = ">=1.24,<3" },
+ { name = "numpy", marker = "extra == 'yolo-cpu'", specifier = ">=1.24,<3" },
+ { name = "pillow", marker = "extra == 'image'", specifier = ">=10,<13" },
+ { name = "protobuf", marker = "extra == 'grpc'", specifier = ">=6.31.1,<7" },
+ { name = "pydantic", specifier = ">=2.8,<3" },
+ { name = "pyyaml", specifier = ">=6,<7" },
+ { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'yolo-cpu'", specifier = "==2.7.0", index = "https://download.pytorch.org/whl/cpu", conflict = { package = "cmvr-edge-ai", extra = "yolo-cpu" } },
+ { name = "torch", marker = "(platform_machine != 'x86_64' and extra == 'yolo-cpu') or (sys_platform != 'linux' and extra == 'yolo-cpu')", specifier = "==2.7.0" },
+ { name = "torchvision", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'yolo-cpu'", specifier = "==0.22.0", index = "https://download.pytorch.org/whl/cpu", conflict = { package = "cmvr-edge-ai", extra = "yolo-cpu" } },
+ { name = "torchvision", marker = "(platform_machine != 'x86_64' and extra == 'yolo-cpu') or (sys_platform != 'linux' and extra == 'yolo-cpu')", specifier = "==0.22.0" },
+ { name = "ultralytics", marker = "extra == 'yolo'", specifier = "==8.4.31" },
+ { name = "ultralytics", marker = "extra == 'yolo-cpu'", specifier = "==8.4.31" },
+]
+provides-extras = ["grpc", "http", "video", "image", "yolo", "yolo-cpu"]
+
+[package.metadata.requires-dev]
+codegen = [{ name = "grpcio-tools", specifier = ">=1.76,<2" }]
+dev = [
+ { name = "grpcio-tools", specifier = ">=1.76,<2" },
+ { name = "pytest", specifier = ">=7.4,<9" },
+ { name = "pytest-asyncio", specifier = ">=0.23,<1" },
+]
+test = [
+ { name = "pytest", specifier = ">=7.4,<9" },
+ { name = "pytest-asyncio", specifier = ">=0.23,<1" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
+ { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
+ { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
+ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
+ { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra != 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.31.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/55/1e19b2b56a24a4b94624f7e819e1bb87fa6c5609dbaf621df3aa6568a761/filelock-3.31.1.tar.gz", hash = "sha256:9e0c4e88ebe90833c1beafd3a547ccbc0bf7f491cd3858c3ec7aed63efe02163", size = 196656, upload-time = "2026-07-20T03:14:32.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl", hash = "sha256:9ea33146c780161bf67cb20c7cb26b651566820d65ad8dfdd79422602a2dcfc0", size = 97189, upload-time = "2026-07-20T03:14:31.307Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.63.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" },
+ { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" },
+ { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" },
+ { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" },
+ { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" },
+ { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" },
+ { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" },
+ { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" },
+ { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.82.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/14/5d05bfd85c101cbe44a12d7c1cea9c40698e0438cddf3a70019f735b5a27/grpcio-1.82.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17", size = 6177087, upload-time = "2026-07-08T12:34:06.825Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2e/c906f8e6d0b54c0137885fff6f7b5883c6bbc381b44a0ba5ea07d7d1579b/grpcio-1.82.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31", size = 11960907, upload-time = "2026-07-08T12:34:10.583Z" },
+ { url = "https://files.pythonhosted.org/packages/de/be/ec4aa76cdf25539b9e960cbb9d5739f892ea6cde58078b5293860c1159d3/grpcio-1.82.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560", size = 6754802, upload-time = "2026-07-08T12:34:13.082Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/dd/47519c2a8fd9db47ec4493f44bd9f5b0175307e07089b1132e54b7b5b19c/grpcio-1.82.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6e8a08f7038ba7a77f71e250804e4aba84fe91d22cfc54ff43c07b7529c4728", size = 7484535, upload-time = "2026-07-08T12:34:15.164Z" },
+ { url = "https://files.pythonhosted.org/packages/63/99/659711e9689c4dd553bcd4eacff9cb9f458f34b60edf7afb3bbc1b0a58a2/grpcio-1.82.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50fd2fe83426b1b1c6cdc4d72d555223b7dddf8ce07c5bac218b13fc6d684c6f", size = 6919066, upload-time = "2026-07-08T12:34:17.367Z" },
+ { url = "https://files.pythonhosted.org/packages/29/39/f2b772356b4f593ffe439795509fcbf675b0ff98211ae8ce2a180f2e559f/grpcio-1.82.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b758540a24d5394a9c578bf9f6126389f474b106ac3d9df1d53de56cb14c9fd9", size = 7525855, upload-time = "2026-07-08T12:34:19.479Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/b28cfffb989a84d8272593498bddd2d68148cce1813ad55189c469b0f1f8/grpcio-1.82.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4ba4aac238f685743575d9d700003ac16537cce26e7c774993134f530652464", size = 8565122, upload-time = "2026-07-08T12:34:21.951Z" },
+ { url = "https://files.pythonhosted.org/packages/97/f9/54956cb0c701190cbc9d7e535c3f84acf0285c6b9ed198a902766e17c3cd/grpcio-1.82.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed6fc621d6f366c88a60f0b971d5afd21d441d9aa561ee688de5b7acdb2cf901", size = 7933872, upload-time = "2026-07-08T12:34:24.539Z" },
+ { url = "https://files.pythonhosted.org/packages/76/85/5f9cd1f965bbe4329556a212f178ae0c072b18b446cae05ed32fa8847c53/grpcio-1.82.1-cp310-cp310-win32.whl", hash = "sha256:bd2f45e46fff5b91c10997d0743a987517a7dde67c64c592835c2dcaac66f587", size = 4257373, upload-time = "2026-07-08T12:34:26.566Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b0/c4f42f7c69c53d27ed41643421b55908bcbe885b68f5a208135c72917c98/grpcio-1.82.1-cp310-cp310-win_amd64.whl", hash = "sha256:5e171d5f0d6a0af78ea7512783f170a44f80c165259d8773e3a354a7f991f2b5", size = 5006571, upload-time = "2026-07-08T12:34:28.778Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" },
+ { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" },
+ { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" },
+ { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" },
+]
+
+[[package]]
+name = "grpcio-tools"
+version = "1.81.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpcio" },
+ { name = "protobuf" },
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/e1/1fcf884902ae7255d8da224cfa638ea88a46d50f62a33d06d35c8960b029/grpcio_tools-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965", size = 2586261, upload-time = "2026-06-11T12:49:21.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/d7/1815110b2d40ec99dbb0a7e6d7eafd591cd1f1e9bf9d3858cd9cf3ffacbd/grpcio_tools-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88", size = 5817509, upload-time = "2026-06-11T12:49:25.958Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e8/af99579842b5a555312fa782f32ce0f99bd35b2b7a1243294b2755468857/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831", size = 2634112, upload-time = "2026-06-11T12:49:27.937Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/cd/235ad56ac728c49c17e9218c4daccd5831e6ec7af94236bec0cc66c71c68/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26", size = 2957950, upload-time = "2026-06-11T12:49:29.619Z" },
+ { url = "https://files.pythonhosted.org/packages/77/3e/9103e8b4610597bf89db49eb112091c91bf5d63ddef2a951e11a4be05f2b/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6", size = 2697765, upload-time = "2026-06-11T12:49:31.702Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/86/beb2a43fbb93570a2305696083f6736566301d957869f463308ec6839f95/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8", size = 3147588, upload-time = "2026-06-11T12:49:33.748Z" },
+ { url = "https://files.pythonhosted.org/packages/af/4d/b0182d9948631cd837a372b6625cf59d6e335d4aab0f425d4b7306619074/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f", size = 3708798, upload-time = "2026-06-11T12:49:35.979Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/f452a189d399051d85cf82fe2f27a070efaa52512a2c5e3ae6ef1ae99a1f/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440", size = 3366969, upload-time = "2026-06-11T12:49:37.911Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/48/0075cb4f6ae7db280f461de2dbba700b22ae62e351ae13e6e461cd6804de/grpcio_tools-1.81.1-cp310-cp310-win32.whl", hash = "sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de", size = 1008713, upload-time = "2026-06-11T12:49:39.791Z" },
+ { url = "https://files.pythonhosted.org/packages/17/bd/7692bc698259e5645b68720e77e7b176d376f6ae0c9db8b5b750a02f1958/grpcio_tools-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa", size = 1174752, upload-time = "2026-06-11T12:49:41.43Z" },
+ { url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" },
+ { url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" },
+ { url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" },
+ { url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" },
+ { url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" },
+ { url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" },
+ { url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" },
+ { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" },
+ { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" },
+ { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" },
+ { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" },
+ { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" },
+ { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" },
+ { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
+ { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
+ { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
+ { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
+ { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
+ { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
+ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
+ { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
+ { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "cycler", marker = "python_full_version < '3.11'" },
+ { name = "fonttools", marker = "python_full_version < '3.11'" },
+ { name = "kiwisolver", marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "packaging", marker = "python_full_version < '3.11'" },
+ { name = "pillow", marker = "python_full_version < '3.11'" },
+ { name = "pyparsing", marker = "python_full_version < '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" },
+ { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" },
+ { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" },
+ { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" },
+ { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" },
+ { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" },
+ { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" },
+ { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.11.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cycler", marker = "python_full_version >= '3.11'" },
+ { name = "fonttools", marker = "python_full_version >= '3.11'" },
+ { name = "kiwisolver", marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pillow", marker = "python_full_version >= '3.11'" },
+ { name = "pyparsing", marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" },
+ { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" },
+ { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" },
+ { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" },
+ { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" },
+ { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
+ { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
+ { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
+ { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
+ { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
+ { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
+ { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
+ { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
+ { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
+ { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
+ { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.5.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" },
+ { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" },
+ { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" },
+ { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" },
+ { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" },
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.6.4.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" },
+ { url = "https://files.pythonhosted.org/packages/97/0d/f1f0cadbf69d5b9ef2e4f744c9466cb0a850741d08350736dfdb4aa89569/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668", size = 390794615, upload-time = "2024-11-20T17:39:52.715Z" },
+ { url = "https://files.pythonhosted.org/packages/84/f7/985e9bdbe3e0ac9298fcc8cfa51a392862a46a0ffaccbbd56939b62a9c83/nvidia_cublas_cu12-12.6.4.1-py3-none-win_amd64.whl", hash = "sha256:9e4fa264f4d8a4eb0cdbd34beadc029f453b3bafae02401e999cf3d5a5af75f8", size = 434535301, upload-time = "2024-11-20T17:50:41.681Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.6.80"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/8b/2f6230cb715646c3a9425636e513227ce5c93c4d65823a734f4bb86d43c3/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc", size = 8236764, upload-time = "2024-11-20T17:35:41.03Z" },
+ { url = "https://files.pythonhosted.org/packages/25/0f/acb326ac8fd26e13c799e0b4f3b2751543e1834f04d62e729485872198d4/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4", size = 8236756, upload-time = "2024-10-01T16:57:45.507Z" },
+ { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/81/7796f096afaf726796b1b648f3bc80cafc61fe7f77f44a483c89e6c5ef34/nvidia_cuda_cupti_cu12-12.6.80-py3-none-win_amd64.whl", hash = "sha256:bbe6ae76e83ce5251b56e8c8e61a964f757175682bbad058b170b136266ab00a", size = 5724175, upload-time = "2024-10-01T17:09:47.955Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.6.77"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/2f/72df534873235983cc0a5371c3661bebef7c4682760c275590b972c7b0f9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13", size = 23162955, upload-time = "2024-10-01T16:59:50.922Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/46/d3a1cdda8bb113c80f43a0a6f3a853356d487b830f3483f92d49ce87fa55/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:f7007dbd914c56bd80ea31bc43e8e149da38f68158f423ba845fc3292684e45a", size = 39026742, upload-time = "2024-10-01T17:10:49.058Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.6.77"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/ea/590b2ac00d772a8abd1c387a92b46486d2679ca6622fd25c18ff76265663/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd", size = 908052, upload-time = "2024-11-20T17:35:19.905Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/3d/159023799677126e20c8fd580cca09eeb28d5c5a624adc7f793b9aa8bbfa/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e", size = 908040, upload-time = "2024-10-01T16:57:22.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/76/4c80fa138333cc975743fd0687a745fccb30d167f906f13c1c7f9a85e5ea/nvidia_cuda_runtime_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:86c58044c824bf3c173c49a2dbc7a6c8b53cb4e4dca50068be0bf64e9dab3f7f", size = 891773, upload-time = "2024-10-01T17:09:26.362Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.5.1.17"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/93/a201a12d3ec1caa8c6ac34c1c2f9eeb696b886f0c36ff23c638b46603bd0/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def", size = 570523509, upload-time = "2024-10-25T19:53:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/b2/3f60d15f037fa5419d9d7f788b100ef33ea913ae5315c87ca6d6fa606c35/nvidia_cudnn_cu12-9.5.1.17-py3-none-win_amd64.whl", hash = "sha256:d7af0f8a4f3b4b9dbb3122f2ef553b45694ed9c384d5a75bab197b8eefb79ab8", size = 565440743, upload-time = "2024-10-25T19:55:49.74Z" },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.3.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/37/c50d2b2f2c07e146776389e3080f4faf70bcc4fa6e19d65bb54ca174ebc3/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6", size = 200164144, upload-time = "2024-11-20T17:40:58.288Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/f5/188566814b7339e893f8d210d3a5332352b1409815908dad6a363dcceac1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb", size = 200164135, upload-time = "2024-10-01T17:03:24.212Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" },
+ { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/38/36fd800cec8f6e89b7c1576edaaf8076e69ec631644cdbc1b5f2e2b5a9df/nvidia_cufft_cu12-11.3.0.4-py3-none-win_amd64.whl", hash = "sha256:6048ebddfb90d09d2707efb1fd78d4e3a77cb3ae4dc60e19aab6be0ece2ae464", size = 199356881, upload-time = "2024-10-01T17:13:01.861Z" },
+]
+
+[[package]]
+name = "nvidia-cufile-cu12"
+version = "1.11.1.6"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" },
+ { url = "https://files.pythonhosted.org/packages/17/bf/cc834147263b929229ce4aadd62869f0b195e98569d4c28b23edc72b85d9/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db", size = 1066155, upload-time = "2024-11-20T17:41:49.376Z" },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.7.77"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/ac/36543605358a355632f1a6faa3e2d5dfb91eab1e4bc7d552040e0383c335/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8", size = 56289881, upload-time = "2024-10-01T17:04:18.981Z" },
+ { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/5362a9396f23f7de1dd8a64369e87c85ffff8216fc8194ace0fa45ba27a5/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7b2ed8e95595c3591d984ea3603dd66fe6ce6812b886d59049988a712ed06b6e", size = 56289882, upload-time = "2024-11-20T17:42:25.222Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/a8/0cd0cec757bd4b4b4ef150fca62ec064db7d08a291dced835a0be7d2c147/nvidia_curand_cu12-10.3.7.77-py3-none-win_amd64.whl", hash = "sha256:6d6d935ffba0f3d439b7cd968192ff068fafd9018dbf1b85b37261b13cfc9905", size = 55783873, upload-time = "2024-10-01T17:13:30.377Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.7.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cusparse-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-nvjitlink-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/17/dbe1aa865e4fdc7b6d4d0dd308fdd5aaab60f939abfc0ea1954eac4fb113/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0", size = 157833628, upload-time = "2024-10-01T17:05:05.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/07d0ba3b7f19be5a5ec32a8679fc9384cfd9fc6c869825e93be9f28d6690/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dbbe4fc38ec1289c7e5230e16248365e375c3673c9c8bac5796e2e20db07f56e", size = 157833630, upload-time = "2024-11-20T17:43:16.77Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/53/fff50a0808df7113d77e3bbc7c2b7eaed6f57d5eb80fbe93ead2aea1e09a/nvidia_cusolver_cu12-11.7.1.2-py3-none-win_amd64.whl", hash = "sha256:6813f9d8073f555444a8705f3ab0296d3e1cb37a16d694c5fc8b862a0d8706d7", size = 149287877, upload-time = "2024-10-01T17:13:49.804Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.5.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/eb/6681efd0aa7df96b4f8067b3ce7246833dd36830bb4cec8896182773db7d/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887", size = 216451147, upload-time = "2024-11-20T17:44:18.055Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/56/3af21e43014eb40134dea004e8d0f1ef19d9596a39e4d497d5a7de01669f/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1", size = 216451135, upload-time = "2024-10-01T17:06:03.826Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" },
+ { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ef/876ad8e4260e1128e6d4aac803d9d51baf3791ebdb4a9b8d9b8db032b4b0/nvidia_cusparse_cu12-12.5.4.2-py3-none-win_amd64.whl", hash = "sha256:4acb8c08855a26d737398cba8fb6f8f5045d93f82612b4cfd84645a2332ccf20", size = 213712630, upload-time = "2024-10-01T17:14:23.779Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/da/4de092c61c6dea1fc9c936e69308a02531d122e12f1f649825934ad651b5/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1", size = 156402859, upload-time = "2024-10-16T02:23:17.184Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3e/9e1e394a02a06f694be2c97bbe47288bb7c90ea84c7e9cf88f7b28afe165/nvidia_cusparselt_cu12-0.6.3-py3-none-win_amd64.whl", hash = "sha256:3b325bcbd9b754ba43df5a311488fca11a6b5dc3d11df4d190c000cf1a0765c7", size = 155595972, upload-time = "2024-10-15T22:58:35.426Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.26.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/5b/ca2f213f637305633814ae8c36b153220e40a07ea001966dcd87391f3acb/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522", size = 291671495, upload-time = "2025-03-13T00:30:07.805Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.6.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" },
+ { url = "https://files.pythonhosted.org/packages/31/db/dc71113d441f208cdfe7ae10d4983884e13f464a6252450693365e166dcf/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41", size = 19270338, upload-time = "2024-11-20T17:46:29.758Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/93c1467b1387387440a4d25102d86b7794535449b689f8e2dc22c1c8ff7f/nvidia_nvjitlink_cu12-12.6.85-py3-none-win_amd64.whl", hash = "sha256:e61120e52ed675747825cdd16febc6a0730537451d867ee58bee3853b1b13d1c", size = 161908572, upload-time = "2024-11-20T17:52:40.124Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.6.77"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/93/80f8a520375af9d7ee44571a6544653a176e53c2b8ccce85b97b83c2491b/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b", size = 90549, upload-time = "2024-11-20T17:38:17.387Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/53/36e2fd6c7068997169b49ffc8c12d5af5e5ff209df6e1a2c4d373b3a638f/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059", size = 90539, upload-time = "2024-10-01T17:00:27.179Z" },
+ { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/cd/98a447919d4ed14d407ac82b14b0a0c9c1dbfe81099934b1fc3bfd1e6316/nvidia_nvtx_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:2fb11a4af04a5e6c84073e6404d26588a34afd35379f0855a99797897efa75c0", size = 56434, upload-time = "2024-10-01T17:11:13.124Z" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "5.0.0.93"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
+ { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
+ { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
+ { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
+ { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
+ { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
+ { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
+ { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
+ { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
+ { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
+ { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
+ { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "polars"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "polars-runtime-32" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/27/99/fe77f10a13a778705ef05b499fc708c9a0b0a3680d9eb6bc6e1b6a6b9914/polars-1.42.1.tar.gz", hash = "sha256:2fe94f3059334650bd850ae19a9c165dcd5d9cb12cd95ea04de2201662e70e8a", size = 741532, upload-time = "2026-06-30T04:57:51.504Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl", hash = "sha256:3c0c65cdfa21a621650c4bdcbbccf93964d052fd766c3e70e84a55d961c259fd", size = 837622, upload-time = "2026-06-30T04:56:34.686Z" },
+]
+
+[[package]]
+name = "polars-runtime-32"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/59/15bcc4dac380c6d63efa5446d8317f22671cbd6c9dadd576bd17a334c45a/polars_runtime_32-1.42.1.tar.gz", hash = "sha256:4d4809e1c1b9a6611f6944f27b24abea902b5159e6b6fa262fd716e947af5afd", size = 3045460, upload-time = "2026-06-30T04:57:52.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/29/16ff6e4e91d71e530d3581f45e342a9cc35072ac6b31dcbc2fa33de2569e/polars_runtime_32-1.42.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bbdc26d68ee5b23b0ce227fa0599220aa35b77c826b6b0a6b2d8e7f6c1c36974", size = 53117325, upload-time = "2026-06-30T04:56:37.972Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8e/4f8296fcfd1347f1351342fecf13bf2430d7efbae2f1f45964ec7930a99e/polars_runtime_32-1.42.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f6c0288be940b607dc4a7476c01e67fb6bbee93f5f1dd42c64970274c71008ba", size = 47446251, upload-time = "2026-06-30T04:56:41.459Z" },
+ { url = "https://files.pythonhosted.org/packages/88/2e/0d66a7deadc453b890c3391034ca8ab4b05d0beaebbb92a7d65199fba61b/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635d9dbcae2302ae223afb395d5cd220bffa61a53d0ab6871d17c8bc830101cf", size = 51359402, upload-time = "2026-06-30T04:56:44.595Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/10/ffb85fa380bc9c9000dc35f40f44954dde49023018501c54faab94b3a39e/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d059e8e53cc114ff82f9bd791fd341dc53534a2c745e6f6aa37594c3a93f01fe", size = 57302723, upload-time = "2026-06-30T04:56:47.609Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/63/ca50adc62e44224ca5c622a842ba6f35ee87d1d40ef0df7ea2ed6c6edb08/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f91f0b13588324905682809d270e1de5f1990c908721c8527657d77a044c9919", size = 51515673, upload-time = "2026-06-30T04:56:50.88Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c3/08fbbf38deaa17bf34a601d327cb7451074098673c78b7c1a8538dde9794/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b8bf972d99d48aaaa2582e2bce966a6f43bc815bd8725d15f5cab9e2fb15d17", size = 55217259, upload-time = "2026-06-30T04:56:53.834Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/0e/51db89361668fe077a835fc579277f824ba526e7daf7b94d23d25439e0d0/polars_runtime_32-1.42.1-cp310-abi3-win_amd64.whl", hash = "sha256:e9364c26da389a8b7339e4d29e20a3d12af730247e6ed3b7804bddce2477f428", size = 52715432, upload-time = "2026-06-30T04:56:57.109Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c5/2fb8592d691bd114de25d9c84300b23541dca7060eac11d7b4bed0327786/polars_runtime_32-1.42.1-cp310-abi3-win_arm64.whl", hash = "sha256:7051226e6b42ffc395a7a9190377cd28649fbfb991b8f85c6271f4e1cfb736fb", size = 46718300, upload-time = "2026-06-30T04:56:59.855Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
+ { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
+ { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
+ { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+ { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+ { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+ { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+ { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.15.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" },
+ { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" },
+ { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" },
+ { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" },
+ { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" },
+ { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" },
+ { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" },
+ { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" },
+ { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
+ { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
+ { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
+ { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
+ { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.18.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" },
+ { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "83.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "filelock", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "fsspec", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "jinja2", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.11' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cufile-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-curand-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-cusparselt-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "setuptools", marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "sympy", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "typing-extensions", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/c2/3fb87940fa160d956ee94d644d37b99a24b9c05a4222bf34f94c71880e28/torch-2.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9afea41b11e1a1ab1b258a5c31afbd646d6319042bfe4f231b408034b51128b", size = 99158447, upload-time = "2025-04-23T14:35:10.557Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2c/91d1de65573fce563f5284e69d9c56b57289625cffbbb6d533d5d56c36a5/torch-2.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b9960183b6e5b71239a3e6c883d8852c304e691c0b2955f7045e8a6d05b9183", size = 865164221, upload-time = "2025-04-23T14:33:27.864Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/7e/1b1cc4e0e7cc2666cceb3d250eef47a205f0821c330392cf45eb08156ce5/torch-2.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:2ad79d0d8c2a20a37c5df6052ec67c2078a2c4e9a96dd3a8b55daaff6d28ea29", size = 212521189, upload-time = "2025-04-23T14:34:53.898Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/0b/b2b83f30b8e84a51bf4f96aa3f5f65fdf7c31c591cc519310942339977e2/torch-2.7.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:34e0168ed6de99121612d72224e59b2a58a83dae64999990eada7260c5dd582d", size = 68559462, upload-time = "2025-04-23T14:35:39.889Z" },
+ { url = "https://files.pythonhosted.org/packages/40/da/7378d16cc636697f2a94f791cb496939b60fb8580ddbbef22367db2c2274/torch-2.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b7813e904757b125faf1a9a3154e1d50381d539ced34da1992f52440567c156", size = 99159397, upload-time = "2025-04-23T14:35:35.304Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/6b/87fcddd34df9f53880fa1f0c23af7b6b96c935856473faf3914323588c40/torch-2.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd5cfbb4c3bbadd57ad1b27d56a28008f8d8753733411a140fcfb84d7f933a25", size = 865183681, upload-time = "2025-04-23T14:34:21.802Z" },
+ { url = "https://files.pythonhosted.org/packages/13/85/6c1092d4b06c3db1ed23d4106488750917156af0b24ab0a2d9951830b0e9/torch-2.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:58df8d5c2eeb81305760282b5069ea4442791a6bbf0c74d9069b7b3304ff8a37", size = 212520100, upload-time = "2025-04-23T14:35:27.473Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3f/85b56f7e2abcfa558c5fbf7b11eb02d78a4a63e6aeee2bbae3bb552abea5/torch-2.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0a8d43caa342b9986101ec5feb5bbf1d86570b5caa01e9cb426378311258fdde", size = 68569377, upload-time = "2025-04-23T14:35:20.361Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5e/ac759f4c0ab7c01feffa777bd68b43d2ac61560a9770eeac074b450f81d4/torch-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:36a6368c7ace41ad1c0f69f18056020b6a5ca47bedaca9a2f3b578f5a104c26c", size = 99013250, upload-time = "2025-04-23T14:35:15.589Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/58/2d245b6f1ef61cf11dfc4aceeaacbb40fea706ccebac3f863890c720ab73/torch-2.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15aab3e31c16feb12ae0a88dba3434a458874636f360c567caa6a91f6bfba481", size = 865042157, upload-time = "2025-04-23T14:32:56.011Z" },
+ { url = "https://files.pythonhosted.org/packages/44/80/b353c024e6b624cd9ce1d66dcb9d24e0294680f95b369f19280e241a0159/torch-2.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:f56d4b2510934e072bab3ab8987e00e60e1262fb238176168f5e0c43a1320c6d", size = 212482262, upload-time = "2025-04-23T14:35:03.527Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8d/b2939e5254be932db1a34b2bd099070c509e8887e0c5a90c498a917e4032/torch-2.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:30b7688a87239a7de83f269333651d8e582afffce6f591fff08c046f7787296e", size = 68574294, upload-time = "2025-04-23T14:34:47.098Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.7.0+cpu"
+source = { registry = "https://download.pytorch.org/whl/cpu" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+]
+dependencies = [
+ { name = "filelock", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "fsspec", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "jinja2", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "setuptools", marker = "(python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "sympy", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "typing-extensions", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2386859dee6191a2571ce15c65c3e18008d4e6f17d5256d49b4660e5464dcae8", upload-time = "2025-05-14T03:33:25Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c98c4f48f42a2237e079f3de48e8549de2c8cf68cdcf2041564c7794bbce0b59", upload-time = "2025-04-22T18:14:18Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:f874c1ba4c834db5848eaafd6e63dfce87fb44bb2d9234978c3ad47b5b0f37dd", upload-time = "2025-04-22T18:14:19Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ce510375ed79223db3ec144fe14cbcffc8a361ac57f39674397ff2d8db3b2c21", upload-time = "2025-05-14T03:33:27Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6b7edcbf8bb0b9ac2e6c001434797c5ec3f25394f91eb0ed7aeeeeed9ad4500f", upload-time = "2025-04-22T18:14:20Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:e32f385dc0b5007ca410035c3b91ef4b1b34b142e9bcdb31d3f0224b7748e992", upload-time = "2025-04-22T18:14:22Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a845b6f3bda3c40f736847dede95d8bfec81fb7e11458cd25973ba13542cf1f6", upload-time = "2025-05-14T03:33:30Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:64123c05615e27368c7a7816f6e39c6d219998693beabde0b0b9cedf91b5ed8b", upload-time = "2025-04-22T18:14:26Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:69e25c973bdd7ea24b0fa9f9792114950afaeb8f819e5723819b923f50989175", upload-time = "2025-04-22T18:14:24Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.7.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:1d7a6f33868276770a657beec7f77c7726b4da9d0739eff1b3ae64cc9a09d8e3", upload-time = "2025-07-16T16:40:01Z" },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "(python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')",
+ "python_full_version >= '3.12' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform == 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version >= '3.12' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version == '3.11.*' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+ "python_full_version < '3.11' and sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra != 'extra-12-cmvr-edge-ai-yolo-cpu'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version != '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "pillow", marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "torch", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/03/a514766f068b088180f273913e539d08e830be3ae46ef8577ea62584a27c/torchvision-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:72256f1d7ff510b16c9fb4dd488584d0693f40c792f286a9620674438a81ccca", size = 1947829, upload-time = "2025-04-23T14:42:04.652Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/e5/ec4b52041cd8c440521b75864376605756bd2d112d6351ea6a1ab25008c1/torchvision-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:810ea4af3bc63cf39e834f91f4218ff5999271caaffe2456247df905002bd6c0", size = 2512604, upload-time = "2025-04-23T14:41:56.515Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/9e/e898a377e674da47e95227f3d7be2c49550ce381eebd8c7831c1f8bb7d39/torchvision-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6fbca169c690fa2b9b8c39c0ad76d5b8992296d0d03df01e11df97ce12b4e0ac", size = 7446399, upload-time = "2025-04-23T14:41:49.793Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ec/2cdb90c6d9d61410b3df9ca67c210b60bf9b07aac31f800380b20b90386c/torchvision-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c869df2e8e00f7b1d80a34439e6d4609b50fe3141032f50b38341ec2b59404e", size = 1716700, upload-time = "2025-04-23T14:42:03.562Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/43/28bc858b022f6337326d75f4027d2073aad5432328f01ee1236d847f1b82/torchvision-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:191ea28321fc262d8aa1a7fe79c41ff2848864bf382f9f6ea45c41dde8313792", size = 1947828, upload-time = "2025-04-23T14:42:00.439Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/71/ce9a303b94e64fe25d534593522ffc76848c4e64c11e4cbe9f6b8d537210/torchvision-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6c5620e10ffe388eb6f4744962106ed7cf1508d26e6fdfa0c10522d3249aea24", size = 2514016, upload-time = "2025-04-23T14:41:48.566Z" },
+ { url = "https://files.pythonhosted.org/packages/09/42/6908bff012a1dcc4fc515e52339652d7f488e208986542765c02ea775c2f/torchvision-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ce292701c77c64dd3935e3e31c722c3b8b176a75f76dc09b804342efc1db5494", size = 7447546, upload-time = "2025-04-23T14:41:47.297Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/cf/8f9305cc0ea26badbbb3558ecae54c04a245429f03168f7fad502f8a5b25/torchvision-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:e4017b5685dbab4250df58084f07d95e677b2f3ed6c2e507a1afb8eb23b580ca", size = 1716472, upload-time = "2025-04-23T14:42:01.999Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ea/887d1d61cf4431a46280972de665f350af1898ce5006cd046326e5d0a2f2/torchvision-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31c3165418fe21c3d81fe3459e51077c2f948801b8933ed18169f54652796a0f", size = 1947826, upload-time = "2025-04-23T14:41:59.188Z" },
+ { url = "https://files.pythonhosted.org/packages/72/ef/21f8b6122e13ae045b8e49658029c695fd774cd21083b3fa5c3f9c5d3e35/torchvision-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f116bc82e0c076e70ba7776e611ed392b9666aa443662e687808b08993d26af", size = 2514571, upload-time = "2025-04-23T14:41:53.458Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/48/5f7617f6c60d135f86277c53f9d5682dfa4e66f4697f505f1530e8b69fb1/torchvision-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ce4dc334ebd508de2c534817c9388e928bc2500cf981906ae8d6e2ca3bf4727a", size = 7446522, upload-time = "2025-04-23T14:41:34.9Z" },
+ { url = "https://files.pythonhosted.org/packages/99/94/a015e93955f5d3a68689cc7c385a3cfcd2d62b84655d18b61f32fb04eb67/torchvision-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:24b8c9255c209ca419cc7174906da2791c8b557b75c23496663ec7d73b55bebf", size = 1716664, upload-time = "2025-04-23T14:41:58.019Z" },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.22.0+cpu"
+source = { registry = "https://download.pytorch.org/whl/cpu" }
+resolution-markers = [
+ "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version >= '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version != '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (python_full_version < '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "pillow", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torch", version = "2.7.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1070b55bed045e0f41afe8d6617fc1b6d47088aa3b3430d87f8fcac48f43386", upload-time = "2025-04-22T18:30:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:619f1a227797622aaffc781d694e66e682ab522fc6de75ac3551641326675cfa", upload-time = "2025-04-22T18:30:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:670082705cfb51a35ae35090b5a0e66ec09e6d9c3845e16417399adec7a17ff2", upload-time = "2025-04-22T18:30:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:b180a2c568665b8ffa442ff3d7dfdba0eaf99c0ea22e987f6c47ade3cc13eadf", upload-time = "2025-04-22T18:30:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:effb34eabe87ae0d811fe2d6e3433ed1eee2d1850ff0670964d1475a06512c73", upload-time = "2025-04-22T18:30:17Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.22.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:e74be2e4efe2253cd145a80a7c21defe2e48125a114445f06bf02640f6579109", upload-time = "2025-04-22T18:30:17Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "setuptools", marker = "(sys_platform != 'win32' and extra == 'extra-12-cmvr-edge-ai-yolo') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/04/d54d3a6d077c646624dc9461b0059e23fd5d30e0dbe67471e3654aec81f9/triton-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fad99beafc860501d7fcc1fb7045d9496cbe2c882b1674640304949165a916e7", size = 156441993, upload-time = "2025-04-09T20:27:25.107Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/c5/4874a81131cc9e934d88377fbc9d24319ae1fb540f3333b4e9c696ebc607/triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984", size = 156528461, upload-time = "2025-04-09T20:27:32.599Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/ce18470914ab6cfbec9384ee565d23c4d1c55f0548160b1c7b33000b11fd/triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3", size = 156504509, upload-time = "2025-04-09T20:27:40.413Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "ultralytics"
+version = "8.4.31"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "opencv-python" },
+ { name = "pillow" },
+ { name = "polars" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torch", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "torch", version = "2.7.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torchvision", version = "0.22.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "torchvision", version = "0.22.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "ultralytics-thop" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/89/e3ccefc5cc19fe45c963f82919238e5d40e46ed77f49a0aca50c68fccb75/ultralytics-8.4.31.tar.gz", hash = "sha256:2ebdccde73e7abbcfd2488d3d96818e9f130e8f47a808022a5afc8d9a2b28d51", size = 1028212, upload-time = "2026-03-28T20:16:49.833Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/3e/686d39b3bda7bf26899e6dd872389282ca4c5f20b7a3e67ac803e41da251/ultralytics-8.4.31-py3-none-any.whl", hash = "sha256:4cbc3bbe1fd024c2acf0b3e38b5cdf60d98515c63f1d957adf79212dd597871f", size = 1218848, upload-time = "2026-03-28T20:16:46.163Z" },
+]
+
+[[package]]
+name = "ultralytics-thop"
+version = "2.0.20"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version < '3.11' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version == '3.11.*' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo') or (python_full_version >= '3.12' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+ { name = "torch", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or extra == 'extra-12-cmvr-edge-ai-yolo'" },
+ { name = "torch", version = "2.7.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (platform_machine != 'x86_64' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu') or (sys_platform != 'linux' and extra == 'extra-12-cmvr-edge-ai-yolo' and extra == 'extra-12-cmvr-edge-ai-yolo-cpu')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/c6/d25cb53e141242f74950744179c21b8798bb09b9e7161465ecda4f577ddf/ultralytics_thop-2.0.20.tar.gz", hash = "sha256:f3595e0d8c6fd0b9f62fc2cd9be921755e2649a05c34f1fabaea0bff7295d641", size = 34682, upload-time = "2026-06-06T11:42:42.184Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/a9/5441a28af447c0ffe193f1f42dfebe2a518ab3e160e9e0d164e8901edead/ultralytics_thop-2.0.20-py3-none-any.whl", hash = "sha256:ef1b326404aeb10954f50db58982ec8dcab5946ee04a9b10e1107ec7ba4d5c22", size = 28954, upload-time = "2026-06-06T11:42:40.796Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]