From c001f6a1512affb48e9ae5c068e201bda3412679 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:59:45 +0800 Subject: [PATCH] v0.10.19 fast registry load minor fix on skill & registry stripe ros2 schema desc add create-device-skill new registry system backwards to yaml remove not exist resource new registry sys exp. support with add device add ai conventions correct raise create resource error ret info fix revert ret info fix fix prcxi check add create_resource schema re signal host ready event add websocket connection timeout and improve reconnection logic add open_timeout parameter to websocket connection add TimeoutError and InvalidStatus exception handling implement exponential backoff for reconnection attempts simplify reconnection logic flow add gzip change pose extra to any add isFlapY --- .conda/base/recipe.yaml | 4 +- .conda/environment/recipe.yaml | 2 +- .conda/full/recipe.yaml | 4 +- .cursor/skills/create-device-skill/SKILL.md | 328 ++ .../scripts/extract_device_actions.py | 200 + .../create-device-skill/scripts/gen_auth.py | 69 + .github/workflows/ci-check.yml | 2 +- .gitignore | 1 + AGENTS.md | 87 + CLAUDE.md | 4 + docs/developer_guide/add_device.md | 109 +- docs/developer_guide/add_registry.md | 65 +- docs/developer_guide/networking_overview.md | 4 +- docs/user_guide/launch.md | 5 +- recipes/msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- unilabos/__init__.py | 2 +- unilabos/app/main.py | 154 +- unilabos/app/register.py | 56 +- unilabos/app/web/api.py | 6 +- unilabos/app/web/client.py | 44 +- unilabos/app/web/server.py | 4 +- unilabos/app/ws_client.py | 179 +- unilabos/config/config.py | 3 +- unilabos/device_comms/universal_driver.py | 1 - .../devices/liquid_handling/prcxi/prcxi.py | 8 +- unilabos/devices/virtual/workbench.py | 351 +- unilabos/registry/ast_registry_scanner.py | 1037 +++++ unilabos/registry/decorators.py | 614 +++ unilabos/registry/devices/Qone_nmr.yaml | 58 +- unilabos/registry/devices/bioyond_cell.yaml | 91 +- .../devices/bioyond_dispensing_station.yaml | 217 +- unilabos/registry/devices/camera.yaml | 81 - unilabos/registry/devices/cameraSII.yaml | 8 +- .../devices/characterization_chromatic.yaml | 64 +- .../devices/characterization_optic.yaml | 15 +- unilabos/registry/devices/chinwe.yaml | 231 +- .../devices/coin_cell_workstation.yaml | 76 +- unilabos/registry/devices/gas_handler.yaml | 82 +- unilabos/registry/devices/hotel.yaml | 2 +- .../registry/devices/laiyu_liquid_test.yaml | 93 +- unilabos/registry/devices/liquid_handler.yaml | 3520 ++++++++--------- .../devices/neware_battery_test_system.yaml | 228 +- unilabos/registry/devices/opcua_example.yaml | 10 +- unilabos/registry/devices/opsky_ATR30007.yaml | 3 +- .../devices/organic_miscellaneous.yaml | 75 +- .../devices/post_process_station.yaml | 187 +- unilabos/registry/devices/pump_and_valve.yaml | 144 +- .../devices/reaction_station_bioyond.yaml | 308 +- unilabos/registry/devices/robot_agv.yaml | 15 +- unilabos/registry/devices/robot_arm.yaml | 120 +- unilabos/registry/devices/robot_gripper.yaml | 62 +- .../registry/devices/robot_linear_motion.yaml | 236 +- .../registry/devices/solid_dispenser.yaml | 130 +- unilabos/registry/devices/temperature.yaml | 130 +- unilabos/registry/devices/virtual_device.yaml | 1894 ++++----- unilabos/registry/devices/work_station.yaml | 1272 ++++-- unilabos/registry/devices/xrd_d7mate.yaml | 226 +- unilabos/registry/devices/zhida_gcms.yaml | 112 +- unilabos/registry/registry.py | 2579 ++++++++---- .../registry/resources/bioyond/YB_bottle.yaml | 7 - .../resources/bioyond/YB_bottle_carriers.yaml | 14 - .../resources/bioyond/bottle_carriers.yaml | 4 - unilabos/registry/resources/bioyond/deck.yaml | 4 - .../resources/common/resource_container.yaml | 103 - .../registry/resources/laiyu/container.yaml | 2 - unilabos/registry/resources/laiyu/deck.yaml | 1 - .../registry/resources/opentrons/deck.yaml | 2 - .../resources/opentrons/plate_adapters.yaml | 1 - .../registry/resources/opentrons/plates.yaml | 15 - .../resources/opentrons/reservoirs.yaml | 6 - .../resources/opentrons/tip_racks.yaml | 11 - .../resources/opentrons/tube_racks.yaml | 20 - .../registry/resources/organic/container.yaml | 1 - .../post_process/bottle_carriers.yaml | 2 - .../registry/resources/post_process/deck.yaml | 1 - .../resources/prcxi/plate_adapters.yaml | 9 - unilabos/registry/resources/prcxi/plates.yaml | 11 - .../registry/resources/prcxi/tip_racks.yaml | 6 - unilabos/registry/resources/prcxi/trash.yaml | 1 - .../registry/resources/prcxi/tube_racks.yaml | 1 - unilabos/registry/utils.py | 724 ++++ unilabos/resources/container.py | 4 +- unilabos/resources/graphio.py | 20 +- unilabos/resources/resource_tracker.py | 101 +- unilabos/ros/msgs/message_converter.py | 58 +- unilabos/ros/nodes/base_device_node.py | 135 +- unilabos/ros/nodes/presets/camera.py | 7 + unilabos/ros/nodes/presets/host_node.py | 250 +- unilabos/ros/nodes/presets/workstation.py | 99 +- unilabos/utils/decorator.py | 200 +- unilabos/utils/environment_check.py | 3 +- unilabos/utils/import_manager.py | 570 +-- unilabos/utils/log.py | 1 - unilabos/utils/requirements.txt | 3 +- unilabos/utils/tools.py | 37 +- unilabos/utils/type_check.py | 22 +- unilabos_msgs/package.xml | 2 +- 99 files changed, 10885 insertions(+), 7191 deletions(-) create mode 100644 .cursor/skills/create-device-skill/SKILL.md create mode 100644 .cursor/skills/create-device-skill/scripts/extract_device_actions.py create mode 100644 .cursor/skills/create-device-skill/scripts/gen_auth.py create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 unilabos/registry/ast_registry_scanner.py create mode 100644 unilabos/registry/decorators.py delete mode 100644 unilabos/registry/devices/camera.yaml create mode 100644 unilabos/registry/utils.py diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index cf908c9d..a63dda77 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos - version: 0.10.18 + version: 0.10.19 source: path: ../../unilabos @@ -54,7 +54,7 @@ requirements: - pymodbus - matplotlib - pylibftdi - - uni-lab::unilabos-env ==0.10.18 + - uni-lab::unilabos-env ==0.10.19 about: repository: https://github.com/deepmodeling/Uni-Lab-OS diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index 56ff44d4..e9fd3e24 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -2,7 +2,7 @@ package: name: unilabos-env - version: 0.10.18 + version: 0.10.19 build: noarch: generic diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index ff8b4824..ab0e0c9f 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos-full - version: 0.10.18 + version: 0.10.19 build: noarch: generic @@ -11,7 +11,7 @@ build: requirements: run: # Base unilabos package (includes unilabos-env) - - uni-lab::unilabos ==0.10.18 + - uni-lab::unilabos ==0.10.19 # Documentation tools - sphinx - sphinx_rtd_theme diff --git a/.cursor/skills/create-device-skill/SKILL.md b/.cursor/skills/create-device-skill/SKILL.md new file mode 100644 index 00000000..8f524141 --- /dev/null +++ b/.cursor/skills/create-device-skill/SKILL.md @@ -0,0 +1,328 @@ +--- +name: create-device-skill +description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device. +--- + +# 创建设备 Skill 指南 + +本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。 + +## 数据源 + +- **设备注册表**: `unilabos_data/req_device_registry_upload.json` +- **结构**: `{ "resources": [{ "id": "", "class": { "module": "", "action_value_mappings": { ... } } }] }` +- **生成时机**: `unilab` 启动并完成注册表上传后自动生成 +- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为 + +## 创建流程 + +### Step 0 — 收集必备信息(缺一不可,否则询问后终止) + +开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。 + +向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」 + +#### 必备项 ①:ak / sk(认证凭据) + +来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`。 + +获取后立即生成 AUTH token: + +```bash +python ./scripts/gen_auth.py +# 或从 config.py 提取 +python ./scripts/gen_auth.py --config +``` + +认证算法:`base64(ak:sk)` → `Authorization: Lab ` + +#### 必备项 ②:--addr(目标环境) + +决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取: + +| `--addr` 值 | BASE URL | +|-------------|----------| +| `test` | `https://uni-lab.test.bohrium.com` | +| `uat` | `https://uni-lab.uat.bohrium.com` | +| `local` | `http://127.0.0.1:48197` | +| 不传(默认) | `https://uni-lab.bohrium.com` | +| 其他自定义 URL | 直接使用该 URL | + +#### 必备项 ③:req_device_registry_upload.json(设备注册表) + +数据文件由 `unilab` 启动时自动生成,需要定位它: + +**推断 working_dir**(即 `unilabos_data` 所在目录): + +| 条件 | working_dir 取值 | +|------|------------------| +| 传了 `--working_dir` | `/unilabos_data/`(若子目录已存在则直接用) | +| 仅传了 `--config` | `/unilabos_data/` | +| 都没传 | `<当前工作目录>/unilabos_data/` | + +**按优先级搜索文件**: + +``` +<推断的 working_dir>/unilabos_data/req_device_registry_upload.json +<推断的 working_dir>/req_device_registry_upload.json +/unilabos_data/req_device_registry_upload.json +``` + +也可以直接 Glob 搜索:`**/req_device_registry_upload.json` + +找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`。 + +**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。** + +#### 必备项 ④:目标设备 + +用户需要明确要为哪个设备创建 skill。可以是设备名称(如「PRCXI 移液站」)或 device_id(如 `liquid_handler.prcxi`)。 + +如果用户不确定,运行提取脚本列出所有设备供选择: + +```bash +python ./scripts/extract_device_actions.py --registry <找到的文件路径> +``` + +#### 完整示例 + +用户提供: + +``` +--ak a1fd9d4e-xxxx-xxxx-xxxx-d9a69c09f0fd +--sk 136ff5c6-xxxx-xxxx-xxxx-a03e301f827b +--addr test +--port 8003 +--disable_browser +``` + +从中提取: +- ✅ ak/sk → 运行 `gen_auth.py` 得到 `AUTH="Authorization: Lab YTFmZDlk..."` +- ✅ addr=test → `BASE=https://uni-lab.test.bohrium.com` +- ✅ 搜索 `unilabos_data/req_device_registry_upload.json` → 找到并确认时间 +- ✅ 用户指明目标设备 → 如 `liquid_handler.prcxi` + +**四项全部就绪后才进入 Step 1。** + +### Step 1 — 列出可用设备 + +运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择: + +```bash +# 自动搜索(默认在 unilabos_data/ 和当前目录查找) +python ./scripts/extract_device_actions.py + +# 指定注册表文件路径 +python ./scripts/extract_device_actions.py --registry +``` + +脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。 + +### Step 2 — 提取 Action Schema + +用户选择设备后,运行提取脚本: + +```bash +python ./scripts/extract_device_actions.py [--registry ] ./skills//actions/ +``` + +脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。 + +每个 action 生成一个 JSON 文件,包含: +- `type` — 作为 API 调用的 `action_type` +- `schema` — 完整 JSON Schema(含 `properties.goal.properties` 参数定义) +- `goal` — goal 字段映射(含占位符 `$placeholder`) +- `goal_default` — 默认值 + +### Step 3 — 写 action-index.md + +按模板为每个 action 写条目: + +```markdown +### `` + +<用途描述(一句话)> + +- **Schema**: [`actions/.json`](actions/.json) +- **核心参数**: `param1`, `param2`(从 schema.required 获取) +- **可选参数**: `param3`, `param4` +- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头) +``` + +描述规则: +- 从 `schema.properties` 读参数列表(schema 已提升为 goal 内容) +- 从 `schema.required` 区分核心/可选参数 +- 按功能分类(移液、枪头、外设等) +- 标注 `placeholder_keys` 中的字段类型: + - `unilabos_resources` → **ResourceSlot**,填入 `{id, name, uuid}`(id 是路径格式,从资源树取物料节点) + - `unilabos_devices` → **DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device) + - `unilabos_nodes` → **NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点) + - `unilabos_class` → **ClassSlot**,填入类名字符串如 `"container"`(从注册表查找) +- array 类型字段 → `[{id, name, uuid}, ...]` +- 特殊:`create_resource` 的 `res_id`(ResourceSlot)可填不存在的路径 + +### Step 4 — 写 SKILL.md + +直接复用 `unilab-device-api` 的 API 模板(10 个 endpoint),修改: +- 设备名称 +- Action 数量 +- 目录列表 +- Session state 中的 `device_name` +- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab `(不要硬编码 `Api` 类型的 key) +- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义 +- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot(物料/设备/节点/类名) + +API 模板结构: + +```markdown +## 设备信息 +- device_id, Python 源码路径, 设备类名 + +## 前置条件(缺一不可) +- ak/sk → AUTH, --addr → BASE URL + +## Session State +- lab_uuid(通过 API #1 自动匹配,不要问用户), device_name + +## API Endpoints (10 个) +# 注意: +# - #1 获取 lab 列表 + 自动匹配 lab_uuid(遍历 is_admin 的 lab, +# 调用 /lab/info/{uuid} 比对 access_key == ak) +# - #2 创建工作流用 POST /lab/workflow +# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid} + +## Placeholder Slot 填写规则 +- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"} +- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串 +- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串 +- unilabos_class → ClassSlot → "class_name" 字符串 +- 特例:create_resource 的 res_id 允许填不存在的路径 +- 列出本设备所有 Slot 字段、类型及含义 + +## 渐进加载策略 +## 完整工作流 Checklist +``` + +### Step 5 — 验证 + +检查文件完整性: +- [ ] `SKILL.md` 包含 10 个 API endpoint +- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表 +- [ ] `action-index.md` 列出所有 action 并有描述 +- [ ] `actions/` 目录中每个 action 有对应 JSON 文件 +- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段 +- [ ] 描述能让 agent 判断该用哪个 action + +## Action JSON 文件结构 + +```json +{ + "type": "LiquidHandlerTransfer", // → API 的 action_type + "goal": { // goal 字段映射 + "sources": "sources", + "targets": "targets", + "tip_racks": "tip_racks", + "asp_vols": "asp_vols" + }, + "schema": { // ← 直接是 goal 的 schema(已提升) + "type": "object", + "properties": { // 参数定义(即请求中 goal 的字段) + "sources": { "type": "array", "items": { "type": "object" } }, + "targets": { "type": "array", "items": { "type": "object" } }, + "asp_vols": { "type": "array", "items": { "type": "number" } } + }, + "required": [...], + "_unilabos_placeholder_info": { // ← Slot 类型标记 + "sources": "unilabos_resources", + "targets": "unilabos_resources", + "tip_racks": "unilabos_resources" + } + }, + "goal_default": { ... }, // 默认值 + "placeholder_keys": { // ← 汇总所有 Slot 字段 + "sources": "unilabos_resources", // ResourceSlot + "targets": "unilabos_resources", + "tip_racks": "unilabos_resources", + "target_device_id": "unilabos_devices" // DeviceSlot + } +} +``` + +> **注意**:`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。 +> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。 + +## Placeholder Slot 类型体系 + +`placeholder_keys` / `_unilabos_placeholder_info` 中有 4 种值,对应不同的填写方式: + +| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 | +|---------------|-----------|---------|---------| +| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) | +| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点(type=device),路径字符串 | +| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 | +| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name | + +### ResourceSlot(`unilabos_resources`) + +最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等): + +```json +{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"} +``` + +- 单个(schema type=object):`{"id": "/path/name", "name": "name", "uuid": "xxx"}` +- 数组(schema type=array):`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]` +- `id` 本身是从 parent 计算的路径格式 +- 根据 action 语义选择正确的物料(如 `sources` = 液体来源,`targets` = 目标位置) + +> **特例**:`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。 + +### DeviceSlot(`unilabos_devices`) + +填写**设备路径字符串**。从资源树中筛选 type=device 的节点,从 parent 计算路径: + +``` +"/host_node" +"/bioyond_cell/reaction_station" +``` + +- 只填路径字符串,不需要 `{id, uuid}` 对象 +- 根据 action 语义选择正确的设备(如 `target_device_id` = 目标设备) + +### NodeSlot(`unilabos_nodes`) + +范围 = 设备 + 物料。即资源树中**所有节点**都可以选,填写**路径字符串**: + +``` +"/PRCXI/PRCXI_Deck" +``` + +- 使用场景:当参数既可能指向物料也可能指向设备时(如 `PumpTransferProtocol` 的 `from_vessel`/`to_vessel`,`create_resource` 的 `parent`) + +### ClassSlot(`unilabos_class`) + +填写注册表中已上报的**资源类 name**。从本地 `req_resource_registry_upload.json` 中查找: + +``` +"container" +``` + +### 通过 API #10 获取资源树 + +```bash +curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH" +``` + +注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name`、`uuid`、`type`、`parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。 + +## 最终目录结构 + +``` +.// +├── SKILL.md # API 端点 + 渐进加载指引 +├── action-index.md # 动作索引:描述/用途/核心参数 +└── actions/ # 每个 action 的完整 JSON Schema + ├── action1.json + ├── action2.json + └── ... +``` diff --git a/.cursor/skills/create-device-skill/scripts/extract_device_actions.py b/.cursor/skills/create-device-skill/scripts/extract_device_actions.py new file mode 100644 index 00000000..c17f6102 --- /dev/null +++ b/.cursor/skills/create-device-skill/scripts/extract_device_actions.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +从 req_device_registry_upload.json 中提取指定设备的 action schema。 + +用法: + # 列出所有设备及 action 数量(自动搜索注册表文件) + python extract_device_actions.py + + # 指定注册表文件路径 + python extract_device_actions.py --registry + + # 提取指定设备的 action 到目录 + python extract_device_actions.py + python extract_device_actions.py --registry + +示例: + python extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json + python extract_device_actions.py liquid_handler.prcxi .cursor/skills/unilab-device-api/actions/ +""" +import json +import os +import sys +from datetime import datetime + +REGISTRY_FILENAME = "req_device_registry_upload.json" + +def find_registry(explicit_path=None): + """ + 查找 req_device_registry_upload.json 文件。 + + 搜索优先级: + 1. 用户通过 --registry 显式指定的路径 + 2. /unilabos_data/req_device_registry_upload.json + 3. /req_device_registry_upload.json + 4. /../../.. (workspace根) 下的 unilabos_data/ + 5. 向上逐级搜索父目录(最多 5 层) + """ + if explicit_path: + if os.path.isfile(explicit_path): + return explicit_path + if os.path.isdir(explicit_path): + fp = os.path.join(explicit_path, REGISTRY_FILENAME) + if os.path.isfile(fp): + return fp + print(f"警告: 指定的路径不存在: {explicit_path}") + return None + + candidates = [ + os.path.join("unilabos_data", REGISTRY_FILENAME), + REGISTRY_FILENAME, + ] + + for c in candidates: + if os.path.isfile(c): + return c + + script_dir = os.path.dirname(os.path.abspath(__file__)) + workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", "..")) + for c in candidates: + path = os.path.join(workspace_root, c) + if os.path.isfile(path): + return path + + cwd = os.getcwd() + for _ in range(5): + parent = os.path.dirname(cwd) + if parent == cwd: + break + cwd = parent + for c in candidates: + path = os.path.join(cwd, c) + if os.path.isfile(path): + return path + + return None + +def load_registry(path): + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + +def list_devices(data): + """列出所有包含 action_value_mappings 的设备,同时返回 module 路径""" + resources = data.get('resources', []) + devices = [] + for res in resources: + rid = res.get('id', '') + cls = res.get('class', {}) + avm = cls.get('action_value_mappings', {}) + module = cls.get('module', '') + if avm: + devices.append((rid, len(avm), module)) + return devices + +def flatten_schema_to_goal(action_data): + """将 schema 中嵌套的 goal 内容提升为顶层 schema,去掉 feedback/result 包装""" + schema = action_data.get('schema', {}) + goal_schema = schema.get('properties', {}).get('goal', {}) + if goal_schema: + action_data = dict(action_data) + action_data['schema'] = goal_schema + return action_data + + +def extract_actions(data, device_id, output_dir): + """提取指定设备的 action schema 到独立 JSON 文件""" + resources = data.get('resources', []) + for res in resources: + if res.get('id') == device_id: + cls = res.get('class', {}) + module = cls.get('module', '') + avm = cls.get('action_value_mappings', {}) + if not avm: + print(f"设备 {device_id} 没有 action_value_mappings") + return [] + + if module: + py_path = module.split(":")[0].replace(".", "/") + ".py" + class_name = module.split(":")[-1] if ":" in module else "" + print(f"Python 源码: {py_path}") + if class_name: + print(f"设备类: {class_name}") + + os.makedirs(output_dir, exist_ok=True) + written = [] + for action_name in sorted(avm.keys()): + action_data = flatten_schema_to_goal(avm[action_name]) + filename = action_name.replace('-', '_') + '.json' + filepath = os.path.join(output_dir, filename) + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(action_data, f, indent=2, ensure_ascii=False) + written.append(filename) + print(f" {filepath}") + return written + + print(f"设备 {device_id} 未找到") + return [] + +def main(): + args = sys.argv[1:] + explicit_registry = None + + if "--registry" in args: + idx = args.index("--registry") + if idx + 1 < len(args): + explicit_registry = args[idx + 1] + args = args[:idx] + args[idx + 2:] + else: + print("错误: --registry 需要指定路径") + sys.exit(1) + + registry_path = find_registry(explicit_registry) + if not registry_path: + print(f"错误: 找不到 {REGISTRY_FILENAME}") + print() + print("解决方法:") + print(" 1. 先运行 unilab 启动命令,等待注册表生成") + print(" 2. 用 --registry 指定文件路径:") + print(f" python {sys.argv[0]} --registry ") + print() + print("搜索过的路径:") + for p in [ + os.path.join("unilabos_data", REGISTRY_FILENAME), + REGISTRY_FILENAME, + os.path.join("", "unilabos_data", REGISTRY_FILENAME), + ]: + print(f" - {p}") + sys.exit(1) + + print(f"注册表: {registry_path}") + mtime = os.path.getmtime(registry_path) + gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S") + size_mb = os.path.getsize(registry_path) / (1024 * 1024) + print(f"生成时间: {gen_time} (文件大小: {size_mb:.1f} MB)") + data = load_registry(registry_path) + + if len(args) == 0: + devices = list_devices(data) + print(f"\n找到 {len(devices)} 个设备:") + print(f"{'设备 ID':<50} {'Actions':>7} {'Python 模块'}") + print("-" * 120) + for did, count, module in sorted(devices, key=lambda x: x[0]): + py_path = module.split(":")[0].replace(".", "/") + ".py" if module else "" + print(f"{did:<50} {count:>7} {py_path}") + + elif len(args) == 2: + device_id = args[0] + output_dir = args[1] + print(f"\n提取 {device_id} 的 actions 到 {output_dir}/") + written = extract_actions(data, device_id, output_dir) + if written: + print(f"\n共写入 {len(written)} 个 action 文件") + + else: + print("用法:") + print(" python extract_device_actions.py [--registry ] # 列出设备") + print(" python extract_device_actions.py [--registry ] # 提取 actions") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/.cursor/skills/create-device-skill/scripts/gen_auth.py b/.cursor/skills/create-device-skill/scripts/gen_auth.py new file mode 100644 index 00000000..f0cb9c9b --- /dev/null +++ b/.cursor/skills/create-device-skill/scripts/gen_auth.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +从 ak/sk 生成 UniLab API Authorization header。 + +算法: base64(ak:sk) → "Authorization: Lab " + +用法: + python gen_auth.py + python gen_auth.py --config + +示例: + python gen_auth.py myak mysk + python gen_auth.py --config experiments/config.py +""" +import base64 +import re +import sys + + +def gen_auth(ak: str, sk: str) -> str: + token = base64.b64encode(f"{ak}:{sk}".encode("utf-8")).decode("utf-8") + return token + + +def extract_from_config(config_path: str) -> tuple: + """从 config.py 中提取 ak 和 sk""" + with open(config_path, "r", encoding="utf-8") as f: + content = f.read() + ak_match = re.search(r'''ak\s*=\s*["']([^"']+)["']''', content) + sk_match = re.search(r'''sk\s*=\s*["']([^"']+)["']''', content) + if not ak_match or not sk_match: + return None, None + return ak_match.group(1), sk_match.group(1) + + +def main(): + args = sys.argv[1:] + + if len(args) == 2 and args[0] == "--config": + ak, sk = extract_from_config(args[1]) + if not ak or not sk: + print(f"错误: 在 {args[1]} 中未找到 ak/sk 配置") + print("期望格式: ak = \"xxx\" sk = \"xxx\"") + sys.exit(1) + print(f"配置文件: {args[1]}") + elif len(args) == 2: + ak, sk = args + else: + print("用法:") + print(" python gen_auth.py ") + print(" python gen_auth.py --config ") + sys.exit(1) + + token = gen_auth(ak, sk) + print(f"ak: {ak}") + print(f"sk: {sk}") + print() + print(f"Authorization header:") + print(f" Authorization: Lab {token}") + print() + print(f"curl 用法:") + print(f' curl -H "Authorization: Lab {token}" ...') + print() + print(f"Shell 变量:") + print(f' AUTH="Authorization: Lab {token}"') + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 57245d94..402edc26 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -49,7 +49,7 @@ jobs: uv pip uninstall enum34 || echo enum34 not installed, skipping uv pip install . - - name: Run check mode (complete_registry) + - name: Run check mode (AST registry validation) run: | call conda activate check-env echo Running check mode... diff --git a/.gitignore b/.gitignore index 838331e3..12b344d6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ output/ unilabos_data/ pyrightconfig.json .cursorignore +device_package*/ ## Python # Byte-compiled / optimized / DLL files diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2f9efa06 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# AGENTS.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +Also follow the monorepo-level rules in `../AGENTS.md`. + +## Build & Development + +```bash +# Install in editable mode (requires mamba env with python 3.11) +pip install -e . +uv pip install -r unilabos/utils/requirements.txt + +# Run with a device graph +unilab --graph --config --backend ros +unilab --graph --config --backend simple # no ROS2 needed + +# Common CLI flags +unilab --app_bridges websocket fastapi # communication bridges +unilab --test_mode # simulate hardware, no real execution +unilab --check_mode # CI validation of registry imports +unilab --skip_env_check # skip auto-install of dependencies +unilab --visual rviz|web|disable # visualization mode +unilab --is_slave # run as slave node + +# Workflow upload subcommand +unilab workflow_upload -f -n --tags tag1 tag2 + +# Tests +pytest tests/ # all tests +pytest tests/resources/test_resourcetreeset.py # single test file +pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test +``` + +## Architecture + +### Startup Flow + +`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client. + +### Core Layers + +**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`. + +**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict` → `ResourceDictInstance` → `ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`). + +**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers. + +**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases). + +**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences. + +**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud. + +**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default. + +### Configuration System + +- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files +- Config files are `.py` files with matching class names (see `config/example_config.py`) +- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`) +- Device topology defined in graph files (JSON with node-link format, or GraphML) + +### Key Data Flow + +1. Graph file → `graphio.read_node_link_json()` → `(nx.Graph, ResourceTreeSet, resource_links)` +2. `ResourceTreeSet` + `Registry` → `initialize_device.initialize_device_from_dict()` → `ROS2DeviceNode` instances +3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend) +4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`) + +### Test Data + +Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`. + +## Code Conventions + +- Code comments and log messages in simplified Chinese +- Python 3.11+, type hints expected +- Pydantic models for data validation (`resource_tracker.py`) +- Singleton pattern via `@singleton` decorator (`utils/decorator.py`) +- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths +- CLI argument dashes auto-converted to underscores for consistency + +## Licensing + +- Framework code: GPL-3.0 +- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bd5ce566 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,4 @@ + +Please follow the rules defined in: + +@AGENTS.md diff --git a/docs/developer_guide/add_device.md b/docs/developer_guide/add_device.md index dc95274f..15ba4e08 100644 --- a/docs/developer_guide/add_device.md +++ b/docs/developer_guide/add_device.md @@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用, **示例:** ```python +from unilabos.registry.decorators import device, topic_config + +@device(id="mock_gripper", category=["gripper"], description="Mock Gripper") class MockGripper: def __init__(self): self._position: float = 0.0 @@ -23,19 +26,23 @@ class MockGripper: self._status = "Idle" @property + @topic_config() # 添加 @topic_config 才会定时广播 def position(self) -> float: return self._position @property + @topic_config() def velocity(self) -> float: return self._velocity @property + @topic_config() def torque(self) -> float: return self._torque - # 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 + # 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播 @property + @topic_config(period=2.0) # 可自定义发布周期 def status(self) -> str: return self._status @@ -149,7 +156,7 @@ my_device: # 设备唯一标识符 系统会自动分析您的 Python 驱动类并生成: -- `status_types`:从 `@property` 装饰的方法自动识别状态属性 +- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性 - `action_value_mappings`:从类方法自动生成动作映射 - `init_param_schema`:从 `__init__` 方法分析初始化参数 - `schema`:前端显示用的属性类型定义 @@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构: ```python from typing import Dict, Any +from unilabos.registry.decorators import device, topic_config +@device(id="my_device", category=["general"], description="My Device") class MyDevice: """设备类文档字符串 @@ -198,8 +207,9 @@ class MyDevice: # 初始化硬件连接 @property + @topic_config() # 必须添加 @topic_config 才会广播 def status(self) -> str: - """设备状态(会自动广播)""" + """设备状态(通过 @topic_config 广播)""" return self._status def my_action(self, param: float) -> Dict[str, Any]: @@ -217,34 +227,61 @@ class MyDevice: ## 状态属性 vs 动作方法 -### 状态属性(@property) +### 状态属性(@property + @topic_config) -状态属性会被自动识别并定期广播: +状态属性需要同时使用 `@property` 和 `@topic_config` 装饰器才会被识别并定期广播: ```python +from unilabos.registry.decorators import topic_config + @property +@topic_config() # 必须添加,否则不会广播 def temperature(self) -> float: """当前温度""" return self._read_temperature() @property +@topic_config(period=2.0) # 可自定义发布周期(秒) def status(self) -> str: """设备状态: idle, running, error""" return self._status @property +@topic_config(name="ready") # 可自定义发布名称 def is_ready(self) -> bool: """设备是否就绪""" return self._status == "idle" ``` +也可以使用普通方法(非 @property)配合 `@topic_config`: + +```python +@topic_config(period=10.0) +def get_sensor_data(self) -> Dict[str, float]: + """获取传感器数据(get_ 前缀会自动去除,发布名为 sensor_data)""" + return {"temp": self._temp, "humidity": self._humidity} +``` + +**`@topic_config` 参数**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `period` | float | 5.0 | 发布周期(秒) | +| `print_publish` | bool | 节点默认 | 是否打印发布日志 | +| `qos` | int | 10 | QoS 深度 | +| `name` | str | None | 自定义发布名称 | + +**发布名称优先级**:`@topic_config(name=...)` > `get_` 前缀去除 > 方法名 + **特点**: -- 使用`@property`装饰器 -- 只读,不能有参数 -- 自动添加到注册表的`status_types` +- 必须使用 `@topic_config` 装饰器 +- 支持 `@property` 和普通方法 +- 添加到注册表的 `status_types` - 定期发布到 ROS2 topic +> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。 + ### 动作方法 动作方法是设备可以执行的操作: @@ -497,6 +534,7 @@ class LiquidHandler: self._status = "idle" @property + @topic_config() def status(self) -> str: return self._status @@ -886,7 +924,52 @@ class MyDevice: ## 最佳实践 -### 1. 类型注解 +### 1. 使用 `@device` 装饰器标识设备类 + +```python +from unilabos.registry.decorators import device + +@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp") +class MyDevice: + ... +``` + +- `id`:设备唯一标识符,用于注册表匹配 +- `category`:分类列表,前端用于分组显示 +- `description`:设备描述 +- `icon`:图标文件名(可选) + +### 2. 使用 `@topic_config` 声明需要广播的状态 + +```python +from unilabos.registry.decorators import topic_config + +# ✓ @property + @topic_config → 会广播 +@property +@topic_config(period=2.0) +def temperature(self) -> float: + return self._temp + +# ✓ 普通方法 + @topic_config → 会广播(get_ 前缀自动去除) +@topic_config(period=10.0) +def get_sensor_data(self) -> Dict[str, float]: + return {"temp": self._temp} + +# ✓ 使用 name 参数自定义发布名称 +@property +@topic_config(name="ready") +def is_ready(self) -> bool: + return self._status == "idle" + +# ✗ 仅有 @property,没有 @topic_config → 不会广播 +@property +def internal_state(self) -> str: + return self._state +``` + +> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。 + +### 3. 类型注解 ```python from typing import Dict, Any, Optional, List @@ -901,7 +984,7 @@ def method( pass ``` -### 2. 文档字符串 +### 4. 文档字符串 ```python def method(self, param: float) -> Dict[str, Any]: @@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]: pass ``` -### 3. 配置验证 +### 5. 配置验证 ```python def __init__(self, config: Dict[str, Any]): @@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]): self.baudrate = config['baudrate'] ``` -### 4. 资源清理 +### 6. 资源清理 ```python def __del__(self): @@ -946,7 +1029,7 @@ def __del__(self): self.connection.close() ``` -### 5. 设计前端友好的返回值 +### 7. 设计前端友好的返回值 **记住:返回值会直接显示在 Web 界面** diff --git a/docs/developer_guide/add_registry.md b/docs/developer_guide/add_registry.md index 36caa943..38d3f893 100644 --- a/docs/developer_guide/add_registry.md +++ b/docs/developer_guide/add_registry.md @@ -422,18 +422,20 @@ placeholder_keys: ### status_types -系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分: +系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分: ```yaml status_types: - current_temperature: float # 从 get_current_temperature() 或 @property current_temperature - is_heating: bool # 从 get_is_heating() 或 @property is_heating - status: str # 从 get_status() 或 @property status + current_temperature: float # 从 @topic_config 装饰的 @property 或方法 + is_heating: bool + status: str ``` **注意事项**: -- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 +- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性 +- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播 +- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`) - 类型会自动转成相应的类型(如 `str`、`float`、`bool`) - 如果类型是 `Any`、`None` 或未知的,默认使用 `String` @@ -537,11 +539,13 @@ class AdvancedLiquidHandler: self._temperature = 25.0 @property + @topic_config() def status(self) -> str: """设备状态""" return self._status @property + @topic_config() def temperature(self) -> float: """当前温度""" return self._temperature @@ -809,21 +813,23 @@ my_temperature_controller: 你的设备类需要符合以下要求: ```python -from unilabos.common.device_base import DeviceBase +from unilabos.registry.decorators import device, topic_config -class MyDevice(DeviceBase): +@device(id="my_device", category=["temperature"], description="My Device") +class MyDevice: def __init__(self, config): """初始化,参数会自动分析到 init_param_schema.config""" - super().__init__(config) self.port = config.get('port', '/dev/ttyUSB0') - # 状态方法(会自动生成到 status_types) + # 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播) @property + @topic_config() def status(self): """返回设备状态""" return "idle" @property + @topic_config() def temperature(self): """返回当前温度""" return 25.0 @@ -1039,7 +1045,34 @@ resource.type # "resource" ### 代码规范 -1. **始终使用类型注解** +1. **使用 `@device` 装饰器标识设备类** + +```python +from unilabos.registry.decorators import device + +@device(id="my_device", category=["heating"], description="My Device") +class MyDevice: + ... +``` + +2. **使用 `@topic_config` 声明广播属性** + +```python +from unilabos.registry.decorators import topic_config + +# ✓ 需要广播的状态属性 +@property +@topic_config(period=2.0) +def temperature(self) -> float: + return self._temp + +# ✗ 仅有 @property 不会广播 +@property +def internal_counter(self) -> int: + return self._counter +``` + +3. **始终使用类型注解** ```python # ✓ 好 @@ -1051,7 +1084,7 @@ def method(self, resource, device): pass ``` -2. **提供有意义的参数名** +4. **提供有意义的参数名** ```python # ✓ 好 - 清晰的参数名 @@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot): pass ``` -3. **使用 Optional 表示可选参数** +5. **使用 Optional 表示可选参数** ```python from typing import Optional @@ -1076,7 +1109,7 @@ def method( pass ``` -4. **添加详细的文档字符串** +6. **添加详细的文档字符串** ```python def method( @@ -1096,13 +1129,13 @@ def method( pass ``` -5. **方法命名规范** +7. **方法命名规范** - - 状态方法使用 `@property` 装饰器或 `get_` 前缀 + - 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config` - 动作方法使用动词开头 - 保持命名清晰、一致 -6. **完善的错误处理** +8. **完善的错误处理** - 实现完善的错误处理 - 添加日志记录 - 提供有意义的错误信息 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index 40b308d3..19f16312 100644 --- a/docs/developer_guide/networking_overview.md +++ b/docs/developer_guide/networking_overview.md @@ -221,10 +221,10 @@ Laboratory A Laboratory B ```bash # 实验室A -unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource +unilab --ak your_ak --sk your_sk --upload_registry # 实验室B -unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource +unilab --ak your_ak --sk your_sk --upload_registry ``` --- diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 402e39aa..34caa5b9 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -22,7 +22,6 @@ options: --is_slave Run the backend as slave node (without host privileges). --slave_no_host Skip waiting for host service in slave mode --upload_registry Upload registry information when starting unilab - --use_remote_resource Use remote resources when starting unilab --config CONFIG Configuration file path, supports .py format Python config files --port PORT Port for web service information page --disable_browser Disable opening information page on startup @@ -85,7 +84,7 @@ Uni-Lab 的启动过程分为以下几个阶段: 支持两种方式: - **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式) -- **远程资源**:使用 `--use_remote_resource` 从云端获取 +- **远程资源**:不指定本地文件即可 ### 7. 注册表构建 @@ -196,7 +195,7 @@ unilab --config path/to/your/config.py unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry # 使用远程资源启动 -unilab --ak your_ak --sk your_sk --use_remote_resource +unilab --ak your_ak --sk your_sk # 更新注册表 unilab --ak your_ak --sk your_sk --complete_registry diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index a3c2d2bd..fc8a5ccf 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.10.18 + version: 0.10.19 source: path: ../../unilabos_msgs target_directory: src diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index aeb76a0c..91e07b24 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.18" + version: "0.10.19" source: path: ../.. diff --git a/setup.py b/setup.py index dc7bbc73..7ca06f2e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.10.18', + version='0.10.19', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index 63e3face..eebdd757 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.10.18" +__version__ = "0.10.19" diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 93751262..fa7bc35d 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -4,6 +4,7 @@ import os import platform import shutil import signal +import subprocess import sys import threading import time @@ -25,6 +26,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig _restart_requested: bool = False _restart_reason: str = "" +RESTART_EXIT_CODE = 42 + + +def _build_child_argv(): + """Build sys.argv for child process, stripping supervisor-only arguments.""" + result = [] + skip_next = False + for arg in sys.argv: + if skip_next: + skip_next = False + continue + if arg in ("--restart_mode", "--restart-mode"): + continue + if arg in ("--auto_restart_count", "--auto-restart-count"): + skip_next = True + continue + if arg.startswith("--auto_restart_count=") or arg.startswith("--auto-restart-count="): + continue + result.append(arg) + return result + + +def _run_as_supervisor(max_restarts: int): + """ + Supervisor process that spawns and monitors child processes. + + Similar to Uvicorn's --reload: the supervisor itself does no heavy work, + it only launches the real process as a child and restarts it when the child + exits with RESTART_EXIT_CODE. + """ + child_argv = [sys.executable] + _build_child_argv() + restart_count = 0 + + print_status( + f"[Supervisor] Restart mode enabled (max restarts: {max_restarts}), " + f"child command: {' '.join(child_argv)}", + "info", + ) + + while True: + print_status( + f"[Supervisor] Launching process (restart {restart_count}/{max_restarts})...", + "info", + ) + + try: + process = subprocess.Popen(child_argv) + exit_code = process.wait() + except KeyboardInterrupt: + print_status("[Supervisor] Interrupted, terminating child process...", "info") + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + sys.exit(1) + + if exit_code == RESTART_EXIT_CODE: + restart_count += 1 + if restart_count > max_restarts: + print_status( + f"[Supervisor] Maximum restart count ({max_restarts}) reached, exiting", + "warning", + ) + sys.exit(1) + print_status( + f"[Supervisor] Child requested restart ({restart_count}/{max_restarts}), restarting in 2s...", + "info", + ) + time.sleep(2) + else: + if exit_code != 0: + print_status(f"[Supervisor] Child exited with code {exit_code}", "warning") + else: + print_status("[Supervisor] Child exited normally", "info") + sys.exit(exit_code) + def load_config_from_file(config_path): if config_path is None: @@ -66,6 +145,13 @@ def parse_args(): action="append", help="Path to the registry directory", ) + parser.add_argument( + "--devices", + type=str, + default=None, + action="append", + help="Path to Python code directory for AST-based device/resource scanning", + ) parser.add_argument( "--working_dir", type=str, @@ -155,18 +241,18 @@ def parse_args(): action="store_true", help="Skip environment dependency check on startup", ) - parser.add_argument( - "--complete_registry", - action="store_true", - default=False, - help="Complete registry information", - ) parser.add_argument( "--check_mode", action="store_true", default=False, help="Run in check mode for CI: validates registry imports and ensures no file changes", ) + parser.add_argument( + "--complete_registry", + action="store_true", + default=False, + help="Complete and rewrite YAML registry files using AST analysis results", + ) parser.add_argument( "--no_update_feedback", action="store_true", @@ -178,6 +264,24 @@ def parse_args(): default=False, help="Test mode: all actions simulate execution and return mock results without running real hardware", ) + parser.add_argument( + "--extra_resource", + action="store_true", + default=False, + help="Load extra lab_ prefixed labware resources (529 auto-generated definitions from lab_resources.py)", + ) + parser.add_argument( + "--restart_mode", + action="store_true", + default=False, + help="Enable supervisor mode: automatically restart the process when triggered via WebSocket", + ) + parser.add_argument( + "--auto_restart_count", + type=int, + default=500, + help="Maximum number of automatic restarts in restart mode (default: 500)", + ) # workflow upload subcommand workflow_parser = subparsers.add_parser( "workflow_upload", @@ -228,6 +332,11 @@ def main(): args = parser.parse_args() args_dict = vars(args) + # Supervisor mode: spawn child processes and monitor for restart + if args_dict.get("restart_mode", False): + _run_as_supervisor(args_dict.get("auto_restart_count", 5)) + return + # 环境检查 - 检查并自动安装必需的包 (可选) skip_env_check = args_dict.get("skip_env_check", False) check_mode = args_dict.get("check_mode", False) @@ -358,6 +467,9 @@ def main(): BasicConfig.test_mode = args_dict.get("test_mode", False) if BasicConfig.test_mode: print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning") + BasicConfig.extra_resource = args_dict.get("extra_resource", False) + if BasicConfig.extra_resource: + print_status("启用额外资源加载:将加载lab_开头的labware资源定义", "info") BasicConfig.communication_protocol = "websocket" machine_name = platform.node() machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) @@ -382,22 +494,32 @@ def main(): # 显示启动横幅 print_unilab_banner(args_dict) - # 注册表 - check_mode 时强制启用 complete_registry + # Step 0: AST 分析优先 + YAML 注册表加载 + # check_mode 和 upload_registry 都会执行实际 import 验证 + devices_dirs = args_dict.get("devices", None) complete_registry = args_dict.get("complete_registry", False) or check_mode - lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry) + lab_registry = build_registry( + registry_paths=args_dict["registry_path"], + devices_dirs=devices_dirs, + upload_registry=BasicConfig.upload_registry, + check_mode=check_mode, + complete_registry=complete_registry, + ) - # Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行 + # Check mode: 注册表验证完成后直接退出 if check_mode: - print_status("Check mode: complete_registry 完成,退出", "info") + device_count = len(lab_registry.device_type_registry) + resource_count = len(lab_registry.resource_type_registry) + print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info") os._exit(0) + # Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data if BasicConfig.upload_registry: - # 设备注册到服务端 - 需要 ak 和 sk if BasicConfig.ak and BasicConfig.sk: - print_status("开始注册设备到服务端...", "info") + # print_status("开始注册设备到服务端...", "info") try: register_devices_and_resources(lab_registry) - print_status("设备注册完成", "info") + # print_status("设备注册完成", "info") except Exception as e: print_status(f"设备注册失败: {e}", "error") else: @@ -482,7 +604,7 @@ def main(): continue # 如果从远端获取了物料信息,则与本地物料进行同步 - if request_startup_json and "nodes" in request_startup_json: + if file_path is not None and request_startup_json and "nodes" in request_startup_json: print_status("开始同步远端物料到本地...", "info") remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"]) resource_tree_set.merge_remote_resources(remote_tree_set) @@ -579,6 +701,10 @@ def main(): open_browser=not args_dict["disable_browser"], port=BasicConfig.port, ) + if restart_requested: + print_status("[Main] Restart requested, cleaning up...", "info") + cleanup_for_restart() + os._exit(RESTART_EXIT_CODE) if __name__ == "__main__": diff --git a/unilabos/app/register.py b/unilabos/app/register.py index 5918b43a..5940364e 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -1,9 +1,8 @@ -import json import time -from typing import Optional, Tuple, Dict, Any +from typing import Any, Dict, Optional, Tuple from unilabos.utils.log import logger -from unilabos.utils.type_check import TypeEncoder +from unilabos.utils.tools import normalize_json as _normalize_device def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]: @@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[ 注册设备和资源到服务器(仅支持HTTP) """ - # 注册资源信息 - 使用HTTP方式 from unilabos.app.web.client import http_client logger.info("[UniLab Register] 开始注册设备和资源...") - # 注册设备信息 devices_to_register = {} for device_info in lab_registry.obtain_registry_device_info(): - devices_to_register[device_info["id"]] = json.loads( - json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder) - ) - logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}") + devices_to_register[device_info["id"]] = _normalize_device(device_info) + logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}") resources_to_register = {} for resource_info in lab_registry.obtain_registry_resource_info(): resources_to_register[resource_info["id"]] = resource_info - logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}") + logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}") if gather_only: return devices_to_register, resources_to_register - # 注册设备 + if devices_to_register: try: start_time = time.time() - response = http_client.resource_registry({"resources": list(devices_to_register.values())}) + response = http_client.resource_registry( + {"resources": list(devices_to_register.values())}, + tag="device_registry", + ) cost_time = time.time() - start_time - if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s") + res_data = response.json() if response.status_code == 200 else {} + skipped = res_data.get("data", {}).get("skipped", False) + if skipped: + logger.info( + f"[UniLab Register] 设备注册跳过(内容未变化)" + f" {len(devices_to_register)} 个 {cost_time:.3f}s" + ) + elif response.status_code in [200, 201]: + logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s") else: - logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}s") + logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s") except Exception as e: logger.error(f"[UniLab Register] 设备注册异常: {e}") - # 注册资源 if resources_to_register: try: start_time = time.time() - response = http_client.resource_registry({"resources": list(resources_to_register.values())}) + response = http_client.resource_registry( + {"resources": list(resources_to_register.values())}, + tag="resource_registry", + ) cost_time = time.time() - start_time - if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s") + res_data = response.json() if response.status_code == 200 else {} + skipped = res_data.get("data", {}).get("skipped", False) + if skipped: + logger.info( + f"[UniLab Register] 资源注册跳过(内容未变化)" + f" {len(resources_to_register)} 个 {cost_time:.3f}s" + ) + elif response.status_code in [200, 201]: + logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s") else: - logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}s") + logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s") except Exception as e: logger.error(f"[UniLab Register] 资源注册异常: {e}") - - logger.info("[UniLab Register] 设备和资源注册完成.") diff --git a/unilabos/app/web/api.py b/unilabos/app/web/api.py index 0f6077c8..99981f77 100644 --- a/unilabos/app/web/api.py +++ b/unilabos/app/web/api.py @@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict): "result": {}, "schema": lab_registry._generate_unilab_json_command_schema(v["args"], k), "goal_default": {i["name"]: i["default"] for i in v["args"]}, - "handles": [], + "handles": {}, } # 不生成已配置action的动作 for k, v in enhanced_info["action_methods"].items() @@ -1340,5 +1340,5 @@ def setup_api_routes(app): # 启动广播任务 @app.on_event("startup") async def startup_event(): - asyncio.create_task(broadcast_device_status()) - asyncio.create_task(broadcast_status_page_data()) + asyncio.create_task(broadcast_device_status(), name="web-api-startup-device") + asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status") diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b43b0f44..b1cc67eb 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -3,11 +3,13 @@ HTTP客户端模块 提供与远程服务器通信的客户端功能,只有host需要用 """ - +import gzip import json import os from typing import List, Dict, Any, Optional +from unilabos.utils.tools import fast_dumps as _fast_dumps, fast_dumps_pretty as _fast_dumps_pretty + import requests from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.utils.log import info @@ -280,22 +282,54 @@ class HTTPClient: ) return response - def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response: + def resource_registry( + self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry", + ) -> requests.Response: """ - 注册资源到服务器 + 注册资源到服务器,同步保存请求/响应到 unilabos_data Args: registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}] + tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry") Returns: Response: API响应对象 """ + # 序列化一次,同时用于保存和发送 + json_bytes = _fast_dumps(registry_data) + + # 保存请求数据到 unilabos_data + req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json") + try: + os.makedirs(BasicConfig.working_dir, exist_ok=True) + with open(req_path, "wb") as f: + f.write(_fast_dumps_pretty(registry_data)) + logger.trace(f"注册表请求数据已保存: {req_path}") + except Exception as e: + logger.warning(f"保存注册表请求数据失败: {e}") + + compressed_body = gzip.compress(json_bytes) + headers = { + "Authorization": f"Lab {self.auth}", + "Content-Type": "application/json", + "Content-Encoding": "gzip", + } response = requests.post( f"{self.remote_addr}/lab/resource", - json=registry_data, - headers={"Authorization": f"Lab {self.auth}"}, + data=compressed_body, + headers=headers, timeout=30, ) + + # 保存响应数据到 unilabos_data + res_path = os.path.join(BasicConfig.working_dir, f"res_{tag}_upload.json") + try: + with open(res_path, "w", encoding="utf-8") as f: + f.write(f"{response.status_code}\n{response.text}") + logger.trace(f"注册表响应数据已保存: {res_path}") + except Exception as e: + logger.warning(f"保存注册表响应数据失败: {e}") + if response.status_code not in [200, 201]: logger.error(f"注册资源失败: {response.status_code}, {response.text}") if response.status_code == 200: diff --git a/unilabos/app/web/server.py b/unilabos/app/web/server.py index 8d090162..981edeca 100644 --- a/unilabos/app/web/server.py +++ b/unilabos/app/web/server.py @@ -86,7 +86,7 @@ def setup_server() -> FastAPI: # 设置页面路由 try: setup_web_pages(pages) - info("[Web] 已加载Web UI模块") + # info("[Web] 已加载Web UI模块") except ImportError as e: info(f"[Web] 未找到Web页面模块: {str(e)}") except Exception as e: @@ -138,7 +138,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server") server_thread.start() - info("[Web] Server started, monitoring for restart requests...") + # info("[Web] Server started, monitoring for restart requests...") # 监控重启标志 import unilabos.app.main as main_module diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 2a7f9b15..cbbb58ef 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -26,6 +26,7 @@ from enum import Enum from typing_extensions import TypedDict from unilabos.app.model import JobAddReq +from unilabos.resources.resource_tracker import ResourceDictType from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.utils.type_check import serialize_result_info from unilabos.app.communication import BaseCommunicationClient @@ -408,6 +409,7 @@ class MessageProcessor: # 线程控制 self.is_running = False self.thread = None + self._loop = None # asyncio event loop引用,用于外部关闭websocket self.reconnect_count = 0 logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}") @@ -434,22 +436,31 @@ class MessageProcessor: def stop(self) -> None: """停止消息处理线程""" self.is_running = False + # 主动关闭websocket以快速中断消息接收循环 + ws = self.websocket + loop = self._loop + if ws and loop and loop.is_running(): + try: + asyncio.run_coroutine_threadsafe(ws.close(), loop) + except Exception: + pass if self.thread and self.thread.is_alive(): self.thread.join(timeout=2) logger.info("[MessageProcessor] Stopped") def _run(self): """运行消息处理主循环""" - loop = asyncio.new_event_loop() + self._loop = asyncio.new_event_loop() try: - asyncio.set_event_loop(loop) - loop.run_until_complete(self._connection_handler()) + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._connection_handler()) except Exception as e: logger.error(f"[MessageProcessor] Thread error: {str(e)}") logger.error(traceback.format_exc()) finally: - if loop: - loop.close() + if self._loop: + self._loop.close() + self._loop = None async def _connection_handler(self): """处理WebSocket连接和重连逻辑""" @@ -466,8 +477,10 @@ class MessageProcessor: async with websockets.connect( self.websocket_url, ssl=ssl_context, + open_timeout=20, ping_interval=WSConfig.ping_interval, ping_timeout=10, + close_timeout=5, additional_headers={ "Authorization": f"Lab {BasicConfig.auth_secret()}", "EdgeSession": f"{self.session_id}", @@ -478,85 +491,98 @@ class MessageProcessor: self.connected = True self.reconnect_count = 0 - logger.info(f"[MessageProcessor] Connected to {self.websocket_url}") + logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}") # 启动发送协程 - send_task = asyncio.create_task(self._send_handler()) + send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task") + + # 每次连接(含重连)后重新向服务端注册, + # 否则服务端不知道客户端已上线,不会推送消息。 + if self.websocket_client: + self.websocket_client.publish_host_ready() try: # 接收消息循环 await self._message_handler() finally: + # 必须在 async with __aexit__ 之前停止 send_task, + # 否则 send_task 会在关闭握手期间继续发送数据, + # 干扰 websockets 库的内部清理,导致 task 泄漏。 + self.connected = False send_task.cancel() try: await send_task except asyncio.CancelledError: pass - self.connected = False except websockets.exceptions.ConnectionClosed: - logger.warning("[MessageProcessor] Connection closed") - self.connected = False + logger.warning("[MessageProcessor] 与服务端连接中断") + except TimeoutError: + logger.warning( + f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况" + ) + except websockets.exceptions.InvalidStatus as e: + logger.warning( + f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出" + ) except Exception as e: - logger.error(f"[MessageProcessor] Connection error: {str(e)}") logger.error(traceback.format_exc()) - self.connected = False + logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}") finally: + self.connected = False self.websocket = None # 重连逻辑 - if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts: + if not self.is_running: + break + if self.reconnect_count < WSConfig.max_reconnect_attempts: self.reconnect_count += 1 + backoff = WSConfig.reconnect_interval logger.info( - f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s " - f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})" + f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})" ) - await asyncio.sleep(WSConfig.reconnect_interval) - elif self.reconnect_count >= WSConfig.max_reconnect_attempts: + await asyncio.sleep(backoff) + else: logger.error("[MessageProcessor] Max reconnection attempts reached") break - else: - self.reconnect_count -= 1 async def _message_handler(self): - """处理接收到的消息""" + """处理接收到的消息。 + + ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler, + 以便 async with websockets.connect() 的 __aexit__ 能感知连接已断, + 正确清理内部 task,避免 task 泄漏。 + """ if not self.websocket: logger.error("[MessageProcessor] WebSocket connection is None") return - try: - async for message in self.websocket: - try: - data = json.loads(message) - message_type = data.get("action", "") - message_data = data.get("data") - if self.session_id and self.session_id == data.get("edge_session"): - await self._process_message(message_type, message_data) + async for message in self.websocket: + try: + data = json.loads(message) + message_type = data.get("action", "") + message_data = data.get("data") + if self.session_id and self.session_id == data.get("edge_session"): + await self._process_message(message_type, message_data) + else: + if message_type.endswith("_material"): + logger.trace( + f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}" + ) + logger.debug( + f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}" + ) else: - if message_type.endswith("_material"): - logger.trace( - f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}" - ) - logger.debug( - f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}" - ) - else: - await self._process_message(message_type, message_data) - except json.JSONDecodeError: - logger.error(f"[MessageProcessor] Invalid JSON received: {message}") - except Exception as e: - logger.error(f"[MessageProcessor] Error processing message: {str(e)}") - logger.error(traceback.format_exc()) - - except websockets.exceptions.ConnectionClosed: - logger.info("[MessageProcessor] Message handler stopped - connection closed") - except Exception as e: - logger.error(f"[MessageProcessor] Message handler error: {str(e)}") - logger.error(traceback.format_exc()) + await self._process_message(message_type, message_data) + except json.JSONDecodeError: + logger.error(f"[MessageProcessor] Invalid JSON received: {message}") + except Exception as e: + logger.error(f"[MessageProcessor] Error processing message: {str(e)}") + logger.error(traceback.format_exc()) async def _send_handler(self): """处理发送队列中的消息""" - logger.debug("[MessageProcessor] Send handler started") + logger.trace("[MessageProcessor] Send handler started") try: while self.connected and self.websocket: @@ -601,6 +627,7 @@ class MessageProcessor: except asyncio.CancelledError: logger.debug("[MessageProcessor] Send handler cancelled") + raise except Exception as e: logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}") logger.error(traceback.format_exc()) @@ -632,6 +659,10 @@ class MessageProcessor: # elif message_type == "session_id": # self.session_id = message_data.get("session_id") # logger.info(f"[MessageProcessor] Session ID: {self.session_id}") + elif message_type == "add_device": + await self._handle_device_manage(message_data, "add") + elif message_type == "remove_device": + await self._handle_device_manage(message_data, "remove") elif message_type == "request_restart": await self._handle_request_restart(message_data) else: @@ -968,6 +999,37 @@ class MessageProcessor: ) thread.start() + async def _handle_device_manage(self, device_list: list[ResourceDictType], action: str): + """Handle add_device / remove_device from LabGo server.""" + if not device_list: + return + + for item in device_list: + target_node_id = item.get("target_node_id", "host_node") + + def _notify(target_id: str, act: str, cfg: ResourceDictType): + try: + host_node = HostNode.get_instance(timeout=5) + if not host_node: + logger.error(f"[DeviceManage] HostNode not available for {act}_device") + return + success = host_node.notify_device_manage(target_id, act, cfg) + if success: + logger.info(f"[DeviceManage] {act}_device completed on {target_id}") + else: + logger.warning(f"[DeviceManage] {act}_device failed on {target_id}") + except Exception as e: + logger.error(f"[DeviceManage] Error in {act}_device: {e}") + logger.error(traceback.format_exc()) + + thread = threading.Thread( + target=_notify, + args=(target_node_id, action, item), + daemon=True, + name=f"DeviceManage-{action}-{item.get('id', '')}", + ) + thread.start() + async def _handle_request_restart(self, data: Dict[str, Any]): """ 处理重启请求 @@ -979,10 +1041,9 @@ class MessageProcessor: logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") # 发送确认消息 - if self.websocket_client: - await self.websocket_client.send_message( - {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} - ) + self.send_message( + {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} + ) # 设置全局重启标志 import unilabos.app.main as main_module @@ -1084,13 +1145,14 @@ class QueueProcessor: def stop(self) -> None: """停止队列处理线程""" self.is_running = False + self.queue_update_event.set() # 立即唤醒等待中的线程 if self.thread and self.thread.is_alive(): self.thread.join(timeout=2) logger.info("[QueueProcessor] Stopped") def _run(self): """运行队列处理主循环""" - logger.debug("[QueueProcessor] Queue processor started") + logger.trace("[QueueProcessor] Queue processor started") while self.is_running: try: @@ -1305,7 +1367,6 @@ class WebSocketClient(BaseCommunicationClient): else: url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule" - logger.debug(f"[WebSocketClient] URL: {url}") return url def start(self) -> None: @@ -1318,13 +1379,11 @@ class WebSocketClient(BaseCommunicationClient): logger.error("[WebSocketClient] WebSocket URL not configured") return - logger.info(f"[WebSocketClient] Starting connection to {self.websocket_url}") - # 启动两个核心线程 self.message_processor.start() self.queue_processor.start() - logger.info("[WebSocketClient] All threads started") + logger.trace("[WebSocketClient] All threads started") def stop(self) -> None: """停止WebSocket客户端""" @@ -1340,8 +1399,8 @@ class WebSocketClient(BaseCommunicationClient): message = {"action": "normal_exit", "data": {"session_id": session_id}} self.message_processor.send_message(message) logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}") - # 给一点时间让消息发送出去 - time.sleep(1) + # send_handler 每100ms检查一次队列,等300ms足以让消息发出 + time.sleep(0.3) except Exception as e: logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}") diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 4b7d91a4..b80d3b60 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -24,6 +24,7 @@ class BasicConfig: port = 8002 # 本地HTTP服务 check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果 + extra_resource = False # 是否加载lab_开头的额外资源 # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG" @@ -40,7 +41,7 @@ class BasicConfig: class WSConfig: reconnect_interval = 5 # 重连间隔(秒) max_reconnect_attempts = 999 # 最大重连次数 - ping_interval = 30 # ping间隔(秒) + ping_interval = 20 # ping间隔(秒) # HTTP配置 diff --git a/unilabos/device_comms/universal_driver.py b/unilabos/device_comms/universal_driver.py index 281e0cd9..0ff41805 100644 --- a/unilabos/device_comms/universal_driver.py +++ b/unilabos/device_comms/universal_driver.py @@ -1,4 +1,3 @@ - from abc import abstractmethod from functools import wraps import inspect diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index f34583fe..47b213ad 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -634,7 +634,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): def __init__( self, - deck: Deck, + deck: PRCXI9300Deck, host: str, port: int, timeout: float, @@ -648,11 +648,11 @@ class PRCXI9300Handler(LiquidHandlerAbstract): is_9320=False, ): tablets_info = [] - count = 0 - for child in deck.children: + for site_id in range(len(deck.sites)): + child = deck._get_site_resource(site_id) # 如果放其他类型的物料,是不可以的 if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state: - number = int(child.name.replace("T", "")) + number = site_id + 1 tablets_info.append( WorkTablets( Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"] diff --git a/unilabos/devices/virtual/workbench.py b/unilabos/devices/virtual/workbench.py index f5fae47e..d67db398 100644 --- a/unilabos/devices/virtual/workbench.py +++ b/unilabos/devices/virtual/workbench.py @@ -1,15 +1,15 @@ """ Virtual Workbench Device - 模拟工作台设备 -包含: +包含: - 1个机械臂 (每次操作3s, 独占锁) - 3个加热台 (每次加热10s, 可并行) -工作流程: -1. A1-A5 物料同时启动,竞争机械臂 +工作流程: +1. A1-A5 物料同时启动, 竞争机械臂 2. 机械臂将物料移动到空闲加热台 -3. 加热完成后,机械臂将物料移动到C1-C5 +3. 加热完成后, 机械臂将物料移动到C1-C5 -注意:调用来自线程池,使用 threading.Lock 进行同步 +注意: 调用来自线程池, 使用 threading.Lock 进行同步 """ import logging @@ -21,9 +21,11 @@ from threading import Lock, RLock from typing_extensions import TypedDict +from unilabos.registry.decorators import ( + device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action +) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode -from unilabos.utils.decorator import not_action, always_free -from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES +from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample # ============ TypedDict 返回类型定义 ============ @@ -57,6 +59,8 @@ class MoveToOutputResult(TypedDict): success: bool station_id: int material_id: str + output_position: str + message: str unilabos_samples: List[LabSample] @@ -81,9 +85,9 @@ class HeatingStationState(Enum): """加热台状态枚举""" IDLE = "idle" # 空闲 - OCCUPIED = "occupied" # 已放置物料,等待加热 + OCCUPIED = "occupied" # 已放置物料, 等待加热 HEATING = "heating" # 加热中 - COMPLETED = "completed" # 加热完成,等待取走 + COMPLETED = "completed" # 加热完成, 等待取走 class ArmState(Enum): @@ -105,19 +109,24 @@ class HeatingStation: heating_progress: float = 0.0 +@device( + id="virtual_workbench", + category=["virtual_device"], + description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing", +) class VirtualWorkbench: """ Virtual Workbench Device - 虚拟工作台设备 模拟一个包含1个机械臂和3个加热台的工作站 - - 机械臂操作耗时3秒,同一时间只能执行一个操作 - - 加热台加热耗时10秒,3个加热台可并行工作 + - 机械臂操作耗时3秒, 同一时间只能执行一个操作 + - 加热台加热耗时10秒, 3个加热台可并行工作 工作流: - 1. 物料A1-A5并发启动(线程池),竞争机械臂使用权 - 2. 获取机械臂后,查找空闲加热台 - 3. 机械臂将物料放入加热台,开始加热 - 4. 加热完成后,机械臂将物料移动到目标位置Cn + 1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权 + 2. 获取机械臂后, 查找空闲加热台 + 3. 机械臂将物料放入加热台, 开始加热 + 4. 加热完成后, 机械臂将物料移动到目标位置Cn """ _ros_node: BaseROS2DeviceNode @@ -145,19 +154,19 @@ class VirtualWorkbench: self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME)) self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS)) - # 机械臂状态和锁 (使用threading.Lock) + # 机械臂状态和锁 self._arm_lock = Lock() self._arm_state = ArmState.IDLE self._arm_current_task: Optional[str] = None - # 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize() + # 加热台状态 self._heating_stations: Dict[int, HeatingStation] = { i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1) } - self._stations_lock = RLock() # 可重入锁,保护加热台状态 + self._stations_lock = RLock() # 任务追踪 - self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info + self._active_tasks: Dict[str, Dict[str, Any]] = {} self._tasks_lock = Lock() # 处理其他kwargs参数 @@ -183,7 +192,6 @@ class VirtualWorkbench: """初始化虚拟工作台""" self.logger.info(f"初始化虚拟工作台 {self.device_id}") - # 重置加热台状态 (已在__init__中创建,这里重置为初始状态) with self._stations_lock: for station in self._heating_stations.values(): station.state = HeatingStationState.IDLE @@ -191,7 +199,6 @@ class VirtualWorkbench: station.material_number = None station.heating_progress = 0.0 - # 初始化状态 self.data.update( { "status": "Ready", @@ -257,11 +264,7 @@ class VirtualWorkbench: self.data["message"] = message def _find_available_heating_station(self) -> Optional[int]: - """查找空闲的加热台 - - Returns: - 空闲加热台ID,如果没有则返回None - """ + """查找空闲的加热台""" with self._stations_lock: for station_id, station in self._heating_stations.items(): if station.state == HeatingStationState.IDLE: @@ -269,23 +272,12 @@ class VirtualWorkbench: return None def _acquire_arm(self, task_description: str) -> bool: - """获取机械臂使用权(阻塞直到获取) - - Args: - task_description: 任务描述,用于日志 - - Returns: - 是否成功获取 - """ + """获取机械臂使用权(阻塞直到获取)""" self.logger.info(f"[{task_description}] 等待获取机械臂...") - - # 阻塞等待获取锁 self._arm_lock.acquire() - self._arm_state = ArmState.BUSY self._arm_current_task = task_description self._update_data_status(f"机械臂执行: {task_description}") - self.logger.info(f"[{task_description}] 成功获取机械臂使用权") return True @@ -298,6 +290,22 @@ class VirtualWorkbench: self._update_data_status(f"机械臂已释放 (完成: {task})") self.logger.info(f"机械臂已释放 (完成: {task})") + @action( + auto_prefix=True, + description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用", + handles=[ + ActionOutputHandle(key="channel_1", data_type="workbench_material", + label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="channel_2", data_type="workbench_material", + label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="channel_3", data_type="workbench_material", + label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="channel_4", data_type="workbench_material", + label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="channel_5", data_type="workbench_material", + label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR), + ], + ) def prepare_materials( self, sample_uuids: SampleUUIDsType, @@ -306,19 +314,14 @@ class VirtualWorkbench: """ 批量准备物料 - 虚拟起始节点 - 作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。 - 输出5个handle (material_1 ~ material_5),分别对应实验1~5。 - - Args: - count: 待生成的物料数量,默认5 (生成 A1-A5) - - Returns: - PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station + 作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。 + 输出5个handle (material_1 ~ material_5), 分别对应实验1~5。 """ - # 生成物料列表 A1 - A{count} materials = [i for i in range(1, count + 1)] - self.logger.info(f"[准备物料] 生成 {count} 个物料: " f"A1-A{count} -> material_1~material_{count}") + self.logger.info( + f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}" + ) return { "success": True, @@ -329,9 +332,28 @@ class VirtualWorkbench: "material_4": materials[3] if len(materials) > 3 else 0, "material_5": materials[4] if len(materials) > 4 else 0, "message": f"已准备 {count} 个物料: A1-A{count}", - "unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()] + "unilabos_samples": [ + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}), + ) + for sample_uuid, content in sample_uuids.items() + ], } + @action( + auto_prefix=True, + description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID", + handles=[ + ActionInputHandle(key="material_input", data_type="workbench_material", + label="物料编号", data_key="material_number", data_source=DataSource.HANDLE), + ActionOutputHandle(key="heating_station_output", data_type="workbench_station", + label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="material_number_output", data_type="workbench_material", + label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR), + ], + ) def move_to_heating_station( self, sample_uuids: SampleUUIDsType, @@ -340,20 +362,12 @@ class VirtualWorkbench: """ 将物料从An位置移动到加热台 - 多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台 - - Args: - material_number: 物料编号 (1-5) - - Returns: - MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点 + 多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台 """ - # 根据物料编号生成物料ID material_id = f"A{material_number}" task_desc = f"移动{material_id}到加热台" self.logger.info(f"[任务] {task_desc} - 开始执行") - # 记录任务 with self._tasks_lock: self._active_tasks[material_id] = { "status": "waiting_for_arm", @@ -361,33 +375,27 @@ class VirtualWorkbench: } try: - # 步骤1: 等待获取机械臂使用权(竞争) with self._tasks_lock: self._active_tasks[material_id]["status"] = "waiting_for_arm" self._acquire_arm(task_desc) - # 步骤2: 查找空闲加热台 with self._tasks_lock: self._active_tasks[material_id]["status"] = "finding_station" station_id = None - # 循环等待直到找到空闲加热台 while station_id is None: station_id = self._find_available_heating_station() if station_id is None: - self.logger.info(f"[{material_id}] 没有空闲加热台,等待中...") - # 释放机械臂,等待后重试 + self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...") self._release_arm() time.sleep(0.5) self._acquire_arm(task_desc) - # 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.OCCUPIED self._heating_stations[station_id].current_material = material_id self._heating_stations[station_id].material_number = material_number - # 步骤4: 模拟机械臂移动操作 (3秒) with self._tasks_lock: self._active_tasks[material_id]["status"] = "arm_moving" self._active_tasks[material_id]["assigned_station"] = station_id @@ -395,11 +403,11 @@ class VirtualWorkbench: time.sleep(self.ARM_OPERATION_TIME) - # 步骤5: 放入加热台完成 self._update_data_status(f"{material_id}已放入加热台{station_id}") - self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)") + self.logger.info( + f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)" + ) - # 释放机械臂 self._release_arm() with self._tasks_lock: @@ -412,8 +420,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"{material_id}已成功移动到加热台{station_id}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } except Exception as e: @@ -427,11 +443,33 @@ class VirtualWorkbench: "material_number": material_number, "message": f"移动失败: {str(e)}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } - @always_free + @action( + auto_prefix=True, + always_free=True, + description="启动指定加热台的加热程序", + handles=[ + ActionInputHandle(key="station_id_input", data_type="workbench_station", + label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE), + ActionInputHandle(key="material_number_input", data_type="workbench_material", + label="物料编号", data_key="material_number", data_source=DataSource.HANDLE), + ActionOutputHandle(key="heating_done_station", data_type="workbench_station", + label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR), + ActionOutputHandle(key="heating_done_material", data_type="workbench_material", + label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR), + ], + ) def start_heating( self, sample_uuids: SampleUUIDsType, @@ -440,13 +478,6 @@ class VirtualWorkbench: ) -> StartHeatingResult: """ 启动指定加热台的加热程序 - - Args: - station_id: 加热台ID (1-3),从 move_to_heating_station 的 handle 传入 - material_number: 物料编号,从 move_to_heating_station 的 handle 传入 - - Returns: - StartHeatingResult: 包含 station_id, material_number 等用于传递给下一个节点 """ self.logger.info(f"[加热台{station_id}] 开始加热") @@ -458,8 +489,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"无效的加热台ID: {station_id}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } with self._stations_lock: @@ -473,8 +512,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}上没有物料", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } if station.state == HeatingStationState.HEATING: @@ -485,13 +532,20 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}已经在加热中", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } material_id = station.current_material - # 开始加热 station.state = HeatingStationState.HEATING station.heating_start_time = time.time() station.heating_progress = 0.0 @@ -502,7 +556,6 @@ class VirtualWorkbench: self._update_data_status(f"加热台{station_id}开始加热{material_id}") - # 打印当前所有正在加热的台位 with self._stations_lock: heating_list = [ f"加热台{sid}:{s.current_material}" @@ -511,7 +564,6 @@ class VirtualWorkbench: ] self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}") - # 模拟加热过程 start_time = time.time() last_countdown_log = start_time while True: @@ -524,7 +576,6 @@ class VirtualWorkbench: self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%") - # 每5秒打印一次倒计时 if time.time() - last_countdown_log >= 5.0: self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s") last_countdown_log = time.time() @@ -534,7 +585,6 @@ class VirtualWorkbench: time.sleep(1.0) - # 加热完成 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.COMPLETED self._heating_stations[station_id].heating_progress = 100.0 @@ -553,10 +603,28 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}加热完成", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } + @action( + auto_prefix=True, + description="将物料从加热台移动到输出位置Cn", + handles=[ + ActionInputHandle(key="output_station_input", data_type="workbench_station", + label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE), + ActionInputHandle(key="output_material_input", data_type="workbench_material", + label="物料编号", data_key="material_number", data_source=DataSource.HANDLE), + ], + ) def move_to_output( self, sample_uuids: SampleUUIDsType, @@ -565,15 +633,8 @@ class VirtualWorkbench: ) -> MoveToOutputResult: """ 将物料从加热台移动到输出位置Cn - - Args: - station_id: 加热台ID (1-3),从 start_heating 的 handle 传入 - material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn - - Returns: - MoveToOutputResult: 包含执行结果 """ - output_number = material_number # 物料编号决定输出位置 + output_number = material_number if station_id not in self._heating_stations: return { @@ -583,8 +644,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"无效的加热台ID: {station_id}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } with self._stations_lock: @@ -599,8 +668,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"加热台{station_id}上没有物料", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } if station.state != HeatingStationState.COMPLETED: @@ -611,8 +688,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } output_position = f"C{output_number}" @@ -624,18 +709,17 @@ class VirtualWorkbench: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "waiting_for_arm_output" - # 获取机械臂 self._acquire_arm(task_desc) with self._tasks_lock: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "arm_moving_to_output" - # 模拟机械臂操作 (3秒) - self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...") + self.logger.info( + f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..." + ) time.sleep(self.ARM_OPERATION_TIME) - # 清空加热台 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.IDLE self._heating_stations[station_id].current_material = None @@ -643,17 +727,17 @@ class VirtualWorkbench: self._heating_stations[station_id].heating_progress = 0.0 self._heating_stations[station_id].heating_start_time = None - # 释放机械臂 self._release_arm() - # 任务完成 with self._tasks_lock: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "completed" self._active_tasks[material_id]["end_time"] = time.time() self._update_data_status(f"{material_id}已移动到{output_position}") - self.logger.info(f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)") + self.logger.info( + f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)" + ) return { "success": True, @@ -662,8 +746,17 @@ class VirtualWorkbench: "output_position": output_position, "message": f"{material_id}已成功移动到{output_position}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) + else (content.serialize() if content is not None else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } except Exception as e: @@ -677,83 +770,105 @@ class VirtualWorkbench: "output_position": output_position, "message": f"移动失败: {str(e)}", "unilabos_samples": [ - LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for - sample_uuid, content in sample_uuids.items()] + LabSample( + sample_uuid=sample_uuid, + oss_path="", + extra=( + {"material_uuid": content} + if isinstance(content, str) else (content.serialize() if content else {}) + ), + ) + for sample_uuid, content in sample_uuids.items() + ], } # ============ 状态属性 ============ @property + @topic_config() def status(self) -> str: return self.data.get("status", "Unknown") @property + @topic_config() def arm_state(self) -> str: return self._arm_state.value @property + @topic_config() def arm_current_task(self) -> str: return self._arm_current_task or "" @property + @topic_config() def heating_station_1_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(1) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_1_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(1) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_1_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(1) return station.heating_progress if station else 0.0 @property + @topic_config() def heating_station_2_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(2) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_2_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(2) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_2_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(2) return station.heating_progress if station else 0.0 @property + @topic_config() def heating_station_3_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(3) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_3_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(3) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_3_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(3) return station.heating_progress if station else 0.0 @property + @topic_config() def active_tasks_count(self) -> int: with self._tasks_lock: return len(self._active_tasks) @property + @topic_config() def message(self) -> str: return self.data.get("message", "") diff --git a/unilabos/registry/ast_registry_scanner.py b/unilabos/registry/ast_registry_scanner.py new file mode 100644 index 00000000..86c3602e --- /dev/null +++ b/unilabos/registry/ast_registry_scanner.py @@ -0,0 +1,1037 @@ +""" +AST-based Registry Scanner + +Statically parse Python files to extract @device, @action, @topic_config, @resource +decorator metadata without importing any modules. This is ~100x faster than importlib +since it only reads and parses text files. + +Includes a file-level cache: each file's MD5 hash, size and mtime are tracked so +unchanged files skip AST parsing entirely. The cache is persisted as JSON in the +working directory (``unilabos_data/ast_scan_cache.json``). + +Usage: + from unilabos.registry.ast_registry_scanner import scan_directory + + # Scan all device and resource files under a package directory + result = scan_directory("unilabos", python_path="/project") + # => {"devices": {device_id: {...}, ...}, "resources": {resource_id: {...}, ...}} +""" + +import ast +import hashlib +import json +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +MAX_SCAN_DEPTH = 10 # 最大目录递归深度 +MAX_SCAN_FILES = 1000 # 最大扫描文件数量 +_CACHE_VERSION = 1 # 缓存格式版本号,格式变更时递增 + +# 合法的装饰器来源模块 +_REGISTRY_DECORATOR_MODULE = "unilabos.registry.decorators" + + +# --------------------------------------------------------------------------- +# File-level cache helpers +# --------------------------------------------------------------------------- + + +def _file_fingerprint(filepath: Path) -> Dict[str, Any]: + """Return size, mtime and MD5 hash for *filepath*.""" + stat = filepath.stat() + md5 = hashlib.md5(filepath.read_bytes()).hexdigest() + return {"size": stat.st_size, "mtime": stat.st_mtime, "md5": md5} + + +def load_scan_cache(cache_path: Optional[Path]) -> Dict[str, Any]: + """Load the AST scan cache from *cache_path*. Returns empty structure on any error.""" + if cache_path is None or not cache_path.is_file(): + return {"version": _CACHE_VERSION, "files": {}} + try: + raw = cache_path.read_text(encoding="utf-8") + data = json.loads(raw) + if data.get("version") != _CACHE_VERSION: + return {"version": _CACHE_VERSION, "files": {}} + return data + except Exception: + return {"version": _CACHE_VERSION, "files": {}} + + +def save_scan_cache(cache_path: Optional[Path], cache: Dict[str, Any]) -> None: + """Persist *cache* to *cache_path* (atomic-ish via temp file).""" + if cache_path is None: + return + try: + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp = cache_path.with_suffix(".tmp") + tmp.write_text(json.dumps(cache, ensure_ascii=False, indent=1), encoding="utf-8") + tmp.replace(cache_path) + except Exception: + pass + + +def _is_cache_hit(entry: Dict[str, Any], fp: Dict[str, Any]) -> bool: + """Check if a cache entry matches the current file fingerprint.""" + return ( + entry.get("md5") == fp["md5"] + and entry.get("size") == fp["size"] + ) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def _collect_py_files( + root_dir: Path, + max_depth: int = MAX_SCAN_DEPTH, + max_files: int = MAX_SCAN_FILES, + exclude_files: Optional[set] = None, +) -> List[Path]: + """ + 收集 root_dir 下的 .py 文件,限制最大递归深度和文件数量。 + + Args: + root_dir: 扫描根目录 + max_depth: 最大递归深度 (默认 10 层) + max_files: 最大文件数量 (默认 1000 个) + exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"}) + + Returns: + 排序后的 .py 文件路径列表 + """ + result: List[Path] = [] + _exclude = exclude_files or set() + + def _walk(dir_path: Path, depth: int): + if depth > max_depth or len(result) >= max_files: + return + try: + entries = sorted(dir_path.iterdir()) + except (PermissionError, OSError): + return + for entry in entries: + if len(result) >= max_files: + return + if entry.is_file() and entry.suffix == ".py" and not entry.name.startswith("__"): + if entry.name not in _exclude: + result.append(entry) + elif entry.is_dir() and not entry.name.startswith(("__", ".")): + _walk(entry, depth + 1) + + _walk(root_dir, 0) + return result + + +def scan_directory( + root_dir: Union[str, Path], + python_path: Union[str, Path] = "", + max_depth: int = MAX_SCAN_DEPTH, + max_files: int = MAX_SCAN_FILES, + executor: ThreadPoolExecutor = None, + exclude_files: Optional[set] = None, + cache: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Recursively scan .py files under *root_dir* for @device and @resource + decorated classes/functions. + + Uses a thread pool to parse files in parallel for faster I/O. + When *cache* is provided, files whose fingerprint (MD5 + size) hasn't + changed since the last scan are served from cache without re-parsing. + + Returns: + {"devices": {device_id: meta, ...}, "resources": {resource_id: meta, ...}} + + Args: + root_dir: Directory to scan (e.g. "unilabos/devices"). + python_path: The directory that should be on sys.path, i.e. the parent + of the top-level package. Module paths are derived as + filepath relative to this directory. If empty, defaults to + root_dir's parent. + max_depth: Maximum directory recursion depth (default 10). + max_files: Maximum number of .py files to scan (default 1000). + executor: Shared ThreadPoolExecutor (required). The caller manages its + lifecycle. + exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"}) + cache: Mutable cache dict (``load_scan_cache()`` result). Hits are read + from here; misses are written back so the caller can persist later. + """ + if executor is None: + raise ValueError("executor is required and must not be None") + + root_dir = Path(root_dir).resolve() + if not python_path: + python_path = root_dir.parent + else: + python_path = Path(python_path).resolve() + + # --- Collect files (depth/count limited) --- + py_files = _collect_py_files(root_dir, max_depth=max_depth, max_files=max_files, exclude_files=exclude_files) + + cache_files: Dict[str, Any] = cache.get("files", {}) if cache else {} + + # --- Parallel scan (with cache fast-path) --- + devices: Dict[str, dict] = {} + resources: Dict[str, dict] = {} + cache_hits = 0 + cache_misses = 0 + + def _parse_one_cached(py_file: Path) -> Tuple[List[dict], List[dict], bool]: + """Returns (devices, resources, was_cache_hit).""" + key = str(py_file) + try: + fp = _file_fingerprint(py_file) + except OSError: + return [], [], False + + cached_entry = cache_files.get(key) + if cached_entry and _is_cache_hit(cached_entry, fp): + return cached_entry.get("devices", []), cached_entry.get("resources", []), True + + try: + devs, ress = _parse_file(py_file, python_path) + except (SyntaxError, Exception): + devs, ress = [], [] + + cache_files[key] = { + "md5": fp["md5"], + "size": fp["size"], + "mtime": fp["mtime"], + "devices": devs, + "resources": ress, + } + return devs, ress, False + + def _collect_results(futures_dict: Dict): + nonlocal cache_hits, cache_misses + for future in as_completed(futures_dict): + devs, ress, hit = future.result() + if hit: + cache_hits += 1 + else: + cache_misses += 1 + for dev in devs: + device_id = dev.get("device_id") + if device_id: + if device_id in devices: + existing = devices[device_id].get("file_path", "?") + new_file = dev.get("file_path", "?") + raise ValueError( + f"@device id 重复: '{device_id}' 同时出现在 {existing} 和 {new_file}" + ) + devices[device_id] = dev + for res in ress: + resource_id = res.get("resource_id") + if resource_id: + if resource_id in resources: + existing = resources[resource_id].get("file_path", "?") + new_file = res.get("file_path", "?") + raise ValueError( + f"@resource id 重复: '{resource_id}' 同时出现在 {existing} 和 {new_file}" + ) + resources[resource_id] = res + + futures = {executor.submit(_parse_one_cached, f): f for f in py_files} + _collect_results(futures) + + if cache is not None: + cache["files"] = cache_files + + return { + "devices": devices, + "resources": resources, + "_cache_stats": {"hits": cache_hits, "misses": cache_misses, "total": len(py_files)}, + } + + + + +# --------------------------------------------------------------------------- +# File-level parsing +# --------------------------------------------------------------------------- + +# 已知继承自 rclpy.node.Node 的基类名 (用于 AST 静态检测) +_KNOWN_ROS2_BASE_CLASSES = {"Node", "BaseROS2DeviceNode"} +_KNOWN_ROS2_MODULES = {"rclpy", "rclpy.node"} + + +def _detect_class_type(cls_node: ast.ClassDef, import_map: Dict[str, str]) -> str: + """ + 检测类是否继承自 rclpy Node,返回 'ros2' 或 'python'。 + + 通过检查类的基类名称和 import_map 中的模块路径来判断: + 1. 基类名在已知 ROS2 基类集合中 + 2. 基类在 import_map 中解析到 rclpy 相关模块 + 3. 基类在 import_map 中解析到 BaseROS2DeviceNode + """ + for base in cls_node.bases: + base_name = "" + if isinstance(base, ast.Name): + base_name = base.id + elif isinstance(base, ast.Attribute): + base_name = base.attr + elif isinstance(base, ast.Subscript) and isinstance(base.value, ast.Name): + # Generic[T] 形式,如 BaseROS2DeviceNode[SomeType] + base_name = base.value.id + + if not base_name: + continue + + # 直接匹配已知 ROS2 基类名 + if base_name in _KNOWN_ROS2_BASE_CLASSES: + return "ros2" + + # 通过 import_map 检查模块路径 + module_path = import_map.get(base_name, "") + if any(mod in module_path for mod in _KNOWN_ROS2_MODULES): + return "ros2" + if "BaseROS2DeviceNode" in module_path: + return "ros2" + + return "python" + + +def _parse_file( + filepath: Path, + python_path: Path, +) -> Tuple[List[dict], List[dict]]: + """ + Parse a single .py file using ast and extract all @device-decorated classes + and @resource-decorated functions/classes. + + Returns: + (devices, resources) -- two lists of metadata dicts. + """ + source = filepath.read_text(encoding="utf-8", errors="replace") + tree = ast.parse(source, filename=str(filepath)) + + # Derive module path from file path + module_path = _filepath_to_module(filepath, python_path) + + # Build import map from the file (includes same-file class defs) + import_map = _collect_imports(tree, module_path) + + devices: List[dict] = [] + resources: List[dict] = [] + + for node in ast.iter_child_nodes(tree): + # --- @device on classes --- + if isinstance(node, ast.ClassDef): + device_decorator = _find_decorator(node, "device") + if device_decorator is not None and _is_registry_decorator("device", import_map): + device_args = _extract_decorator_args(device_decorator, import_map) + class_body = _extract_class_body(node, import_map) + + # Support ids + id_meta (multi-device) or id (single device) + device_ids: List[str] = [] + if device_args.get("ids") is not None: + device_ids = list(device_args["ids"]) + else: + did = device_args.get("id") or device_args.get("device_id") + device_ids = [did] if did else [f"{module_path}:{node.name}"] + + id_meta = device_args.get("id_meta") or {} + base_meta = { + "class_name": node.name, + "module": f"{module_path}:{node.name}", + "file_path": str(filepath).replace("\\", "/"), + "category": device_args.get("category", []), + "description": device_args.get("description", ""), + "display_name": device_args.get("display_name", ""), + "icon": device_args.get("icon", ""), + "version": device_args.get("version", "1.0.0"), + "device_type": _detect_class_type(node, import_map), + "handles": device_args.get("handles", []), + "model": device_args.get("model"), + "hardware_interface": device_args.get("hardware_interface"), + "actions": class_body.get("actions", {}), + "status_properties": class_body.get("status_properties", {}), + "init_params": class_body.get("init_params", []), + "auto_methods": class_body.get("auto_methods", {}), + "import_map": import_map, + } + for did in device_ids: + meta = dict(base_meta) + meta["device_id"] = did + overrides = id_meta.get(did, {}) + for key in ("handles", "description", "icon", "model", "hardware_interface"): + if key in overrides: + meta[key] = overrides[key] + devices.append(meta) + + # --- @resource on classes --- + resource_decorator = _find_decorator(node, "resource") + if resource_decorator is not None and _is_registry_decorator("resource", import_map): + res_meta = _extract_resource_meta( + resource_decorator, node.name, module_path, filepath, import_map, + is_function=False, + init_node=_find_init_in_class(node), + ) + resources.append(res_meta) + + # --- @resource on module-level functions --- + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + resource_decorator = _find_method_decorator(node, "resource") + if resource_decorator is not None and _is_registry_decorator("resource", import_map): + res_meta = _extract_resource_meta( + resource_decorator, node.name, module_path, filepath, import_map, + is_function=True, + func_node=node, + ) + resources.append(res_meta) + + return devices, resources + + +def _find_init_in_class(cls_node: ast.ClassDef) -> Optional[ast.FunctionDef]: + """Find __init__ method in a class.""" + for item in cls_node.body: + if isinstance(item, ast.FunctionDef) and item.name == "__init__": + return item + return None + + +def _extract_resource_meta( + decorator_node: Union[ast.Call, ast.Name], + name: str, + module_path: str, + filepath: Path, + import_map: Dict[str, str], + is_function: bool = False, + func_node: Optional[Union[ast.FunctionDef, ast.AsyncFunctionDef]] = None, + init_node: Optional[ast.FunctionDef] = None, +) -> dict: + """ + Extract resource metadata from a @resource decorator on a function or class. + """ + res_args = _extract_decorator_args(decorator_node, import_map) + + resource_id = res_args.get("id") or res_args.get("resource_id") + if resource_id is None: + resource_id = f"{module_path}:{name}" + + # Extract init/function params + init_params: List[dict] = [] + if is_function and func_node is not None: + init_params = _extract_method_params(func_node, import_map) + elif not is_function and init_node is not None: + init_params = _extract_method_params(init_node, import_map) + + return { + "resource_id": resource_id, + "name": name, + "module": f"{module_path}:{name}", + "file_path": str(filepath).replace("\\", "/"), + "is_function": is_function, + "category": res_args.get("category", []), + "description": res_args.get("description", ""), + "icon": res_args.get("icon", ""), + "version": res_args.get("version", "1.0.0"), + "class_type": res_args.get("class_type", "pylabrobot"), + "handles": res_args.get("handles", []), + "model": res_args.get("model"), + "init_params": init_params, + } + + +# --------------------------------------------------------------------------- +# Import map collection +# --------------------------------------------------------------------------- + + +def _collect_imports(tree: ast.Module, module_path: str = "") -> Dict[str, str]: + """ + Walk all Import/ImportFrom nodes in the AST tree, build a mapping from + local name to fully-qualified import path. + + Also includes top-level class/function definitions from the same file, + so that same-file TypedDict / Enum / dataclass references can be resolved. + + Returns: + {"SendCmd": "unilabos_msgs.action:SendCmd", + "StrSingleInput": "unilabos_msgs.action:StrSingleInput", + "InputHandle": "unilabos.registry.decorators:InputHandle", + "SetLiquidReturn": "unilabos.devices.liquid_handling.liquid_handler_abstract:SetLiquidReturn", + ...} + """ + import_map: Dict[str, str] = {} + + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + module = node.module or "" + for alias in node.names: + local_name = alias.asname if alias.asname else alias.name + import_map[local_name] = f"{module}:{alias.name}" + elif isinstance(node, ast.Import): + for alias in node.names: + local_name = alias.asname if alias.asname else alias.name + import_map[local_name] = alias.name + + # 同文件顶层 class / function 定义 + if module_path: + for node in tree.body: + if isinstance(node, ast.ClassDef): + import_map.setdefault(node.name, f"{module_path}:{node.name}") + elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef): + import_map.setdefault(node.name, f"{module_path}:{node.name}") + elif isinstance(node, ast.Assign): + # 顶层赋值 (如 MotorAxis = Enum(...)) + for target in node.targets: + if isinstance(target, ast.Name): + import_map.setdefault(target.id, f"{module_path}:{target.id}") + + return import_map + + + +# --------------------------------------------------------------------------- +# Decorator finding & argument extraction +# --------------------------------------------------------------------------- + + +def _find_decorator( + node: Union[ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef], + decorator_name: str, +) -> Optional[ast.Call]: + """ + Find a specific decorator call on a class or function definition. + + Handles both: + - @device(...) -> ast.Call with func=ast.Name(id="device") + - @module.device(...) -> ast.Call with func=ast.Attribute(attr="device") + """ + for dec in node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return dec + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return dec + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + # @device without parens (unlikely but handle it) + return None # Can't extract args from bare decorator + return None + + +def _find_method_decorator(func_node: ast.FunctionDef, decorator_name: str) -> Optional[Union[ast.Call, ast.Name]]: + """Find a decorator on a method.""" + for dec in func_node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return dec + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return dec + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + # @action without parens, or @topic_config without parens + return dec + return None + + +def _has_decorator(func_node: ast.FunctionDef, decorator_name: str) -> bool: + """Check if a method has a specific decorator (with or without call).""" + for dec in func_node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return True + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return True + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + return True + return False + + +def _is_registry_decorator(name: str, import_map: Dict[str, str]) -> bool: + """Check that *name* was imported from ``unilabos.registry.decorators``.""" + source = import_map.get(name, "") + return _REGISTRY_DECORATOR_MODULE in source + + +def _extract_decorator_args( + node: Union[ast.Call, ast.Name], + import_map: Dict[str, str], +) -> dict: + """ + Extract keyword arguments from a decorator call AST node. + + Resolves Name references (e.g. SendCmd, Side.NORTH) via import_map. + Handles literal values (strings, ints, bools, lists, dicts, None). + """ + if isinstance(node, ast.Name): + return {} # Bare decorator, no args + if not isinstance(node, ast.Call): + return {} + + result: dict = {} + + for kw in node.keywords: + if kw.arg is None: + continue # **kwargs, skip + result[kw.arg] = _ast_node_to_value(kw.value, import_map) + + return result + + +# --------------------------------------------------------------------------- +# AST node value conversion +# --------------------------------------------------------------------------- + + +def _ast_node_to_value(node: ast.expr, import_map: Dict[str, str]) -> Any: + """ + Convert an AST expression node to a Python value. + + Handles: + - Literals (str, int, float, bool, None) + - Lists, Tuples, Dicts, Sets + - Name references (e.g. SendCmd -> resolved via import_map) + - Attribute access (e.g. Side.NORTH -> resolved) + - Function/class calls (e.g. InputHandle(...) -> structured dict) + - Unary operators (e.g. -1) + """ + # --- Constant (str, int, float, bool, None) --- + if isinstance(node, ast.Constant): + return node.value + + # --- Name (e.g. SendCmd, True, False, None) --- + if isinstance(node, ast.Name): + return _resolve_name(node.id, import_map) + + # --- Attribute (e.g. Side.NORTH, DataSource.HANDLE) --- + if isinstance(node, ast.Attribute): + return _resolve_attribute(node, import_map) + + # --- List --- + if isinstance(node, ast.List): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Tuple --- + if isinstance(node, ast.Tuple): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Dict --- + if isinstance(node, ast.Dict): + result = {} + for k, v in zip(node.keys, node.values): + if k is None: + continue # **kwargs spread + key = _ast_node_to_value(k, import_map) + val = _ast_node_to_value(v, import_map) + result[key] = val + return result + + # --- Set --- + if isinstance(node, ast.Set): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Call (e.g. InputHandle(...), OutputHandle(...)) --- + if isinstance(node, ast.Call): + return _ast_call_to_value(node, import_map) + + # --- UnaryOp (e.g. -1, -0.5) --- + if isinstance(node, ast.UnaryOp): + if isinstance(node.op, ast.USub): + operand = _ast_node_to_value(node.operand, import_map) + if isinstance(operand, (int, float)): + return -operand + elif isinstance(node.op, ast.Not): + operand = _ast_node_to_value(node.operand, import_map) + return not operand + + # --- BinOp (e.g. "a" + "b") --- + if isinstance(node, ast.BinOp): + if isinstance(node.op, ast.Add): + left = _ast_node_to_value(node.left, import_map) + right = _ast_node_to_value(node.right, import_map) + if isinstance(left, str) and isinstance(right, str): + return left + right + + # --- JoinedStr (f-string) --- + if isinstance(node, ast.JoinedStr): + return "" + + # Fallback: return the AST dump as a string marker + return f"" + + +def _resolve_name(name: str, import_map: Dict[str, str]) -> str: + """ + Resolve a bare Name reference via import_map. + + E.g. "SendCmd" -> "unilabos_msgs.action:SendCmd" + "True" -> True (handled by ast.Constant in Python 3.8+) + """ + if name in import_map: + return import_map[name] + # Fallback: return the name as-is + return name + + +def _resolve_attribute(node: ast.Attribute, import_map: Dict[str, str]) -> str: + """ + Resolve an attribute access like Side.NORTH or DataSource.HANDLE. + + Returns a string like "NORTH" for enum values, or + "module.path:Class.attr" for imported references. + """ + # Get the full dotted path + parts = [] + current = node + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + + parts.reverse() + # parts = ["Side", "NORTH"] or ["DataSource", "HANDLE"] + + if len(parts) >= 2: + base = parts[0] + attr = ".".join(parts[1:]) + + # If the base is an imported name, resolve it + if base in import_map: + return f"{import_map[base]}.{attr}" + + # For known enum-like patterns, return just the value + # e.g. Side.NORTH -> "NORTH" + if base in ("Side", "DataSource"): + return parts[-1] + + return ".".join(parts) + + +def _ast_call_to_value(node: ast.Call, import_map: Dict[str, str]) -> dict: + """ + Convert a function/class call like InputHandle(key="in", ...) to a structured dict. + + Returns: + {"_call": "unilabos.registry.decorators:InputHandle", + "key": "in", "data_type": "fluid", ...} + """ + # Resolve the call target + if isinstance(node.func, ast.Name): + call_name = _resolve_name(node.func.id, import_map) + elif isinstance(node.func, ast.Attribute): + call_name = _resolve_attribute(node.func, import_map) + else: + call_name = "" + + result: dict = {"_call": call_name} + + # Positional args + for i, arg in enumerate(node.args): + result[f"_pos_{i}"] = _ast_node_to_value(arg, import_map) + + # Keyword args + for kw in node.keywords: + if kw.arg is None: + continue + result[kw.arg] = _ast_node_to_value(kw.value, import_map) + + return result + + +# --------------------------------------------------------------------------- +# Class body extraction +# --------------------------------------------------------------------------- + + +def _extract_class_body( + cls_node: ast.ClassDef, + import_map: Dict[str, str], +) -> dict: + """ + Walk the class body to extract: + - @action-decorated methods + - @property with @topic_config (status properties) + - get_* methods with @topic_config + - __init__ parameters + - Public methods without @action (auto-actions) + """ + result: dict = { + "actions": {}, # method_name -> action_info + "status_properties": {}, # prop_name -> status_info + "init_params": [], # [{"name": ..., "type": ..., "default": ...}, ...] + "auto_methods": {}, # method_name -> method_info (no @action decorator) + } + + for item in cls_node.body: + if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + + method_name = item.name + + # --- __init__ --- + if method_name == "__init__": + result["init_params"] = _extract_method_params(item, import_map) + continue + + # --- Skip private/dunder --- + if method_name.startswith("_"): + continue + + # --- Check for @property or @topic_config → status property --- + is_property = _has_decorator(item, "property") + has_topic = ( + _has_decorator(item, "topic_config") + and _is_registry_decorator("topic_config", import_map) + ) + + if is_property or has_topic: + topic_args = {} + topic_dec = _find_method_decorator(item, "topic_config") + if topic_dec is not None: + topic_args = _extract_decorator_args(topic_dec, import_map) + + return_type = _get_annotation_str(item.returns, import_map) + # 非 @property 的 @topic_config 方法,用去掉 get_ 前缀的名称 + prop_name = method_name[4:] if method_name.startswith("get_") and not is_property else method_name + + result["status_properties"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "is_property": is_property, + "topic_config": topic_args if topic_args else None, + } + continue + + # --- Check for @action --- + action_dec = _find_method_decorator(item, "action") + if action_dec is not None and _is_registry_decorator("action", import_map): + action_args = _extract_decorator_args(action_dec, import_map) + # 补全 @action 装饰器的默认值(与 decorators.py 中 action() 签名一致) + action_args.setdefault("action_type", None) + action_args.setdefault("goal", {}) + action_args.setdefault("feedback", {}) + action_args.setdefault("result", {}) + action_args.setdefault("handles", {}) + action_args.setdefault("goal_default", {}) + action_args.setdefault("placeholder_keys", {}) + action_args.setdefault("always_free", False) + action_args.setdefault("is_protocol", False) + action_args.setdefault("description", "") + action_args.setdefault("auto_prefix", False) + action_args.setdefault("parent", False) + method_params = _extract_method_params(item, import_map) + return_type = _get_annotation_str(item.returns, import_map) + is_async = isinstance(item, ast.AsyncFunctionDef) + method_doc = ast.get_docstring(item) + + result["actions"][method_name] = { + "action_args": action_args, + "params": method_params, + "return_type": return_type, + "is_async": is_async, + "docstring": method_doc, + } + continue + + # --- Check for @not_action --- + if _has_decorator(item, "not_action") and _is_registry_decorator("not_action", import_map): + continue + + # --- get_ 前缀且无额外参数(仅 self)→ status property --- + if method_name.startswith("get_"): + real_args = [a for a in item.args.args if a.arg != "self"] + if len(real_args) == 0: + prop_name = method_name[4:] + return_type = _get_annotation_str(item.returns, import_map) + if prop_name not in result["status_properties"]: + result["status_properties"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "is_property": False, + "topic_config": None, + } + continue + + # --- Public method without @action => auto-action --- + if method_name in ("post_init", "__str__", "__repr__"): + continue + + method_params = _extract_method_params(item, import_map) + return_type = _get_annotation_str(item.returns, import_map) + is_async = isinstance(item, ast.AsyncFunctionDef) + method_doc = ast.get_docstring(item) + + auto_entry: dict = { + "params": method_params, + "return_type": return_type, + "is_async": is_async, + "docstring": method_doc, + } + if _has_decorator(item, "always_free") and _is_registry_decorator("always_free", import_map): + auto_entry["always_free"] = True + result["auto_methods"][method_name] = auto_entry + + return result + + +# --------------------------------------------------------------------------- +# Method parameter extraction +# --------------------------------------------------------------------------- + + +_PARAM_SKIP_NAMES = frozenset({"sample_uuids"}) + + +def _extract_method_params( + func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], + import_map: Optional[Dict[str, str]] = None, +) -> List[dict]: + """ + Extract parameters from a class method definition. + + Automatically skips the first positional argument (self / cls) and any + domain-specific names listed in ``_PARAM_SKIP_NAMES``. + + Returns: + [{"name": "position", "type": "str", "default": None, "required": True}, ...] + """ + if import_map is None: + import_map = {} + params: List[dict] = [] + + args = func_node.args + + # Skip the first positional arg (self/cls) -- always present for class methods + # noinspection PyUnresolvedReferences + positional_args = args.args[1:] if args.args else [] + + # defaults align to the *end* of the args list; offset must account for the skipped arg + num_args = len(args.args) + num_defaults = len(args.defaults) + first_default_idx = num_args - num_defaults + + for i, arg in enumerate(positional_args, start=1): + name = arg.arg + if name in _PARAM_SKIP_NAMES: + continue + + param_info: dict = {"name": name} + + # Type annotation + if arg.annotation: + param_info["type"] = _get_annotation_str(arg.annotation, import_map) + else: + param_info["type"] = "" + + # Default value + default_idx = i - first_default_idx + if 0 <= default_idx < len(args.defaults): + default_val = _ast_node_to_value(args.defaults[default_idx], import_map) + param_info["default"] = default_val + param_info["required"] = False + else: + param_info["default"] = None + param_info["required"] = True + + params.append(param_info) + + # Keyword-only arguments (self/cls never appear here) + for i, arg in enumerate(args.kwonlyargs): + name = arg.arg + if name in _PARAM_SKIP_NAMES: + continue + + param_info: dict = {"name": name} + + if arg.annotation: + param_info["type"] = _get_annotation_str(arg.annotation, import_map) + else: + param_info["type"] = "" + + if i < len(args.kw_defaults) and args.kw_defaults[i] is not None: + param_info["default"] = _ast_node_to_value(args.kw_defaults[i], import_map) + param_info["required"] = False + else: + param_info["default"] = None + param_info["required"] = True + + params.append(param_info) + + return params + + +def _get_annotation_str(node: Optional[ast.expr], import_map: Dict[str, str]) -> str: + """Convert a type annotation AST node to a string representation. + + 保持类型字符串为合法 Python 表达式 (可被 ast.parse 解析)。 + 不在此处做 import_map 替换 — 由上层在需要时通过 import_map 解析。 + """ + if node is None: + return "" + + if isinstance(node, ast.Constant): + return str(node.value) + + if isinstance(node, ast.Name): + return node.id + + if isinstance(node, ast.Attribute): + parts = [] + current = node + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + parts.reverse() + return ".".join(parts) + + # Handle subscript types like List[str], Dict[str, int], Optional[str] + if isinstance(node, ast.Subscript): + base = _get_annotation_str(node.value, import_map) + if isinstance(node.slice, ast.Tuple): + args = ", ".join(_get_annotation_str(elt, import_map) for elt in node.slice.elts) + else: + args = _get_annotation_str(node.slice, import_map) + return f"{base}[{args}]" + + # Handle Union types (X | Y in Python 3.10+) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + left = _get_annotation_str(node.left, import_map) + right = _get_annotation_str(node.right, import_map) + return f"Union[{left}, {right}]" + + return ast.dump(node) + + +# --------------------------------------------------------------------------- +# Module path derivation +# --------------------------------------------------------------------------- + + +def _filepath_to_module(filepath: Path, python_path: Path) -> str: + """ + 通过 *python_path*(sys.path 中的根目录)推导 Python 模块路径。 + + 做法:取 filepath 相对于 python_path 的路径,将目录分隔符替换为 '.'。 + + E.g. filepath = "/project/unilabos/devices/pump/valve.py" + python_path = "/project" + => "unilabos.devices.pump.valve" + """ + try: + relative = filepath.relative_to(python_path) + except ValueError: + return str(filepath) + + parts = list(relative.parts) + # 去掉 .py 后缀 + if parts and parts[-1].endswith(".py"): + parts[-1] = parts[-1][:-3] + # 去掉 __init__ + if parts and parts[-1] == "__init__": + parts.pop() + + return ".".join(parts) diff --git a/unilabos/registry/decorators.py b/unilabos/registry/decorators.py new file mode 100644 index 00000000..e8c65ac8 --- /dev/null +++ b/unilabos/registry/decorators.py @@ -0,0 +1,614 @@ +""" +装饰器注册表系统 + +通过 @device, @action, @resource 装饰器替代 YAML 配置文件来定义设备/动作/资源注册表信息。 + +Usage: + from unilabos.registry.decorators import ( + device, action, resource, + InputHandle, OutputHandle, + ActionInputHandle, ActionOutputHandle, + HardwareInterface, Side, DataSource, + ) + + @device( + id="solenoid_valve.mock", + category=["pump_and_valve"], + description="模拟电磁阀设备", + handles=[ + InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH), + OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH), + ], + hardware_interface=HardwareInterface( + name="hardware_interface", + read="send_command", + write="send_command", + ), + ) + class SolenoidValveMock: + @action(action_type=EmptyIn) + def close(self): + ... + + @action( + handles=[ + ActionInputHandle(key="in", data_type="fluid", label="in"), + ActionOutputHandle(key="out", data_type="fluid", label="out"), + ], + ) + def set_valve_position(self, position): + ... + + # 无 @action 装饰器 => auto- 前缀动作 + def is_open(self): + ... +""" + +from enum import Enum +from functools import wraps +from typing import Any, Callable, Dict, List, Optional, TypeVar + +from pydantic import BaseModel, ConfigDict, Field + +F = TypeVar("F", bound=Callable[..., Any]) + +# --------------------------------------------------------------------------- +# 枚举 +# --------------------------------------------------------------------------- + + +class Side(str, Enum): + """UI 上 Handle 的显示位置""" + + NORTH = "NORTH" + SOUTH = "SOUTH" + EAST = "EAST" + WEST = "WEST" + + +class DataSource(str, Enum): + """Handle 的数据来源""" + + HANDLE = "handle" # 从上游 handle 获取数据 (用于 InputHandle) + EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle) + + +# --------------------------------------------------------------------------- +# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type) +# --------------------------------------------------------------------------- + + +class _DeviceHandleBase(BaseModel): + """设备/资源端口基类 (内部使用)""" + + model_config = ConfigDict(populate_by_name=True) + + key: str = Field(serialization_alias="handler_key") + data_type: str + label: str + side: Optional[Side] = None + data_key: Optional[str] = None + data_source: Optional[str] = None + description: Optional[str] = None + + # 子类覆盖 + io_type: str = "" + + def to_registry_dict(self) -> Dict[str, Any]: + return self.model_dump(by_alias=True, exclude_none=True) + + +class InputHandle(_DeviceHandleBase): + """ + 输入端口 (io_type="target"), 用于 @device / @resource handles + + Example: + InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH) + """ + + io_type: str = "target" + + +class OutputHandle(_DeviceHandleBase): + """ + 输出端口 (io_type="source"), 用于 @device / @resource handles + + Example: + OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH) + """ + + io_type: str = "source" + + +# --------------------------------------------------------------------------- +# Action Handle (动作级别端口, 序列化时不含 io_type, 按类型自动分组) +# --------------------------------------------------------------------------- + + +class _ActionHandleBase(BaseModel): + """动作端口基类 (内部使用)""" + + model_config = ConfigDict(populate_by_name=True) + + key: str = Field(serialization_alias="handler_key") + data_type: str + label: str + side: Optional[Side] = None + data_key: Optional[str] = None + data_source: Optional[str] = None + description: Optional[str] = None + io_type: Optional[str] = None # source/sink (dataflow) or target/source (device-style) + + def to_registry_dict(self) -> Dict[str, Any]: + return self.model_dump(by_alias=True, exclude_none=True) + + +class ActionInputHandle(_ActionHandleBase): + """ + 动作输入端口, 用于 @action handles, 序列化后归入 "input" 组 + + Example: + ActionInputHandle( + key="material_input", data_type="workbench_material", + label="物料编号", data_key="material_number", data_source="handle", + ) + """ + + pass + + +class ActionOutputHandle(_ActionHandleBase): + """ + 动作输出端口, 用于 @action handles, 序列化后归入 "output" 组 + + Example: + ActionOutputHandle( + key="station_output", data_type="workbench_station", + label="加热台ID", data_key="station_id", data_source="executor", + ) + """ + + pass + + +# --------------------------------------------------------------------------- +# HardwareInterface +# --------------------------------------------------------------------------- + + +class HardwareInterface(BaseModel): + """ + 硬件通信接口定义 + + 描述设备与底层硬件通信的方式 (串口、Modbus 等)。 + + Example: + HardwareInterface(name="hardware_interface", read="send_command", write="send_command") + """ + + name: str + read: Optional[str] = None + write: Optional[str] = None + extra_info: Optional[List[str]] = None + + +# --------------------------------------------------------------------------- +# 全局注册表 -- 记录所有被装饰器标记的类/函数 +# --------------------------------------------------------------------------- +_registered_devices: Dict[str, type] = {} # device_id -> class +_registered_resources: Dict[str, Any] = {} # resource_id -> class or function + + +def _device_handles_to_list( + handles: Optional[List[_DeviceHandleBase]], +) -> List[Dict[str, Any]]: + """将设备/资源 Handle 列表序列化为字典列表 (含 io_type)""" + if handles is None: + return [] + return [h.to_registry_dict() for h in handles] + + +def _action_handles_to_dict( + handles: Optional[List[_ActionHandleBase]], +) -> Dict[str, Any]: + """ + 将动作 Handle 列表序列化为 {"input": [...], "output": [...]} 格式。 + + ActionInputHandle => "input", ActionOutputHandle => "output" + """ + if handles is None: + return {} + input_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionInputHandle)] + output_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionOutputHandle)] + result: Dict[str, Any] = {} + if input_list: + result["input"] = input_list + if output_list: + result["output"] = output_list + return result + + +# --------------------------------------------------------------------------- +# @device 类装饰器 +# --------------------------------------------------------------------------- + + +# noinspection PyShadowingBuiltins +def device( + id: Optional[str] = None, + ids: Optional[List[str]] = None, + id_meta: Optional[Dict[str, Dict[str, Any]]] = None, + category: Optional[List[str]] = None, + description: str = "", + display_name: str = "", + icon: str = "", + version: str = "1.0.0", + handles: Optional[List[_DeviceHandleBase]] = None, + model: Optional[Dict[str, Any]] = None, + device_type: str = "python", + hardware_interface: Optional[HardwareInterface] = None, +): + """ + 设备类装饰器 + + 将类标记为一个 UniLab-OS 设备,并附加注册表元数据。 + + 支持两种模式: + 1. 单设备: id="xxx", category=[...] + 2. 多设备: ids=["id1","id2"], id_meta={"id1":{handles:[...]}, "id2":{...}} + + Args: + id: 单设备时的注册表唯一标识 + ids: 多设备时的 id 列表,与 id_meta 配合使用 + id_meta: 每个 device_id 的覆盖元数据 (handles/description/icon/model) + category: 设备分类标签列表 (必填) + description: 设备描述 + display_name: 人类可读的设备显示名称,缺失时默认使用 id + icon: 图标路径 + version: 版本号 + handles: 设备端口列表 (单设备或 id_meta 未覆盖时使用) + model: 可选的 3D 模型配置 + device_type: 设备实现类型 ("python" / "ros2") + hardware_interface: 硬件通信接口 (HardwareInterface) + """ + # Resolve device ids + if ids is not None: + device_ids = list(ids) + if not device_ids: + raise ValueError("@device ids 不能为空") + id_meta = id_meta or {} + elif id is not None: + device_ids = [id] + id_meta = {} + else: + raise ValueError("@device 必须提供 id 或 ids") + + if category is None: + raise ValueError("@device category 必填") + + base_meta = { + "category": category, + "description": description, + "display_name": display_name, + "icon": icon, + "version": version, + "handles": _device_handles_to_list(handles), + "model": model, + "device_type": device_type, + "hardware_interface": (hardware_interface.model_dump(exclude_none=True) if hardware_interface else None), + } + + def decorator(cls): + cls._device_registry_meta = base_meta + cls._device_registry_id_meta = id_meta + cls._device_registry_ids = device_ids + + for did in device_ids: + if did in _registered_devices: + raise ValueError(f"@device id 重复: '{did}' 已被 {_registered_devices[did]} 注册") + _registered_devices[did] = cls + + return cls + + return decorator + + +# --------------------------------------------------------------------------- +# @action 方法装饰器 +# --------------------------------------------------------------------------- + +# 区分 "用户没传 action_type" 和 "用户传了 None" +_ACTION_TYPE_UNSET = object() + + +# noinspection PyShadowingNames +def action( + action_type: Any = _ACTION_TYPE_UNSET, + goal: Optional[Dict[str, str]] = None, + feedback: Optional[Dict[str, str]] = None, + result: Optional[Dict[str, str]] = None, + handles: Optional[List[_ActionHandleBase]] = None, + goal_default: Optional[Dict[str, Any]] = None, + placeholder_keys: Optional[Dict[str, str]] = None, + always_free: bool = False, + is_protocol: bool = False, + description: str = "", + auto_prefix: bool = False, + parent: bool = False, +): + """ + 动作方法装饰器 + + 标记方法为注册表动作。有三种用法: + 1. @action(action_type=EmptyIn, ...) -- 非 auto, 使用指定 ROS Action 类型 + 2. @action() -- 非 auto, UniLabJsonCommand (从方法签名生成 schema) + 3. 不加 @action -- auto- 前缀, UniLabJsonCommand + + Protocol 用法: + @action(action_type=Add, is_protocol=True) + def AddProtocol(self): ... + 标记该动作为高级协议 (protocol),运行时通过 ROS Action 路由到 + protocol generator 执行。action_type 指向 unilabos_msgs 的 Action 类型。 + + Args: + action_type: ROS Action 消息类型 (如 EmptyIn, SendCmd, HeatChill). + 不传/默认 = UniLabJsonCommand (非 auto). + goal: Goal 字段映射 (ROS字段名 -> 设备参数名). + protocol 模式下可留空,系统自动生成 identity 映射. + feedback: Feedback 字段映射 + result: Result 字段映射 + handles: 动作端口列表 (ActionInputHandle / ActionOutputHandle) + goal_default: Goal 字段默认值映射 (字段名 -> 默认值), 与自动生成的 goal_default 合并 + placeholder_keys: 参数占位符配置 + always_free: 是否为永久闲置动作 (不受排队限制) + is_protocol: 是否为工作站协议 (protocol)。True 时运行时走 protocol generator 路径。 + description: 动作描述 + auto_prefix: 若为 True,动作名使用 auto-{method_name} 形式(与无 @action 时一致) + parent: 若为 True,当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数 + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + # action_type 为哨兵值 => 用户没传, 视为 None (UniLabJsonCommand) + resolved_type = None if action_type is _ACTION_TYPE_UNSET else action_type + + meta = { + "action_type": resolved_type, + "goal": goal or {}, + "feedback": feedback or {}, + "result": result or {}, + "handles": _action_handles_to_dict(handles), + "goal_default": goal_default or {}, + "placeholder_keys": placeholder_keys or {}, + "always_free": always_free, + "is_protocol": is_protocol, + "description": description, + "auto_prefix": auto_prefix, + "parent": parent, + } + wrapper._action_registry_meta = meta # type: ignore[attr-defined] + + # 设置 _is_always_free 保持与旧 @always_free 装饰器兼容 + if always_free: + wrapper._is_always_free = True # type: ignore[attr-defined] + + return wrapper # type: ignore[return-value] + + return decorator + + +def get_action_meta(func) -> Optional[Dict[str, Any]]: + """获取方法上的 @action 装饰器元数据""" + return getattr(func, "_action_registry_meta", None) + + +def has_action_decorator(func) -> bool: + """检查函数是否带有 @action 装饰器""" + return hasattr(func, "_action_registry_meta") + + +# --------------------------------------------------------------------------- +# @resource 类/函数装饰器 +# --------------------------------------------------------------------------- + + +def resource( + id: str, + category: List[str], + description: str = "", + icon: str = "", + version: str = "1.0.0", + handles: Optional[List[_DeviceHandleBase]] = None, + model: Optional[Dict[str, Any]] = None, + class_type: str = "pylabrobot", +): + """ + 资源类/函数装饰器 + + 将类或工厂函数标记为一个 UniLab-OS 资源,附加注册表元数据。 + + Args: + id: 注册表唯一标识 (必填, 不可重复) + category: 资源分类标签列表 (必填) + description: 资源描述 + icon: 图标路径 + version: 版本号 + handles: 端口列表 (InputHandle / OutputHandle) + model: 可选的 3D 模型配置 + class_type: 资源实现类型 ("python" / "pylabrobot" / "unilabos") + """ + + def decorator(obj): + meta = { + "resource_id": id, + "category": category, + "description": description, + "icon": icon, + "version": version, + "handles": _device_handles_to_list(handles), + "model": model, + "class_type": class_type, + } + obj._resource_registry_meta = meta + + if id in _registered_resources: + raise ValueError(f"@resource id 重复: '{id}' 已被 {_registered_resources[id]} 注册") + _registered_resources[id] = obj + + return obj + + return decorator + + +def get_device_meta(cls, device_id: Optional[str] = None) -> Optional[Dict[str, Any]]: + """ + 获取类上的 @device 装饰器元数据。 + + 当 device_id 存在且类使用 ids+id_meta 时,返回合并后的 meta + (base_meta 与 id_meta[device_id] 深度合并)。 + """ + base = getattr(cls, "_device_registry_meta", None) + if base is None: + return None + id_meta = getattr(cls, "_device_registry_id_meta", None) or {} + if device_id is None or device_id not in id_meta: + result = dict(base) + ids = getattr(cls, "_device_registry_ids", None) + result["device_id"] = device_id if device_id is not None else (ids[0] if ids else None) + return result + + overrides = id_meta[device_id] + result = dict(base) + result["device_id"] = device_id + for key in ["handles", "description", "icon", "model"]: + if key in overrides: + val = overrides[key] + if key == "handles" and isinstance(val, list): + # handles 必须是 Handle 对象列表 + result[key] = [h.to_registry_dict() for h in val] + else: + result[key] = val + return result + + +def get_resource_meta(obj) -> Optional[Dict[str, Any]]: + """获取对象上的 @resource 装饰器元数据""" + return getattr(obj, "_resource_registry_meta", None) + + +def get_all_registered_devices() -> Dict[str, type]: + """获取所有已注册的设备类""" + return _registered_devices.copy() + + +def get_all_registered_resources() -> Dict[str, Any]: + """获取所有已注册的资源""" + return _registered_resources.copy() + + +def clear_registry(): + """清空全局注册表 (用于测试)""" + _registered_devices.clear() + _registered_resources.clear() + + +# --------------------------------------------------------------------------- +# topic_config / not_action / always_free 装饰器 +# --------------------------------------------------------------------------- + + +def topic_config( + period: Optional[float] = None, + print_publish: Optional[bool] = None, + qos: Optional[int] = None, + name: Optional[str] = None, +) -> Callable[[F], F]: + """ + Topic发布配置装饰器 + + 用于装饰 get_{attr_name} 方法或 @property,控制对应属性的ROS topic发布行为。 + + Args: + period: 发布周期(秒)。None 表示使用默认值 5.0 + print_publish: 是否打印发布日志。None 表示使用节点默认配置 + qos: QoS深度配置。None 表示使用默认值 10 + name: 自定义发布名称。None 表示使用方法名(去掉 get_ 前缀) + + Note: + 与 @property 连用时,@topic_config 必须放在 @property 下面, + 这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。 + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + wrapper._topic_period = period # type: ignore[attr-defined] + wrapper._topic_print_publish = print_publish # type: ignore[attr-defined] + wrapper._topic_qos = qos # type: ignore[attr-defined] + wrapper._topic_name = name # type: ignore[attr-defined] + wrapper._has_topic_config = True # type: ignore[attr-defined] + + return wrapper # type: ignore[return-value] + + return decorator + + +def get_topic_config(func) -> dict: + """获取函数上的 topic 配置 (period, print_publish, qos, name)""" + if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False): + return { + "period": getattr(func, "_topic_period", None), + "print_publish": getattr(func, "_topic_print_publish", None), + "qos": getattr(func, "_topic_qos", None), + "name": getattr(func, "_topic_name", None), + } + return {} + + +def always_free(func: F) -> F: + """ + 标记动作为永久闲置(不受busy队列限制)的装饰器 + + 被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制, + 任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。 + """ + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + wrapper._is_always_free = True # type: ignore[attr-defined] + + return wrapper # type: ignore[return-value] + + +def is_always_free(func) -> bool: + """检查函数是否被标记为永久闲置""" + return getattr(func, "_is_always_free", False) + + +def not_action(func: F) -> F: + """ + 标记方法为非动作的装饰器 + + 用于装饰 driver 类中的方法,使其在注册表扫描时不被识别为动作。 + 适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。 + """ + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + wrapper._is_not_action = True # type: ignore[attr-defined] + + return wrapper # type: ignore[return-value] + + +def is_not_action(func) -> bool: + """检查函数是否被标记为非动作""" + return getattr(func, "_is_not_action", False) diff --git a/unilabos/registry/devices/Qone_nmr.yaml b/unilabos/registry/devices/Qone_nmr.yaml index fa182c77..5c5f1f8a 100644 --- a/unilabos/registry/devices/Qone_nmr.yaml +++ b/unilabos/registry/devices/Qone_nmr.yaml @@ -13,21 +13,18 @@ Qone_nmr: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -71,31 +68,6 @@ Qone_nmr: title: monitor_folder_for_new_content参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-strings_to_txt: feedback: {} goal: {} @@ -138,21 +110,18 @@ Qone_nmr: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -167,32 +136,31 @@ Qone_nmr: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index fc4b75cb..f57cd35c 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -22,7 +22,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: auto_batch_outbound_from_xlsx参数 @@ -490,7 +491,9 @@ bioyond_cell: goal: properties: material_names: - type: string + items: + type: string + type: array type_id: default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 type: string @@ -499,7 +502,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: create_and_inbound_materials参数 @@ -535,7 +539,8 @@ bioyond_cell: - type_id - warehouse_name type: object - result: {} + result: + type: object required: - goal title: create_material参数 @@ -556,11 +561,16 @@ bioyond_cell: goal: properties: mappings: + additionalProperties: + type: object type: object required: - mappings type: object - result: {} + result: + items: + type: object + type: array required: - goal title: create_materials参数 @@ -592,7 +602,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: create_orders参数 @@ -624,7 +635,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: create_orders_v2参数 @@ -665,7 +677,8 @@ bioyond_cell: - bottle_type - location_code type: object - result: {} + result: + type: object required: - goal title: create_sample参数 @@ -718,7 +731,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: order_list_v2参数 @@ -821,7 +835,8 @@ bioyond_cell: required: - material_obj type: object - result: {} + result: + type: object required: - goal title: report_material_change参数 @@ -875,7 +890,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_continue参数 @@ -896,7 +912,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_reset参数 @@ -917,7 +934,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start参数 @@ -1362,7 +1380,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start_and_auto_feeding参数 @@ -1807,7 +1826,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start_and_auto_feeding_v2参数 @@ -1828,7 +1848,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_stop参数 @@ -1850,12 +1871,15 @@ bioyond_cell: properties: items: items: + additionalProperties: + type: string type: object type: array required: - items type: object - result: {} + result: + type: object required: - goal title: storage_batch_inbound参数 @@ -1884,7 +1908,8 @@ bioyond_cell: - material_id - location_id type: object - result: {} + result: + type: object required: - goal title: storage_inbound参数 @@ -1905,7 +1930,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_1_to_2参数 @@ -1946,7 +1972,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_3_to_2参数 @@ -1983,7 +2010,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_3_to_2_to_1参数 @@ -2007,10 +2035,11 @@ bioyond_cell: ip: type: string port: - type: string + type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: update_push_ip参数 @@ -2039,7 +2068,8 @@ bioyond_cell: required: - order_code type: object - result: {} + result: + type: object required: - goal title: wait_for_order_finish参数 @@ -2072,7 +2102,8 @@ bioyond_cell: required: - order_code type: object - result: {} + result: + type: object required: - goal title: wait_for_order_finish_polling参数 @@ -2104,7 +2135,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: boolean required: - goal title: wait_for_transfer_task参数 @@ -2112,8 +2144,7 @@ bioyond_cell: type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation status_types: - device_id: String - material_info: dict + device_id: '' type: python config_info: [] description: '' @@ -2134,11 +2165,7 @@ bioyond_cell: properties: device_id: type: string - material_info: - type: object required: - device_id - - material_info type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 7b9ebc90..547b54ff 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -24,7 +24,8 @@ bioyond_dispensing_station: required: - data type: object - result: {} + result: + type: object required: - goal title: brief_step_parameters参数 @@ -53,7 +54,8 @@ bioyond_dispensing_station: - report_request - used_materials type: object - result: {} + result: + type: object required: - goal title: process_order_finish_report参数 @@ -78,7 +80,8 @@ bioyond_dispensing_station: required: - order_id type: object - result: {} + result: + type: object required: - goal title: project_order_report参数 @@ -128,7 +131,8 @@ bioyond_dispensing_station: required: - workflow_id type: object - result: {} + result: + type: object required: - goal title: workflow_sample_locations参数 @@ -144,12 +148,12 @@ bioyond_dispensing_station: temperature: temperature titration: titration goal_default: - delay_time: '600' - hold_m_name: '' + delay_time: null + hold_m_name: null liquid_material_name: NMP - speed: '400' - temperature: '40' - titration: '' + speed: null + temperature: null + titration: null handles: input: - data_key: titration @@ -165,20 +169,16 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。 properties: feedback: - properties: {} - required: [] title: BatchCreate9010VialFeedingTasks_Feedback - type: object goal: properties: delay_time: - default: '600' description: 延迟时间(秒),默认600 type: string hold_m_name: @@ -189,11 +189,9 @@ bioyond_dispensing_station: description: 10%物料的液体物料名称,默认为"NMP" type: string speed: - default: '400' description: 搅拌速度,默认400 type: string temperature: - default: '40' description: 温度(℃),默认40 type: string titration: @@ -202,21 +200,14 @@ bioyond_dispensing_station: type: string required: - titration - - hold_m_name title: BatchCreate9010VialFeedingTasks_Goal type: object result: - properties: - return_info: - description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息) - type: string - required: - - return_info title: BatchCreate9010VialFeedingTasks_Result - type: object + type: string required: - goal - title: BatchCreate9010VialFeedingTasks + title: batch_create_90_10_vial_feeding_tasks参数 type: object type: UniLabJsonCommand batch_create_diamine_solution_tasks: @@ -228,11 +219,11 @@ bioyond_dispensing_station: speed: speed temperature: temperature goal_default: - delay_time: '600' + delay_time: null liquid_material_name: NMP - solutions: '' - speed: '400' - temperature: '20' + solutions: null + speed: null + temperature: null handles: input: - data_key: solutions @@ -248,20 +239,16 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。 properties: feedback: - properties: {} - required: [] title: BatchCreateDiamineSolutionTasks_Feedback - type: object goal: properties: delay_time: - default: '600' description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒 type: string liquid_material_name: @@ -275,11 +262,9 @@ bioyond_dispensing_station: 4.5, "solvent_volume": 18}]' type: string speed: - default: '400' description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟 type: string temperature: - default: '20' description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温) type: string required: @@ -287,17 +272,11 @@ bioyond_dispensing_station: title: BatchCreateDiamineSolutionTasks_Goal type: object result: - properties: - return_info: - description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息) - type: string - required: - - return_info title: BatchCreateDiamineSolutionTasks_Result - type: object + type: string required: - goal - title: BatchCreateDiamineSolutionTasks + title: batch_create_diamine_solution_tasks参数 type: object type: UniLabJsonCommand compute_experiment_design: @@ -309,7 +288,7 @@ bioyond_dispensing_station: wt_percent: wt_percent goal_default: m_tot: '70' - ratio: '' + ratio: null titration_percent: '0.03' wt_percent: '0.25' handles: @@ -338,12 +317,8 @@ bioyond_dispensing_station: handler_key: feeding_order io_type: sink label: Feeding Order Data From Calculation Node - result: - feeding_order: feeding_order - return_info: return_info - solutions: solutions - solvents: solvents - titration: titration + placeholder_keys: {} + result: {} schema: description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。 properties: @@ -356,7 +331,7 @@ bioyond_dispensing_station: type: string ratio: description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} - type: string + type: object titration_percent: default: '0.03' description: 滴定比例(10%部分) @@ -371,14 +346,23 @@ bioyond_dispensing_station: result: properties: feeding_order: + items: {} + title: Feeding Order type: array return_info: + title: Return Info type: string solutions: + items: {} + title: Solutions type: array solvents: + additionalProperties: true + title: Solvents type: object titration: + additionalProperties: true + title: Titration type: object required: - solutions @@ -386,11 +370,11 @@ bioyond_dispensing_station: - solvents - feeding_order - return_info - title: ComputeExperimentDesign_Result + title: ComputeExperimentDesignReturn type: object required: - goal - title: ComputeExperimentDesign + title: compute_experiment_design参数 type: object type: UniLabJsonCommand create_90_10_vial_feeding_task: @@ -444,17 +428,18 @@ bioyond_dispensing_station: speed: '' temperature: '' handles: {} + placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: DispenStationVialFeed_Feedback type: object goal: + additionalProperties: false properties: delay_time: type: string @@ -502,38 +487,13 @@ bioyond_dispensing_station: type: string temperature: type: string - required: - - order_name - - percent_90_1_assign_material_name - - percent_90_1_target_weigh - - percent_90_2_assign_material_name - - percent_90_2_target_weigh - - percent_90_3_assign_material_name - - percent_90_3_target_weigh - - percent_10_1_assign_material_name - - percent_10_1_target_weigh - - percent_10_1_volume - - percent_10_1_liquid_material_name - - percent_10_2_assign_material_name - - percent_10_2_target_weigh - - percent_10_2_volume - - percent_10_2_liquid_material_name - - percent_10_3_assign_material_name - - percent_10_3_target_weigh - - percent_10_3_volume - - percent_10_3_liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name title: DispenStationVialFeed_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: DispenStationVialFeed_Result type: object required: @@ -564,17 +524,18 @@ bioyond_dispensing_station: temperature: '' volume: '' handles: {} + placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: DispenStationSolnPrep_Feedback type: object goal: + additionalProperties: false properties: delay_time: type: string @@ -594,24 +555,13 @@ bioyond_dispensing_station: type: string volume: type: string - required: - - order_name - - material_name - - target_weigh - - volume - - liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name title: DispenStationSolnPrep_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: DispenStationSolnPrep_Result type: object required: @@ -624,8 +574,8 @@ bioyond_dispensing_station: goal: {} goal_default: {} handles: {} - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务 properties: @@ -635,12 +585,6 @@ bioyond_dispensing_station: required: [] type: object result: - properties: - return_info: - description: 调度器启动结果,成功返回1,失败返回0 - type: integer - required: - - return_info title: scheduler_start结果 type: object required: @@ -654,8 +598,8 @@ bioyond_dispensing_station: target_device_id: target_device_id transfer_groups: transfer_groups goal_default: - target_device_id: '' - transfer_groups: '' + target_device_id: null + transfer_groups: null handles: {} placeholder_keys: target_device_id: unilabos_devices @@ -671,32 +615,13 @@ bioyond_dispensing_station: type: string transfer_groups: description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组 - items: - properties: - materials: - description: 物料名称(手动输入,系统将通过RPC查询验证) - type: string - target_sites: - description: 目标库位(手动输入,如"A01") - type: string - target_stack: - description: 目标堆栈名称(从列表选择) - enum: - - 堆栈1左 - - 堆栈1右 - - 站内试剂存放堆栈 - type: string - required: - - materials - - target_stack - - target_sites - type: object type: array required: - target_device_id - transfer_groups type: object - result: {} + result: + type: object required: - goal title: transfer_materials_to_reaction_station参数 @@ -709,9 +634,9 @@ bioyond_dispensing_station: check_interval: check_interval timeout: timeout goal_default: - batch_create_result: '' - check_interval: '10' - timeout: '7200' + batch_create_result: null + check_interval: 10 + timeout: 7200 handles: input: - data_key: batch_create_result @@ -727,47 +652,35 @@ bioyond_dispensing_station: handler_key: batch_reports_result io_type: sink label: Batch Order Completion Reports - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。 properties: feedback: - properties: {} - required: [] title: WaitForMultipleOrdersAndGetReports_Feedback - type: object goal: properties: batch_create_result: description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递 type: string check_interval: - default: '10' + default: 10 description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务 - type: string + type: integer timeout: - default: '7200' + default: 7200 description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout - type: string - required: - - batch_create_result + type: integer + required: [] title: WaitForMultipleOrdersAndGetReports_Goal type: object result: - properties: - return_info: - description: 'JSON格式的批量任务完成信息,包含: total(总数), completed(成功数), timeout(超时数), - error(错误数), elapsed_time(总耗时), reports(报告数组,每个元素包含order_code, - order_id, status, completion_status, report, elapsed_time)' - type: string - required: - - return_info title: WaitForMultipleOrdersAndGetReports_Result type: object required: - goal - title: WaitForMultipleOrdersAndGetReports + title: wait_for_multiple_orders_and_get_reports参数 type: object type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation diff --git a/unilabos/registry/devices/camera.yaml b/unilabos/registry/devices/camera.yaml deleted file mode 100644 index c8b9d944..00000000 --- a/unilabos/registry/devices/camera.yaml +++ /dev/null @@ -1,81 +0,0 @@ -camera: - category: - - camera - class: - action_value_mappings: - auto-destroy_node: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: 用于安全地关闭摄像头设备,释放摄像头资源,停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。 - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: destroy_node参数 - type: object - type: UniLabJsonCommand - auto-timer_callback: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧,将OpenCV格式的图像转换为ROS Image消息格式,并发布到指定的视频话题。默认以10Hz频率执行,确保视频流的连续性和实时性。 - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: timer_callback参数 - type: object - type: UniLabJsonCommand - module: unilabos.ros.nodes.presets.camera:VideoPublisher - status_types: {} - type: ros2 - config_info: [] - description: VideoPublisher摄像头设备节点,用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头(如USB摄像头、内置摄像头等),定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。 - handles: [] - icon: '' - init_param_schema: - config: - properties: - camera_index: - default: 0 - type: string - device_id: - default: video_publisher - type: string - device_uuid: - default: '' - type: string - period: - default: 0.1 - type: number - registry_name: - default: '' - type: string - resource_tracker: - type: object - required: [] - type: object - data: - properties: {} - required: [] - type: object - version: 1.0.0 diff --git a/unilabos/registry/devices/cameraSII.yaml b/unilabos/registry/devices/cameraSII.yaml index ad2df955..446357d0 100644 --- a/unilabos/registry/devices/cameraSII.yaml +++ b/unilabos/registry/devices/cameraSII.yaml @@ -18,7 +18,7 @@ cameracontroller_device: goal: properties: config: - type: string + type: object required: [] type: object result: {} @@ -42,7 +42,8 @@ cameracontroller_device: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: stop参数 @@ -50,7 +51,7 @@ cameracontroller_device: type: UniLabJsonCommand module: unilabos.devices.cameraSII.cameraUSB:CameraController status_types: - status: dict + status: Dict[str, Any] type: python config_info: [] description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ) @@ -103,5 +104,4 @@ cameracontroller_device: required: - status type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/characterization_chromatic.yaml b/unilabos/registry/devices/characterization_chromatic.yaml index f3059b58..1b33b9e2 100644 --- a/unilabos/registry/devices/characterization_chromatic.yaml +++ b/unilabos/registry/devices/characterization_chromatic.yaml @@ -141,30 +141,26 @@ hplc.agilent: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -175,7 +171,6 @@ hplc.agilent: module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver status_types: could_run: bool - data_file: String device_status: str driver_init_ok: bool finish_status: str @@ -199,10 +194,6 @@ hplc.agilent: properties: could_run: type: boolean - data_file: - items: - type: string - type: array device_status: type: string driver_init_ok: @@ -216,14 +207,13 @@ hplc.agilent: success: type: boolean required: - - status_text - - device_status - could_run + - device_status - driver_init_ok - - is_running - - success - finish_status - - data_file + - is_running + - status_text + - success type: object version: 1.0.0 hplc.agilent-zhida: @@ -236,26 +226,25 @@ hplc.agilent-zhida: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -315,21 +304,18 @@ hplc.agilent-zhida: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -341,35 +327,35 @@ hplc.agilent-zhida: feedback: {} goal: string: string + text: text goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -407,7 +393,7 @@ hplc.agilent-zhida: status: type: object required: - - status - methods + - status type: object version: 1.0.0 diff --git a/unilabos/registry/devices/characterization_optic.yaml b/unilabos/registry/devices/characterization_optic.yaml index 80dcf93d..a7c0e98d 100644 --- a/unilabos/registry/devices/characterization_optic.yaml +++ b/unilabos/registry/devices/characterization_optic.yaml @@ -120,42 +120,41 @@ raman.home_made: type: object type: UniLabJsonCommand raman_cmd: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/chinwe.yaml b/unilabos/registry/devices/chinwe.yaml index 2078d0f0..ac4d00bb 100644 --- a/unilabos/registry/devices/chinwe.yaml +++ b/unilabos/registry/devices/chinwe.yaml @@ -19,7 +19,8 @@ separator.chinwe: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: connect参数 @@ -65,135 +66,145 @@ separator.chinwe: required: - command_dict type: object - result: {} + result: + type: boolean required: - goal title: execute_command_from_outer参数 type: object type: UniLabJsonCommand motor_rotate_quarter: + feedback: {} goal: direction: 顺时针 motor_id: 4 speed: 60 + goal_default: + direction: 顺时针 + motor_id: null + speed: 60 handles: {} + placeholder_keys: {} + result: {} schema: description: 电机旋转 1/4 圈 properties: + feedback: {} goal: properties: direction: default: 顺时针 description: 旋转方向 - enum: - - 顺时针 - - 逆时针 type: string motor_id: - default: '4' description: 选择电机 (4:搅拌, 5:旋钮) - enum: - - '4' - - '5' - type: string + type: integer speed: default: 60 description: 速度 (RPM) type: integer required: - motor_id - - speed type: object + result: {} + required: + - goal + title: motor_rotate_quarter参数 + type: object type: UniLabJsonCommand motor_run_continuous: + feedback: {} goal: direction: 顺时针 motor_id: 4 speed: 60 + goal_default: + direction: 顺时针 + motor_id: null + speed: null handles: {} + placeholder_keys: {} + result: {} schema: description: 电机一直旋转 (速度模式) properties: + feedback: {} goal: properties: direction: default: 顺时针 description: 旋转方向 - enum: - - 顺时针 - - 逆时针 type: string motor_id: - default: '4' description: 选择电机 (4:搅拌, 5:旋钮) - enum: - - '4' - - '5' - type: string + type: integer speed: - default: 60 description: 速度 (RPM) type: integer required: - motor_id - speed type: object + result: {} + required: + - goal + title: motor_run_continuous参数 + type: object type: UniLabJsonCommand motor_stop: + feedback: {} goal: motor_id: 4 + goal_default: + motor_id: null handles: {} + placeholder_keys: {} + result: {} schema: description: 停止指定步进电机 properties: + feedback: {} goal: properties: motor_id: - default: '4' description: 选择电机 - enum: - - '4' - - '5' title: '注: 4=搅拌, 5=旋钮' - type: string + type: integer required: - motor_id type: object + result: {} + required: + - goal + title: motor_stop参数 + type: object type: UniLabJsonCommand pump_aspirate: + feedback: {} goal: pump_id: 1 valve_port: 1 volume: 1000 + goal_default: + pump_id: null + valve_port: null + volume: null handles: {} + placeholder_keys: {} + result: {} schema: description: 注射泵吸液 properties: + feedback: {} goal: properties: pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer valve_port: - default: '1' description: 阀门端口 - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer volume: - default: 1000 description: 吸液步数 type: integer required: @@ -201,41 +212,38 @@ separator.chinwe: - volume - valve_port type: object + result: {} + required: + - goal + title: pump_aspirate参数 + type: object type: UniLabJsonCommand pump_dispense: + feedback: {} goal: pump_id: 1 valve_port: 1 volume: 1000 + goal_default: + pump_id: null + valve_port: null + volume: null handles: {} + placeholder_keys: {} + result: {} schema: description: 注射泵排液 properties: + feedback: {} goal: properties: pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer valve_port: - default: '1' description: 阀门端口 - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer volume: - default: 1000 description: 排液步数 type: integer required: @@ -243,121 +251,152 @@ separator.chinwe: - volume - valve_port type: object + result: {} + required: + - goal + title: pump_dispense参数 + type: object type: UniLabJsonCommand pump_initialize: + feedback: {} goal: drain_port: 0 output_port: 0 pump_id: 1 speed: 10 + goal_default: + drain_port: 0 + output_port: 0 + pump_id: null + speed: 10 handles: {} + placeholder_keys: {} + result: {} schema: description: 初始化指定注射泵 properties: + feedback: {} goal: properties: drain_port: default: 0 description: 排液口索引 - type: integer + type: string output_port: default: 0 description: 输出口索引 - type: integer - pump_id: - default: '1' - description: 选择泵 - enum: - - '1' - - '2' - - '3' - title: '注: 1号泵, 2号泵, 3号泵' type: string + pump_id: + description: 选择泵 + title: '注: 1号泵, 2号泵, 3号泵' + type: integer speed: default: 10 description: 运动速度 - type: integer + type: string required: - pump_id type: object + result: {} + required: + - goal + title: pump_initialize参数 + type: object type: UniLabJsonCommand pump_valve: + feedback: {} goal: port: 1 pump_id: 1 + goal_default: + port: null + pump_id: null handles: {} + placeholder_keys: {} + result: {} schema: description: 切换指定泵的阀门端口 properties: + feedback: {} goal: properties: port: - default: '1' description: 阀门端口号 (1-8) - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer required: - pump_id - port type: object + result: {} + required: + - goal + title: pump_valve参数 + type: object type: UniLabJsonCommand wait_sensor_level: + feedback: {} goal: target_state: 有液 timeout: 30 + goal_default: + target_state: 有液 + timeout: 30 handles: {} + placeholder_keys: {} + result: {} schema: description: 等待传感器液位条件 properties: + feedback: {} goal: properties: target_state: default: 有液 description: 目标液位状态 - enum: - - 有液 - - 无液 type: string timeout: default: 30 description: 超时时间 (秒) type: integer - required: - - target_state + required: [] type: object + result: + type: boolean + required: + - goal + title: wait_sensor_level参数 + type: object type: UniLabJsonCommand wait_time: + feedback: {} goal: duration: 10 + goal_default: + duration: null handles: {} + placeholder_keys: {} + result: {} schema: description: 等待指定时间 properties: + feedback: {} goal: properties: duration: - default: 10 description: 等待时间 (秒) type: integer required: - duration type: object + result: + type: boolean + required: + - goal + title: wait_time参数 + type: object type: UniLabJsonCommand module: unilabos.devices.separator.chinwe:ChinweDevice status_types: @@ -406,8 +445,8 @@ separator.chinwe: sensor_rssi: type: integer required: + - is_connected - sensor_level - sensor_rssi - - is_connected type: object version: 2.1.0 diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index 2e9f6073..df5a3508 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -64,7 +64,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: fun_wuliao_test参数 @@ -109,7 +110,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_allpack_cmd参数 @@ -220,7 +222,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_allpack_cmd_simp参数 @@ -309,7 +312,8 @@ coincellassemblyworkstation_device: type: boolean required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_device_init_auto_start_combined参数 @@ -351,7 +355,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_device_stop参数 @@ -376,7 +381,8 @@ coincellassemblyworkstation_device: type: string required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_get_msg_cmd参数 @@ -430,7 +436,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_send_finished_cmd参数 @@ -467,7 +474,8 @@ coincellassemblyworkstation_device: - assembly_type - assembly_pressure type: object - result: {} + result: + type: boolean required: - goal title: func_pack_send_msg_cmd参数 @@ -611,7 +619,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_sendbottle_allpack_multi参数 @@ -663,31 +672,6 @@ coincellassemblyworkstation_device: title: modify_deck_name参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-qiming_coin_cell_code: feedback: {} goal: {} @@ -735,7 +719,8 @@ coincellassemblyworkstation_device: required: - fujipian_panshu type: object - result: {} + result: + type: boolean required: - goal title: qiming_coin_cell_code参数 @@ -826,25 +811,24 @@ coincellassemblyworkstation_device: sys_status: type: string required: - - sys_status - - sys_mode - - request_rec_msg_status - - request_send_msg_status - data_assembly_coin_cell_num + - data_assembly_pressure - data_assembly_time - - data_open_circuit_voltage - data_axis_x_pos - data_axis_y_pos - data_axis_z_pos - - data_pole_weight - - data_assembly_pressure - - data_electrolyte_volume - - data_coin_num - data_coin_cell_code + - data_coin_num - data_electrolyte_code - - data_glove_box_pressure + - data_electrolyte_volume - data_glove_box_o2_content + - data_glove_box_pressure - data_glove_box_water_content + - data_open_circuit_voltage + - data_pole_weight + - request_rec_msg_status + - request_send_msg_status + - sys_mode + - sys_status type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/gas_handler.yaml b/unilabos/registry/devices/gas_handler.yaml index 65218619..ad212575 100644 --- a/unilabos/registry/devices/gas_handler.yaml +++ b/unilabos/registry/devices/gas_handler.yaml @@ -50,26 +50,25 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -82,26 +81,25 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -116,32 +114,31 @@ gas_source.mock: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -232,26 +229,25 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -264,26 +260,25 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -298,32 +293,31 @@ vacuum_pump.mock: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: diff --git a/unilabos/registry/devices/hotel.yaml b/unilabos/registry/devices/hotel.yaml index 3fd0ea5b..fdcc89dd 100644 --- a/unilabos/registry/devices/hotel.yaml +++ b/unilabos/registry/devices/hotel.yaml @@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel: action_value_mappings: {} module: unilabos.devices.resource_container.container:HotelContainer status_types: - rotation: String + rotation: '' type: python config_info: [] description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。 diff --git a/unilabos/registry/devices/laiyu_liquid_test.yaml b/unilabos/registry/devices/laiyu_liquid_test.yaml index dcaa9818..6d87f429 100644 --- a/unilabos/registry/devices/laiyu_liquid_test.yaml +++ b/unilabos/registry/devices/laiyu_liquid_test.yaml @@ -22,7 +22,8 @@ xyz_stepper_controller: required: - degrees type: object - result: {} + result: + type: integer required: - goal title: degrees_to_steps参数 @@ -47,7 +48,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: emergency_stop参数 @@ -72,7 +74,10 @@ xyz_stepper_controller: type: boolean required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: enable_all_axes参数 @@ -101,7 +106,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: enable_motor参数 @@ -122,7 +128,10 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: home_all_axes参数 @@ -147,7 +156,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: home_axis参数 @@ -188,7 +198,8 @@ xyz_stepper_controller: - axis - position type: object - result: {} + result: + type: boolean required: - goal title: move_to_position参数 @@ -229,7 +240,8 @@ xyz_stepper_controller: - axis - degrees type: object - result: {} + result: + type: boolean required: - goal title: move_to_position_degrees参数 @@ -270,7 +282,8 @@ xyz_stepper_controller: - axis - revolutions type: object - result: {} + result: + type: boolean required: - goal title: move_to_position_revolutions参数 @@ -301,14 +314,17 @@ xyz_stepper_controller: default: 5000 type: integer x: - type: string + type: integer y: - type: string + type: integer z: - type: string + type: integer required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz参数 @@ -339,14 +355,17 @@ xyz_stepper_controller: default: 5000 type: integer x_deg: - type: string + type: number y_deg: - type: string + type: number z_deg: - type: string + type: number required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz_degrees参数 @@ -377,14 +396,17 @@ xyz_stepper_controller: default: 5000 type: integer x_rev: - type: string + type: number y_rev: - type: string + type: number z_rev: - type: string + type: number required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz_revolutions参数 @@ -409,7 +431,8 @@ xyz_stepper_controller: required: - revolutions type: object - result: {} + result: + type: integer required: - goal title: revolutions_to_steps参数 @@ -442,7 +465,8 @@ xyz_stepper_controller: - axis - speed type: object - result: {} + result: + type: boolean required: - goal title: set_speed_mode参数 @@ -467,7 +491,8 @@ xyz_stepper_controller: required: - steps type: object - result: {} + result: + type: number required: - goal title: steps_to_degrees参数 @@ -492,7 +517,8 @@ xyz_stepper_controller: required: - steps type: object - result: {} + result: + type: number required: - goal title: steps_to_revolutions参数 @@ -513,7 +539,10 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: stop_all_axes参数 @@ -542,7 +571,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: wait_for_completion参数 @@ -550,8 +580,7 @@ xyz_stepper_controller: type: UniLabJsonCommand module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController status_types: - all_positions: dict - motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition + all_positions: Dict[MotorAxis, MotorPosition] type: python config_info: [] description: 新XYZ控制器 @@ -574,12 +603,10 @@ xyz_stepper_controller: data: properties: all_positions: - type: object - motor_status: + additionalProperties: + type: object type: object required: - - motor_status - all_positions type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b04d6317..4d2f7288 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -8,6 +8,7 @@ liquid_handler: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -23,84 +24,38 @@ liquid_handler: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -125,6 +80,8 @@ liquid_handler: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -155,7 +112,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -230,7 +186,6 @@ liquid_handler: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -307,43 +262,21 @@ liquid_handler: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -362,41 +295,14 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} result: name: name @@ -404,11 +310,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -435,7 +341,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array resources: @@ -510,41 +415,27 @@ liquid_handler: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -574,7 +465,9 @@ liquid_handler: properties: none_keys: default: [] - type: string + items: + type: string + type: array protocol_author: type: string protocol_date: @@ -644,41 +537,19 @@ liquid_handler: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object - result: {} + result: + type: string required: - goal title: iter_tips参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-set_group: feedback: {} goal: {} @@ -698,9 +569,13 @@ liquid_handler: group_name: type: string volumes: - type: string + items: + type: number + type: array wells: - type: string + items: + type: object + type: array required: - group_name - wells @@ -712,6 +587,259 @@ liquid_handler: title: set_group参数 type: object type: UniLabJsonCommand + auto-set_liquid: + feedback: {} + goal: {} + goal_default: + liquid_names: null + volumes: null + wells: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: set_liquid的参数schema + properties: + feedback: {} + goal: + properties: + liquid_names: + items: + type: string + type: array + volumes: + items: + type: number + type: array + wells: + items: + type: object + type: array + required: + - wells + - liquid_names + - volumes + type: object + result: + $defs: + ResourceDict: + properties: + class: + description: Resource class name + title: Class + type: string + config: + additionalProperties: true + description: Resource configuration + title: Config + type: object + data: + additionalProperties: true + description: 'Resource data, eg: container liquid data' + title: Data + type: object + description: + default: '' + description: Resource description + title: Description + type: string + extra: + additionalProperties: true + description: 'Extra data, eg: slot index' + title: Extra + type: object + icon: + default: '' + description: Resource icon + title: Icon + type: string + id: + description: Resource ID + title: Id + type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string + model: + additionalProperties: true + description: Resource model + title: Model + type: object + name: + description: Resource name + title: Name + type: string + parent: + anyOf: + - $ref: '#/$defs/ResourceDict' + - type: 'null' + default: null + description: Parent resource object + parent_uuid: + anyOf: + - type: string + - type: 'null' + default: null + description: Parent resource uuid + title: Parent Uuid + pose: + $ref: '#/$defs/ResourceDictPosition' + description: Resource position + schema: + additionalProperties: true + description: Resource schema + title: Schema + type: object + type: + anyOf: + - const: device + type: string + - type: string + description: Resource type + title: Type + uuid: + description: Resource UUID + title: Uuid + type: string + required: + - id + - uuid + - name + - type + - class + - config + - data + - extra + title: ResourceDict + type: object + ResourceDictPosition: + properties: + cross_section_type: + default: rectangle + description: Cross section type + enum: + - rectangle + - circle + - rounded_rectangle + title: Cross Section Type + type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra + layout: + default: x-y + description: Resource layout + enum: + - 2d + - x-y + - z-y + - x-z + title: Layout + type: string + position: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position + position3d: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position in 3D space + rotation: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource rotation + scale: + $ref: '#/$defs/ResourceDictPositionScale' + description: Resource scale + size: + $ref: '#/$defs/ResourceDictPositionSize' + description: Resource size + title: ResourceDictPosition + type: object + ResourceDictPositionObject: + properties: + x: + default: 0.0 + description: X coordinate + title: X + type: number + y: + default: 0.0 + description: Y coordinate + title: Y + type: number + z: + default: 0.0 + description: Z coordinate + title: Z + type: number + title: ResourceDictPositionObject + type: object + ResourceDictPositionScale: + properties: + x: + default: 0.0 + description: x scale + title: X + type: number + y: + default: 0.0 + description: y scale + title: Y + type: number + z: + default: 0.0 + description: z scale + title: Z + type: number + title: ResourceDictPositionScale + type: object + ResourceDictPositionSize: + properties: + depth: + default: 0.0 + description: Depth + title: Depth + type: number + height: + default: 0.0 + description: Height + title: Height + type: number + width: + default: 0.0 + description: Width + title: Width + type: number + title: ResourceDictPositionSize + type: object + properties: + volumes: + items: + type: number + title: Volumes + type: array + wells: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Wells + type: array + required: + - wells + - volumes + title: SetLiquidReturn + type: object + required: + - goal + title: set_liquid参数 + type: object + type: UniLabJsonCommand auto-set_liquid_from_plate: feedback: {} goal: {} @@ -721,7 +849,8 @@ liquid_handler: volumes: null well_names: null handles: {} - placeholder_keys: {} + placeholder_keys: + plate: unilabos_resources result: {} schema: description: '' @@ -730,20 +859,326 @@ liquid_handler: goal: properties: liquid_names: - type: string + items: + type: string + type: array plate: - type: string + additionalProperties: false + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + additionalProperties: false + properties: + orientation: + additionalProperties: false + properties: + w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + additionalProperties: false + properties: + x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + title: plate + type: object volumes: - type: string + items: + type: number + type: array well_names: - type: string + items: + type: string + type: array required: - plate - well_names - liquid_names - volumes type: object - result: {} + result: + $defs: + ResourceDict: + properties: + class: + description: Resource class name + title: Class + type: string + config: + additionalProperties: true + description: Resource configuration + title: Config + type: object + data: + additionalProperties: true + description: 'Resource data, eg: container liquid data' + title: Data + type: object + description: + default: '' + description: Resource description + title: Description + type: string + extra: + additionalProperties: true + description: 'Extra data, eg: slot index' + title: Extra + type: object + icon: + default: '' + description: Resource icon + title: Icon + type: string + id: + description: Resource ID + title: Id + type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string + model: + additionalProperties: true + description: Resource model + title: Model + type: object + name: + description: Resource name + title: Name + type: string + parent: + anyOf: + - $ref: '#/$defs/ResourceDict' + - type: 'null' + default: null + description: Parent resource object + parent_uuid: + anyOf: + - type: string + - type: 'null' + default: null + description: Parent resource uuid + title: Parent Uuid + pose: + $ref: '#/$defs/ResourceDictPosition' + description: Resource position + schema: + additionalProperties: true + description: Resource schema + title: Schema + type: object + type: + anyOf: + - const: device + type: string + - type: string + description: Resource type + title: Type + uuid: + description: Resource UUID + title: Uuid + type: string + required: + - id + - uuid + - name + - type + - class + - config + - data + - extra + title: ResourceDict + type: object + ResourceDictPosition: + properties: + cross_section_type: + default: rectangle + description: Cross section type + enum: + - rectangle + - circle + - rounded_rectangle + title: Cross Section Type + type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra + layout: + default: x-y + description: Resource layout + enum: + - 2d + - x-y + - z-y + - x-z + title: Layout + type: string + position: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position + position3d: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position in 3D space + rotation: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource rotation + scale: + $ref: '#/$defs/ResourceDictPositionScale' + description: Resource scale + size: + $ref: '#/$defs/ResourceDictPositionSize' + description: Resource size + title: ResourceDictPosition + type: object + ResourceDictPositionObject: + properties: + x: + default: 0.0 + description: X coordinate + title: X + type: number + y: + default: 0.0 + description: Y coordinate + title: Y + type: number + z: + default: 0.0 + description: Z coordinate + title: Z + type: number + title: ResourceDictPositionObject + type: object + ResourceDictPositionScale: + properties: + x: + default: 0.0 + description: x scale + title: X + type: number + y: + default: 0.0 + description: y scale + title: Y + type: number + z: + default: 0.0 + description: z scale + title: Z + type: number + title: ResourceDictPositionScale + type: object + ResourceDictPositionSize: + properties: + depth: + default: 0.0 + description: Depth + title: Depth + type: number + height: + default: 0.0 + description: Height + title: Height + type: number + width: + default: 0.0 + description: Width + title: Width + type: number + title: ResourceDictPositionSize + type: object + properties: + plate: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Plate + type: array + volumes: + items: + type: number + title: Volumes + type: array + wells: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Wells + type: array + required: + - plate + - wells + - volumes + title: SetLiquidFromPlateReturn + type: object required: - goal title: set_liquid_from_plate参数 @@ -764,7 +1199,9 @@ liquid_handler: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object @@ -789,7 +1226,9 @@ liquid_handler: goal: properties: targets: - type: string + items: + type: object + type: array required: - targets type: object @@ -837,8 +1276,7 @@ liquid_handler: goal: use_channels: use_channels goal_default: - use_channels: - - 0 + use_channels: [] handles: {} result: name: name @@ -846,31 +1284,25 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDiscardTips_Feedback type: object goal: + additionalProperties: false properties: use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels title: LiquidHandlerDiscardTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDiscardTips_Result type: object required: @@ -889,39 +1321,13 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} result: name: name @@ -929,16 +1335,14 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -958,7 +1362,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array resources: @@ -1033,40 +1436,27 @@ liquid_handler: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -1083,32 +1473,9 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources @@ -1118,11 +1485,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -1139,7 +1506,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array tip_spots: @@ -1214,31 +1580,21 @@ liquid_handler: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -1285,21 +1641,28 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips96_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1308,6 +1671,7 @@ liquid_handler: title: offset type: object tip_rack: + additionalProperties: false properties: category: type: string @@ -1326,16 +1690,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1345,12 +1719,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1380,21 +1761,15 @@ liquid_handler: - data title: tip_rack type: object - required: - - tip_rack - - offset - - allow_nonzero_volume title: LiquidHandlerDropTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips96_Result type: object required: @@ -1417,47 +1792,31 @@ liquid_handler: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -1484,7 +1843,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array targets: @@ -1559,28 +1917,17 @@ liquid_handler: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -1608,10 +1955,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] lid: category: '' children: [] @@ -1666,19 +2010,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveLid_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1703,10 +2054,10 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array lid: + additionalProperties: false properties: category: type: string @@ -1725,16 +2076,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1744,12 +2105,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1782,16 +2150,25 @@ liquid_handler: pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1800,6 +2177,7 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -1818,16 +2196,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1837,12 +2225,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1872,28 +2267,15 @@ liquid_handler: - data title: to type: object - required: - - lid - - to - - intermediate_locations - - resource_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMoveLid_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveLid_Result type: object required: @@ -1921,10 +2303,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -1983,19 +2362,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMovePlate_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2020,20 +2406,28 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number pickup_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2042,6 +2436,7 @@ liquid_handler: title: pickup_offset type: object plate: + additionalProperties: false properties: category: type: string @@ -2060,16 +2455,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2079,12 +2484,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2117,12 +2529,19 @@ liquid_handler: put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2131,6 +2550,7 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -2149,16 +2569,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2168,12 +2598,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2203,29 +2640,15 @@ liquid_handler: - data title: to type: object - required: - - plate - - to - - intermediate_locations - - resource_offset - - pickup_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMovePlate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMovePlate_Result type: object required: @@ -2253,10 +2676,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 put_direction: '' @@ -2295,19 +2715,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveResource_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2332,16 +2759,18 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number put_direction: type: string resource: + additionalProperties: false properties: category: type: string @@ -2360,16 +2789,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2379,12 +2818,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2415,12 +2861,19 @@ liquid_handler: title: resource type: object resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2429,12 +2882,19 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2442,28 +2902,15 @@ liquid_handler: - z title: to type: object - required: - - resource - - to - - intermediate_locations - - resource_offset - - destination_offset - - pickup_distance_from_top - - pickup_direction - - drop_direction - - get_direction - - put_direction title: LiquidHandlerMoveResource_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveResource_Result type: object required: @@ -2501,24 +2948,30 @@ liquid_handler: sample_id: '' type: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveTo_Feedback type: object goal: + additionalProperties: false properties: channel: maximum: 2147483647 minimum: -2147483648 type: integer dis_to_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number well: + additionalProperties: false properties: category: type: string @@ -2537,16 +2990,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2556,12 +3019,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2591,21 +3061,15 @@ liquid_handler: - data title: well type: object - required: - - well - - dis_to_top - - channel title: LiquidHandlerMoveTo_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveTo_Result type: object required: @@ -2620,32 +3084,9 @@ liquid_handler: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} result: name: name @@ -2653,11 +3094,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -2672,7 +3113,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array tip_spots: @@ -2747,30 +3187,21 @@ liquid_handler: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -2815,19 +3246,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips96_Feedback type: object goal: + additionalProperties: false properties: offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2836,6 +3274,7 @@ liquid_handler: title: offset type: object tip_rack: + additionalProperties: false properties: category: type: string @@ -2854,16 +3293,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2873,12 +3322,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2908,20 +3364,15 @@ liquid_handler: - data title: tip_rack type: object - required: - - tip_rack - - offset title: LiquidHandlerPickUpTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips96_Result type: object required: @@ -2946,48 +3397,18 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -3014,11 +3435,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -3026,8 +3447,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -3057,7 +3476,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -3132,7 +3550,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -3143,8 +3560,6 @@ liquid_handler: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -3152,6 +3567,7 @@ liquid_handler: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -3170,16 +3586,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3189,12 +3615,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3224,31 +3657,15 @@ liquid_handler: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -3273,48 +3690,18 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -3339,16 +3726,18 @@ liquid_handler: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -3356,8 +3745,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -3387,7 +3774,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -3462,7 +3848,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -3473,8 +3858,6 @@ liquid_handler: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -3482,6 +3865,7 @@ liquid_handler: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -3500,16 +3884,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3519,12 +3913,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3554,31 +3955,15 @@ liquid_handler: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -3593,8 +3978,7 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - use_channels: - - 0 + use_channels: [] handles: {} result: name: name @@ -3602,34 +3986,27 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerReturnTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels - - allow_nonzero_volume title: LiquidHandlerReturnTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerReturnTips_Result type: object required: @@ -3650,27 +4027,23 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerReturnTips96_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean - required: - - allow_nonzero_volume title: LiquidHandlerReturnTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerReturnTips96_Result type: object required: @@ -3737,17 +4110,22 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerStamp_Feedback type: object goal: + additionalProperties: false properties: aspiration_flow_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number dispense_flow_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number source: + additionalProperties: false properties: category: type: string @@ -3766,16 +4144,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3785,12 +4173,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3821,6 +4216,7 @@ liquid_handler: title: source type: object target: + additionalProperties: false properties: category: type: string @@ -3839,16 +4235,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3858,12 +4264,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3894,24 +4307,18 @@ liquid_handler: title: target type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - source - - target - - volume - - aspiration_flow_rate - - dispense_flow_rate title: LiquidHandlerStamp_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerStamp_Result type: object required: @@ -3944,20 +4351,22 @@ liquid_handler: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -3970,31 +4379,27 @@ liquid_handler: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4002,10 +4407,6 @@ liquid_handler: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -4038,96 +4439,27 @@ liquid_handler: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -4160,16 +4492,18 @@ liquid_handler: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -4185,8 +4519,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -4204,6 +4536,8 @@ liquid_handler: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -4236,7 +4570,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -4311,7 +4644,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -4388,7 +4720,6 @@ liquid_handler: - pose - config - data - title: targets type: object type: array tip_racks: @@ -4463,50 +4794,23 @@ liquid_handler: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -4525,12 +4829,12 @@ liquid_handler: config: properties: backend: - type: string + type: object channel_num: default: 8 type: integer deck: - type: string + type: object simulator: default: false type: boolean @@ -4573,6 +4877,8 @@ liquid_handler.biomek: goal: properties: bind_location: + additionalProperties: + type: number type: object bind_parent_id: type: string @@ -4612,6 +4918,36 @@ liquid_handler.biomek: title: create_resource参数 type: object type: UniLabJsonCommand + auto-deserialize: + feedback: {} + goal: {} + goal_default: + allow_marshal: false + data: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: deserialize的参数schema + properties: + feedback: {} + goal: + properties: + allow_marshal: + default: false + type: boolean + data: + type: object + required: + - data + type: object + result: + type: object + required: + - goal + title: deserialize参数 + type: object + type: UniLabJsonCommand auto-instrument_setup_biomek: feedback: {} goal: {} @@ -4678,8 +5014,7 @@ liquid_handler.biomek: protocol_type: protocol_type protocol_version: protocol_version goal_default: - none_keys: - - '' + none_keys: [] protocol_author: '' protocol_date: '' protocol_description: '' @@ -4687,16 +5022,18 @@ liquid_handler.biomek: protocol_type: '' protocol_version: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerProtocolCreation_Feedback type: object goal: + additionalProperties: false properties: none_keys: items: @@ -4714,22 +5051,13 @@ liquid_handler.biomek: type: string protocol_version: type: string - required: - - protocol_name - - protocol_description - - protocol_version - - protocol_author - - protocol_date - - protocol_type - - none_keys title: LiquidHandlerProtocolCreation_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: LiquidHandlerProtocolCreation_Result type: object required: @@ -4756,34 +5084,33 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerIncubateBiomek_Feedback type: object goal: + additionalProperties: false properties: time: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - time title: LiquidHandlerIncubateBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerIncubateBiomek_Result type: object required: @@ -4794,8 +5121,10 @@ liquid_handler.biomek: move_biomek: feedback: {} goal: - source: sources - target: targets + source: source + sources: sources + target: target + targets: targets goal_default: sources: '' targets: '' @@ -4812,36 +5141,33 @@ liquid_handler.biomek: data_type: resource handler_key: targets label: targets + placeholder_keys: {} result: - name: name + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveBiomek_Feedback type: object goal: + additionalProperties: false properties: sources: type: string targets: type: string - required: - - sources - - targets title: LiquidHandlerMoveBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveBiomek_Result type: object required: @@ -4870,16 +5196,19 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerOscillateBiomek_Feedback type: object goal: + additionalProperties: false properties: rpm: maximum: 2147483647 @@ -4889,20 +5218,15 @@ liquid_handler.biomek: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - rpm - - time title: LiquidHandlerOscillateBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerOscillateBiomek_Result type: object required: @@ -4915,26 +5239,25 @@ liquid_handler.biomek: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -4945,9 +5268,13 @@ liquid_handler.biomek: transfer_biomek: feedback: {} goal: + aspirate_technique: aspirate_technique aspirate_techniques: aspirate_techniques + dispense_technique: dispense_technique dispense_techniques: dispense_techniques + source: source sources: sources + target: target targets: targets tip_rack: tip_rack volume: volume @@ -4986,16 +5313,19 @@ liquid_handler.biomek: data_type: resource handler_key: targets_out label: targets - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransferBiomek_Feedback type: object goal: + additionalProperties: false properties: aspirate_technique: type: string @@ -5008,25 +5338,18 @@ liquid_handler.biomek: tip_rack: type: string volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - sources - - targets - - tip_rack - - volume - - aspirate_technique - - dispense_technique title: LiquidHandlerTransferBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransferBiomek_Result type: object required: @@ -5059,96 +5382,27 @@ liquid_handler.biomek: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -5183,16 +5437,18 @@ liquid_handler.biomek: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -5208,8 +5464,6 @@ liquid_handler.biomek: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -5227,6 +5481,8 @@ liquid_handler.biomek: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5259,7 +5515,6 @@ liquid_handler.biomek: - x - y - z - title: offsets type: object type: array sources: @@ -5334,7 +5589,6 @@ liquid_handler.biomek: - pose - config - data - title: sources type: object type: array spread: @@ -5411,7 +5665,6 @@ liquid_handler.biomek: - pose - config - data - title: targets type: object type: array tip_racks: @@ -5486,50 +5739,23 @@ liquid_handler.biomek: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -5539,7 +5765,7 @@ liquid_handler.biomek: type: LiquidHandlerTransfer module: unilabos.devices.liquid_handling.biomek:LiquidHandlerBiomek status_types: - success: String + success: '' type: python config_info: [] description: Biomek液体处理器设备,基于pylabrobot控制 @@ -5568,6 +5794,7 @@ liquid_handler.laiyu: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -5583,84 +5810,38 @@ liquid_handler.laiyu: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -5685,6 +5866,8 @@ liquid_handler.laiyu: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5715,7 +5898,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -5790,7 +5972,6 @@ liquid_handler.laiyu: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -5867,43 +6048,21 @@ liquid_handler.laiyu: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -5919,57 +6078,33 @@ liquid_handler.laiyu: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -5996,7 +6131,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array resources: @@ -6071,41 +6205,27 @@ liquid_handler.laiyu: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -6148,54 +6268,93 @@ liquid_handler.laiyu: goal: properties: asp_flow_rates: - type: string + items: + type: number + type: array asp_vols: - type: string + anyOf: + - items: + type: number + type: array + - type: number blow_out_air_volume: - type: string + items: + type: number + type: array delays: - type: string + items: + type: integer + type: array dis_flow_rates: - type: string + items: + type: number + type: array dis_vols: - type: string + anyOf: + - items: + type: number + type: array + - type: number is_96_well: default: false type: boolean liquid_height: - type: string + items: + type: number + type: array mix_liquid_height: - type: string + type: number mix_rate: - type: string + type: integer mix_stage: default: none + enum: + - none + - before + - after + - both type: string mix_times: - type: string + items: + type: integer + type: array mix_vol: - type: string + type: integer none_keys: default: [] items: type: string type: array offsets: - type: string + items: + type: object + type: array sources: - type: string + items: + type: object + type: array spread: default: wide + enum: + - wide + - tight + - custom type: string targets: - type: string + items: + type: object + type: array tip_racks: - type: string + items: + type: object + type: array touch_tip: default: false type: boolean use_channels: - type: string + items: + type: integer + type: array required: - sources - targets @@ -6217,60 +6376,35 @@ liquid_handler.laiyu: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -6290,7 +6424,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array resources: @@ -6365,40 +6498,27 @@ liquid_handler.laiyu: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -6415,45 +6535,24 @@ liquid_handler.laiyu: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -6470,7 +6569,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array tip_spots: @@ -6545,31 +6643,21 @@ liquid_handler.laiyu: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -6592,49 +6680,32 @@ liquid_handler.laiyu: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} placeholder_keys: targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -6661,7 +6732,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array targets: @@ -6736,28 +6806,17 @@ liquid_handler.laiyu: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -6772,45 +6831,24 @@ liquid_handler.laiyu: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -6825,7 +6863,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array tip_spots: @@ -6900,30 +6937,21 @@ liquid_handler.laiyu: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -6978,6 +7006,7 @@ liquid_handler.prcxi: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -6993,84 +7022,38 @@ liquid_handler.prcxi: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -7095,6 +7078,8 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -7125,7 +7110,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -7200,7 +7184,6 @@ liquid_handler.prcxi: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -7277,43 +7260,21 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -7329,57 +7290,33 @@ liquid_handler.prcxi: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -7406,7 +7343,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array resources: @@ -7481,41 +7417,27 @@ liquid_handler.prcxi: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -7646,11 +7568,14 @@ liquid_handler.prcxi: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object - result: {} + result: + type: string required: - goal title: iter_tips参数 @@ -7689,31 +7614,6 @@ liquid_handler.prcxi: title: move_to参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-run_protocol: feedback: {} goal: {} @@ -7824,7 +7724,9 @@ liquid_handler.prcxi: goal: properties: targets: - type: string + items: + type: object + type: array required: - targets type: object @@ -7870,41 +7772,39 @@ liquid_handler.prcxi: discard_tips: feedback: {} goal: + allow_nonzero_volume: allow_nonzero_volume + offsets: offsets use_channels: use_channels goal_default: - use_channels: - - 0 + use_channels: [] handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDiscardTips_Feedback type: object goal: + additionalProperties: false properties: use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels title: LiquidHandlerDiscardTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDiscardTips_Result type: object required: @@ -7917,63 +7817,38 @@ liquid_handler.prcxi: goal: blow_out_air_volume: blow_out_air_volume flow_rates: flow_rates + liquid_height: liquid_height offsets: offsets resources: resources spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -7993,7 +7868,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array resources: @@ -8068,40 +7942,27 @@ liquid_handler.prcxi: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -8118,45 +7979,24 @@ liquid_handler.prcxi: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -8173,7 +8013,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array tip_spots: @@ -8248,31 +8087,21 @@ liquid_handler.prcxi: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -8295,49 +8124,32 @@ liquid_handler.prcxi: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} placeholder_keys: targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -8364,7 +8176,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array targets: @@ -8439,28 +8250,17 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -8476,6 +8276,7 @@ liquid_handler.prcxi: get_direction: get_direction intermediate_locations: intermediate_locations pickup_direction: pickup_direction + pickup_distance_from_top: pickup_distance_from_top pickup_offset: pickup_offset plate: plate put_direction: put_direction @@ -8488,10 +8289,7 @@ liquid_handler.prcxi: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -8548,24 +8346,32 @@ liquid_handler.prcxi: plate: unilabos_resources to: unilabos_resources result: - name: name + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMovePlate_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8590,20 +8396,28 @@ liquid_handler.prcxi: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number pickup_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8612,6 +8426,7 @@ liquid_handler.prcxi: title: pickup_offset type: object plate: + additionalProperties: false properties: category: type: string @@ -8630,16 +8445,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8649,12 +8474,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8687,12 +8519,19 @@ liquid_handler.prcxi: put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8701,6 +8540,7 @@ liquid_handler.prcxi: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -8719,16 +8559,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8738,12 +8588,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8773,29 +8630,15 @@ liquid_handler.prcxi: - data title: to type: object - required: - - plate - - to - - intermediate_locations - - resource_offset - - pickup_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMovePlate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMovePlate_Result type: object required: @@ -8810,45 +8653,24 @@ liquid_handler.prcxi: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -8863,7 +8685,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array tip_spots: @@ -8938,30 +8759,21 @@ liquid_handler.prcxi: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -8986,48 +8798,18 @@ liquid_handler.prcxi: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -9052,16 +8834,18 @@ liquid_handler.prcxi: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -9069,8 +8853,6 @@ liquid_handler.prcxi: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -9100,7 +8882,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array sources: @@ -9175,7 +8956,6 @@ liquid_handler.prcxi: - pose - config - data - title: sources type: object type: array spread: @@ -9186,8 +8966,6 @@ liquid_handler.prcxi: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -9195,6 +8973,7 @@ liquid_handler.prcxi: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -9213,16 +8992,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9232,12 +9021,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9267,31 +9063,15 @@ liquid_handler.prcxi: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -9306,30 +9086,9 @@ liquid_handler.prcxi: volumes: volumes wells: wells goal_default: - liquid_names: - - '' - volumes: - - 0.0 - wells: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_names: [] + volumes: [] + wells: [] handles: input: - data_key: wells @@ -9345,16 +9104,17 @@ liquid_handler.prcxi: label: 已设定液体孔 placeholder_keys: wells: unilabos_resources - result: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerSetLiquid_Feedback type: object goal: + additionalProperties: false properties: liquid_names: items: @@ -9436,21 +9196,15 @@ liquid_handler.prcxi: - pose - config - data - title: wells type: object type: array - required: - - wells - - liquid_names - - volumes title: LiquidHandlerSetLiquid_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: LiquidHandlerSetLiquid_Result type: object required: @@ -9503,6 +9257,7 @@ liquid_handler.prcxi: type: string type: array plate: + additionalProperties: false properties: category: type: string @@ -9521,16 +9276,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9540,12 +9305,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9562,17 +9334,6 @@ liquid_handler.prcxi: type: string type: type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data title: plate type: object volumes: @@ -9626,6 +9387,11 @@ liquid_handler.prcxi: description: Resource ID title: Id type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string model: additionalProperties: true description: Resource model @@ -9689,6 +9455,14 @@ liquid_handler.prcxi: - rounded_rectangle title: Cross Section Type type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra layout: default: x-y description: Resource layout @@ -9809,39 +9583,22 @@ liquid_handler.prcxi: goal: tip_racks: tip_racks goal_default: - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + tip_racks: [] handles: {} placeholder_keys: tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerSetTipRack_Feedback type: object goal: + additionalProperties: false properties: tip_racks: items: @@ -9915,22 +9672,17 @@ liquid_handler.prcxi: - pose - config - data - title: tip_racks type: object type: array - required: - - tip_racks title: LiquidHandlerSetTipRack_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerSetTipRack_Result type: object required: @@ -9963,20 +9715,22 @@ liquid_handler.prcxi: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -9989,31 +9743,27 @@ liquid_handler.prcxi: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -10021,10 +9771,6 @@ liquid_handler.prcxi: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -10057,96 +9803,27 @@ liquid_handler.prcxi: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -10179,16 +9856,18 @@ liquid_handler.prcxi: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -10204,8 +9883,6 @@ liquid_handler.prcxi: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -10223,6 +9900,8 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -10255,7 +9934,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array sources: @@ -10330,7 +10008,6 @@ liquid_handler.prcxi: - pose - config - data - title: sources type: object type: array spread: @@ -10407,7 +10084,6 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array tip_racks: @@ -10482,50 +10158,23 @@ liquid_handler.prcxi: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -10597,11 +10246,13 @@ liquid_handler.revvity: action_value_mappings: run: feedback: + gantt: gantt status: status goal: + file_path: file_path params: params resource: resource - wf_name: file_path + wf_name: wf_name goal_default: params: '' resource: @@ -10626,27 +10277,29 @@ liquid_handler.revvity: type: '' wf_name: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: gantt: type: string status: type: string - required: - - status - - gantt title: WorkStationRun_Feedback type: object goal: + additionalProperties: false properties: params: type: string resource: + additionalProperties: false properties: category: type: string @@ -10665,16 +10318,26 @@ liquid_handler.revvity: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -10684,12 +10347,19 @@ liquid_handler.revvity: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -10721,21 +10391,15 @@ liquid_handler.revvity: type: object wf_name: type: string - required: - - wf_name - - params - - resource title: WorkStationRun_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: WorkStationRun_Result type: object required: @@ -10764,7 +10428,7 @@ liquid_handler.revvity: success: type: boolean required: - - success - status + - success type: object version: 1.0.0 diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml index ea6bedc4..4f3b972a 100644 --- a/unilabos/registry/devices/neware_battery_test_system.yaml +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -5,31 +5,6 @@ neware_battery_test_system: - battery_test class: action_value_mappings: - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-print_status_summary: feedback: {} goal: {} @@ -66,7 +41,8 @@ neware_battery_test_system: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: test_connection参数 @@ -77,9 +53,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 调试方法:显示所有资源的实际名称 properties: @@ -89,19 +64,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 资源调试信息 - type: string - success: - description: 是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: debug_resource_names参数 type: object type: UniLabJsonCommand export_status_json: @@ -111,9 +77,8 @@ neware_battery_test_system: goal_default: filepath: bts_status.json handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 导出当前状态数据到JSON文件 properties: @@ -127,19 +92,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 导出操作结果信息 - type: string - success: - description: 导出是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: export_status_json参数 type: object type: UniLabJsonCommand get_device_summary: @@ -181,10 +137,8 @@ neware_battery_test_system: goal_default: plate_num: null handles: {} - result: - plate_data: plate_data - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 获取指定盘或所有盘的状态信息 properties: @@ -193,29 +147,14 @@ neware_battery_test_system: properties: plate_num: description: 盘号 (1 或 2),如果为null则返回所有盘的状态 - maximum: 2 - minimum: 1 type: integer required: [] type: object result: - properties: - plate_data: - description: 盘状态数据(单盘或所有盘) - type: object - return_info: - description: 操作结果信息 - type: string - success: - description: 查询是否成功 - type: boolean - required: - - return_info - - success - - plate_data type: object required: - goal + title: get_plate_status参数 type: object type: UniLabJsonCommand print_status_summary_action: @@ -223,9 +162,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 打印通道状态摘要信息到控制台 properties: @@ -235,28 +173,21 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 打印操作结果信息 - type: string - success: - description: 打印是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: print_status_summary_action参数 type: object type: UniLabJsonCommand query_plate_action: feedback: {} goal: - string: plate_id + plate_id: plate_id + string: string goal_default: string: '' handles: {} + placeholder_keys: {} result: return_info: return_info success: success @@ -264,27 +195,23 @@ neware_battery_test_system: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -298,13 +225,11 @@ neware_battery_test_system: csv_path: string output_dir: string goal_default: - csv_path: '' + csv_path: null output_dir: . handles: {} - result: - return_info: return_info - submitted_count: submitted_count - success: success + placeholder_keys: {} + result: {} schema: description: 从CSV文件批量提交Neware测试任务 properties: @@ -315,31 +240,17 @@ neware_battery_test_system: description: 输入CSV文件的绝对路径 type: string output_dir: + default: . description: 输出目录(用于存储XML和备份文件),默认当前目录 type: string required: - csv_path type: object result: - properties: - return_info: - description: 执行结果详细信息 - type: string - submitted_count: - description: 成功提交的任务数量 - type: integer - success: - description: 是否成功 - type: boolean - total_count: - description: CSV文件中的总行数 - type: integer - required: - - return_info - - success type: object required: - goal + title: submit_from_csv参数 type: object type: UniLabJsonCommand test_connection_action: @@ -347,9 +258,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 测试与电池测试系统的TCP连接 properties: @@ -359,19 +269,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 连接测试结果信息 - type: string - success: - description: 连接测试是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: test_connection_action参数 type: object type: UniLabJsonCommand upload_backup_to_oss: @@ -392,12 +293,8 @@ neware_battery_test_system: handler_key: uploaded_files io_type: sink label: Uploaded Files (with standard flow info) - result: - failed_files: failed_files - return_info: return_info - success: success - total_count: total_count - uploaded_count: uploaded_count + placeholder_keys: {} + result: {} schema: description: 上传备份文件到阿里云OSS properties: @@ -417,65 +314,17 @@ neware_battery_test_system: required: [] type: object result: - properties: - failed_files: - description: 上传失败的文件名列表 - items: - type: string - type: array - return_info: - description: 上传操作结果信息 - type: string - success: - description: 上传是否成功 - type: boolean - total_count: - description: 总文件数 - type: integer - uploaded_count: - description: 成功上传的文件数 - type: integer - uploaded_files: - description: 成功上传的文件详情列表 - items: - properties: - Battery_Code: - description: 电池编码 - type: string - Electrolyte_Code: - description: 电解液编码 - type: string - filename: - description: 文件名 - type: string - url: - description: OSS下载链接 - type: string - required: - - filename - - url - - Battery_Code - - Electrolyte_Code - type: object - type: array - required: - - return_info - - success - - uploaded_count - - total_count - - failed_files - - uploaded_files type: object required: - goal + title: upload_backup_to_oss参数 type: object type: UniLabJsonCommand module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem status_types: - channel_status: dict - connection_info: dict + channel_status: Dict[int, Dict] + connection_info: Dict[str, str] device_summary: dict - plate_status: dict status: str total_channels: int type: python @@ -517,23 +366,24 @@ neware_battery_test_system: data: properties: channel_status: + additionalProperties: + type: object type: object connection_info: + additionalProperties: + type: string type: object device_summary: type: object - plate_status: - type: object status: type: string total_channels: type: integer required: - - status - channel_status - connection_info - - total_channels - - plate_status - device_summary + - status + - total_channels type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opcua_example.yaml b/unilabos/registry/devices/opcua_example.yaml index a7e6b4e3..271fd682 100644 --- a/unilabos/registry/devices/opcua_example.yaml +++ b/unilabos/registry/devices/opcua_example.yaml @@ -142,8 +142,7 @@ opcua_example: type: object type: UniLabJsonCommand module: unilabos.device_comms.opcua_client.client:OpcUaClient - status_types: - node_value: String + status_types: {} type: python config_info: [] description: null @@ -167,10 +166,7 @@ opcua_example: - url type: object data: - properties: - node_value: - type: string - required: - - node_value + properties: {} + required: [] type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opsky_ATR30007.yaml b/unilabos/registry/devices/opsky_ATR30007.yaml index ee8b8871..a3fa7df8 100644 --- a/unilabos/registry/devices/opsky_ATR30007.yaml +++ b/unilabos/registry/devices/opsky_ATR30007.yaml @@ -80,7 +80,8 @@ opsky_ATR30007: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: run_once参数 diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index 3085c823..c1290bea 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -100,42 +100,41 @@ rotavap.one: type: object type: UniLabJsonCommand set_timer: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -250,9 +249,13 @@ separator.homemade: feedback: status: status goal: + event: event settling_time: settling_time stir_speed: stir_speed - stir_time: stir_time, + stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: event: '' settling_time: '' @@ -281,34 +284,42 @@ separator.homemade: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -327,16 +338,26 @@ separator.homemade: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -346,12 +367,19 @@ separator.homemade: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -381,17 +409,10 @@ separator.homemade: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -399,10 +420,6 @@ separator.homemade: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -418,36 +435,34 @@ separator.homemade: goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/post_process_station.yaml b/unilabos/registry/devices/post_process_station.yaml index be42bad4..1614a2c3 100644 --- a/unilabos/registry/devices/post_process_station.yaml +++ b/unilabos/registry/devices/post_process_station.yaml @@ -28,31 +28,6 @@ post_process_station: title: load_config参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-print_cache_stats: feedback: {} goal: {} @@ -104,42 +79,41 @@ post_process_station: type: object type: UniLabJsonCommand disconnect: - feedback: {} + feedback: + status: status goal: - command: {} + command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -149,42 +123,41 @@ post_process_station: type: SendCmd read_node: feedback: - result: result + status: status goal: - command: node_name + command: command + node_name: node_name goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -283,17 +256,19 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessTriggerClean_Feedback type: object goal: + additionalProperties: false properties: acetone_inner_wall_cleaning_count: maximum: 2147483647 minimum: -2147483648 type: integer acetone_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -304,6 +279,8 @@ post_process_station: minimum: -2147483648 type: integer acetone_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -322,6 +299,8 @@ post_process_station: minimum: -2147483648 type: integer acetone_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -348,6 +327,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -358,6 +339,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -376,6 +359,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -394,6 +379,8 @@ post_process_station: minimum: -2147483648 type: integer water_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -404,6 +391,8 @@ post_process_station: minimum: -2147483648 type: integer water_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -422,6 +411,8 @@ post_process_station: minimum: -2147483648 type: integer water_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -431,55 +422,13 @@ post_process_station: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - nmp_outer_wall_cleaning_injection - - nmp_outer_wall_cleaning_count - - nmp_outer_wall_cleaning_wait_time - - nmp_outer_wall_cleaning_waste_time - - nmp_inner_wall_cleaning_injection - - nmp_inner_wall_cleaning_count - - nmp_pump_cleaning_suction_count - - nmp_inner_wall_cleaning_waste_time - - nmp_stirrer_cleaning_injection - - nmp_stirrer_cleaning_count - - nmp_stirrer_cleaning_wait_time - - nmp_stirrer_cleaning_waste_time - - water_outer_wall_cleaning_injection - - water_outer_wall_cleaning_count - - water_outer_wall_cleaning_wait_time - - water_outer_wall_cleaning_waste_time - - water_inner_wall_cleaning_injection - - water_inner_wall_cleaning_count - - water_pump_cleaning_suction_count - - water_inner_wall_cleaning_waste_time - - water_stirrer_cleaning_injection - - water_stirrer_cleaning_count - - water_stirrer_cleaning_wait_time - - water_stirrer_cleaning_waste_time - - acetone_outer_wall_cleaning_injection - - acetone_outer_wall_cleaning_count - - acetone_outer_wall_cleaning_wait_time - - acetone_outer_wall_cleaning_waste_time - - acetone_inner_wall_cleaning_injection - - acetone_inner_wall_cleaning_count - - acetone_pump_cleaning_suction_count - - acetone_inner_wall_cleaning_waste_time - - acetone_stirrer_cleaning_injection - - acetone_stirrer_cleaning_count - - acetone_stirrer_cleaning_wait_time - - acetone_stirrer_cleaning_waste_time - - pipe_blowing_time - - injection_pump_forward_empty_suction_count - - injection_pump_reverse_empty_suction_count - - filtration_liquid_selection title: PostProcessTriggerClean_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessTriggerClean_Result type: object required: @@ -502,11 +451,11 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessGrab_Feedback type: object goal: + additionalProperties: false properties: raw_tank_number: maximum: 2147483647 @@ -516,17 +465,13 @@ post_process_station: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - reaction_tank_number - - raw_tank_number title: PostProcessGrab_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessGrab_Result type: object required: @@ -573,13 +518,15 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessTriggerPostPro_Feedback type: object goal: + additionalProperties: false properties: atomization_fast_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number atomization_pressure_kpa: maximum: 2147483647 @@ -594,8 +541,12 @@ post_process_station: minimum: -2147483648 type: integer first_wash_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number initial_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number injection_pump_push_speed: maximum: 2147483647 @@ -622,32 +573,20 @@ post_process_station: minimum: -2147483648 type: integer second_wash_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number wash_slow_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - atomization_fast_speed - - wash_slow_speed - - injection_pump_suction_speed - - injection_pump_push_speed - - raw_liquid_suction_count - - first_wash_water_amount - - second_wash_water_amount - - first_powder_mixing_tim - - second_powder_mixing_time - - first_powder_wash_count - - second_powder_wash_count - - initial_water_amount - - pre_filtration_mixing_time - - atomization_pressure_kpa title: PostProcessTriggerPostPro_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessTriggerPostPro_Result type: object required: @@ -669,30 +608,26 @@ post_process_station: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -702,8 +637,7 @@ post_process_station: type: SendCmd module: unilabos.devices.workstation.post_process.post_process:OpcUaClient status_types: - cache_stats: dict - node_value: String + cache_stats: Dict[str, Any] type: python config_info: [] description: 后处理站 @@ -718,7 +652,9 @@ post_process_station: config_path: type: string deck: - type: string + anyOf: + - type: object + - type: object password: type: string subscription_interval: @@ -738,10 +674,7 @@ post_process_station: properties: cache_stats: type: object - node_value: - type: string required: - - node_value - cache_stats type: object version: 1.0.0 diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index 40fd9d3e..95a082d5 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -136,36 +136,36 @@ solenoid_valve: set_valve_position: feedback: {} goal: - string: position + position: position + string: string goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -278,26 +278,25 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -310,26 +309,25 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -422,6 +420,27 @@ syringe_pump_with_valve.runze.SY03B-T06: title: initialize参数 type: object type: UniLabJsonCommand + auto-list: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: list的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: list参数 + type: object + type: UniLabJsonCommand auto-pull_plunger: feedback: {} goal: {} @@ -695,7 +714,10 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: position: - type: string + anyOf: + - type: integer + - type: string + - type: number required: - position type: object @@ -720,7 +742,9 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: velocity: - type: string + anyOf: + - type: integer + - type: string required: - velocity type: object @@ -780,13 +804,13 @@ syringe_pump_with_valve.runze.SY03B-T06: status_types: max_velocity: float mode: int - plunger_position: String + plunger_position: '' position: float status: str valve_position: str - velocity_end: String - velocity_grade: String - velocity_init: String + velocity_end: '' + velocity_grade: '' + velocity_init: '' type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -885,15 +909,15 @@ syringe_pump_with_valve.runze.SY03B-T06: velocity_init: type: string required: - - status - - mode - max_velocity + - mode + - plunger_position + - position + - status + - valve_position + - velocity_end - velocity_grade - velocity_init - - velocity_end - - valve_position - - position - - plunger_position type: object version: 1.0.0 syringe_pump_with_valve.runze.SY03B-T08: @@ -943,6 +967,27 @@ syringe_pump_with_valve.runze.SY03B-T08: title: initialize参数 type: object type: UniLabJsonCommand + auto-list: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: list的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: list参数 + type: object + type: UniLabJsonCommand auto-pull_plunger: feedback: {} goal: {} @@ -1216,7 +1261,10 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: position: - type: string + anyOf: + - type: integer + - type: string + - type: number required: - position type: object @@ -1241,7 +1289,9 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: velocity: - type: string + anyOf: + - type: integer + - type: string required: - velocity type: object @@ -1301,13 +1351,13 @@ syringe_pump_with_valve.runze.SY03B-T08: status_types: max_velocity: float mode: int - plunger_position: String + plunger_position: '' position: float status: str valve_position: str - velocity_end: String - velocity_grade: String - velocity_init: String + velocity_end: '' + velocity_grade: '' + velocity_init: '' type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -1422,14 +1472,14 @@ syringe_pump_with_valve.runze.SY03B-T08: velocity_init: type: string required: - - status - - mode - max_velocity + - mode + - plunger_position + - position + - status + - valve_position + - velocity_end - velocity_grade - velocity_init - - velocity_end - - valve_position - - position - - plunger_position type: object version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 8b4622dc..1372140d 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -13,12 +13,13 @@ reaction_station.bioyond: start_point: start_point start_step_key: start_step_key goal_default: - duration: 0 + duration: null end_point: 0 end_step_key: '' start_point: 0 start_step_key: '' handles: {} + placeholder_keys: {} result: {} schema: description: 添加时间约束 - 在两个工作流之间添加时间约束 @@ -30,23 +31,19 @@ reaction_station.bioyond: description: 时间(秒) type: integer end_point: - default: Start + default: 0 description: 终点计时点 (Start=开始前, End=结束后) - enum: - - Start - - End - type: string + type: integer end_step_key: + default: '' description: 终点步骤Key (可选, 默认为空则自动选择) type: string start_point: - default: Start + default: 0 description: 起点计时点 (Start=开始前, End=结束后) - enum: - - Start - - End - type: string + type: integer start_step_key: + default: '' description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) type: string required: @@ -98,7 +95,8 @@ reaction_station.bioyond: required: - json_str type: object - result: {} + result: + type: object required: - goal title: create_order参数 @@ -125,7 +123,8 @@ reaction_station.bioyond: required: - workflow_ids type: object - result: {} + result: + type: object required: - goal title: hard_delete_merged_workflows参数 @@ -150,7 +149,8 @@ reaction_station.bioyond: required: - json_str type: object - result: {} + result: + type: object required: - goal title: merge_workflow_with_parameters参数 @@ -175,7 +175,8 @@ reaction_station.bioyond: required: - report_request type: object - result: {} + result: + type: object required: - goal title: process_temperature_cutoff_report参数 @@ -200,7 +201,12 @@ reaction_station.bioyond: required: - web_workflow_json type: object - result: {} + result: + items: + additionalProperties: + type: string + type: object + type: array required: - goal title: process_web_workflows参数 @@ -229,7 +235,8 @@ reaction_station.bioyond: - reactor_id - temperature type: object - result: {} + result: + type: string required: - goal title: set_reactor_temperature参数 @@ -254,7 +261,8 @@ reaction_station.bioyond: required: - preintake_id type: object - result: {} + result: + type: object required: - goal title: skip_titration_steps参数 @@ -275,7 +283,8 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: sync_workflow_sequence_from_bioyond参数 @@ -307,7 +316,8 @@ reaction_station.bioyond: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: wait_for_multiple_orders_and_get_reports参数 @@ -359,7 +369,8 @@ reaction_station.bioyond: required: - workflow_id type: object - result: {} + result: + type: object required: - goal title: workflow_step_query参数 @@ -370,9 +381,8 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - result: - code: code - message: message + placeholder_keys: {} + result: {} schema: description: 清空服务端所有非核心工作流 (保留核心流程) properties: @@ -382,13 +392,6 @@ reaction_station.bioyond: required: [] type: object result: - properties: - code: - description: 操作结果代码(1表示成功) - type: integer - message: - description: 结果描述 - type: string type: object required: - goal @@ -405,13 +408,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' + assign_material_name: null + temperature: 25.0 + time: '90' + titration_type: '1' + torque_variation: 2 + volume: null handles: {} + placeholder_keys: {} result: {} schema: description: 滴回去 @@ -423,33 +427,27 @@ reaction_station.bioyond: description: 物料名称(不能为空) type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '90' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 2 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: description: 分液公式(mL) type: string required: - - volume - assign_material_name - - time - - torque_variation - - titration_type - - temperature + - volume type: object result: {} required: @@ -462,7 +460,7 @@ reaction_station.bioyond: goal: batch_reports_result: batch_reports_result goal_default: - batch_reports_result: '' + batch_reports_result: null handles: input: - data_key: batch_reports_result @@ -478,8 +476,8 @@ reaction_station.bioyond: handler_key: ACTUALS_EXTRACTED io_type: sink label: Extracted Actuals - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。 properties: @@ -493,13 +491,6 @@ reaction_station.bioyond: - batch_reports_result type: object result: - properties: - return_info: - description: JSON字符串,包含actuals数组,每项含order_code, order_id, actualTargetWeigh, - actualVolume - type: string - required: - - return_info title: extract_actuals_from_batch_reports结果 type: object required: @@ -517,13 +508,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' + assign_material_name: BAPP + temperature: 25.0 + time: '0' + titration_type: '1' + torque_variation: 1 + volume: '350' handles: {} + placeholder_keys: {} result: {} schema: description: 液体进料烧杯 @@ -532,36 +524,30 @@ reaction_station.bioyond: goal: properties: assign_material_name: + default: BAPP description: 物料名称 type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: + default: '350' description: 分液公式(mL) type: string - required: - - volume - - assign_material_name - - time - - torque_variation - - titration_type - - temperature + required: [] type: object result: {} required: @@ -580,13 +566,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - solvents: '' - temperature: '25.00' + assign_material_name: null + solvents: null + temperature: 25.0 time: '360' titration_type: '1' - torque_variation: '2' - volume: '' + torque_variation: 2 + volume: null handles: input: - data_key: solvents @@ -595,6 +581,7 @@ reaction_station.bioyond: handler_key: solvents io_type: source label: Solvents Data From Calculation Node + placeholder_keys: {} result: {} schema: description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。 @@ -609,27 +596,21 @@ reaction_station.bioyond: description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' type: string temperature: - default: '25.00' + default: 25.0 description: 温度设定(°C),默认25.00 - type: string + type: number time: default: '360' description: 观察时间(分钟),默认360 type: string titration_type: - default: 'NO' + default: '1' description: 是否滴定(NO=否, YES=是),默认NO - enum: - - 'NO' - - 'YES' type: string torque_variation: - default: 'YES' + default: 2 description: 是否观察 (NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 type: string @@ -655,15 +636,15 @@ reaction_station.bioyond: volume_formula: volume_formula x_value: x_value goal_default: - assign_material_name: '' - extracted_actuals: '' - feeding_order_data: '' - temperature: '25.00' + assign_material_name: null + extracted_actuals: null + feeding_order_data: null + temperature: 25.0 time: '90' titration_type: '2' - torque_variation: '2' - volume_formula: '' - x_value: '' + torque_variation: 2 + volume_formula: null + x_value: null handles: input: - data_key: extracted_actuals @@ -678,6 +659,7 @@ reaction_station.bioyond: handler_key: feeding_order io_type: source label: Feeding Order Data From Calculation Node + placeholder_keys: {} result: {} schema: description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定" @@ -696,27 +678,21 @@ reaction_station.bioyond: {"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}' type: string temperature: - default: '25.00' + default: 25.0 description: 温度设定(°C),默认25.00 - type: string + type: number time: default: '90' description: 观察时间(分钟),默认90 type: string titration_type: - default: 'YES' + default: '2' description: 是否滴定(NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' type: string torque_variation: - default: 'YES' + default: 2 description: 是否观察 (NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' - type: string + type: integer volume_formula: description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 type: string @@ -742,13 +718,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume_formula: volume_formula goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume_formula: '' + assign_material_name: null + temperature: 25.0 + time: '0' + titration_type: '1' + torque_variation: 1 + volume_formula: null handles: {} + placeholder_keys: {} result: {} schema: description: 液体进料小瓶(非滴定) @@ -760,33 +737,27 @@ reaction_station.bioyond: description: 物料名称 type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume_formula: description: 分液公式(mL) type: string required: - volume_formula - assign_material_name - - time - - torque_variation - - titration_type - - temperature type: object result: {} required: @@ -800,9 +771,10 @@ reaction_station.bioyond: task_name: task_name workflow_name: workflow_name goal_default: - task_name: '' - workflow_name: '' + task_name: null + workflow_name: null handles: {} + placeholder_keys: {} result: {} schema: description: 处理并执行工作流 @@ -820,7 +792,8 @@ reaction_station.bioyond: - workflow_name - task_name type: object - result: {} + result: + type: object required: - goal title: process_and_execute_workflow参数 @@ -833,10 +806,11 @@ reaction_station.bioyond: cutoff: cutoff temperature: temperature goal_default: - assign_material_name: '' - cutoff: '' - temperature: '' + assign_material_name: null + cutoff: '900000' + temperature: -10.0 handles: {} + placeholder_keys: {} result: {} schema: description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 @@ -848,14 +822,14 @@ reaction_station.bioyond: description: 物料名称 type: string cutoff: + default: '900000' description: 粘度上限 type: string temperature: + default: -10.0 description: 温度设定(°C) - type: string + type: number required: - - cutoff - - temperature - assign_material_name type: object result: {} @@ -869,6 +843,7 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} + placeholder_keys: {} result: {} schema: description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 @@ -878,15 +853,7 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: - properties: - code: - description: 操作结果代码(1表示成功,0表示失败) - type: integer - return_info: - description: 操作结果详细信息 - type: string - type: object + result: {} required: - goal title: reactor_taken_out参数 @@ -897,8 +864,8 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 properties: @@ -908,12 +875,6 @@ reaction_station.bioyond: required: [] type: object result: - properties: - return_info: - description: 调度器启动结果,成功返回1,失败返回0 - type: integer - required: - - return_info title: scheduler_start结果 type: object required: @@ -930,12 +891,13 @@ reaction_station.bioyond: time: time torque_variation: torque_variation goal_default: - assign_material_name: '' - material_id: '' - temperature: '' - time: '' - torque_variation: '' + assign_material_name: null + material_id: null + temperature: 25.0 + time: '0' + torque_variation: 1 handles: {} + placeholder_keys: {} result: {} schema: description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) @@ -948,29 +910,21 @@ reaction_station.bioyond: type: string material_id: description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟) - enum: - - Salt - - Flour - - BTDA type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer required: - - assign_material_name - material_id - - time - - torque_variation - - temperature type: object result: {} required: diff --git a/unilabos/registry/devices/robot_agv.yaml b/unilabos/registry/devices/robot_agv.yaml index 9f45bd5e..b37a0c46 100644 --- a/unilabos/registry/devices/robot_agv.yaml +++ b/unilabos/registry/devices/robot_agv.yaml @@ -37,42 +37,41 @@ agv.SEER: type: object type: UniLabJsonCommand send_nav_task: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/robot_arm.yaml b/unilabos/registry/devices/robot_arm.yaml index 147eab4d..ff357ad4 100644 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -122,31 +122,6 @@ robotic_arm.SCARA_with_slider.moveit.virtual: title: moveit_task参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: post_init的参数schema - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-resource_manager: feedback: {} goal: {} @@ -198,41 +173,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: UniLabJsonCommand pick_and_place: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -241,41 +216,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: SendCmd set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -284,41 +259,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: SendCmd set_status: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -455,42 +430,41 @@ robotic_arm.UR: type: object type: UniLabJsonCommand move_pos_task: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -532,8 +506,8 @@ robotic_arm.UR: type: string required: - arm_pose - - gripper_pose - arm_status + - gripper_pose - gripper_status type: object version: 1.0.0 @@ -726,41 +700,41 @@ robotic_arm.elite: type: object type: UniLabJsonCommand modbus_task_cmd: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -770,8 +744,8 @@ robotic_arm.elite: type: SendCmd module: unilabos.devices.arm.elite_robot:EliteRobot status_types: - actual_joint_positions: String - arm_pose: String + actual_joint_positions: '' + arm_pose: list[float] type: python config_info: [] description: Elite robot arm @@ -797,8 +771,8 @@ robotic_arm.elite: type: number type: array required: - - arm_pose - actual_joint_positions + - arm_pose type: object model: mesh: elite_robot diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 295c48a0..4f579e24 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -114,11 +114,12 @@ gripper.misumi_rz: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: modbus_crc参数 @@ -398,30 +399,26 @@ gripper.misumi_rz: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -504,71 +501,82 @@ gripper.mock: type: UniLabJsonCommand push_to: feedback: - effort: torque + effort: effort position: position + reached_goal: reached_goal + stalled: stalled goal: - command.max_effort: torque - command.position: position + command: command + position: position + torque: torque + velocity: velocity goal_default: command: max_effort: 0.0 position: 0.0 handles: {} + placeholder_keys: {} result: - effort: torque + effort: effort position: position + reached_goal: reached_goal + stalled: stalled schema: description: '' properties: feedback: + additionalProperties: false properties: effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reached_goal: type: boolean stalled: type: boolean - required: - - position - - effort - - stalled - - reached_goal title: GripperCommand_Feedback type: object goal: + additionalProperties: false properties: command: + additionalProperties: false properties: max_effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - position - max_effort title: command type: object - required: - - command title: GripperCommand_Goal type: object result: + additionalProperties: false properties: effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reached_goal: type: boolean stalled: type: boolean - required: - - position - - effort - - stalled - - reached_goal title: GripperCommand_Result type: object required: @@ -604,8 +612,8 @@ gripper.mock: type: number required: - position - - velocity - - torque - status + - torque + - velocity type: object version: 1.0.0 diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml index 0f8506e9..74b01e80 100644 --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -24,6 +24,27 @@ linear_motion.grbl: title: initialize参数 type: object type: UniLabJsonCommand + auto-list: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: list的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: list参数 + type: object + type: UniLabJsonCommand auto-set_position: feedback: {} goal: {} @@ -93,44 +114,39 @@ linear_motion.grbl: type: UniLabJsonCommandAsync move_through_points: feedback: - current_pose.pose.position: position - estimated_time_remaining.sec: time_remaining - navigation_time.sec: time_spent - number_of_poses_remaining: pose_number_remaining + current_pose: current_pose + distance_remaining: distance_remaining + estimated_time_remaining: estimated_time_remaining + navigation_time: navigation_time + number_of_poses_remaining: number_of_poses_remaining + number_of_recoveries: number_of_recoveries goal: - poses[].pose.position: positions[] + behavior_tree: behavior_tree + poses: poses + positions: positions goal_default: behavior_tree: '' - poses: - - header: - frame_id: '' - stamp: - nanosec: 0 - sec: 0 - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 + poses: [] handles: {} - result: {} + placeholder_keys: {} + result: + result: result schema: description: '' properties: feedback: + additionalProperties: false properties: current_pose: + additionalProperties: false properties: header: + additionalProperties: false properties: frame_id: type: string stamp: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -151,16 +167,26 @@ linear_motion.grbl: title: header type: object pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -170,12 +196,19 @@ linear_motion.grbl: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -194,8 +227,11 @@ linear_motion.grbl: title: current_pose type: object distance_remaining: + maximum: 3.4028235e+38 + minimum: -3.4028235e+38 type: number estimated_time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -211,6 +247,7 @@ linear_motion.grbl: title: estimated_time_remaining type: object navigation_time: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -233,16 +270,10 @@ linear_motion.grbl: maximum: 32767 minimum: -32768 type: integer - required: - - current_pose - - navigation_time - - estimated_time_remaining - - number_of_recoveries - - distance_remaining - - number_of_poses_remaining title: NavigateThroughPoses_Feedback type: object goal: + additionalProperties: false properties: behavior_tree: type: string @@ -256,12 +287,8 @@ linear_motion.grbl: stamp: properties: nanosec: - maximum: 4294967295 - minimum: 0 type: integer sec: - maximum: 2147483647 - minimum: -2147483648 type: integer required: - sec @@ -314,23 +341,17 @@ linear_motion.grbl: required: - header - pose - title: poses type: object type: array - required: - - poses - - behavior_tree title: NavigateThroughPoses_Goal type: object result: + additionalProperties: false properties: result: - properties: {} - required: [] + additionalProperties: true title: result type: object - required: - - result title: NavigateThroughPoses_Result type: object required: @@ -340,9 +361,15 @@ linear_motion.grbl: type: NavigateThroughPoses set_spindle_speed: feedback: - position: spindle_speed + error: error + header: header + position: position + velocity: velocity goal: - position: spindle_speed + max_velocity: max_velocity + min_duration: min_duration + position: position + spindle_speed: spindle_speed goal_default: max_velocity: 0.0 min_duration: @@ -350,19 +377,25 @@ linear_motion.grbl: sec: 0 position: 0.0 handles: {} + placeholder_keys: {} result: {} schema: description: '' properties: feedback: + additionalProperties: false properties: error: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number header: + additionalProperties: false properties: frame_id: type: string stamp: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -383,21 +416,24 @@ linear_motion.grbl: title: header type: object position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - header - - position - - velocity - - error title: SingleJointPosition_Feedback type: object goal: + additionalProperties: false properties: max_velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number min_duration: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -413,16 +449,13 @@ linear_motion.grbl: title: min_duration type: object position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - position - - min_duration - - max_velocity title: SingleJointPosition_Goal type: object result: - properties: {} - required: [] + additionalProperties: true title: SingleJointPosition_Result type: object required: @@ -432,7 +465,7 @@ linear_motion.grbl: type: SingleJointPosition module: unilabos.devices.cnc.grbl_sync:GrblCNC status_types: - position: unilabos.messages:Point3D + position: Point3D spindle_speed: float status: str type: python @@ -471,9 +504,9 @@ linear_motion.grbl: status: type: string required: - - status - position - spindle_speed + - status type: object version: 1.0.0 linear_motion.toyo_xyz.sim: @@ -600,31 +633,6 @@ linear_motion.toyo_xyz.sim: title: moveit_task参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: post_init的参数schema - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-resource_manager: feedback: {} goal: {} @@ -676,41 +684,41 @@ linear_motion.toyo_xyz.sim: type: object type: UniLabJsonCommand pick_and_place: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -719,41 +727,41 @@ linear_motion.toyo_xyz.sim: type: object type: SendCmd set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -762,41 +770,41 @@ linear_motion.toyo_xyz.sim: type: object type: SendCmd set_status: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -939,30 +947,26 @@ motor.iCL42: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -1000,8 +1004,8 @@ motor.iCL42: success: type: boolean required: - - motor_position - is_executing_run + - motor_position - success type: object version: 1.0.0 diff --git a/unilabos/registry/devices/solid_dispenser.yaml b/unilabos/registry/devices/solid_dispenser.yaml index 9bceb54b..46280631 100644 --- a/unilabos/registry/devices/solid_dispenser.yaml +++ b/unilabos/registry/devices/solid_dispenser.yaml @@ -14,19 +14,24 @@ solid_dispenser.laiyu: powder_tube_number: 0 target_tube_position: '' handles: {} + placeholder_keys: {} result: actual_mass_mg: actual_mass_mg + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: SolidDispenseAddPowderTube_Feedback type: object goal: + additionalProperties: false properties: compound_mass: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number powder_tube_number: maximum: 2147483647 @@ -34,24 +39,19 @@ solid_dispenser.laiyu: type: integer target_tube_position: type: string - required: - - powder_tube_number - - target_tube_position - - compound_mass title: SolidDispenseAddPowderTube_Goal type: object result: + additionalProperties: false properties: actual_mass_mg: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number return_info: type: string success: type: boolean - required: - - return_info - - actual_mass_mg - - success title: SolidDispenseAddPowderTube_Result type: object required: @@ -74,11 +74,12 @@ solid_dispenser.laiyu: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: calculate_crc参数 @@ -99,11 +100,12 @@ solid_dispenser.laiyu: goal: properties: command: - type: string + type: object required: - command type: object - result: {} + result: + type: object required: - goal title: send_command参数 @@ -112,36 +114,37 @@ solid_dispenser.laiyu: discharge: feedback: {} goal: - float_input: float_input + float_in: float_in goal_default: float_in: 0.0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: FloatSingleInput_Feedback type: object goal: + additionalProperties: false properties: float_in: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - float_in title: FloatSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: FloatSingleInput_Result type: object required: @@ -156,32 +159,31 @@ solid_dispenser.laiyu: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -200,38 +202,41 @@ solid_dispenser.laiyu: y: 0.0 z: 0.0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: Point3DSeparateInput_Feedback type: object goal: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - x - - y - - z title: Point3DSeparateInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Point3DSeparateInput_Result type: object required: @@ -246,34 +251,33 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -288,34 +292,33 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -328,26 +331,25 @@ solid_dispenser.laiyu: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 874fe517..9e60adb1 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -34,7 +34,8 @@ chiller: - register_address - value type: object - result: {} + result: + type: object required: - goal title: build_modbus_frame参数 @@ -63,7 +64,8 @@ chiller: required: - temperature type: object - result: {} + result: + type: integer required: - goal title: convert_temperature_to_modbus_value参数 @@ -84,11 +86,12 @@ chiller: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: modbus_crc参数 @@ -116,42 +119,41 @@ chiller: type: object type: UniLabJsonCommand set_temperature: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -266,9 +268,15 @@ heaterstirrer.dalong: feedback: status: status goal: + pressure: pressure purpose: purpose + reflux_solvent: reflux_solvent + stir: stir + stir_speed: stir_speed temp: temp + temp_spec: temp_spec time: time + time_spec: time_spec vessel: vessel goal_default: pressure: '' @@ -301,20 +309,23 @@ heaterstirrer.dalong: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -325,8 +336,12 @@ heaterstirrer.dalong: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -335,6 +350,7 @@ heaterstirrer.dalong: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -353,16 +369,26 @@ heaterstirrer.dalong: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -372,12 +398,19 @@ heaterstirrer.dalong: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -407,20 +440,10 @@ heaterstirrer.dalong: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -428,10 +451,6 @@ heaterstirrer.dalong: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -440,42 +459,42 @@ heaterstirrer.dalong: type: object type: HeatChill set_temp_target: - feedback: {} + feedback: + status: status goal: - command: temp + command: command + temp: temp goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -484,42 +503,42 @@ heaterstirrer.dalong: type: object type: SendCmd set_temp_warning: - feedback: {} + feedback: + status: status goal: - command: temp + command: command + temp: temp goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -569,8 +588,8 @@ heaterstirrer.dalong: - status - stir_speed - temp - - temp_warning - temp_target + - temp_warning type: object version: 1.0.0 tempsensor: @@ -691,42 +710,41 @@ tempsensor: type: object type: UniLabJsonCommand set_warning: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index f0635755..67560f2f 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -18,7 +18,8 @@ virtual_centrifuge: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -39,41 +40,17 @@ virtual_centrifuge: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand centrifuge: feedback: current_speed: current_speed - current_status: status + current_status: current_status current_temp: current_temp progress: progress goal: @@ -106,38 +83,50 @@ virtual_centrifuge: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_temp - - current_status title: Centrifuge_Feedback type: object goal: + additionalProperties: false properties: speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -156,16 +145,26 @@ virtual_centrifuge: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -175,12 +174,19 @@ virtual_centrifuge: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -210,14 +216,10 @@ virtual_centrifuge: - data title: vessel type: object - required: - - vessel - - speed - - time - - temp title: Centrifuge_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -225,10 +227,6 @@ virtual_centrifuge: type: string success: type: boolean - required: - - success - - message - - return_info title: Centrifuge_Result type: object required: @@ -267,7 +265,7 @@ virtual_centrifuge: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -299,18 +297,18 @@ virtual_centrifuge: time_remaining: type: number required: - - status - centrifuge_state - current_speed - - target_speed - current_temp - - target_temp - max_speed - max_temp - - min_temp - - time_remaining - - progress - message + - min_temp + - progress + - status + - target_speed + - target_temp + - time_remaining type: object version: 1.0.0 virtual_column: @@ -333,7 +331,8 @@ virtual_column: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -354,45 +353,26 @@ virtual_column: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand run_column: feedback: - current_status: current_status - processed_volume: processed_volume progress: progress + status: status goal: column: column from_vessel: from_vessel + pct1: pct1 + pct2: pct2 + ratio: ratio + rf: rf + solvent1: solvent1 + solvent2: solvent2 to_vessel: to_vessel goal_default: column: '' @@ -443,29 +423,32 @@ virtual_column: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: - message: current_status - return_info: current_status + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: RunColumn_Feedback type: object goal: + additionalProperties: false properties: column: type: string from_vessel: + additionalProperties: false properties: category: type: string @@ -484,16 +467,26 @@ virtual_column: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -503,12 +496,19 @@ virtual_column: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -551,6 +551,7 @@ virtual_column: solvent2: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -569,16 +570,26 @@ virtual_column: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -588,12 +599,19 @@ virtual_column: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -623,19 +641,10 @@ virtual_column: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio title: RunColumn_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -643,10 +652,6 @@ virtual_column: type: string success: type: boolean - required: - - success - - message - - return_info title: RunColumn_Result type: object required: @@ -722,17 +727,17 @@ virtual_column: status: type: string required: - - status + - column_diameter + - column_length - column_state - current_flow_rate + - current_phase + - current_status + - final_volume - max_flow_rate - - column_length - - column_diameter - processed_volume - progress - - current_status - - current_phase - - final_volume + - status type: object version: 1.0.0 virtual_filter: @@ -755,7 +760,8 @@ virtual_filter: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -776,37 +782,13 @@ virtual_filter: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand filter: feedback: current_status: current_status @@ -868,35 +850,40 @@ virtual_filter: type: '' volume: 0.0 handles: {} + placeholder_keys: {} result: message: message - return_info: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filtered_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_temp - - filtered_volume - - current_status title: Filter_Feedback type: object goal: + additionalProperties: false properties: continue_heatchill: type: boolean filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -915,16 +902,26 @@ virtual_filter: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -934,12 +931,19 @@ virtual_filter: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -972,10 +976,15 @@ virtual_filter: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -994,16 +1003,26 @@ virtual_filter: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1013,12 +1032,19 @@ virtual_filter: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1049,18 +1075,13 @@ virtual_filter: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume title: Filter_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1068,10 +1089,6 @@ virtual_filter: type: string success: type: boolean - required: - - success - - message - - return_info title: Filter_Result type: object required: @@ -1123,7 +1140,7 @@ virtual_filter: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -1149,15 +1166,15 @@ virtual_filter: status: type: string required: - - status - - progress - - current_temp - current_status + - current_temp - filtered_volume - - message - - max_temp - max_stir_speed + - max_temp - max_volume + - message + - progress + - status type: object version: 1.0.0 virtual_gas_source: @@ -1180,7 +1197,8 @@ virtual_gas_source: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -1201,7 +1219,8 @@ virtual_gas_source: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -1254,26 +1273,25 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -1286,26 +1304,25 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -1320,32 +1337,31 @@ virtual_gas_source: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -1373,7 +1389,7 @@ virtual_gas_source: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -1406,7 +1422,8 @@ virtual_heatchill: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -1427,46 +1444,26 @@ virtual_heatchill: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand heat_chill: feedback: status: status goal: + pressure: pressure purpose: purpose + reflux_solvent: reflux_solvent stir: stir stir_speed: stir_speed temp: temp + temp_spec: temp_spec time: time + time_spec: time_spec vessel: vessel goal_default: pressure: '' @@ -1499,20 +1496,23 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -1523,8 +1523,12 @@ virtual_heatchill: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -1533,6 +1537,7 @@ virtual_heatchill: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1551,16 +1556,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1570,12 +1585,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1605,20 +1627,10 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1626,10 +1638,6 @@ virtual_heatchill: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -1668,26 +1676,31 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStart_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -1706,16 +1719,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1725,12 +1748,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1760,21 +1790,15 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel - - temp - - purpose title: HeatChillStart_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStart_Result type: object required: @@ -1809,22 +1833,25 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStop_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -1843,16 +1870,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1862,12 +1899,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1897,19 +1941,15 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel title: HeatChillStop_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStop_Result type: object required: @@ -1971,15 +2011,15 @@ virtual_heatchill: stir_speed: type: number required: - - status - - operation_mode - is_stirring - - stir_speed - - remaining_time - - progress + - max_stir_speed - max_temp - min_temp - - max_stir_speed + - operation_mode + - progress + - remaining_time + - status + - stir_speed type: object version: 1.0.0 virtual_multiway_valve: @@ -2027,7 +2067,8 @@ virtual_multiway_valve: required: - port_number type: object - result: {} + result: + type: boolean required: - goal title: is_at_port参数 @@ -2052,7 +2093,8 @@ virtual_multiway_valve: required: - position type: object - result: {} + result: + type: boolean required: - goal title: is_at_position参数 @@ -2073,7 +2115,8 @@ virtual_multiway_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_at_pump_position参数 @@ -2193,42 +2236,41 @@ virtual_multiway_valve: type: object type: UniLabJsonCommand set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -2237,42 +2279,41 @@ virtual_multiway_valve: type: object type: SendCmd set_valve_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -2402,13 +2443,13 @@ virtual_multiway_valve: valve_state: type: string required: - - status - - valve_state - - current_position - - target_position - current_port - - valve_position + - current_position - flow_path + - status + - target_position + - valve_position + - valve_state type: object version: 1.0.0 virtual_rotavap: @@ -2431,7 +2472,8 @@ virtual_rotavap: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -2452,43 +2494,22 @@ virtual_rotavap: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand evaporate: feedback: current_device: current_device status: status + time_remaining: time_remaining + time_spent: time_spent goal: pressure: pressure + solvent: solvent stir_speed: stir_speed temp: temp time: time @@ -2520,19 +2541,22 @@ virtual_rotavap: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: - message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2548,6 +2572,7 @@ virtual_rotavap: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2562,26 +2587,29 @@ virtual_rotavap: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Evaporate_Feedback type: object goal: + additionalProperties: false properties: pressure: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -2600,16 +2628,26 @@ virtual_rotavap: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2619,12 +2657,19 @@ virtual_rotavap: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2654,24 +2699,15 @@ virtual_rotavap: - data title: vessel type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent title: Evaporate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Evaporate_Result type: object required: @@ -2725,7 +2761,7 @@ virtual_rotavap: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -2755,17 +2791,17 @@ virtual_rotavap: vacuum_pressure: type: number required: - - status - - rotavap_state - current_temp - - rotation_speed - - vacuum_pressure - evaporated_volume - - progress - - message - - max_temp - max_rotation_speed + - max_temp + - message + - progress - remaining_time + - rotation_speed + - rotavap_state + - status + - vacuum_pressure type: object version: 1.0.0 virtual_separator: @@ -2788,7 +2824,8 @@ virtual_separator: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -2809,44 +2846,21 @@ virtual_separator: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand separate: feedback: - current_status: status progress: progress + status: status goal: from_vessel: from_vessel product_phase: product_phase + product_vessel: product_vessel purpose: purpose repeats: repeats separation_vessel: separation_vessel @@ -2857,7 +2871,10 @@ virtual_separator: stir_time: stir_time through: through to_vessel: to_vessel + vessel: vessel + volume: volume waste_phase_to_vessel: waste_phase_to_vessel + waste_vessel: waste_vessel goal_default: from_vessel: category: '' @@ -3010,26 +3027,30 @@ virtual_separator: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Separate_Feedback type: object goal: + additionalProperties: false properties: from_vessel: + additionalProperties: false properties: category: type: string @@ -3048,16 +3069,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3067,12 +3098,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3105,6 +3143,7 @@ virtual_separator: product_phase: type: string product_vessel: + additionalProperties: false properties: category: type: string @@ -3123,16 +3162,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3142,12 +3191,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3184,6 +3240,7 @@ virtual_separator: minimum: -2147483648 type: integer separation_vessel: + additionalProperties: false properties: category: type: string @@ -3202,16 +3259,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3221,12 +3288,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3257,18 +3331,25 @@ virtual_separator: title: separation_vessel type: object settling_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string solvent_volume: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number through: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -3287,16 +3368,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3306,12 +3397,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3342,6 +3440,7 @@ virtual_separator: title: to_vessel type: object vessel: + additionalProperties: false properties: category: type: string @@ -3360,16 +3459,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3379,12 +3488,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3417,6 +3533,7 @@ virtual_separator: volume: type: string waste_phase_to_vessel: + additionalProperties: false properties: category: type: string @@ -3435,16 +3552,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3454,12 +3581,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3490,6 +3624,7 @@ virtual_separator: title: waste_phase_to_vessel type: object waste_vessel: + additionalProperties: false properties: category: type: string @@ -3508,16 +3643,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3527,12 +3672,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3562,27 +3714,10 @@ virtual_separator: - data title: waste_vessel type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time title: Separate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3590,10 +3725,6 @@ virtual_separator: type: string success: type: boolean - required: - - success - - message - - return_info title: Separate_Result type: object required: @@ -3645,7 +3776,7 @@ virtual_separator: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -3671,15 +3802,15 @@ virtual_separator: volume: type: number required: - - status - - separator_state - - volume - has_phases - - phase_separation - - stir_speed - - settling_time - - progress - message + - phase_separation + - progress + - separator_state + - settling_time + - status + - stir_speed + - volume type: object version: 1.0.0 virtual_solenoid_valve: @@ -3702,7 +3833,8 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -3723,7 +3855,8 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -3744,37 +3877,13 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_closed参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-reset: feedback: {} goal: {} @@ -3819,31 +3928,28 @@ virtual_solenoid_valve: type: UniLabJsonCommand close: feedback: {} - goal: - command: CLOSED + goal: {} goal_default: {} handles: {} + placeholder_keys: {} result: - success: success + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -3856,26 +3962,25 @@ virtual_solenoid_valve: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -3890,32 +3995,31 @@ virtual_solenoid_valve: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -3924,44 +4028,41 @@ virtual_solenoid_valve: type: object type: StrSingleInput set_valve_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: - message: message + return_info: return_info success: success - valve_position: valve_position schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -4016,10 +4117,10 @@ virtual_solenoid_valve: valve_state: type: string required: - - status - - valve_state - is_open + - status - valve_position + - valve_state type: object version: 1.0.0 virtual_solid_dispenser: @@ -4029,9 +4130,10 @@ virtual_solid_dispenser: action_value_mappings: add_solid: feedback: - current_status: status + current_status: current_status progress: progress goal: + amount: amount equiv: equiv event: event mass: mass @@ -4040,7 +4142,12 @@ virtual_solid_dispenser: rate_spec: rate_spec ratio: ratio reagent: reagent + stir: stir + stir_speed: stir_speed + time: time vessel: vessel + viscous: viscous + volume: volume goal_default: amount: '' equiv: '' @@ -4077,6 +4184,7 @@ virtual_solid_dispenser: viscous: false volume: '' handles: {} + placeholder_keys: {} result: message: message return_info: return_info @@ -4085,17 +4193,18 @@ virtual_solid_dispenser: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: Add_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -4118,10 +4227,13 @@ virtual_solid_dispenser: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -4140,16 +4252,26 @@ virtual_solid_dispenser: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4159,12 +4281,19 @@ virtual_solid_dispenser: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4198,25 +4327,10 @@ virtual_solid_dispenser: type: boolean volume: type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio title: Add_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4224,10 +4338,6 @@ virtual_solid_dispenser: type: string success: type: boolean - required: - - success - - message - - return_info title: Add_Result type: object required: @@ -4250,7 +4360,8 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -4275,7 +4386,8 @@ virtual_solid_dispenser: required: - reagent_name type: object - result: {} + result: + type: string required: - goal title: find_solid_reagent_bottle参数 @@ -4296,7 +4408,8 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -4321,7 +4434,8 @@ virtual_solid_dispenser: required: - mass_str type: object - result: {} + result: + type: number required: - goal title: parse_mass_string参数 @@ -4346,37 +4460,13 @@ virtual_solid_dispenser: required: - mol_str type: object - result: {} + result: + type: number required: - goal title: parse_mol_string参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser status_types: current_reagent: str @@ -4425,9 +4515,9 @@ virtual_solid_dispenser: total_operations: type: integer required: - - status - current_reagent - dispensed_amount + - status - total_operations type: object version: 1.0.0 @@ -4451,7 +4541,8 @@ virtual_stirrer: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -4472,40 +4563,18 @@ virtual_stirrer: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand start_stir: feedback: - status: status + current_speed: current_speed + current_status: current_status + progress: progress goal: purpose: purpose stir_speed: stir_speed @@ -4534,32 +4603,40 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_status title: StartStir_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -4578,16 +4655,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4597,12 +4684,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4632,13 +4726,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel - - stir_speed - - purpose title: StartStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4646,10 +4737,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: StartStir_Result type: object required: @@ -4661,9 +4748,13 @@ virtual_stirrer: feedback: status: status goal: + event: event settling_time: settling_time stir_speed: stir_speed stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: event: '' settling_time: '' @@ -4692,34 +4783,42 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -4738,16 +4837,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4757,12 +4866,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4792,17 +4908,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4810,10 +4919,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -4823,7 +4928,8 @@ virtual_stirrer: type: Stir stop_stir: feedback: - status: status + current_status: current_status + progress: progress goal: vessel: vessel goal_default: @@ -4848,25 +4954,30 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: StopStir_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -4885,16 +4996,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4904,12 +5025,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4939,11 +5067,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel title: StopStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4951,10 +5078,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: StopStir_Result type: object required: @@ -4966,7 +5089,7 @@ virtual_stirrer: status_types: current_speed: float current_vessel: str - device_info: dict + device_info: Dict[str, Any] is_stirring: bool max_speed: float min_speed: float @@ -5016,15 +5139,15 @@ virtual_stirrer: status: type: string required: - - status - - operation_mode - - current_vessel - current_speed + - current_vessel + - device_info - is_stirring - - remaining_time - max_speed - min_speed - - device_info + - operation_mode + - remaining_time + - status type: object version: 1.0.0 virtual_transfer_pump: @@ -5075,7 +5198,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -5172,7 +5296,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -5193,7 +5318,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_empty参数 @@ -5214,37 +5340,13 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_full参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-pull_plunger: feedback: {} goal: {} @@ -5359,38 +5461,44 @@ virtual_transfer_pump: max_velocity: 0.0 position: 0.0 handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - current_position - - progress title: SetPumpPosition_Feedback type: object goal: + additionalProperties: false properties: max_velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - position - - max_velocity title: SetPumpPosition_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5398,10 +5506,6 @@ virtual_transfer_pump: type: string success: type: boolean - required: - - return_info - - success - - message title: SetPumpPosition_Result type: object required: @@ -5416,6 +5520,8 @@ virtual_transfer_pump: transferred_volume: transferred_volume goal: amount: amount + aspirate_velocity: aspirate_velocity + dispense_velocity: dispense_velocity from_vessel: from_vessel rinsing_repeats: rinsing_repeats rinsing_solvent: rinsing_solvent @@ -5437,27 +5543,31 @@ virtual_transfer_pump: viscous: false volume: 0.0 handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -5470,31 +5580,27 @@ virtual_transfer_pump: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5502,10 +5608,6 @@ virtual_transfer_pump: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -5558,12 +5660,12 @@ virtual_transfer_pump: transfer_rate: type: number required: - - status - - position - current_volume - max_velocity - - transfer_rate + - position - remaining_capacity + - status + - transfer_rate type: object version: 1.0.0 virtual_vacuum_pump: @@ -5586,7 +5688,8 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -5607,7 +5710,8 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -5660,26 +5764,25 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -5692,26 +5795,25 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -5726,32 +5828,31 @@ virtual_vacuum_pump: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -5779,7 +5880,7 @@ virtual_vacuum_pump: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -5792,482 +5893,3 @@ virtual_vacuum_pump: - status type: object version: 1.0.0 -virtual_workbench: - category: - - virtual_device - class: - action_value_mappings: - auto-move_to_heating_station: - feedback: {} - goal: {} - goal_default: - material_number: null - handles: - input: - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: material_input - label: 物料编号 - output: - - data_key: station_id - data_source: executor - data_type: workbench_station - handler_key: heating_station_output - label: 加热台ID - - data_key: material_number - data_source: executor - data_type: workbench_material - handler_key: material_number_output - label: 物料编号 - placeholder_keys: {} - result: {} - schema: - description: 将物料从An位置移动到空闲加热台,返回分配的加热台ID - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,1-5,物料ID自动生成为A{n} - type: integer - required: - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: move_to_heating_station 返回类型 - properties: - material_id: - title: Material Id - type: string - material_number: - title: Material Number - type: integer - message: - title: Message - type: string - station_id: - description: 分配的加热台ID - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - material_number - - message - - unilabos_samples - title: MoveToHeatingStationResult - type: object - required: - - goal - title: move_to_heating_station参数 - type: object - type: UniLabJsonCommand - auto-move_to_output: - feedback: {} - goal: {} - goal_default: - material_number: null - station_id: null - handles: - input: - - data_key: station_id - data_source: handle - data_type: workbench_station - handler_key: output_station_input - label: 加热台ID - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: output_material_input - label: 物料编号 - placeholder_keys: {} - result: {} - schema: - description: 将物料从加热台移动到输出位置Cn - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,用于确定输出位置Cn - type: integer - station_id: - description: 加热台ID,1-3,从上一节点传入 - type: integer - required: - - station_id - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: move_to_output 返回类型 - properties: - material_id: - title: Material Id - type: string - station_id: - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - unilabos_samples - title: MoveToOutputResult - type: object - required: - - goal - title: move_to_output参数 - type: object - type: UniLabJsonCommand - auto-prepare_materials: - feedback: {} - goal: {} - goal_default: - count: 5 - handles: - output: - - data_key: material_1 - data_source: executor - data_type: workbench_material - handler_key: channel_1 - label: 实验1 - - data_key: material_2 - data_source: executor - data_type: workbench_material - handler_key: channel_2 - label: 实验2 - - data_key: material_3 - data_source: executor - data_type: workbench_material - handler_key: channel_3 - label: 实验3 - - data_key: material_4 - data_source: executor - data_type: workbench_material - handler_key: channel_4 - label: 实验4 - - data_key: material_5 - data_source: executor - data_type: workbench_material - handler_key: channel_5 - label: 实验5 - placeholder_keys: {} - result: {} - schema: - description: 批量准备物料 - 虚拟起始节点,生成A1-A5物料,输出5个handle供后续节点使用 - properties: - feedback: {} - goal: - properties: - count: - default: 5 - description: 待生成的物料数量,默认5 (生成 A1-A5) - type: integer - required: [] - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: prepare_materials 返回类型 - 批量准备物料 - properties: - count: - title: Count - type: integer - material_1: - title: Material 1 - type: integer - material_2: - title: Material 2 - type: integer - material_3: - title: Material 3 - type: integer - material_4: - title: Material 4 - type: integer - material_5: - title: Material 5 - type: integer - message: - title: Message - type: string - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - count - - material_1 - - material_2 - - material_3 - - material_4 - - material_5 - - message - - unilabos_samples - title: PrepareMaterialsResult - type: object - required: - - goal - title: prepare_materials参数 - type: object - type: UniLabJsonCommand - auto-start_heating: - always_free: true - feedback: {} - goal: {} - goal_default: - material_number: null - station_id: null - handles: - input: - - data_key: station_id - data_source: handle - data_type: workbench_station - handler_key: station_id_input - label: 加热台ID - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: material_number_input - label: 物料编号 - output: - - data_key: station_id - data_source: executor - data_type: workbench_station - handler_key: heating_done_station - label: 加热完成-加热台ID - - data_key: material_number - data_source: executor - data_type: workbench_material - handler_key: heating_done_material - label: 加热完成-物料编号 - placeholder_keys: {} - result: {} - schema: - description: 启动指定加热台的加热程序 - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,从上一节点传入 - type: integer - station_id: - description: 加热台ID,1-3,从上一节点传入 - type: integer - required: - - station_id - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: start_heating 返回类型 - properties: - material_id: - title: Material Id - type: string - material_number: - title: Material Number - type: integer - message: - title: Message - type: string - station_id: - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - material_number - - message - - unilabos_samples - title: StartHeatingResult - type: object - required: - - goal - title: start_heating参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.virtual.workbench:VirtualWorkbench - status_types: - active_tasks_count: int - arm_current_task: str - arm_state: str - heating_station_1_material: str - heating_station_1_progress: float - heating_station_1_state: str - heating_station_2_material: str - heating_station_2_progress: float - heating_station_2_state: str - heating_station_3_material: str - heating_station_3_progress: float - heating_station_3_state: str - message: str - status: str - type: python - config_info: [] - description: Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent - material processing - handles: [] - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - active_tasks_count: - type: integer - arm_current_task: - type: string - arm_state: - type: string - heating_station_1_material: - type: string - heating_station_1_progress: - type: number - heating_station_1_state: - type: string - heating_station_2_material: - type: string - heating_station_2_progress: - type: number - heating_station_2_state: - type: string - heating_station_3_material: - type: string - heating_station_3_progress: - type: number - heating_station_3_state: - type: string - message: - type: string - status: - type: string - required: - - status - - arm_state - - arm_current_task - - heating_station_1_state - - heating_station_1_material - - heating_station_1_progress - - heating_station_2_state - - heating_station_2_material - - heating_station_2_progress - - heating_station_3_state - - heating_station_3_material - - heating_station_3_progress - - active_tasks_count - - message - type: object - version: 1.0.0 diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index e1be7f3d..87a2fabe 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -59,16 +59,17 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: AGVTransfer_Feedback type: object goal: + additionalProperties: false properties: from_repo: + additionalProperties: false properties: category: type: string @@ -87,16 +88,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -106,12 +117,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -144,6 +162,7 @@ workstation: from_repo_position: type: string to_repo: + additionalProperties: false properties: category: type: string @@ -162,16 +181,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -181,12 +210,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -218,22 +254,15 @@ workstation: type: object to_repo_position: type: string - required: - - from_repo - - from_repo_position - - to_repo - - to_repo_position title: AGVTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: AGVTransfer_Result type: object required: @@ -319,17 +348,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: Add_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -352,10 +382,13 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -374,16 +407,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -393,12 +436,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -432,25 +482,10 @@ workstation: type: boolean volume: type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio title: Add_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -458,10 +493,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Add_Result type: object required: @@ -528,23 +559,27 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: AdjustPH_Feedback type: object goal: + additionalProperties: false properties: ph_value: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reagent: type: string vessel: + additionalProperties: false properties: category: type: string @@ -563,16 +598,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -582,12 +627,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -617,13 +669,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - ph_value - - reagent title: AdjustPH_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -631,10 +680,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: AdjustPH_Result type: object required: @@ -693,31 +738,41 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_temp - - current_status title: Centrifuge_Feedback type: object goal: + additionalProperties: false properties: speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -736,16 +791,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -755,12 +820,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -790,14 +862,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - speed - - time - - temp title: Centrifuge_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -805,10 +873,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Centrifuge_Result type: object required: @@ -874,12 +938,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -895,6 +961,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -909,14 +976,10 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Clean_Feedback type: object goal: + additionalProperties: false properties: repeats: maximum: 2147483647 @@ -925,8 +988,11 @@ workstation: solvent: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -945,16 +1011,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -964,12 +1040,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1000,24 +1083,18 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - solvent - - volume - - temp - - repeats title: Clean_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Clean_Result type: object required: @@ -1083,17 +1160,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: CleanVessel_Feedback type: object goal: + additionalProperties: false properties: repeats: maximum: 2147483647 @@ -1102,8 +1180,11 @@ workstation: solvent: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -1122,16 +1203,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1141,12 +1232,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1177,16 +1275,13 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - solvent - - volume - - temp - - repeats title: CleanVessel_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1194,10 +1289,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: CleanVessel_Result type: object required: @@ -1280,17 +1371,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Dissolve_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -1305,12 +1397,15 @@ workstation: solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: type: string time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1329,16 +1424,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1348,12 +1453,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1385,21 +1497,10 @@ workstation: type: object volume: type: string - required: - - vessel - - solvent - - volume - - amount - - temp - - time - - stir_speed - - mass - - mol - - reagent - - event title: Dissolve_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1407,10 +1508,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Dissolve_Result type: object required: @@ -1465,21 +1562,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Dry_Feedback type: object goal: + additionalProperties: false properties: compound: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1498,16 +1597,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1517,12 +1626,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1552,12 +1668,10 @@ workstation: - data title: vessel type: object - required: - - compound - - vessel title: Dry_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1565,10 +1679,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Dry_Result type: object required: @@ -1623,12 +1733,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1644,6 +1756,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1658,18 +1771,15 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: EvacuateAndRefill_Feedback type: object goal: + additionalProperties: false properties: gas: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1688,16 +1798,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1707,12 +1827,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1742,20 +1869,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - gas title: EvacuateAndRefill_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: EvacuateAndRefill_Result type: object required: @@ -1823,12 +1945,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1844,6 +1968,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1858,26 +1983,29 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Evaporate_Feedback type: object goal: + additionalProperties: false properties: pressure: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1896,16 +2024,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1915,12 +2053,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1950,24 +2095,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent title: Evaporate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Evaporate_Result type: object required: @@ -2062,27 +2198,31 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filtered_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_temp - - filtered_volume - - current_status title: Filter_Feedback type: object goal: + additionalProperties: false properties: continue_heatchill: type: boolean filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -2101,16 +2241,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2120,12 +2270,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2158,10 +2315,15 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -2180,16 +2342,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2199,12 +2371,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2235,18 +2414,13 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume title: Filter_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2254,10 +2428,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Filter_Result type: object required: @@ -2376,17 +2546,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: FilterThrough_Feedback type: object goal: + additionalProperties: false properties: eluting_repeats: maximum: 2147483647 @@ -2395,8 +2566,11 @@ workstation: eluting_solvent: type: string eluting_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filter_through: + additionalProperties: false properties: category: type: string @@ -2415,16 +2589,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2434,12 +2618,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2470,6 +2661,7 @@ workstation: title: filter_through type: object from_vessel: + additionalProperties: false properties: category: type: string @@ -2488,16 +2680,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2507,12 +2709,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2543,8 +2752,11 @@ workstation: title: from_vessel type: object residence_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: + additionalProperties: false properties: category: type: string @@ -2563,16 +2775,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2582,12 +2804,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2617,17 +2846,10 @@ workstation: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - filter_through - - eluting_solvent - - eluting_volume - - eluting_repeats - - residence_time title: FilterThrough_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2635,10 +2857,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: FilterThrough_Result type: object required: @@ -2709,14 +2927,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -2727,8 +2945,12 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -2737,6 +2959,7 @@ workstation: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -2755,16 +2978,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2774,12 +3007,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2809,20 +3049,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2830,10 +3060,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -2890,20 +3116,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStart_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -2922,16 +3151,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2941,12 +3180,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2976,21 +3222,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - temp - - purpose title: HeatChillStart_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStart_Result type: object required: @@ -3043,16 +3283,17 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStop_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -3071,16 +3312,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3090,12 +3341,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3125,19 +3383,15 @@ workstation: - data title: vessel type: object - required: - - vessel title: HeatChillStop_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStop_Result type: object required: @@ -3194,23 +3448,25 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Hydrogenate_Feedback type: object goal: + additionalProperties: false properties: temp: type: string time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3229,16 +3485,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3248,12 +3514,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3283,13 +3556,10 @@ workstation: - data title: vessel type: object - required: - - temp - - time - - vessel title: Hydrogenate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3297,10 +3567,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Hydrogenate_Result type: object required: @@ -3416,12 +3682,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3437,6 +3705,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3451,22 +3720,21 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: PumpTransfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string event: type: string flowrate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number from_vessel: + additionalProperties: false properties: category: type: string @@ -3485,16 +3753,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3504,12 +3782,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3548,14 +3833,19 @@ workstation: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean through: type: string time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: + additionalProperties: false properties: category: type: string @@ -3574,16 +3864,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3593,12 +3893,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3629,38 +3936,24 @@ workstation: title: to_vessel type: object transfer_flowrate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid - - flowrate - - transfer_flowrate - - rate_spec - - event - - through title: PumpTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: PumpTransfer_Result type: object required: @@ -3731,17 +4024,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Recrystallize_Feedback type: object goal: + additionalProperties: false properties: ratio: type: string @@ -3750,6 +4044,7 @@ workstation: solvent2: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3768,16 +4063,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3787,12 +4092,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3824,15 +4136,10 @@ workstation: type: object volume: type: string - required: - - ratio - - solvent1 - - solvent2 - - vessel - - volume title: Recrystallize_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3840,10 +4147,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Recrystallize_Result type: object required: @@ -3890,21 +4193,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: ResetHandling_Feedback type: object goal: + additionalProperties: false properties: solvent: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3923,16 +4228,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3942,12 +4257,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3977,12 +4299,10 @@ workstation: - data title: vessel type: object - required: - - solvent - - vessel title: ResetHandling_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3990,10 +4310,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: ResetHandling_Result type: object required: @@ -4087,21 +4403,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: RunColumn_Feedback type: object goal: + additionalProperties: false properties: column: type: string from_vessel: + additionalProperties: false properties: category: type: string @@ -4120,16 +4438,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4139,12 +4467,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4187,6 +4522,7 @@ workstation: solvent2: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -4205,16 +4541,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4224,12 +4570,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4259,19 +4612,10 @@ workstation: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio title: RunColumn_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4279,10 +4623,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: RunColumn_Result type: object required: @@ -4495,19 +4835,21 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Separate_Feedback type: object goal: + additionalProperties: false properties: from_vessel: + additionalProperties: false properties: category: type: string @@ -4526,16 +4868,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4545,12 +4897,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4583,6 +4942,7 @@ workstation: product_phase: type: string product_vessel: + additionalProperties: false properties: category: type: string @@ -4601,16 +4961,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4620,12 +4990,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4662,6 +5039,7 @@ workstation: minimum: -2147483648 type: integer separation_vessel: + additionalProperties: false properties: category: type: string @@ -4680,16 +5058,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4699,12 +5087,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4735,18 +5130,25 @@ workstation: title: separation_vessel type: object settling_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string solvent_volume: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number through: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -4765,16 +5167,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4784,12 +5196,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4820,6 +5239,7 @@ workstation: title: to_vessel type: object vessel: + additionalProperties: false properties: category: type: string @@ -4838,16 +5258,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4857,12 +5287,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4895,6 +5332,7 @@ workstation: volume: type: string waste_phase_to_vessel: + additionalProperties: false properties: category: type: string @@ -4913,16 +5351,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4932,12 +5380,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4968,6 +5423,7 @@ workstation: title: waste_phase_to_vessel type: object waste_vessel: + additionalProperties: false properties: category: type: string @@ -4986,16 +5442,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5005,12 +5471,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5040,27 +5513,10 @@ workstation: - data title: waste_vessel type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time title: Separate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5068,10 +5524,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Separate_Result type: object required: @@ -5128,26 +5580,31 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_status title: StartStir_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -5166,16 +5623,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5185,12 +5652,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5220,13 +5694,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - stir_speed - - purpose title: StartStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5234,10 +5705,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: StartStir_Result type: object required: @@ -5302,28 +5769,33 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -5342,16 +5814,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5361,12 +5843,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5396,17 +5885,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5414,10 +5896,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -5470,19 +5948,21 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: StopStir_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -5501,16 +5981,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5520,12 +6010,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5555,11 +6052,10 @@ workstation: - data title: vessel type: object - required: - - vessel title: StopStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5567,10 +6063,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: StopStir_Result type: object required: @@ -5638,20 +6130,22 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -5664,31 +6158,27 @@ workstation: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5696,10 +6186,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -5807,21 +6293,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: WashSolid_Feedback type: object goal: + additionalProperties: false properties: event: type: string filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -5840,16 +6328,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5859,12 +6357,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5907,12 +6412,17 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -5931,16 +6441,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5950,12 +6470,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5989,23 +6516,10 @@ workstation: type: string volume_spec: type: string - required: - - vessel - - solvent - - volume - - filtrate_vessel - - temp - - stir - - stir_speed - - time - - repeats - - volume_spec - - repeats_spec - - mass - - event title: WashSolid_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -6013,10 +6527,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: WashSolid_Result type: object required: @@ -6035,7 +6545,7 @@ workstation: config: properties: deck: - type: string + type: object protocol_type: items: type: string diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml index cbdf8aa8..2b49ae55 100644 --- a/unilabos/registry/devices/xrd_d7mate.yaml +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -45,31 +45,6 @@ xrd_d7mate: title: connect参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand auto-start_from_string: feedback: {} goal: {} @@ -85,11 +60,14 @@ xrd_d7mate: goal: properties: params: - type: string + anyOf: + - type: string + - type: object required: - params type: object - result: {} + result: + type: object required: - goal title: start_from_string参数 @@ -105,21 +83,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -130,38 +105,38 @@ xrd_d7mate: get_sample_down: feedback: {} goal: - sample_station: 1 + int_input: int_input + sample_station: sample_station goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -179,21 +154,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -211,21 +183,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -238,26 +207,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -274,42 +242,35 @@ xrd_d7mate: sample_id: '' start_theta: 10.0 goal_default: - end_theta: 80.0 - exp_time: 0.5 - increment: 0.02 - sample_id: Sample001 - start_theta: 10.0 + end_theta: null + exp_time: null + increment: null + sample_id: null + start_theta: null handles: {} + placeholder_keys: {} result: {} schema: description: 送样完成后,发送样品信息和采集参数 properties: feedback: - properties: {} - required: [] title: SampleReadyInput_Feedback - type: object goal: properties: end_theta: description: 结束角度(≥5.5°,且必须大于start_theta) - minimum: 5.5 type: number exp_time: description: 曝光时间(0.1-5.0秒) - maximum: 5.0 - minimum: 0.1 type: number increment: description: 角度增量(≥0.005) - minimum: 0.005 type: number sample_id: description: 样品标识符 type: string start_theta: description: 起始角度(≥5°) - minimum: 5.0 type: number required: - sample_id @@ -320,19 +281,11 @@ xrd_d7mate: title: SampleReadyInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: SampleReadyInput_Result type: object required: - goal - title: SampleReadyInput + title: send_sample_ready参数 type: object type: UniLabJsonCommand set_power_off: @@ -340,26 +293,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -372,26 +324,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -405,18 +356,16 @@ xrd_d7mate: current: 30.0 voltage: 40.0 goal_default: - current: 30.0 - voltage: 40.0 + current: null + voltage: null handles: {} + placeholder_keys: {} result: {} schema: description: 设置高压电源电压和电流 properties: feedback: - properties: {} - required: [] title: VoltageCurrentInput_Feedback - type: object goal: properties: current: @@ -431,19 +380,11 @@ xrd_d7mate: title: VoltageCurrentInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: VoltageCurrentInput_Result type: object required: - goal - title: VoltageCurrentInput + title: set_voltage_current参数 type: object type: UniLabJsonCommand start: @@ -453,11 +394,12 @@ xrd_d7mate: end_theta: 80.0 exp_time: 0.1 increment: 0.05 - sample_id: 样品名称 + sample_id: '' start_theta: 10.0 string: '' wait_minutes: 3.0 handles: {} + placeholder_keys: {} result: {} schema: description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。 @@ -466,54 +408,42 @@ xrd_d7mate: goal: properties: end_theta: + default: 80.0 description: 结束角度(≥5.5°,且必须大于start_theta) - minimum: 5.5 - type: string + type: number exp_time: + default: 0.1 description: 曝光时间(0.1-5.0秒) - maximum: 5.0 - minimum: 0.1 - type: string + type: number increment: + default: 0.05 description: 角度增量(≥0.005) - minimum: 0.005 - type: string + type: number sample_id: + default: '' description: 样品标识符 type: string start_theta: + default: 10.0 description: 起始角度(≥5°) - minimum: 5.0 - type: string + type: number string: + default: '' description: 字符串格式的参数输入,如果提供则优先解析使用 type: string wait_minutes: + default: 3.0 description: 允许上样后等待分钟数 - minimum: 0.0 type: number - required: - - sample_id - - start_theta - - end_theta - - increment - - exp_time + required: [] title: StartWorkflow_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: StartWorkflow_Result type: object required: - goal - title: StartWorkflow + title: start参数 type: object type: UniLabJsonCommand start_auto_mode: @@ -521,17 +451,15 @@ xrd_d7mate: goal: status: true goal_default: - status: true + status: null handles: {} + placeholder_keys: {} result: {} schema: description: 启动或停止自动模式 properties: feedback: - properties: {} - required: [] title: BoolSingleInput_Feedback - type: object goal: properties: status: @@ -542,25 +470,16 @@ xrd_d7mate: title: BoolSingleInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: BoolSingleInput_Result type: object required: - goal - title: BoolSingleInput + title: start_auto_mode参数 type: object type: UniLabJsonCommand module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient status_types: current_acquire_data: dict - sample_down: dict sample_request: dict sample_status: dict type: python @@ -586,16 +505,13 @@ xrd_d7mate: properties: current_acquire_data: type: object - sample_down: - type: object sample_request: type: object sample_status: type: object required: - - sample_request - current_acquire_data + - sample_request - sample_status - - sample_down type: object version: 1.0.0 diff --git a/unilabos/registry/devices/zhida_gcms.yaml b/unilabos/registry/devices/zhida_gcms.yaml index 607af9b9..37adbd79 100644 --- a/unilabos/registry/devices/zhida_gcms.yaml +++ b/unilabos/registry/devices/zhida_gcms.yaml @@ -8,26 +8,25 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -77,31 +76,6 @@ zhida_gcms: title: connect参数 type: object type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: string - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand get_methods: feedback: {} goal: {} @@ -112,21 +86,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -144,21 +115,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -176,21 +144,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -203,26 +168,25 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -234,35 +198,35 @@ zhida_gcms: feedback: {} goal: string: string + text: text goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -273,36 +237,36 @@ zhida_gcms: start_with_csv_file: feedback: {} goal: + csv_file_path: csv_file_path string: string goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -343,8 +307,8 @@ zhida_gcms: version: type: object required: - - status - methods + - status - version type: object version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 02d80cca..8841764c 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,20 +1,58 @@ +""" +统一注册表系统 + +合并了原 Registry (YAML 加载) 和 DecoratorRegistry (装饰器/AST 扫描) 的功能, +提供单一入口来构建、验证和查询设备/资源注册表。 +""" + import copy +import importlib +import inspect import io import os import sys -import inspect -import importlib import threading +import time import traceback from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -from typing import Any, Dict, List, Union, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import yaml +from unilabos_msgs.action import EmptyIn, ResourceCreateFromOuter, ResourceCreateFromOuterEasy from unilabos_msgs.msg import Resource from unilabos.config.config import BasicConfig +from unilabos.registry.decorators import ( + get_device_meta, + get_action_meta, + get_resource_meta, + has_action_decorator, + get_all_registered_devices, + get_all_registered_resources, + is_not_action, + is_always_free, + get_topic_config, +) +from unilabos.registry.utils import ( + ROSMsgNotFound, + parse_docstring, + get_json_schema_type, + parse_type_node, + type_node_to_schema, + resolve_type_object, + type_to_schema, + detect_slot_type, + detect_placeholder_keys, + normalize_ast_handles, + normalize_ast_action_handles, + wrap_action_schema, + preserve_field_descriptions, + resolve_method_params_via_import, + SIMPLE_TYPE_MAP, +) from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list +from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.ros.msgs.message_converter import ( msg_converter_manager, ros_action_to_json_schema, @@ -23,234 +61,1427 @@ from unilabos.ros.msgs.message_converter import ( ) from unilabos.utils import logger from unilabos.utils.decorator import singleton -from unilabos.utils.import_manager import get_enhanced_class_info, get_class +from unilabos.utils.cls_creator import import_class +from unilabos.utils.import_manager import get_enhanced_class_info from unilabos.utils.type_check import NoAliasDumper +from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance +from msgcenterpy.instances.ros2_instance import ROS2MessageInstance -DEFAULT_PATHS = [Path(__file__).absolute().parent] - - -class ROSMsgNotFound(Exception): - pass +_module_hash_cache: Dict[str, Optional[str]] = {} @singleton class Registry: + """ + 统一注册表。 + + 核心流程: + 1. AST 静态扫描 @device/@resource 装饰器 (快速, 无需 import) + 2. 加载 YAML 注册表 (兼容旧格式) + 3. 设置 host_node 内置设备 + 4. verify & resolve (实际 import 验证 + 类型解析) + """ + def __init__(self, registry_paths=None): import ctypes try: + # noinspection PyUnusedImports import unilabos_msgs except ImportError: logger.error("[UniLab Registry] unilabos_msgs模块未找到,请确保已根据官方文档安装unilabos_msgs包。") sys.exit(1) try: ctypes.CDLL(str(Path(unilabos_msgs.__file__).parent / "unilabos_msgs_s__rosidl_typesupport_c.pyd")) - except OSError as e: + except OSError: pass - self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 + self.registry_paths = [Path(__file__).absolute().parent] if registry_paths: self.registry_paths.extend(registry_paths) - self.ResourceCreateFromOuter = self._replace_type_with_class( - "ResourceCreateFromOuter", "host_node", f"动作 create_resource_detailed" - ) - self.ResourceCreateFromOuterEasy = self._replace_type_with_class( - "ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource" - ) - self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"") - self.StrSingleInput = self._replace_type_with_class("StrSingleInput", "host_node", f"") - self.device_type_registry = {} - self.device_module_to_registry = {} - self.resource_type_registry = {} - self._setup_called = False # 跟踪setup是否已调用 - self._registry_lock = threading.Lock() # 多线程加载时的锁 - # 其他状态变量 - # self.is_host_mode = False # 移至BasicConfig中 + logger.debug(f"[UniLab Registry] registry_paths: {self.registry_paths}") - def setup(self, complete_registry=False, upload_registry=False): - # 检查是否已调用过setup + self.device_type_registry: Dict[str, Any] = {} + self.resource_type_registry: Dict[str, Any] = {} + self._type_resolve_cache: Dict[str, Any] = {} + + self._setup_called = False + self._startup_executor: Optional[ThreadPoolExecutor] = None + + # ------------------------------------------------------------------ + # 统一入口 + # ------------------------------------------------------------------ + + def setup(self, devices_dirs=None, upload_registry=False, complete_registry=False): + """统一构建注册表入口。""" if self._setup_called: logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用") return - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - - # 获取 HostNode 类的增强信息,用于自动生成 action schema - host_node_enhanced_info = get_enhanced_class_info( - "unilabos.ros.nodes.presets.host_node:HostNode", use_dynamic=True + self._startup_executor = ThreadPoolExecutor( + max_workers=8, thread_name_prefix="RegistryStartup" ) - # 为 test_latency 生成 schema,保留原有 description - test_latency_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_latency", {}) - test_latency_schema = self._generate_unilab_json_command_schema( - test_latency_method_info.get("args", []), - "test_latency", - test_latency_method_info.get("return_annotation"), - ) - test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。" + # 1. AST 静态扫描 (快速, 无需 import) + self._run_ast_scan(devices_dirs, upload_registry=upload_registry) - test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {}) - test_resource_schema = self._generate_unilab_json_command_schema( - test_resource_method_info.get("args", []), - "test_resource", - test_resource_method_info.get("return_annotation"), - ) - test_resource_schema["description"] = "用于测试物料、设备和样本。" + # 2. Host node 内置设备 + self._setup_host_node() - self.device_type_registry.update( - { - "host_node": { - "description": "UniLabOS主机节点", - "class": { - "module": "unilabos.ros.nodes.presets.host_node", - "type": "python", - "status_types": {}, - "action_value_mappings": { - "create_resource_detailed": { - "type": self.ResourceCreateFromOuter, - "goal": { - "resources": "resources", - "device_ids": "device_ids", - "bind_parent_ids": "bind_parent_ids", - "bind_locations": "bind_locations", - "other_calling_params": "other_calling_params", - }, - "feedback": {}, - "result": {"success": "success"}, - "schema": ros_action_to_json_schema( - self.ResourceCreateFromOuter, "用于创建或更新物料资源,每次传入多个物料信息。" - ), - "goal_default": yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal)) - ), - "handles": {}, - }, - "create_resource": { - "type": self.ResourceCreateFromOuterEasy, - "goal": { - "res_id": "res_id", - "class_name": "class_name", - "parent": "parent", - "device_id": "device_id", - "bind_locations": "bind_locations", - "liquid_input_slot": "liquid_input_slot[]", - "liquid_type": "liquid_type[]", - "liquid_volume": "liquid_volume[]", - "slot_on_deck": "slot_on_deck", - }, - "feedback": {}, - "result": {"success": "success"}, - "schema": ros_action_to_json_schema( - self.ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。" - ), - "goal_default": yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal)) - ), - "handles": { - "output": [ - { - "handler_key": "labware", - "data_type": "resource", - "label": "Labware", - "data_source": "executor", - "data_key": "created_resource_tree.@flatten", - }, - { - "handler_key": "liquid_slots", - "data_type": "resource", - "label": "LiquidSlots", - "data_source": "executor", - "data_key": "liquid_input_resource_tree.@flatten", - }, - { - "handler_key": "materials", - "data_type": "resource", - "label": "AllMaterials", - "data_source": "executor", - "data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten", - }, - ] - }, - "placeholder_keys": { - "res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择 - "device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择 - "parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择 - "class_name": "unilabos_class", # 当前实验室物料的class name - "slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index) - }, - }, - "test_latency": { - "type": ( - "UniLabJsonCommandAsync" - if test_latency_method_info.get("is_async", False) - else "UniLabJsonCommand" - ), - "goal": {}, - "feedback": {}, - "result": {}, - "schema": test_latency_schema, - "goal_default": { - arg["name"]: arg["default"] for arg in test_latency_method_info.get("args", []) - }, - "handles": {}, - }, - "auto-test_resource": { - "type": "UniLabJsonCommand", - "goal": {}, - "feedback": {}, - "result": {}, - "schema": test_resource_schema, - "placeholder_keys": { - "device": "unilabos_devices", - "devices": "unilabos_devices", - "resource": "unilabos_resources", - "resources": "unilabos_resources", - }, - "goal_default": {}, - "handles": { - "input": [ - { - "handler_key": "input_resources", - "data_type": "resource", - "label": "InputResources", - "data_source": "handle", - "data_key": "resources", # 不为空 - }, - ] - }, - }, - }, - }, - "version": "1.0.0", - "category": [], - "config_info": [], - "icon": "icon_device.webp", - "registry_type": "device", - "handles": [], # virtue采用了不同的handle - "init_param_schema": {}, - "file_path": "/", - } - } - ) - # 为host_node添加内置的驱动命令动作 - self._add_builtin_actions(self.device_type_registry["host_node"], "host_node") - logger.trace(f"[UniLab Registry] ----------Setup----------") + # 3. YAML 注册表加载 (兼容旧格式) self.registry_paths = [Path(path).absolute() for path in self.registry_paths] for i, path in enumerate(self.registry_paths): sys_path = path.parent logger.trace(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}") sys.path.append(str(sys_path)) - self.load_device_types(path, complete_registry) + self.load_device_types(path, complete_registry=complete_registry) if BasicConfig.enable_resource_load: - self.load_resource_types(path, complete_registry, upload_registry) + self.load_resource_types(path, upload_registry, complete_registry=complete_registry) else: - logger.warning("跳过了资源注册表加载!") - logger.info("[UniLab Registry] 注册表设置完成") - # 标记setup已被调用 + logger.warning( + "[UniLab Registry] 资源加载已禁用 (enable_resource_load=False),跳过资源注册表加载" + ) + self._startup_executor.shutdown(wait=True) + self._startup_executor = None self._setup_called = True + logger.trace(f"[UniLab Registry] ----------Setup Complete----------") + + # ------------------------------------------------------------------ + # Host node 设置 + # ------------------------------------------------------------------ + + def _setup_host_node(self): + """设置 host_node 内置设备 — 基于 _run_ast_scan 已扫描的结果进行覆写。""" + # 从 AST 扫描结果中取出 host_node 的 action_value_mappings + ast_entry = self.device_type_registry.get("host_node", {}) + ast_actions = ast_entry.get("class", {}).get("action_value_mappings", {}) + + # 取出 AST 生成的 auto-method entries, 补充特定覆写 + test_latency_action = ast_actions.get("auto-test_latency", {}) + test_resource_action = ast_actions.get("auto-test_resource", {}) + test_resource_action["handles"] = { + "input": [ + { + "handler_key": "input_resources", + "data_type": "resource", + "label": "InputResources", + "data_source": "handle", + "data_key": "resources", + }, + ] + } + + create_resource_action = ast_actions.get("auto-create_resource", {}) + raw_create_resource_schema = ros_action_to_json_schema( + ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。" + ) + raw_create_resource_schema["properties"]["result"] = create_resource_action["schema"]["properties"]["result"] + + # 覆写: 保留硬编码的 ROS2 action + AST 生成的 auto-method + self.device_type_registry["host_node"] = { + "class": { + "module": "unilabos.ros.nodes.presets.host_node:HostNode", + "status_types": {}, + "action_value_mappings": { + "create_resource": { + "type": ResourceCreateFromOuterEasy, + "goal": { + "res_id": "res_id", + "class_name": "class_name", + "parent": "parent", + "device_id": "device_id", + "bind_locations": "bind_locations", + "liquid_input_slot": "liquid_input_slot[]", + "liquid_type": "liquid_type[]", + "liquid_volume": "liquid_volume[]", + "slot_on_deck": "slot_on_deck", + }, + "feedback": {}, + "result": {"success": "success"}, + "schema": raw_create_resource_schema, + "goal_default": ROS2MessageInstance(ResourceCreateFromOuterEasy.Goal()).get_python_dict(), + "handles": { + "output": [ + { + "handler_key": "labware", + "data_type": "resource", + "label": "Labware", + "data_source": "executor", + "data_key": "created_resource_tree.@flatten", + }, + { + "handler_key": "liquid_slots", + "data_type": "resource", + "label": "LiquidSlots", + "data_source": "executor", + "data_key": "liquid_input_resource_tree.@flatten", + }, + { + "handler_key": "materials", + "data_type": "resource", + "label": "AllMaterials", + "data_source": "executor", + "data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten", + }, + ] + }, + "placeholder_keys": { + "res_id": "unilabos_resources", + "device_id": "unilabos_devices", + "parent": "unilabos_nodes", + "class_name": "unilabos_class", + }, + }, + "test_latency": test_latency_action, + "auto-test_resource": test_resource_action, + }, + "init_params": {}, + }, + "version": "1.0.0", + "category": [], + "config_info": [], + "icon": "icon_device.webp", + "registry_type": "device", + "description": "Host Node", + "handles": [], + "init_param_schema": {}, + "file_path": "/", + } + self._add_builtin_actions(self.device_type_registry["host_node"], "host_node") + + # ------------------------------------------------------------------ + # AST 静态扫描 + # ------------------------------------------------------------------ + + def _run_ast_scan(self, devices_dirs=None, upload_registry=False): + """ + 执行 AST 静态扫描,从 Python 代码中提取 @device / @resource 装饰器元数据。 + 无需 import 任何驱动模块,速度极快。 + + 所有缓存(AST 扫描 / build 结果 / config_info)统一存放在 + registry_cache.pkl 一个文件中,删除即可完全重置。 + """ + import time as _time + from unilabos.registry.ast_registry_scanner import scan_directory + + scan_t0 = _time.perf_counter() + + # 确保 executor 存在 + own_executor = False + if self._startup_executor is None: + self._startup_executor = ThreadPoolExecutor( + max_workers=8, thread_name_prefix="RegistryStartup" + ) + own_executor = True + + # ---- 统一缓存:一个 pkl 包含所有数据 ---- + unified_cache = self._load_config_cache() + ast_cache = unified_cache.setdefault("_ast_scan", {"files": {}}) + + # 默认:扫描 unilabos 包所在的父目录 + pkg_root = Path(__file__).resolve().parent.parent # .../unilabos + python_path = pkg_root.parent # .../Uni-Lab-OS + scan_root = pkg_root # 扫描 unilabos/ 整个包 + + # 额外的 --devices 目录:把它们的父目录加入 sys.path + extra_dirs: list[Path] = [] + if devices_dirs: + for d in devices_dirs: + d_path = Path(d).resolve() + if not d_path.is_dir(): + logger.warning(f"[UniLab Registry] --devices 路径不存在或不是目录: {d_path}") + continue + parent_dir = str(d_path.parent) + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + logger.info(f"[UniLab Registry] 添加 Python 路径: {parent_dir}") + extra_dirs.append(d_path) + + # 主扫描 + exclude_files = {"lab_resources.py"} if not BasicConfig.extra_resource else None + scan_result = scan_directory( + scan_root, python_path=python_path, executor=self._startup_executor, + exclude_files=exclude_files, cache=ast_cache, + ) + if exclude_files: + logger.info( + f"[UniLab Registry] 排除扫描文件: {exclude_files} " + f"(可通过 --extra_resource 启用加载)" + ) + + # 合并缓存统计 + total_stats = scan_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0}) + + # 额外目录逐个扫描并合并 + for d_path in extra_dirs: + extra_result = scan_directory( + d_path, python_path=str(d_path.parent), executor=self._startup_executor, + cache=ast_cache, + ) + extra_stats = extra_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0}) + total_stats["hits"] += extra_stats["hits"] + total_stats["misses"] += extra_stats["misses"] + total_stats["total"] += extra_stats["total"] + + for did, dmeta in extra_result.get("devices", {}).items(): + if did in scan_result.get("devices", {}): + existing = scan_result["devices"][did].get("file_path", "?") + new_file = dmeta.get("file_path", "?") + raise ValueError( + f"@device id 重复: '{did}' 同时出现在 {existing} 和 {new_file}" + ) + scan_result.setdefault("devices", {})[did] = dmeta + for rid, rmeta in extra_result.get("resources", {}).items(): + if rid in scan_result.get("resources", {}): + existing = scan_result["resources"][rid].get("file_path", "?") + new_file = rmeta.get("file_path", "?") + raise ValueError( + f"@resource id 重复: '{rid}' 同时出现在 {existing} 和 {new_file}" + ) + scan_result.setdefault("resources", {})[rid] = rmeta + + # 缓存命中统计 + if total_stats["total"] > 0: + logger.info( + f"[UniLab Registry] AST 缓存统计: " + f"{total_stats['hits']}/{total_stats['total']} 命中, " + f"{total_stats['misses']} 重新解析" + ) + + ast_devices = scan_result.get("devices", {}) + ast_resources = scan_result.get("resources", {}) + + # build 结果缓存:当所有 AST 文件命中时跳过 _build_*_entry_from_ast + all_ast_hit = total_stats["misses"] == 0 and total_stats["total"] > 0 + cached_build = unified_cache.get("_build_results") if all_ast_hit else None + + if cached_build: + cached_devices = cached_build.get("devices", {}) + cached_resources = cached_build.get("resources", {}) + if set(cached_devices) == set(ast_devices) and set(cached_resources) == set(ast_resources): + self.device_type_registry.update(cached_devices) + self.resource_type_registry.update(cached_resources) + logger.info( + f"[UniLab Registry] build 缓存命中: 跳过 {len(cached_devices)} 设备 + " + f"{len(cached_resources)} 资源的 entry 构建" + ) + else: + cached_build = None + + if not cached_build: + build_t0 = _time.perf_counter() + + for device_id, ast_meta in ast_devices.items(): + entry = self._build_device_entry_from_ast(device_id, ast_meta) + if entry: + self.device_type_registry[device_id] = entry + + for resource_id, ast_meta in ast_resources.items(): + entry = self._build_resource_entry_from_ast(resource_id, ast_meta) + if entry: + self.resource_type_registry[resource_id] = entry + + build_elapsed = _time.perf_counter() - build_t0 + logger.info(f"[UniLab Registry] entry 构建耗时: {build_elapsed:.2f}s") + + unified_cache["_build_results"] = { + "devices": {k: v for k, v in self.device_type_registry.items() if k in ast_devices}, + "resources": {k: v for k, v in self.resource_type_registry.items() if k in ast_resources}, + } + + # upload 模式下,利用线程池并行 import pylabrobot 资源并生成 config_info + if upload_registry: + self._populate_resource_config_info(config_cache=unified_cache) + + # 统一保存一次 + self._save_config_cache(unified_cache) + + ast_device_count = len(ast_devices) + ast_resource_count = len(ast_resources) + scan_elapsed = _time.perf_counter() - scan_t0 + if ast_device_count > 0 or ast_resource_count > 0: + logger.info( + f"[UniLab Registry] AST 扫描完成: {ast_device_count} 设备, " + f"{ast_resource_count} 资源 (耗时 {scan_elapsed:.2f}s)" + ) + + if own_executor: + self._startup_executor.shutdown(wait=False) + self._startup_executor = None + + # ------------------------------------------------------------------ + # 类型辅助 (共享, 去重后的单一实现) + # ------------------------------------------------------------------ + + def _replace_type_with_class(self, type_name: str, device_id: str, field_name: str) -> Any: + """将类型名称替换为实际的 ROS 消息类对象(带缓存)""" + if not type_name or type_name == "": + return type_name + + cached = self._type_resolve_cache.get(type_name) + if cached is not None: + return cached + + result = self._resolve_type_uncached(type_name, device_id, field_name) + self._type_resolve_cache[type_name] = result + return result + + def _resolve_type_uncached(self, type_name: str, device_id: str, field_name: str) -> Any: + """实际的类型解析逻辑(无缓存)""" + # 泛型类型映射 + if "[" in type_name: + generic_mapping = { + "List[int]": "Int64MultiArray", + "list[int]": "Int64MultiArray", + "List[float]": "Float64MultiArray", + "list[float]": "Float64MultiArray", + "List[bool]": "Int8MultiArray", + "list[bool]": "Int8MultiArray", + } + mapped = generic_mapping.get(type_name) + if mapped: + cls = msg_converter_manager.search_class(mapped) + if cls: + return cls + logger.debug( + f"[Registry] 设备 {device_id} 的 {field_name} " + f"泛型类型 '{type_name}' 映射为 String" + ) + return String + + convert_manager = { + "str": "String", + "bool": "Bool", + "int": "Int64", + "float": "Float64", + } + type_name = convert_manager.get(type_name, type_name) + if ":" in type_name: + type_class = msg_converter_manager.get_class(type_name) + else: + type_class = msg_converter_manager.search_class(type_name) + if type_class: + return type_class + else: + logger.trace( + f"[Registry] 类型 '{type_name}' 非 ROS2 消息类型 (设备 {device_id} {field_name}),映射为 String" + ) + return String + + # ---- 类型字符串 -> JSON Schema type ---- + # (常量和工具函数已移至 unilabos.registry.utils) + + def _generate_schema_from_info( + self, param_name: str, param_type: Union[str, Tuple[str]], param_default: Any, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """根据参数信息生成 JSON Schema。 + 支持复杂类型字符串如 'Optional[Dict[str, Any]]'、'List[int]' 等。 + 当提供 import_map 时,可解析 TypedDict 等自定义类型。""" + + prop_schema: Dict[str, Any] = {} + + if isinstance(param_type, str) and ("[" in param_type or "|" in param_type): + # 复杂泛型 — ast.parse 解析结构,递归生成 schema + node = parse_type_node(param_type) + if node is not None: + prop_schema = type_node_to_schema(node, import_map) + # slot 标记 fallback(正常不应走到这里,上层会拦截) + if "$slot" in prop_schema: + prop_schema = {"type": "object"} + else: + prop_schema["type"] = "string" + elif isinstance(param_type, str): + # 简单类型名,但可能是 import_map 中的自定义类型 + json_type = SIMPLE_TYPE_MAP.get(param_type.lower()) + if json_type: + prop_schema["type"] = json_type + elif ":" in param_type: + type_obj = resolve_type_object(param_type) + if type_obj is not None: + prop_schema = type_to_schema(type_obj) + else: + prop_schema["type"] = "object" + elif import_map and param_type in import_map: + type_obj = resolve_type_object(import_map[param_type]) + if type_obj is not None: + prop_schema = type_to_schema(type_obj) + else: + prop_schema["type"] = "object" + else: + json_type = get_json_schema_type(param_type) + if json_type == "string" and param_type and param_type.lower() not in SIMPLE_TYPE_MAP: + prop_schema["type"] = "object" + else: + prop_schema["type"] = json_type + elif isinstance(param_type, tuple): + if len(param_type) == 2: + outer_type, inner_type = param_type + outer_json_type = get_json_schema_type(outer_type) + prop_schema["type"] = outer_json_type + # Any 值类型不加 additionalProperties/items (等同于无约束) + if isinstance(inner_type, str) and inner_type in ("Any", "None", "Unknown"): + pass + else: + inner_json_type = get_json_schema_type(inner_type) + if outer_json_type == "array": + prop_schema["items"] = {"type": inner_json_type} + elif outer_json_type == "object": + prop_schema["additionalProperties"] = {"type": inner_json_type} + else: + prop_schema["type"] = "string" + else: + prop_schema["type"] = get_json_schema_type(param_type) + + if param_default is not None: + prop_schema["default"] = param_default + + return prop_schema + + def _generate_unilab_json_command_schema( + self, method_args: list, docstring: Optional[str] = None, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """根据方法参数和 docstring 生成 UniLabJsonCommand schema""" + doc_info = parse_docstring(docstring) + param_descs = doc_info.get("params", {}) + + schema = { + "type": "object", + "properties": {}, + "required": [], + } + for arg_info in method_args: + param_name = arg_info.get("name", "") + param_type = arg_info.get("type", "") + param_default = arg_info.get("default") + param_required = arg_info.get("required", True) + + is_slot, is_list_slot = detect_slot_type(param_type) + if is_slot == "ResourceSlot": + if is_list_slot: + schema["properties"][param_name] = { + "items": ros_message_to_json_schema(Resource, param_name), + "type": "array", + } + else: + schema["properties"][param_name] = ros_message_to_json_schema( + Resource, param_name + ) + elif is_slot == "DeviceSlot": + schema["properties"][param_name] = {"type": "string", "description": "device reference"} + else: + schema["properties"][param_name] = self._generate_schema_from_info( + param_name, param_type, param_default, import_map=import_map + ) + + if param_name in param_descs: + schema["properties"][param_name]["description"] = param_descs[param_name] + + if param_required: + schema["required"].append(param_name) + + return schema + + def _generate_status_types_schema(self, status_methods: Dict[str, Any]) -> Dict[str, Any]: + """根据 status 方法信息生成 status_types schema""" + status_schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for status_name, status_info in status_methods.items(): + return_type = status_info.get("return_type", "str") + status_schema["properties"][status_name] = self._generate_schema_from_info( + status_name, return_type, None + ) + status_schema["required"].append(status_name) + return status_schema + + # ------------------------------------------------------------------ + # 方法签名分析 -- 委托给 ImportManager + # ------------------------------------------------------------------ + + @staticmethod + def _analyze_method_signature(method) -> Dict[str, Any]: + """分析方法签名,提取参数信息""" + from unilabos.utils.import_manager import default_manager + try: + return default_manager._analyze_method_signature(method) + except (ValueError, TypeError): + return {"args": [], "is_async": inspect.iscoroutinefunction(method)} + + @staticmethod + def _get_return_type_from_method(method) -> str: + """获取方法的返回类型字符串""" + from unilabos.utils.import_manager import default_manager + return default_manager._get_return_type_from_method(method) + + # ------------------------------------------------------------------ + # 动态类信息提取 (import-based) + # ------------------------------------------------------------------ + + def _extract_class_info(self, cls: type) -> Dict[str, Any]: + """ + 从类中提取 init 参数、状态方法和动作方法信息。 + """ + result = { + "class_name": cls.__name__, + "init_params": self._analyze_method_signature(cls.__init__)["args"], + "status_methods": {}, + "action_methods": {}, + "explicit_actions": {}, + "decorated_no_type_actions": {}, + } + + for name, method in cls.__dict__.items(): + if name.startswith("_"): + continue + + # property => status + if isinstance(method, property): + return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any" + status_entry = { + "name": name, + "return_type": return_type, + } + if method.fget: + tc = get_topic_config(method.fget) + if tc: + status_entry["topic_config"] = tc + result["status_methods"][name] = status_entry + + if method.fset: + setter_info = self._analyze_method_signature(method.fset) + action_meta = get_action_meta(method.fset) + if action_meta and action_meta.get("action_type") is not None: + result["explicit_actions"][name] = { + "method_info": setter_info, + "action_meta": action_meta, + } + continue + + if not callable(method): + continue + + if is_not_action(method): + continue + + # @topic_config 装饰的非 property 方法视为状态方法,不作为 action + tc = get_topic_config(method) + if tc: + return_type = self._get_return_type_from_method(method) + prop_name = name[4:] if name.startswith("get_") else name + result["status_methods"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "topic_config": tc, + } + continue + + method_info = self._analyze_method_signature(method) + action_meta = get_action_meta(method) + + if action_meta: + action_type = action_meta.get("action_type") + if action_type is not None: + result["explicit_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta, + } + else: + result["decorated_no_type_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta, + } + elif has_action_decorator(method): + result["explicit_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta or {}, + } + else: + result["action_methods"][name] = method_info + + return result + + # ------------------------------------------------------------------ + # 内置动作 + # ------------------------------------------------------------------ + + def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): + """为设备添加内置的驱动命令动作(运行时需要,上报注册表时会过滤掉)""" + str_single_input = self._replace_type_with_class("StrSingleInput", device_id, "内置动作") + for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]: + try: + goal_default = ROS2MessageInstance(str_single_input.Goal()).get_python_dict() + except Exception: + goal_default = {"string": ""} + + device_config["class"]["action_value_mappings"][additional_action] = { + "type": str_single_input, + "goal": {"string": "string"}, + "feedback": {}, + "result": {}, + "schema": ros_action_to_json_schema(str_single_input), + "goal_default": goal_default, + "handles": {}, + } + + # ------------------------------------------------------------------ + # AST-based 注册表条目构建 + # ------------------------------------------------------------------ + + def _build_device_entry_from_ast(self, device_id: str, ast_meta: dict) -> Dict[str, Any]: + """ + Build a device registry entry from AST-scanned metadata. + Uses only string types -- no module imports required (except for TypedDict resolution). + """ + module_str = ast_meta.get("module", "") + file_path = ast_meta.get("file_path", "") + imap = ast_meta.get("import_map") or {} + + # --- status_types (string version) --- + status_types_str: Dict[str, str] = {} + for name, info in ast_meta.get("status_properties", {}).items(): + ret_type = info.get("return_type", "str") + if not ret_type or ret_type in ("Any", "None", "Unknown", ""): + ret_type = "String" + # 归一化泛型容器类型: Dict[str, Any] → dict, List[int] → list 等 + elif "[" in ret_type: + base = ret_type.split("[", 1)[0].strip() + base_lower = base.lower() + if base_lower in ("dict", "mapping", "ordereddict"): + ret_type = "dict" + elif base_lower in ("list", "tuple", "set", "sequence", "iterable"): + ret_type = "list" + elif base_lower == "optional": + # Optional[X] → 取内部类型再归一化 + inner = ret_type.split("[", 1)[1].rsplit("]", 1)[0].strip() + inner_lower = inner.lower() + if inner_lower in ("dict", "mapping"): + ret_type = "dict" + elif inner_lower in ("list", "tuple", "set"): + ret_type = "list" + else: + ret_type = inner + status_types_str[name] = ret_type + status_types_str = dict(sorted(status_types_str.items())) + + # --- action_value_mappings --- + action_value_mappings: Dict[str, Any] = {} + + def _build_json_command_entry(method_name, method_info, action_args=None): + """构建 UniLabJsonCommand 类型的 action entry""" + is_async = method_info.get("is_async", False) + type_str = "UniLabJsonCommandAsync" if is_async else "UniLabJsonCommand" + params = method_info.get("params", []) + method_doc = method_info.get("docstring") + goal_schema = self._generate_schema_from_ast_params(params, method_name, method_doc, imap) + + if action_args is not None: + action_name = action_args.get("action_name", method_name) + if action_args.get("auto_prefix"): + action_name = f"auto-{action_name}" + else: + action_name = f"auto-{method_name}" + + # Source C: 从 schema 生成类型默认值 + goal_default = JSONSchemaMessageInstance.generate_default_from_schema(goal_schema) + # Source B: method param 显式 default 覆盖 Source C + for p in params: + if p.get("default") is not None: + goal_default[p["name"]] = p["default"] + # goal 为 identity mapping {param_name: param_name}, 默认值只放在 goal_default + goal = {p["name"]: p["name"] for p in params} + + # @action 中的显式 goal/goal_default 覆盖 + goal_override = dict((action_args or {}).get("goal", {})) + goal_default_override = dict((action_args or {}).get("goal_default", {})) + if goal_override: + override_values = set(goal_override.values()) + goal = {k: v for k, v in goal.items() if not (k == v and v in override_values)} + goal.update(goal_override) + goal_default.update(goal_default_override) + + # action handles: 从 @action(handles=[...]) 提取并转换为标准格式 + raw_handles = (action_args or {}).get("handles") + handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {}) + + # placeholder_keys: 优先用装饰器显式配置,否则从参数类型检测 + pk = (action_args or {}).get("placeholder_keys") or detect_placeholder_keys(params) + + # 从方法返回值类型生成 result schema + result_schema = None + ret_type_str = method_info.get("return_type", "") + if ret_type_str and ret_type_str not in ("None", "Any", ""): + result_schema = self._generate_schema_from_info( + "result", ret_type_str, None, imap + ) + + entry = { + "type": type_str, + "goal": goal, + "feedback": (action_args or {}).get("feedback") or {}, + "result": (action_args or {}).get("result") or {}, + "schema": wrap_action_schema(goal_schema, action_name, result_schema=result_schema), + "goal_default": goal_default, + "handles": handles, + "placeholder_keys": pk, + } + if (action_args or {}).get("always_free") or method_info.get("always_free"): + entry["always_free"] = True + return action_name, entry + + # 1) auto- actions + for method_name, method_info in ast_meta.get("auto_methods", {}).items(): + action_name, action_entry = _build_json_command_entry(method_name, method_info) + action_value_mappings[action_name] = action_entry + + # 2) @action() without action_type + for method_name, method_info in ast_meta.get("actions", {}).items(): + action_args = method_info.get("action_args", {}) + if action_args.get("action_type"): + continue + action_name, action_entry = _build_json_command_entry(method_name, method_info, action_args) + action_value_mappings[action_name] = action_entry + + # 3) @action(action_type=X) + for method_name, method_info in ast_meta.get("actions", {}).items(): + action_args = method_info.get("action_args", {}) + action_type = action_args.get("action_type") + if not action_type: + continue + + action_name = action_args.get("action_name", method_name) + if action_args.get("auto_prefix"): + action_name = f"auto-{action_name}" + + raw_handles = action_args.get("handles") + handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {}) + + method_params = method_info.get("params", []) + + # goal/feedback/result: 字段映射 + # parent=True 时直接通过 import class + MRO 获取; 否则从 AST 方法参数获取, 最后从 ROS2 Goal 获取 + # feedback/result 从 ROS2 获取; 默认 identity mapping {k: k}, 再用 @action 参数 update + goal_override = dict(action_args.get("goal", {})) + feedback_override = dict(action_args.get("feedback", {})) + result_override = dict(action_args.get("result", {})) + goal_default_override = dict(action_args.get("goal_default", {})) + + if action_args.get("parent"): + # @action(parent=True): 直接通过 import class + MRO 获取父类方法签名 + goal = resolve_method_params_via_import(module_str, method_name) + else: + # 从 AST 方法参数构建 goal identity mapping + real_params = [p for p in method_params if p["name"] not in ("self", "cls")] + goal = {p["name"]: p["name"] for p in real_params} + + feedback = {} + result = {} + schema = {} + goal_default = {} + + # 尝试 import ROS2 action type 获取 feedback/result/schema/goal_default, 以及 goal fallback + if ":" not in action_type: + action_type = imap.get(action_type, action_type) + action_type_obj = resolve_type_object(action_type) if ":" in action_type else None + if action_type_obj is None: + logger.warning( + f"[AST] device action '{action_name}': resolve_type_object('{action_type}') returned None" + ) + if action_type_obj is not None: + # 始终从 ROS2 Goal 获取字段作为基础, 再用方法参数覆盖 + try: + if hasattr(action_type_obj, "Goal"): + goal_fields = action_type_obj.Goal.get_fields_and_field_types() + ros2_goal = {k: k for k in goal_fields} + ros2_goal.update(goal) + goal = ros2_goal + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Goal enrichment from ROS2 failed: {e}") + try: + if hasattr(action_type_obj, "Feedback"): + fb_fields = action_type_obj.Feedback.get_fields_and_field_types() + feedback = {k: k for k in fb_fields} + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Feedback enrichment failed: {e}") + try: + if hasattr(action_type_obj, "Result"): + res_fields = action_type_obj.Result.get_fields_and_field_types() + result = {k: k for k in res_fields} + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Result enrichment failed: {e}") + try: + schema = ros_action_to_json_schema(action_type_obj) + except Exception: + pass + # 直接从 ROS2 Goal 实例获取默认值 (msgcenterpy) + try: + goal_default = ROS2MessageInstance(action_type_obj.Goal()).get_python_dict() + except Exception: + pass + + # 如果 ROS2 action type 未提供 result schema, 用方法返回值类型生成 fallback + if not schema.get("properties", {}).get("result"): + ret_type_str = method_info.get("return_type", "") + if ret_type_str and ret_type_str not in ("None", "Any", ""): + ret_schema = self._generate_schema_from_info( + "result", ret_type_str, None, imap + ) + if ret_schema: + schema.setdefault("properties", {})["result"] = ret_schema + + # @action 中的显式 goal/feedback/result/goal_default 覆盖默认值 + # 移除被 override 取代的 identity 条目 (如 {source: source} 被 {sources: source} 取代) + if goal_override: + override_values = set(goal_override.values()) + goal = {k: v for k, v in goal.items() if not (k == v and v in override_values)} + goal.update(goal_override) + feedback.update(feedback_override) + result.update(result_override) + goal_default.update(goal_default_override) + + action_entry = { + "type": action_type.split(":")[-1], + "goal": goal, + "feedback": feedback, + "result": result, + "schema": schema, + "goal_default": goal_default, + "handles": handles, + "placeholder_keys": action_args.get("placeholder_keys") or detect_placeholder_keys(method_params), + } + if action_args.get("always_free") or method_info.get("always_free"): + action_entry["always_free"] = True + action_value_mappings[action_name] = action_entry + + action_value_mappings = dict(sorted(action_value_mappings.items())) + + # --- init_param_schema = { config: , data: } --- + init_params = ast_meta.get("init_params", []) + config_schema = self._generate_schema_from_ast_params(init_params, "__init__", import_map=imap) + data_schema = self._generate_status_schema_from_ast( + ast_meta.get("status_properties", {}), imap + ) + init_schema: Dict[str, Any] = { + "config": config_schema, + "data": data_schema, + } + + # --- handles --- + handles_raw = ast_meta.get("handles", []) + handles = normalize_ast_handles(handles_raw) + + entry: Dict[str, Any] = { + "category": ast_meta.get("category", []), + "class": { + "module": module_str, + "status_types": status_types_str, + "action_value_mappings": action_value_mappings, + "type": ast_meta.get("device_type", "python"), + }, + "config_info": [], + "description": ast_meta.get("description", ""), + "handles": handles, + "icon": ast_meta.get("icon", ""), + "init_param_schema": init_schema, + "version": ast_meta.get("version", "1.0.0"), + "registry_type": "device", + "file_path": file_path, + } + model = ast_meta.get("model") + if model is not None: + entry["model"] = model + hardware_interface = ast_meta.get("hardware_interface") + if hardware_interface is not None: + # AST 解析 HardwareInterface(...) 得到 {"_call": "...", "name": ..., "read": ..., "write": ...} + # 归一化为 YAML 格式,去掉 _call + if isinstance(hardware_interface, dict) and "_call" in hardware_interface: + hardware_interface = {k: v for k, v in hardware_interface.items() if k != "_call"} + entry["class"]["hardware_interface"] = hardware_interface + return entry + + def _generate_schema_from_ast_params( + self, params: list, method_name: str, docstring: Optional[str] = None, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """Generate JSON Schema from AST-extracted parameter list.""" + doc_info = parse_docstring(docstring) + param_descs = doc_info.get("params", {}) + + schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for p in params: + pname = p.get("name", "") + ptype = p.get("type", "") + pdefault = p.get("default") + prequired = p.get("required", True) + + # --- 检测 ResourceSlot / DeviceSlot (兼容 runtime 和 AST 两种格式) --- + is_slot, is_list_slot = detect_slot_type(ptype) + if is_slot == "ResourceSlot": + if is_list_slot: + schema["properties"][pname] = { + "items": ros_message_to_json_schema(Resource, pname), + "type": "array", + } + else: + schema["properties"][pname] = ros_message_to_json_schema(Resource, pname) + elif is_slot == "DeviceSlot": + schema["properties"][pname] = {"type": "string", "description": "device reference"} + else: + schema["properties"][pname] = self._generate_schema_from_info( + pname, ptype, pdefault, import_map + ) + + if pname in param_descs: + schema["properties"][pname]["description"] = param_descs[pname] + + if prequired: + schema["required"].append(pname) + + return schema + + def _generate_status_schema_from_ast( + self, status_properties: Dict[str, Any], + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """Generate status_types schema from AST-extracted status properties.""" + schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for name, info in status_properties.items(): + ret_type = info.get("return_type", "str") + schema["properties"][name] = self._generate_schema_from_info( + name, ret_type, None, import_map + ) + schema["required"].append(name) + return schema + + def _build_resource_entry_from_ast(self, resource_id: str, ast_meta: dict) -> Dict[str, Any]: + """Build a resource registry entry from AST-scanned metadata.""" + module_str = ast_meta.get("module", "") + file_path = ast_meta.get("file_path", "") + + handles_raw = ast_meta.get("handles", []) + handles = normalize_ast_handles(handles_raw) + + entry: Dict[str, Any] = { + "category": ast_meta.get("category", []), + "class": { + "module": module_str, + "type": ast_meta.get("class_type", "python"), + }, + "config_info": [], + "description": ast_meta.get("description", ""), + "handles": handles, + "icon": ast_meta.get("icon", ""), + "init_param_schema": {}, + "version": ast_meta.get("version", "1.0.0"), + "registry_type": "resource", + "file_path": file_path, + } + + if ast_meta.get("model"): + entry["model"] = ast_meta["model"] + + return entry + + # ------------------------------------------------------------------ + # 定向 AST 扫描(供 complete_registry Case 1 使用) + # ------------------------------------------------------------------ + + def _ast_scan_module(self, module_str: str) -> Optional[Dict[str, Any]]: + """对单个 module_str 做定向 AST 扫描,返回 ast_meta 或 None。 + + 用于 complete_registry 模式下 YAML 中存在但 AST 全量扫描未覆盖的设备/资源。 + 仅做文件定位 + AST 解析,不实例化类。 + """ + from unilabos.registry.ast_registry_scanner import _parse_file + + mod_part = module_str.split(":")[0] + try: + mod = importlib.import_module(mod_part) + src_file = Path(inspect.getfile(mod)) + except Exception: + return None + + python_path = Path(__file__).resolve().parent.parent.parent + try: + devs, ress = _parse_file(src_file, python_path) + except Exception: + return None + + for d in devs: + if d.get("module") == module_str: + return d + for r in ress: + if r.get("module") == module_str: + return r + return None + + # ------------------------------------------------------------------ + # config_info 缓存 (pickle 格式,比 JSON 快 ~10x,debug 模式下差异更大) + # ------------------------------------------------------------------ + + @staticmethod + def _get_config_cache_path() -> Optional[Path]: + if BasicConfig.working_dir: + return Path(BasicConfig.working_dir) / "registry_cache.pkl" + return None + + _CACHE_VERSION = 3 + + def _load_config_cache(self) -> dict: + import pickle + cache_path = self._get_config_cache_path() + if cache_path is None or not cache_path.is_file(): + return {} + try: + data = pickle.loads(cache_path.read_bytes()) + if not isinstance(data, dict) or data.get("_version") != self._CACHE_VERSION: + return {} + return data + except Exception: + return {} + + def _save_config_cache(self, cache: dict) -> None: + import pickle + cache_path = self._get_config_cache_path() + if cache_path is None: + return + try: + cache["_version"] = self._CACHE_VERSION + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp = cache_path.with_suffix(".tmp") + tmp.write_bytes(pickle.dumps(cache, protocol=pickle.HIGHEST_PROTOCOL)) + tmp.replace(cache_path) + except Exception as e: + logger.debug(f"[UniLab Registry] 缓存保存失败: {e}") + + @staticmethod + def _module_source_hash(module_str: str) -> Optional[str]: + """Fast MD5 of the source file backing *module_str*. Results are + cached for the process lifetime so the same file is never read twice.""" + if module_str in _module_hash_cache: + return _module_hash_cache[module_str] + + import hashlib + import importlib.util + mod_part = module_str.split(":")[0] if ":" in module_str else module_str + result = None + try: + spec = importlib.util.find_spec(mod_part) + if spec and spec.origin and os.path.isfile(spec.origin): + result = hashlib.md5(open(spec.origin, "rb").read()).hexdigest() + except Exception: + pass + _module_hash_cache[module_str] = result + return result + + def _populate_resource_config_info(self, config_cache: Optional[dict] = None): + """ + 利用线程池并行 import pylabrobot 资源类,生成 config_info。 + 仅在 upload_registry=True 时调用。 + + 启用缓存:以 module_str 为 key,记录源文件 MD5。若源文件未变则 + 直接复用上次的 config_info,跳过 import + 实例化 + dump。 + + Args: + config_cache: 共享的缓存 dict。未提供时自行加载/保存; + 由 load_resource_types 传入时由调用方统一保存。 + """ + import time as _time + + executor = self._startup_executor + if executor is None: + return + + # 筛选需要 import 的 pylabrobot 资源(跳过已有 config_info 的缓存条目) + pylabrobot_entries = { + rid: entry + for rid, entry in self.resource_type_registry.items() + if entry.get("class", {}).get("type") == "pylabrobot" + and entry.get("class", {}).get("module") + and not entry.get("config_info") + } + if not pylabrobot_entries: + return + + t0 = _time.perf_counter() + own_cache = config_cache is None + if own_cache: + config_cache = self._load_config_cache() + cache_hits = 0 + cache_misses = 0 + + def _import_and_dump(resource_id: str, module_str: str): + """Import class, create instance, dump tree. Returns (rid, config_info).""" + try: + res_class = import_class(module_str) + if callable(res_class) and not isinstance(res_class, type): + res_instance = res_class(res_class.__name__) + tree_set = ResourceTreeSet.from_plr_resources([res_instance], known_newly_created=True, old_size=True) + dumped = tree_set.dump(old_position=True) + return resource_id, dumped[0] if dumped else [] + except Exception as e: + logger.warning(f"[UniLab Registry] 资源 {resource_id} config_info 生成失败: {e}") + return resource_id, [] + + # Separate into cache-hit vs cache-miss + need_generate: dict = {} # rid -> module_str + for rid, entry in pylabrobot_entries.items(): + module_str = entry["class"]["module"] + cached = config_cache.get(module_str) + if cached and isinstance(cached, dict) and "config_info" in cached: + src_hash = self._module_source_hash(module_str) + if src_hash is not None and cached.get("src_hash") == src_hash: + self.resource_type_registry[rid]["config_info"] = cached["config_info"] + cache_hits += 1 + continue + need_generate[rid] = module_str + + cache_misses = len(need_generate) + + if need_generate: + future_to_rid = { + executor.submit(_import_and_dump, rid, mod): rid + for rid, mod in need_generate.items() + } + for future in as_completed(future_to_rid): + try: + resource_id, config_info = future.result() + self.resource_type_registry[resource_id]["config_info"] = config_info + module_str = need_generate[resource_id] + src_hash = self._module_source_hash(module_str) + config_cache[module_str] = { + "src_hash": src_hash, + "config_info": config_info, + } + except Exception as e: + rid = future_to_rid[future] + logger.warning(f"[UniLab Registry] 资源 {rid} config_info 线程异常: {e}") + + if own_cache: + self._save_config_cache(config_cache) + + elapsed = _time.perf_counter() - t0 + total = cache_hits + cache_misses + logger.info( + f"[UniLab Registry] config_info 缓存统计: " + f"{cache_hits}/{total} 命中, {cache_misses} 重新生成 " + f"(耗时 {elapsed:.2f}s)" + ) + + # ------------------------------------------------------------------ + # Verify & Resolve (实际 import 验证) + # ------------------------------------------------------------------ + + def verify_and_resolve_registry(self): + """ + 对 AST 扫描得到的注册表执行实际 import 验证(使用共享线程池并行)。 + """ + errors = [] + import_success_count = 0 + resolved_count = 0 + total_items = len(self.device_type_registry) + len(self.resource_type_registry) + + lock = threading.Lock() + + def _verify_device(device_id: str, entry: dict): + nonlocal import_success_count, resolved_count + module_str = entry.get("class", {}).get("module", "") + if not module_str or ":" not in module_str: + with lock: + import_success_count += 1 + return None + + try: + cls = import_class(module_str) + with lock: + import_success_count += 1 + resolved_count += 1 + + # 尝试用动态信息增强注册表 + try: + self.resolve_types_for_device(device_id, cls) + except Exception as e: + logger.debug(f"[UniLab Registry/Verify] 设备 {device_id} 类型解析失败: {e}") + + return None + except Exception as e: + logger.warning( + f"[UniLab Registry/Verify] 设备 {device_id}: " + f"导入模块 {module_str} 失败: {e}" + ) + return f"device:{device_id}: {e}" + + def _verify_resource(resource_id: str, entry: dict): + nonlocal import_success_count + module_str = entry.get("class", {}).get("module", "") + if not module_str or ":" not in module_str: + with lock: + import_success_count += 1 + return None + + try: + import_class(module_str) + with lock: + import_success_count += 1 + return None + except Exception as e: + logger.warning( + f"[UniLab Registry/Verify] 资源 {resource_id}: " + f"导入模块 {module_str} 失败: {e}" + ) + return f"resource:{resource_id}: {e}" + + executor = self._startup_executor or ThreadPoolExecutor(max_workers=8) + try: + device_futures = {} + resource_futures = {} + + for device_id, entry in list(self.device_type_registry.items()): + fut = executor.submit(_verify_device, device_id, entry) + device_futures[fut] = device_id + + for resource_id, entry in list(self.resource_type_registry.items()): + fut = executor.submit(_verify_resource, resource_id, entry) + resource_futures[fut] = resource_id + + for future in as_completed(device_futures): + result = future.result() + if result: + errors.append(result) + + for future in as_completed(resource_futures): + result = future.result() + if result: + errors.append(result) + finally: + if self._startup_executor is None: + executor.shutdown(wait=True) + + if errors: + logger.warning( + f"[UniLab Registry/Verify] 验证完成: {import_success_count}/{total_items} 成功, " + f"{len(errors)} 个错误" + ) + else: + logger.info( + f"[UniLab Registry/Verify] 验证完成: {import_success_count}/{total_items} 全部通过, " + f"{resolved_count} 设备类型已解析" + ) + + return errors + + def resolve_types_for_device(self, device_id: str, cls=None): + """ + 将 AST 扫描得到的字符串类型引用替换为实际的 ROS 消息类对象。 + """ + entry = self.device_type_registry.get(device_id) + if not entry: + return + + class_info = entry.get("class", {}) + + # 解析 status_types + status_types = class_info.get("status_types", {}) + resolved_status = {} + for name, type_ref in status_types.items(): + if isinstance(type_ref, str): + resolved = self._replace_type_with_class(type_ref, device_id, f"状态 {name}") + if resolved: + resolved_status[name] = resolved + else: + resolved_status[name] = type_ref + else: + resolved_status[name] = type_ref + class_info["status_types"] = resolved_status + + # 解析 action_value_mappings + _KEEP_AS_STRING = {"UniLabJsonCommand", "UniLabJsonCommandAsync"} + action_mappings = class_info.get("action_value_mappings", {}) + for action_name, action_config in action_mappings.items(): + type_ref = action_config.get("type", "") + if isinstance(type_ref, str) and type_ref and type_ref not in _KEEP_AS_STRING: + resolved = self._replace_type_with_class(type_ref, device_id, f"动作 {action_name}") + if resolved: + action_config["type"] = resolved + if not action_config.get("schema"): + try: + action_config["schema"] = ros_action_to_json_schema(resolved) + except Exception: + pass + if not action_config.get("goal_default"): + try: + action_config["goal_default"] = ROS2MessageInstance(resolved.Goal()).get_python_dict() + except Exception: + pass + + # 如果提供了类,用动态信息增强 + if cls is not None: + try: + dynamic_info = self._extract_class_info(cls) + + for name, info in dynamic_info.get("status_methods", {}).items(): + if name not in resolved_status: + ret_type = info.get("return_type", "str") + resolved = self._replace_type_with_class(ret_type, device_id, f"状态 {name}") + if resolved: + class_info["status_types"][name] = resolved + + for action_name_key, action_config in action_mappings.items(): + type_obj = action_config.get("type") + if isinstance(type_obj, str) and type_obj in ( + "UniLabJsonCommand", "UniLabJsonCommandAsync" + ): + method_name = action_name_key + if method_name.startswith("auto-"): + method_name = method_name[5:] + + actual_method = getattr(cls, method_name, None) + if actual_method: + method_info = self._analyze_method_signature(actual_method) + schema = self._generate_unilab_json_command_schema( + method_info["args"], + docstring=getattr(actual_method, "__doc__", None), + ) + action_config["schema"] = schema + except Exception as e: + logger.debug(f"[Registry] 设备 {device_id} 动态增强失败: {e}") + + # 添加内置动作 + self._add_builtin_actions(entry, device_id) + + def resolve_all_types(self): + """将所有注册表条目中的字符串类型引用替换为实际的 ROS2 消息类对象。 + + 仅做 ROS2 消息类型查找,不 import 任何设备模块,速度快且无副作用。 + """ + t0 = time.time() + for device_id in list(self.device_type_registry): + try: + self.resolve_types_for_device(device_id) + except Exception as e: + logger.debug(f"[Registry] 设备 {device_id} 类型解析失败: {e}") + logger.info( + f"[UniLab Registry] 类型解析完成: {len(self.device_type_registry)} 设备 " + f"(耗时 {time.time() - t0:.2f}s)" + ) + + # ------------------------------------------------------------------ + # YAML 注册表加载 (兼容旧格式) + # ------------------------------------------------------------------ def _load_single_resource_file( - self, file: Path, complete_registry: bool, upload_registry: bool + self, file: Path, complete_registry: bool ) -> Tuple[Dict[str, Any], Dict[str, Any], bool]: """ 加载单个资源文件 (线程安全) @@ -269,7 +1500,20 @@ class Registry: return {}, {}, False complete_data = {} + skip_ids = set() for resource_id, resource_info in data.items(): + if not isinstance(resource_info, dict): + continue + + # AST 已有该资源 → 跳过,提示冗余 + if self.resource_type_registry.get(resource_id): + logger.warning( + f"[UniLab Registry] 资源 '{resource_id}' 已由 AST 扫描注册," + f"YAML 定义冗余,跳过 YAML 处理" + ) + skip_ids.add(resource_id) + continue + if "version" not in resource_info: resource_info["version"] = "1.0.0" if "category" not in resource_info: @@ -291,426 +1535,134 @@ class Registry: if "file_path" in resource_info: del resource_info["file_path"] complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items()))) - if upload_registry: - class_info = resource_info.get("class", {}) - if len(class_info) and "module" in class_info: - if class_info.get("type") == "pylabrobot": - res_class = get_class(class_info["module"]) - if callable(res_class) and not isinstance(res_class, type): - res_instance = res_class(res_class.__name__) - res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)]) - resource_info["config_info"] = res_ulr resource_info["registry_type"] = "resource" resource_info["file_path"] = str(file.absolute()).replace("\\", "/") + for rid in skip_ids: + data.pop(rid, None) + complete_data = dict(sorted(complete_data.items())) - complete_data = copy.deepcopy(complete_data) if complete_registry: + write_data = copy.deepcopy(complete_data) + for res_id, res_cfg in write_data.items(): + res_cfg.pop("file_path", None) + res_cfg.pop("registry_type", None) try: with open(file, "w", encoding="utf-8") as f: - yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) + yaml.dump(write_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) except Exception as e: logger.warning(f"[UniLab Registry] 写入资源文件失败: {file}, 错误: {e}") return data, complete_data, True - def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool): + def load_resource_types(self, path: os.PathLike, upload_registry: bool, complete_registry: bool = False): abs_path = Path(path).absolute() - resource_path = abs_path / "resources" - files = list(resource_path.glob("*/*.yaml")) - logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}") + resources_path = abs_path / "resources" + files = list(resources_path.rglob("*.yaml")) + logger.trace( + f"[UniLab Registry] resources: {resources_path.exists()}, total: {len(files)}" + ) if not files: return - # 使用线程池并行加载 - max_workers = min(8, len(files)) - results = [] + import hashlib as _hl - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_file = { - executor.submit(self._load_single_resource_file, file, complete_registry, upload_registry): file - for file in files - } - for future in as_completed(future_to_file): - file = future_to_file[future] - try: - data, complete_data, is_valid = future.result() - if is_valid: - results.append((file, data)) - except Exception as e: - logger.warning(f"[UniLab Registry] 处理资源文件异常: {file}, 错误: {e}") + # --- YAML-level cache: per-file entries with config_info --- + config_cache = self._load_config_cache() if upload_registry else None + yaml_cache: dict = config_cache.get("_yaml_resources", {}) if config_cache else {} + yaml_cache_hits = 0 + yaml_cache_misses = 0 + uncached_files: list[Path] = [] + yaml_file_rids: dict[str, list[str]] = {} - # 线程安全地更新注册表 - current_resource_number = len(self.resource_type_registry) + 1 - with self._registry_lock: - for i, (file, data) in enumerate(results): - self.resource_type_registry.update(data) - logger.trace( - f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(results)} " - + f"Add {list(data.keys())}" - ) - current_resource_number += 1 - - # 记录无效文件 - valid_files = {r[0] for r in results} - for file in files: - if file not in valid_files: - logger.debug(f"[UniLab Registry] Res File Not Valid YAML File: {file.absolute()}") - - def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]: - """ - 从模块字符串中提取类和方法的docstring信息 - - Args: - module_string: 模块字符串,格式为 "module.path:ClassName" - - Returns: - 包含类和方法docstring信息的字典 - """ - docstrings = {"class_docstring": "", "methods": {}} - - if not module_string or ":" not in module_string: - return docstrings - - try: - module_path, class_name = module_string.split(":", 1) - - # 动态导入模块 - module = importlib.import_module(module_path) - - # 获取类 - if hasattr(module, class_name): - cls = getattr(module, class_name) - - # 获取类的docstring - class_doc = inspect.getdoc(cls) - if class_doc: - docstrings["class_docstring"] = class_doc.strip() - - # 获取所有方法的docstring - for method_name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - method_doc = inspect.getdoc(method) - if method_doc: - docstrings["methods"][method_name] = method_doc.strip() - - # 也获取属性方法的docstring - for method_name, method in inspect.getmembers(cls, predicate=lambda x: isinstance(x, property)): - if hasattr(method, "fget") and method.fget: - method_doc = inspect.getdoc(method.fget) - if method_doc: - docstrings["methods"][method_name] = method_doc.strip() - - except Exception as e: - logger.warning(f"[UniLab Registry] 无法提取docstring信息,模块: {module_string}, 错误: {str(e)}") - - return docstrings - - def _replace_type_with_class(self, type_name: str, device_id: str, field_name: str) -> Any: - """ - 将类型名称替换为实际的类对象 - - Args: - type_name: 类型名称 - device_id: 设备ID,用于错误信息 - field_name: 字段名称,用于错误信息 - - Returns: - 找到的类对象或原始字符串 - - Raises: - SystemExit: 如果找不到类型则终止程序 - """ - # 如果类型名为空,跳过替换 - if not type_name or type_name == "": - logger.warning(f"[UniLab Registry] 设备 {device_id} 的 {field_name} 类型为空,跳过替换") - return type_name - convert_manager = { # 将python基本对象转为ros2基本对象 - "str": "String", - "bool": "Bool", - "int": "Int64", - "float": "Float64", - } - type_name = convert_manager.get(type_name, type_name) # 替换为ROS2类型 - if ":" in type_name: - type_class = msg_converter_manager.get_class(type_name) + if complete_registry: + uncached_files = files + yaml_cache_misses = len(files) else: - type_class = msg_converter_manager.search_class(type_name) - if type_class: - return type_class - else: - logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id} 的 {field_name}") - raise ROSMsgNotFound(f"类型 '{type_name}' 未找到,用于设备 {device_id} 的 {field_name}") + for file in files: + file_key = str(file.absolute()).replace("\\", "/") + if upload_registry and yaml_cache: + try: + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + except OSError: + uncached_files.append(file) + yaml_cache_misses += 1 + continue + cached = yaml_cache.get(file_key) + if cached and cached.get("yaml_md5") == yaml_md5: + module_hashes: dict = cached.get("module_hashes", {}) + all_ok = all( + self._module_source_hash(m) == h + for m, h in module_hashes.items() + ) if module_hashes else True + if all_ok and cached.get("entries"): + for rid, entry in cached["entries"].items(): + self.resource_type_registry[rid] = entry + yaml_cache_hits += 1 + continue + uncached_files.append(file) + yaml_cache_misses += 1 - def _get_json_schema_type(self, type_str: str) -> str: - """ - 根据类型字符串返回对应的JSON Schema类型 - - Args: - type_str: 类型字符串 - - Returns: - JSON Schema类型字符串 - """ - type_lower = type_str.lower() - type_mapping = { - ("str", "string"): "string", - ("int", "integer"): "integer", - ("float", "number"): "number", - ("bool", "boolean"): "boolean", - ("list", "array"): "array", - ("dict", "object"): "object", + # Process uncached YAML files with thread pool + executor = self._startup_executor + future_to_file = { + executor.submit(self._load_single_resource_file, file, complete_registry): file + for file in uncached_files } - # 遍历映射找到匹配的类型 - for type_variants, json_type in type_mapping.items(): - if type_lower in type_variants: - return json_type + for future in as_completed(future_to_file): + file = future_to_file[future] + try: + data, complete_data, is_valid = future.result() + if is_valid: + self.resource_type_registry.update(complete_data) + file_key = str(file.absolute()).replace("\\", "/") + yaml_file_rids[file_key] = list(complete_data.keys()) + except Exception as e: + logger.warning(f"[UniLab Registry] 加载资源文件失败: {file}, 错误: {e}") - # 特殊处理包含冒号的类型(如ROS消息类型) - if ":" in type_lower: - return "object" + # upload 模式下,统一利用线程池为 pylabrobot 资源生成 config_info + if upload_registry: + self._populate_resource_config_info(config_cache=config_cache) - # 默认返回字符串类型 - return "string" + # Update YAML cache for newly processed files (entries now have config_info) + if yaml_file_rids and config_cache is not None: + for file_key, rids in yaml_file_rids.items(): + entries = {} + module_hashes = {} + for rid in rids: + entry = self.resource_type_registry.get(rid) + if entry: + entries[rid] = copy.deepcopy(entry) + mod_str = entry.get("class", {}).get("module", "") + if mod_str and mod_str not in module_hashes: + src_h = self._module_source_hash(mod_str) + if src_h: + module_hashes[mod_str] = src_h + try: + yaml_md5 = _hl.md5(Path(file_key).read_bytes()).hexdigest() + except OSError: + continue + yaml_cache[file_key] = { + "yaml_md5": yaml_md5, + "module_hashes": module_hashes, + "entries": entries, + } + config_cache["_yaml_resources"] = yaml_cache + self._save_config_cache(config_cache) - def _generate_schema_from_info( - self, - param_name: str, - param_type: Union[str, Tuple[str]], - param_default: Any, - ) -> Dict[str, Any]: - """ - 根据参数信息生成JSON Schema - """ - prop_schema = {} - - # 处理嵌套类型(Tuple[str]) - if isinstance(param_type, tuple): - if len(param_type) == 2: - outer_type, inner_type = param_type - outer_json_type = self._get_json_schema_type(outer_type) - inner_json_type = self._get_json_schema_type(inner_type) - - prop_schema["type"] = outer_json_type - - # 根据外层类型设置内层类型信息 - if outer_json_type == "array": - prop_schema["items"] = {"type": inner_json_type} - elif outer_json_type == "object": - prop_schema["additionalProperties"] = {"type": inner_json_type} - else: - # 不是标准的嵌套类型,默认为字符串 - prop_schema["type"] = "string" - else: - # 处理非嵌套类型 - if param_type: - prop_schema["type"] = self._get_json_schema_type(param_type) - else: - # 如果没有类型信息,默认为字符串 - prop_schema["type"] = "string" - - # 设置默认值 - if param_default is not None: - prop_schema["default"] = param_default - - return prop_schema - - def _generate_status_types_schema(self, status_types: Dict[str, Any]) -> Dict[str, Any]: - """ - 根据状态类型生成JSON Schema - """ - status_schema = { - "type": "object", - "properties": {}, - "required": [], - } - for status_name, status_type in status_types.items(): - status_schema["properties"][status_name] = self._generate_schema_from_info( - status_name, status_type["return_type"], None + total_yaml = yaml_cache_hits + yaml_cache_misses + if upload_registry and total_yaml > 0: + logger.info( + f"[UniLab Registry] YAML 资源缓存: " + f"{yaml_cache_hits}/{total_yaml} 文件命中, " + f"{yaml_cache_misses} 重新加载" ) - status_schema["required"].append(status_name) - return status_schema - - def _generate_unilab_json_command_schema( - self, - method_args: List[Dict[str, Any]], - method_name: str, - return_annotation: Any = None, - previous_schema: Dict[str, Any] | None = None, - ) -> Dict[str, Any]: - """ - 根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型 - - Args: - method_args: 方法信息字典,包含args等 - method_name: 方法名称 - return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict) - previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description - - Returns: - JSON Schema格式的参数schema - """ - schema = { - "type": "object", - "properties": {}, - "required": [], - } - for arg_info in method_args: - param_name = arg_info.get("name", "") - param_type = arg_info.get("type", "") - param_default = arg_info.get("default") - param_required = arg_info.get("required", True) - if param_type == "unilabos.registry.placeholder_type:ResourceSlot": - schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name) - elif param_type == ("list", "unilabos.registry.placeholder_type:ResourceSlot"): - schema["properties"][param_name] = { - "items": ros_message_to_json_schema(Resource, param_name), - "type": "array", - } - else: - schema["properties"][param_name] = self._generate_schema_from_info( - param_name, param_type, param_default - ) - if param_required: - schema["required"].append(param_name) - - # 生成result schema(仅当return_annotation是TypedDict时) - result_schema = {} - if return_annotation is not None and self._is_typed_dict(return_annotation): - result_schema = self._generate_typed_dict_result_schema(return_annotation) - - final_schema = { - "title": f"{method_name}参数", - "description": f"", - "type": "object", - "properties": {"goal": schema, "feedback": {}, "result": result_schema}, - "required": ["goal"], - } - - # 保留之前 schema 中 goal/feedback/result 下一级字段的 description - if previous_schema: - self._preserve_field_descriptions(final_schema, previous_schema) - - return final_schema - - def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]) -> None: - """ - 保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title - - Args: - new_schema: 新生成的 schema(会被修改) - previous_schema: 之前的 schema - """ - for section in ["goal", "feedback", "result"]: - new_section = new_schema.get("properties", {}).get(section, {}) - prev_section = previous_schema.get("properties", {}).get(section, {}) - - if not new_section or not prev_section: - continue - - new_props = new_section.get("properties", {}) - prev_props = prev_section.get("properties", {}) - - for field_name, field_schema in new_props.items(): - if field_name in prev_props: - prev_field = prev_props[field_name] - # 保留字段的 description - if "description" in prev_field and prev_field["description"]: - field_schema["description"] = prev_field["description"] - # 保留字段的 title(用户自定义的中文名) - if "title" in prev_field and prev_field["title"]: - field_schema["title"] = prev_field["title"] - - def _is_typed_dict(self, annotation: Any) -> bool: - """ - 检查类型注解是否是TypedDict - - Args: - annotation: 类型注解对象 - - Returns: - 是否为TypedDict - """ - if annotation is None or annotation == inspect.Parameter.empty: - return False - - # 使用 typing_extensions.is_typeddict 进行检查(Python < 3.12 兼容) - try: - from typing_extensions import is_typeddict - - return is_typeddict(annotation) - except ImportError: - # 回退方案:检查 TypedDict 特有的属性 - if isinstance(annotation, type): - return hasattr(annotation, "__required_keys__") and hasattr(annotation, "__optional_keys__") - return False - - def _generate_typed_dict_result_schema(self, return_annotation: Any) -> Dict[str, Any]: - """ - 根据TypedDict类型生成result的JSON Schema - - Args: - return_annotation: TypedDict类型注解 - - Returns: - JSON Schema格式的result schema - """ - if not self._is_typed_dict(return_annotation): - return {} - - try: - from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance - - result_schema = TypedDictMessageInstance.get_json_schema_from_typed_dict(return_annotation) - return result_schema - except ImportError: - logger.warning("[UniLab Registry] msgcenterpy未安装,无法生成TypedDict的result schema") - return {} - except Exception as e: - logger.warning(f"[UniLab Registry] 生成TypedDict result schema失败: {e}") - return {} - - def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): - """ - 为设备配置添加内置的执行驱动命令动作 - - Args: - device_config: 设备配置字典 - device_id: 设备ID - """ - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - - if "class" not in device_config: - return - - if "action_value_mappings" not in device_config["class"]: - device_config["class"]["action_value_mappings"] = {} - - for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]: - device_config["class"]["action_value_mappings"][additional_action] = { - "type": self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}"), - "goal": {"string": "string"}, - "feedback": {}, - "result": {}, - "schema": ros_action_to_json_schema( - self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}") - ), - "goal_default": yaml.safe_load( - io.StringIO( - get_yaml_from_goal_type( - self._replace_type_with_class( - "StrSingleInput", device_id, f"动作 {additional_action}" - ).Goal - ) - ) - ), - "handles": {}, - } def _load_single_device_file( - self, file: Path, complete_registry: bool, get_yaml_from_goal_type + self, file: Path, complete_registry: bool ) -> Tuple[Dict[str, Any], Dict[str, Any], bool, List[str]]: """ 加载单个设备文件 (线程安全) @@ -736,7 +1688,12 @@ class Registry: status_str_type_mapping = {} device_ids = [] + skip_ids = set() for device_id, device_config in data.items(): + if not isinstance(device_config, dict): + continue + + # 补全默认字段 if "version" not in device_config: device_config["version"] = "1.0.0" if "category" not in device_config: @@ -753,7 +1710,18 @@ class Registry: device_config["handles"] = [] if "init_param_schema" not in device_config: device_config["init_param_schema"] = {} + if "class" in device_config: + # --- AST 已有该设备 → 跳过,提示冗余 --- + if self.device_type_registry.get(device_id): + logger.warning( + f"[UniLab Registry] 设备 '{device_id}' 已由 AST 扫描注册," + f"YAML 定义冗余,跳过 YAML 处理" + ) + skip_ids.add(device_id) + continue + + # --- 正常 YAML 处理 --- if "status_types" not in device_config["class"] or device_config["class"]["status_types"] is None: device_config["class"]["status_types"] = {} if ( @@ -761,15 +1729,21 @@ class Registry: or device_config["class"]["action_value_mappings"] is None ): device_config["class"]["action_value_mappings"] = {} + enhanced_info = {} + enhanced_import_map: Dict[str, str] = {} if complete_registry: + original_status_keys = set(device_config["class"]["status_types"].keys()) device_config["class"]["status_types"].clear() - enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True) - if not enhanced_info.get("dynamic_import_success", False): + enhanced_info = get_enhanced_class_info(device_config["class"]["module"]) + if not enhanced_info.get("ast_analysis_success", False): continue - device_config["class"]["status_types"].update( - {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()} - ) + enhanced_import_map = enhanced_info.get("import_map", {}) + for st_k, st_v in enhanced_info["status_methods"].items(): + if st_k in original_status_keys: + device_config["class"]["status_types"][st_k] = st_v["return_type"] + + # --- status_types: 字符串 → class 映射 --- for status_name, status_type in device_config["class"]["status_types"].items(): if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]: status_type = "String" @@ -782,68 +1756,137 @@ class Registry: target_type = String status_str_type_mapping[status_type] = target_type device_config["class"]["status_types"] = dict(sorted(device_config["class"]["status_types"].items())) + if complete_registry: - old_action_configs = {} - for action_name, action_config in device_config["class"]["action_value_mappings"].items(): - old_action_configs[action_name] = action_config + old_action_configs = dict(device_config["class"]["action_value_mappings"]) device_config["class"]["action_value_mappings"] = { k: v for k, v in device_config["class"]["action_value_mappings"].items() if not k.startswith("auto-") } - device_config["class"]["action_value_mappings"].update( - { - f"auto-{k}": { - "type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand", - "goal": {}, - "feedback": {}, - "result": {}, - "schema": self._generate_unilab_json_command_schema( - v["args"], - k, - v.get("return_annotation"), - old_action_configs.get(f"auto-{k}", {}).get("schema"), - ), - "goal_default": {i["name"]: i["default"] for i in v["args"]}, - "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), - "placeholder_keys": { - i["name"]: ( - "unilabos_resources" - if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" - or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") - else "unilabos_devices" - ) - for i in v["args"] - if i.get("type", "") - in [ - "unilabos.registry.placeholder_type:ResourceSlot", - "unilabos.registry.placeholder_type:DeviceSlot", - ("list", "unilabos.registry.placeholder_type:ResourceSlot"), - ("list", "unilabos.registry.placeholder_type:DeviceSlot"), - ] - }, - **({"always_free": True} if v.get("always_free") else {}), - } - for k, v in enhanced_info["action_methods"].items() - if k not in device_config["class"]["action_value_mappings"] - } - ) - for action_name, old_config in old_action_configs.items(): - if action_name in device_config["class"]["action_value_mappings"]: - old_schema = old_config.get("schema", {}) - if "description" in old_schema and old_schema["description"]: - device_config["class"]["action_value_mappings"][action_name]["schema"][ - "description" - ] = old_schema["description"] - device_config["init_param_schema"] = {} - device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema( - enhanced_info["init_params"], "__init__" - )["properties"]["goal"] - device_config["init_param_schema"]["data"] = self._generate_status_types_schema( - enhanced_info["status_methods"] - ) + for k, v in enhanced_info["action_methods"].items(): + if k in device_config["class"]["action_value_mappings"]: + action_key = k + elif k.startswith("get_"): + continue + else: + action_key = f"auto-{k}" + goal_schema = self._generate_unilab_json_command_schema( + v["args"], import_map=enhanced_import_map + ) + ret_type = v.get("return_type", "") + result_schema = None + if ret_type and ret_type not in ("None", "Any", ""): + result_schema = self._generate_schema_from_info( + "result", ret_type, None, import_map=enhanced_import_map + ) + old_cfg = old_action_configs.get(action_key) or old_action_configs.get(f"auto-{k}", {}) + new_schema = wrap_action_schema(goal_schema, action_key, result_schema=result_schema) + old_schema = old_cfg.get("schema", {}) + if old_schema: + preserve_field_descriptions(new_schema, old_schema) + if "description" in old_schema: + new_schema["description"] = old_schema["description"] + new_schema.setdefault("description", "") + old_type = old_cfg.get("type", "") + entry_goal = old_cfg.get("goal", {}) + entry_feedback = {} + entry_result = {} + entry_schema = new_schema + entry_goal_default = {i["name"]: i.get("default") for i in v["args"]} + + if old_type and not old_type.startswith("UniLabJsonCommand"): + entry_type = old_type + try: + action_type_obj = self._replace_type_with_class( + old_type, device_id, f"动作 {action_key}" + ) + except ROSMsgNotFound: + action_type_obj = None + if action_type_obj is not None and not isinstance(action_type_obj, str): + real_params = [p for p in v["args"]] + ros_goal = {p["name"]: p["name"] for p in real_params} + try: + if hasattr(action_type_obj, "Goal"): + goal_fields = action_type_obj.Goal.get_fields_and_field_types() + ros2_goal = {f: f for f in goal_fields} + ros2_goal.update(ros_goal) + entry_goal = ros2_goal + except Exception: + pass + try: + if hasattr(action_type_obj, "Feedback"): + fb_fields = action_type_obj.Feedback.get_fields_and_field_types() + entry_feedback = {f: f for f in fb_fields} + except Exception: + pass + try: + if hasattr(action_type_obj, "Result"): + res_fields = action_type_obj.Result.get_fields_and_field_types() + entry_result = {f: f for f in res_fields} + except Exception: + pass + try: + entry_schema = ros_action_to_json_schema(action_type_obj) + if old_schema: + preserve_field_descriptions(entry_schema, old_schema) + if "description" in old_schema: + entry_schema["description"] = old_schema["description"] + entry_schema.setdefault("description", "") + except Exception: + pass + try: + entry_goal_default = ROS2MessageInstance( + action_type_obj.Goal() + ).get_python_dict() + except Exception: + entry_goal_default = old_cfg.get("goal_default", {}) + else: + entry_type = "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand" + + merged_pk = dict(old_cfg.get("placeholder_keys", {})) + merged_pk.update(detect_placeholder_keys(v["args"])) + + entry = { + "type": entry_type, + "goal": entry_goal, + "feedback": entry_feedback, + "result": entry_result, + "schema": entry_schema, + "goal_default": entry_goal_default, + "handles": old_cfg.get("handles", []), + "placeholder_keys": merged_pk, + } + if v.get("always_free"): + entry["always_free"] = True + device_config["class"]["action_value_mappings"][action_key] = entry + + device_config["init_param_schema"] = {} + init_schema = self._generate_unilab_json_command_schema( + enhanced_info["init_params"], "__init__", + import_map=enhanced_import_map, + ) + device_config["init_param_schema"]["config"] = init_schema + + data_schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for st_name in device_config["class"]["status_types"]: + st_type_str = device_config["class"]["status_types"][st_name] + if isinstance(st_type_str, str): + data_schema["properties"][st_name] = self._generate_schema_from_info( + st_name, st_type_str, None, import_map=enhanced_import_map + ) + else: + data_schema["properties"][st_name] = {"type": "string"} + data_schema["required"].append(st_name) + device_config["init_param_schema"]["data"] = data_schema + + # --- action_value_mappings: 处理非 UniLabJsonCommand 类型 --- device_config.pop("schema", None) device_config["class"]["action_value_mappings"] = dict( sorted(device_config["class"]["action_value_mappings"].items()) @@ -868,37 +1911,82 @@ class Registry: continue action_str_type_mapping[action_type_str] = target_type if target_type is not None: - action_config["goal_default"] = yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(target_type.Goal)) - ) + try: + action_config["goal_default"] = ROS2MessageInstance(target_type.Goal()).get_python_dict() + except Exception: + action_config["goal_default"] = {} + prev_schema = action_config.get("schema", {}) action_config["schema"] = ros_action_to_json_schema(target_type) + if prev_schema: + preserve_field_descriptions(action_config["schema"], prev_schema) + if "description" in prev_schema: + action_config["schema"]["description"] = prev_schema["description"] + action_config["schema"].setdefault("description", "") else: logger.warning( f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换" ) + + # deepcopy 保存可序列化的 complete_data(此时 type 字段仍为字符串) + device_config["file_path"] = str(file.absolute()).replace("\\", "/") + device_config["registry_type"] = "device" complete_data[device_id] = copy.deepcopy(dict(sorted(device_config.items()))) + + # 之后才把 type 字符串替换为 class 对象(仅用于运行时 data) for status_name, status_type in device_config["class"]["status_types"].items(): - device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] + if status_type in status_str_type_mapping: + device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] for action_name, action_config in device_config["class"]["action_value_mappings"].items(): - if action_config["type"] not in action_str_type_mapping: - continue - action_config["type"] = action_str_type_mapping[action_config["type"]] + if action_config.get("type") in action_str_type_mapping: + action_config["type"] = action_str_type_mapping[action_config["type"]] + self._add_builtin_actions(device_config, device_id) - device_config["file_path"] = str(file.absolute()).replace("\\", "/") - device_config["registry_type"] = "device" + device_ids.append(device_id) + for did in skip_ids: + data.pop(did, None) + complete_data = dict(sorted(complete_data.items())) complete_data = copy.deepcopy(complete_data) - try: - with open(file, "w", encoding="utf-8") as f: - yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) - except Exception as e: - logger.warning(f"[UniLab Registry] 写入设备文件失败: {file}, 错误: {e}") + if complete_registry: + write_data = copy.deepcopy(complete_data) + for dev_id, dev_cfg in write_data.items(): + dev_cfg.pop("file_path", None) + dev_cfg.pop("registry_type", None) + try: + with open(file, "w", encoding="utf-8") as f: + yaml.dump(write_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) + except Exception as e: + logger.warning(f"[UniLab Registry] 写入设备文件失败: {file}, 错误: {e}") return data, complete_data, True, device_ids - def load_device_types(self, path: os.PathLike, complete_registry: bool): + def _rebuild_device_runtime_data(self, complete_data: Dict[str, Any]) -> Dict[str, Any]: + """从 complete_data(纯字符串)重建运行时数据(type 字段替换为 class 对象)。""" + data = copy.deepcopy(complete_data) + for device_id, device_config in data.items(): + if "class" not in device_config: + continue + # status_types: str → class + for st_name, st_type in device_config["class"].get("status_types", {}).items(): + if isinstance(st_type, str): + device_config["class"]["status_types"][st_name] = self._replace_type_with_class( + st_type, device_id, f"状态 {st_name}" + ) + # action type: str → class (non-UniLabJsonCommand only) + for _act_name, act_cfg in device_config["class"].get("action_value_mappings", {}).items(): + t_ref = act_cfg.get("type", "") + if isinstance(t_ref, str) and t_ref and not t_ref.startswith("UniLabJsonCommand"): + resolved = self._replace_type_with_class(t_ref, device_id, f"动作 {_act_name}") + if resolved: + act_cfg["type"] = resolved + self._add_builtin_actions(device_config, device_id) + return data + + def load_device_types(self, path: os.PathLike, complete_registry: bool = False): + import hashlib as _hl + t0 = time.time() abs_path = Path(path).absolute() devices_path = abs_path / "devices" device_comms_path = abs_path / "device_comms" @@ -911,44 +1999,80 @@ class Registry: if not files: return - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type + config_cache = self._load_config_cache() + yaml_dev_cache: dict = config_cache.get("_yaml_devices", {}) + cache_hits = 0 + uncached_files: list[Path] = [] - # 使用线程池并行加载 - max_workers = min(8, len(files)) - results = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_file = { - executor.submit(self._load_single_device_file, file, complete_registry, get_yaml_from_goal_type): file - for file in files - } - for future in as_completed(future_to_file): - file = future_to_file[future] + if complete_registry: + uncached_files = files + else: + for file in files: + file_key = str(file.absolute()).replace("\\", "/") try: - data, complete_data, is_valid, device_ids = future.result() - if is_valid: - results.append((file, data, device_ids)) - except Exception as e: - traceback.print_exc() - logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}") + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + except OSError: + uncached_files.append(file) + continue + cached = yaml_dev_cache.get(file_key) + if cached and cached.get("yaml_md5") == yaml_md5 and cached.get("entries"): + complete_data = cached["entries"] + # 过滤掉 AST 已有的设备 + complete_data = { + did: cfg for did, cfg in complete_data.items() + if not self.device_type_registry.get(did) + } + runtime_data = self._rebuild_device_runtime_data(complete_data) + self.device_type_registry.update(runtime_data) + cache_hits += 1 + continue + uncached_files.append(file) - # 线程安全地更新注册表 - current_device_number = len(self.device_type_registry) + 1 - with self._registry_lock: - for file, data, device_ids in results: - self.device_type_registry.update(data) - for device_id in device_ids: - logger.trace( - f"[UniLab Registry] Device-{current_device_number} Add {device_id} " - + f"[{data[device_id].get('name', '未命名设备')}]" - ) - current_device_number += 1 + executor = self._startup_executor + future_to_file = { + executor.submit( + self._load_single_device_file, file, complete_registry + ): file + for file in uncached_files + } - # 记录无效文件 - valid_files = {r[0] for r in results} - for file in files: - if file not in valid_files: - logger.debug(f"[UniLab Registry] Device File Not Valid YAML File: {file.absolute()}") + for future in as_completed(future_to_file): + file = future_to_file[future] + try: + data, _complete_data, is_valid, device_ids = future.result() + if is_valid: + runtime_data = {did: data[did] for did in device_ids if did in data} + self.device_type_registry.update(runtime_data) + # 写入缓存 + file_key = str(file.absolute()).replace("\\", "/") + try: + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + yaml_dev_cache[file_key] = { + "yaml_md5": yaml_md5, + "entries": _complete_data, + } + except OSError: + pass + except Exception as e: + logger.warning(f"[UniLab Registry] 加载设备文件失败: {file}, 错误: {e}") + + if uncached_files and yaml_dev_cache: + latest_cache = self._load_config_cache() + latest_cache["_yaml_devices"] = yaml_dev_cache + self._save_config_cache(latest_cache) + + total = len(files) + extra = " (complete_registry 跳过缓存)" if complete_registry else "" + logger.info( + f"[UniLab Registry] YAML 设备加载: " + f"{cache_hits}/{total} 缓存命中, " + f"{len(uncached_files)} 重新加载 " + f"(耗时 {time.time() - t0:.2f}s){extra}" + ) + + # ------------------------------------------------------------------ + # 注册表信息输出 + # ------------------------------------------------------------------ def obtain_registry_device_info(self): devices = [] @@ -956,7 +2080,6 @@ class Registry: device_info_copy = copy.deepcopy(device_info) if "class" in device_info_copy and "action_value_mappings" in device_info_copy["class"]: action_mappings = device_info_copy["class"]["action_value_mappings"] - # 过滤掉内置的驱动命令动作 builtin_actions = ["_execute_driver_command", "_execute_driver_command_async"] filtered_action_mappings = { action_name: action_config @@ -966,6 +2089,9 @@ class Registry: device_info_copy["class"]["action_value_mappings"] = filtered_action_mappings for action_name, action_config in filtered_action_mappings.items(): + type_obj = action_config.get("type") + if hasattr(type_obj, "__name__"): + action_config["type"] = type_obj.__name__ if "schema" in action_config and action_config["schema"]: schema = action_config["schema"] # 确保schema结构存在 @@ -989,6 +2115,10 @@ class Registry: action_config["schema"]["properties"]["goal"]["_unilabos_placeholder_info"] = action_config[ "placeholder_keys" ] + status_types = device_info_copy["class"].get("status_types", {}) + for status_name, status_type in status_types.items(): + if hasattr(status_type, "__name__"): + status_types[status_name] = status_type.__name__ msg = {"id": device_id, **device_info_copy} devices.append(msg) @@ -1001,35 +2131,76 @@ class Registry: resources.append(msg) return resources + def get_yaml_output(self, device_id: str) -> str: + """将指定设备的注册表条目导出为 YAML 字符串。""" + entry = self.device_type_registry.get(device_id) + if not entry: + return "" + + entry = copy.deepcopy(entry) + + if "class" in entry: + status_types = entry["class"].get("status_types", {}) + for name, type_obj in status_types.items(): + if hasattr(type_obj, "__name__"): + status_types[name] = type_obj.__name__ + + for action_name, action_config in entry["class"].get("action_value_mappings", {}).items(): + type_obj = action_config.get("type") + if hasattr(type_obj, "__name__"): + action_config["type"] = type_obj.__name__ + + entry.pop("registry_type", None) + entry.pop("file_path", None) + + if "class" in entry and "action_value_mappings" in entry["class"]: + entry["class"]["action_value_mappings"] = { + k: v + for k, v in entry["class"]["action_value_mappings"].items() + if not k.startswith("_execute_driver_command") + } + + return yaml.dump( + {device_id: entry}, + allow_unicode=True, + default_flow_style=False, + Dumper=NoAliasDumper, + ) + + +# --------------------------------------------------------------------------- +# 全局单例实例 & 构建入口 +# --------------------------------------------------------------------------- -# 全局单例实例 lab_registry = Registry() -def build_registry(registry_paths=None, complete_registry=False, upload_registry=False): +def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False): """ 构建或获取Registry单例实例 - - Args: - registry_paths: 额外的注册表路径列表 - - Returns: - Registry实例 """ logger.info("[UniLab Registry] 构建注册表实例") - # 由于使用了单例,这里不需要重新创建实例 global lab_registry - # 如果有额外路径,添加到registry_paths if registry_paths: current_paths = lab_registry.registry_paths.copy() - # 检查是否有新路径需要添加 for path in registry_paths: if path not in current_paths: lab_registry.registry_paths.append(path) - # 初始化注册表 - lab_registry.setup(complete_registry, upload_registry) + lab_registry.setup(devices_dirs=devices_dirs, upload_registry=upload_registry, complete_registry=complete_registry) + + # 将 AST 扫描的字符串类型替换为实际 ROS2 消息类(仅查找 ROS2 类型,不 import 设备模块) + lab_registry.resolve_all_types() + + if check_mode: + lab_registry.verify_and_resolve_registry() + + # noinspection PyProtectedMember + if lab_registry._startup_executor is not None: + # noinspection PyProtectedMember + lab_registry._startup_executor.shutdown(wait=False) + lab_registry._startup_executor = None return lab_registry diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index f8e17261..19917372 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -9,7 +9,6 @@ YB_20ml_fenyeping: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_5ml_fenyeping: category: @@ -22,7 +21,6 @@ YB_5ml_fenyeping: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_jia_yang_tou_da: category: @@ -35,7 +33,6 @@ YB_jia_yang_tou_da: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_pei_ye_da_Bottle: category: @@ -48,7 +45,6 @@ YB_pei_ye_da_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_pei_ye_xiao_Bottle: category: @@ -61,7 +57,6 @@ YB_pei_ye_xiao_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_qiang_tou: category: @@ -74,7 +69,6 @@ YB_qiang_tou: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye_Bottle: category: @@ -88,5 +82,4 @@ YB_ye_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml index 4698a266..76b6b938 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -9,7 +9,6 @@ YB_100ml_yeti: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_20ml_fenyepingban: category: @@ -22,7 +21,6 @@ YB_20ml_fenyepingban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_5ml_fenyepingban: category: @@ -35,7 +33,6 @@ YB_5ml_fenyepingban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_6StockCarrier: category: @@ -48,7 +45,6 @@ YB_6StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_6VialCarrier: category: @@ -61,7 +57,6 @@ YB_6VialCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_gao_nian_ye_Bottle: category: @@ -74,7 +69,6 @@ YB_gao_nian_ye_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_gaonianye: category: @@ -87,7 +81,6 @@ YB_gaonianye: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_jia_yang_tou_da_Carrier: category: @@ -100,7 +93,6 @@ YB_jia_yang_tou_da_Carrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_peiyepingdaban: category: @@ -113,7 +105,6 @@ YB_peiyepingdaban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_peiyepingxiaoban: category: @@ -126,7 +117,6 @@ YB_peiyepingxiaoban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_qiang_tou_he: category: @@ -139,7 +129,6 @@ YB_qiang_tou_he: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_shi_pei_qi_kuai: category: @@ -152,7 +141,6 @@ YB_shi_pei_qi_kuai: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye: category: @@ -165,7 +153,6 @@ YB_ye: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye_100ml_Bottle: category: @@ -178,5 +165,4 @@ YB_ye_100ml_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index 764a8aa5..f72cc10d 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -8,7 +8,6 @@ BIOYOND_PolymerStation_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_1FlaskCarrier: category: @@ -20,7 +19,6 @@ BIOYOND_PolymerStation_1FlaskCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_6StockCarrier: category: @@ -32,7 +30,6 @@ BIOYOND_PolymerStation_6StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_8StockCarrier: category: @@ -44,5 +41,4 @@ BIOYOND_PolymerStation_8StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index 8d6993b1..5770a2d1 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -8,7 +8,6 @@ BIOYOND_PolymerPreparationStation_Deck: handles: [] icon: 配液站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerReactionStation_Deck: category: @@ -20,7 +19,6 @@ BIOYOND_PolymerReactionStation_Deck: handles: [] icon: 反应站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_YB_Deck: category: @@ -32,7 +30,6 @@ BIOYOND_YB_Deck: handles: [] icon: 配液站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 CoincellDeck: category: @@ -44,5 +41,4 @@ CoincellDeck: handles: [] icon: koudian.webp init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/common/resource_container.yaml b/unilabos/registry/resources/common/resource_container.yaml index 48dcab59..3f0aa9d2 100644 --- a/unilabos/registry/resources/common/resource_container.yaml +++ b/unilabos/registry/resources/common/resource_container.yaml @@ -1,24 +1,3 @@ -disposal: - category: - - disposal - - waste - - resource_container - class: - module: unilabos.resources.disposal:Disposal - type: unilabos - description: 废料处理位置,用于处理实验废料 - handles: - - data_key: disposal_access - data_source: handle - data_type: fluid - handler_key: access - io_type: target - label: access - side: NORTH - icon: '' - init_param_schema: {} - registry_type: resource - version: 1.0.0 hplc_plate: category: - resource_container @@ -40,56 +19,6 @@ hplc_plate: - 3.1416 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro type: resource - registry_type: resource - version: 1.0.0 -maintenance: - category: - - maintenance - - position - - resource_container - class: - module: unilabos.resources.maintenance:Maintenance - type: unilabos - description: 维护位置,用于设备维护和校准 - handles: - - data_key: maintenance_access - data_source: handle - data_type: mechanical - handler_key: access - io_type: target - label: access - side: NORTH - icon: '' - init_param_schema: {} - registry_type: resource - version: 1.0.0 -plate: - category: - - plate - - labware - - resource_container - class: - module: unilabos.resources.plate:Plate - type: unilabos - description: 实验板,用于放置样品和试剂 - handles: - - data_key: plate_access - data_source: handle - data_type: mechanical - handler_key: access - io_type: target - label: access - side: NORTH - - data_key: sample_wells - data_source: handle - data_type: fluid - handler_key: wells - io_type: target - label: wells - side: CENTER - icon: '' - init_param_schema: {} - registry_type: resource version: 1.0.0 plate_96: category: @@ -112,7 +41,6 @@ plate_96: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro type: resource - registry_type: resource version: 1.0.0 plate_96_high: category: @@ -135,35 +63,6 @@ plate_96_high: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro type: resource - registry_type: resource - version: 1.0.0 -tip_rack: - category: - - tip_rack - - labware - - resource_container - class: - module: unilabos.resources.tip_rack:TipRack - type: unilabos - description: 枪头架资源,用于存放和管理移液器枪头 - handles: - - data_key: tip_access - data_source: handle - data_type: mechanical - handler_key: access - io_type: target - label: access - side: NORTH - - data_key: tip_pickup - data_source: handle - data_type: mechanical - handler_key: pickup - io_type: target - label: pickup - side: SOUTH - icon: '' - init_param_schema: {} - registry_type: resource version: 1.0.0 tiprack_96_high: category: @@ -195,7 +94,6 @@ tiprack_96_high: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro type: resource - registry_type: resource version: 1.0.0 tiprack_box: category: @@ -227,5 +125,4 @@ tiprack_box: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro type: resource - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/laiyu/container.yaml b/unilabos/registry/resources/laiyu/container.yaml index 1652956e..586e3cfe 100644 --- a/unilabos/registry/resources/laiyu/container.yaml +++ b/unilabos/registry/resources/laiyu/container.yaml @@ -29,7 +29,6 @@ bottle_container: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro type: resource - registry_type: resource version: 1.0.0 tube_container: category: @@ -62,5 +61,4 @@ tube_container: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro type: resource - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/laiyu/deck.yaml b/unilabos/registry/resources/laiyu/deck.yaml index e6d930a5..85da0ca7 100644 --- a/unilabos/registry/resources/laiyu/deck.yaml +++ b/unilabos/registry/resources/laiyu/deck.yaml @@ -12,5 +12,4 @@ TransformXYZDeck: mesh: liquid_transform_xyz path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro type: device - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/deck.yaml b/unilabos/registry/resources/opentrons/deck.yaml index 8fa35ee5..10e91cef 100644 --- a/unilabos/registry/resources/opentrons/deck.yaml +++ b/unilabos/registry/resources/opentrons/deck.yaml @@ -12,7 +12,6 @@ OTDeck: mesh: opentrons_liquid_handler path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro type: device - registry_type: resource version: 1.0.0 hplc_station: category: @@ -28,5 +27,4 @@ hplc_station: mesh: hplc_station path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro type: device - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/plate_adapters.yaml b/unilabos/registry/resources/opentrons/plate_adapters.yaml index d2942d46..d09bf784 100644 --- a/unilabos/registry/resources/opentrons/plate_adapters.yaml +++ b/unilabos/registry/resources/opentrons/plate_adapters.yaml @@ -8,5 +8,4 @@ Opentrons_96_adapter_Vb: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/plates.yaml b/unilabos/registry/resources/opentrons/plates.yaml index 02267ae0..20a71995 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -8,7 +8,6 @@ appliedbiosystemsmicroamp_384_wellplate_40ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 biorad_384_wellplate_50ul: category: @@ -20,7 +19,6 @@ biorad_384_wellplate_50ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 biorad_96_wellplate_200ul_pcr: category: @@ -32,7 +30,6 @@ biorad_96_wellplate_200ul_pcr: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_12_wellplate_6point9ml_flat: category: @@ -44,7 +41,6 @@ corning_12_wellplate_6point9ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_24_wellplate_3point4ml_flat: category: @@ -56,7 +52,6 @@ corning_24_wellplate_3point4ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_384_wellplate_112ul_flat: category: @@ -68,7 +63,6 @@ corning_384_wellplate_112ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_48_wellplate_1point6ml_flat: category: @@ -80,7 +74,6 @@ corning_48_wellplate_1point6ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_6_wellplate_16point8ml_flat: category: @@ -92,7 +85,6 @@ corning_6_wellplate_16point8ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_96_wellplate_360ul_flat: category: @@ -104,7 +96,6 @@ corning_96_wellplate_360ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_96_wellplate_100ul_pcr_full_skirt: category: @@ -136,7 +127,6 @@ nest_96_wellplate_100ul_pcr_full_skirt: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 nest_96_wellplate_200ul_flat: category: @@ -148,7 +138,6 @@ nest_96_wellplate_200ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_96_wellplate_2ml_deep: category: @@ -171,7 +160,6 @@ nest_96_wellplate_2ml_deep: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 thermoscientificnunc_96_wellplate_1300ul: category: @@ -183,7 +171,6 @@ thermoscientificnunc_96_wellplate_1300ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 thermoscientificnunc_96_wellplate_2000ul: category: @@ -195,7 +182,6 @@ thermoscientificnunc_96_wellplate_2000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 usascientific_96_wellplate_2point4ml_deep: category: @@ -207,5 +193,4 @@ usascientific_96_wellplate_2point4ml_deep: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/reservoirs.yaml b/unilabos/registry/resources/opentrons/reservoirs.yaml index b2f7857b..6b2033d9 100644 --- a/unilabos/registry/resources/opentrons/reservoirs.yaml +++ b/unilabos/registry/resources/opentrons/reservoirs.yaml @@ -8,7 +8,6 @@ agilent_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 axygen_1_reservoir_90ml: category: @@ -20,7 +19,6 @@ axygen_1_reservoir_90ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_12_reservoir_15ml: category: @@ -32,7 +30,6 @@ nest_12_reservoir_15ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_1_reservoir_195ml: category: @@ -44,7 +41,6 @@ nest_1_reservoir_195ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_1_reservoir_290ml: category: @@ -56,7 +52,6 @@ nest_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 usascientific_12_reservoir_22ml: category: @@ -68,5 +63,4 @@ usascientific_12_reservoir_22ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/tip_racks.yaml b/unilabos/registry/resources/opentrons/tip_racks.yaml index cbc7d6f1..d1682b2a 100644 --- a/unilabos/registry/resources/opentrons/tip_racks.yaml +++ b/unilabos/registry/resources/opentrons/tip_racks.yaml @@ -8,7 +8,6 @@ eppendorf_96_tiprack_1000ul_eptips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 eppendorf_96_tiprack_10ul_eptips: category: @@ -20,7 +19,6 @@ eppendorf_96_tiprack_10ul_eptips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 geb_96_tiprack_1000ul: category: @@ -32,7 +30,6 @@ geb_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 geb_96_tiprack_10ul: category: @@ -44,7 +41,6 @@ geb_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_1000ul: category: @@ -75,7 +71,6 @@ opentrons_96_filtertiprack_1000ul: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_10ul: category: @@ -87,7 +82,6 @@ opentrons_96_filtertiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_200ul: category: @@ -99,7 +93,6 @@ opentrons_96_filtertiprack_200ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_20ul: category: @@ -111,7 +104,6 @@ opentrons_96_filtertiprack_20ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_1000ul: category: @@ -123,7 +115,6 @@ opentrons_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_10ul: category: @@ -135,7 +126,6 @@ opentrons_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_20ul: category: @@ -147,7 +137,6 @@ opentrons_96_tiprack_20ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_300ul: category: diff --git a/unilabos/registry/resources/opentrons/tube_racks.yaml b/unilabos/registry/resources/opentrons/tube_racks.yaml index 32bf3e36..33ec5dc9 100644 --- a/unilabos/registry/resources/opentrons/tube_racks.yaml +++ b/unilabos/registry/resources/opentrons/tube_racks.yaml @@ -8,7 +8,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: category: @@ -20,7 +19,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: category: @@ -32,7 +30,6 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_15_tuberack_falcon_15ml_conical: category: @@ -44,7 +41,6 @@ opentrons_15_tuberack_falcon_15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_15_tuberack_nest_15ml_conical: category: @@ -56,7 +52,6 @@ opentrons_15_tuberack_nest_15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_aluminumblock_generic_2ml_screwcap: category: @@ -68,7 +63,6 @@ opentrons_24_aluminumblock_generic_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_aluminumblock_nest_1point5ml_snapcap: category: @@ -80,7 +74,6 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: category: @@ -92,7 +85,6 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: category: @@ -104,7 +96,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: category: @@ -116,7 +107,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: category: @@ -128,7 +118,6 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_generic_2ml_screwcap: category: @@ -140,7 +129,6 @@ opentrons_24_tuberack_generic_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_0point5ml_screwcap: category: @@ -152,7 +140,6 @@ opentrons_24_tuberack_nest_0point5ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_1point5ml_screwcap: category: @@ -164,7 +151,6 @@ opentrons_24_tuberack_nest_1point5ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_1point5ml_snapcap: category: @@ -176,7 +162,6 @@ opentrons_24_tuberack_nest_1point5ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_2ml_screwcap: category: @@ -188,7 +173,6 @@ opentrons_24_tuberack_nest_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_2ml_snapcap: category: @@ -200,7 +184,6 @@ opentrons_24_tuberack_nest_2ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_6_tuberack_falcon_50ml_conical: category: @@ -212,7 +195,6 @@ opentrons_6_tuberack_falcon_50ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_6_tuberack_nest_50ml_conical: category: @@ -224,7 +206,6 @@ opentrons_6_tuberack_nest_50ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_well_aluminum_block: category: @@ -236,5 +217,4 @@ opentrons_96_well_aluminum_block: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/organic/container.yaml b/unilabos/registry/resources/organic/container.yaml index a8fb9b6c..240cdf79 100644 --- a/unilabos/registry/resources/organic/container.yaml +++ b/unilabos/registry/resources/organic/container.yaml @@ -29,5 +29,4 @@ container: side: WEST icon: Flask.webp init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/post_process/bottle_carriers.yaml b/unilabos/registry/resources/post_process/bottle_carriers.yaml index ea30cb7d..45054311 100644 --- a/unilabos/registry/resources/post_process/bottle_carriers.yaml +++ b/unilabos/registry/resources/post_process/bottle_carriers.yaml @@ -8,7 +8,6 @@ POST_PROCESS_Raw_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 POST_PROCESS_Reaction_1BottleCarrier: category: @@ -20,5 +19,4 @@ POST_PROCESS_Reaction_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/post_process/deck.yaml b/unilabos/registry/resources/post_process/deck.yaml index 621cafc6..e5d4cc8d 100644 --- a/unilabos/registry/resources/post_process/deck.yaml +++ b/unilabos/registry/resources/post_process/deck.yaml @@ -9,5 +9,4 @@ post_process_deck: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/plate_adapters.yaml b/unilabos/registry/resources/prcxi/plate_adapters.yaml index a769fee3..3e960f2e 100644 --- a/unilabos/registry/resources/prcxi/plate_adapters.yaml +++ b/unilabos/registry/resources/prcxi/plate_adapters.yaml @@ -9,7 +9,6 @@ PRCXI_30mm_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Adapter: category: @@ -22,7 +21,6 @@ PRCXI_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Deep10_Adapter: category: @@ -35,7 +33,6 @@ PRCXI_Deep10_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Deep300_Adapter: category: @@ -48,7 +45,6 @@ PRCXI_Deep300_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Adapter: category: @@ -61,7 +57,6 @@ PRCXI_PCR_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Reservoir_Adapter: category: @@ -74,7 +69,6 @@ PRCXI_Reservoir_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip10_Adapter: category: @@ -87,7 +81,6 @@ PRCXI_Tip10_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip1250_Adapter: category: @@ -100,7 +93,6 @@ PRCXI_Tip1250_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip300_Adapter: category: @@ -113,5 +105,4 @@ PRCXI_Tip300_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/plates.yaml b/unilabos/registry/resources/prcxi/plates.yaml index 81e2ae96..b8527dbf 100644 --- a/unilabos/registry/resources/prcxi/plates.yaml +++ b/unilabos/registry/resources/prcxi/plates.yaml @@ -9,7 +9,6 @@ PRCXI_48_DeepWell: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_96_DeepWell: category: @@ -22,7 +21,6 @@ PRCXI_96_DeepWell: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_AGenBio_4_troughplate: category: @@ -35,7 +33,6 @@ PRCXI_AGenBio_4_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_BioER_96_wellplate: category: @@ -48,7 +45,6 @@ PRCXI_BioER_96_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_BioRad_384_wellplate: category: @@ -61,7 +57,6 @@ PRCXI_BioRad_384_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_CellTreat_96_wellplate: category: @@ -74,7 +69,6 @@ PRCXI_CellTreat_96_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_nonskirted: category: @@ -87,7 +81,6 @@ PRCXI_PCR_Plate_200uL_nonskirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_semiskirted: category: @@ -100,7 +93,6 @@ PRCXI_PCR_Plate_200uL_semiskirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_skirted: category: @@ -113,7 +105,6 @@ PRCXI_PCR_Plate_200uL_skirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_nest_12_troughplate: category: @@ -126,7 +117,6 @@ PRCXI_nest_12_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_nest_1_troughplate: category: @@ -139,5 +129,4 @@ PRCXI_nest_1_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/tip_racks.yaml b/unilabos/registry/resources/prcxi/tip_racks.yaml index 56a16db8..f6d2e7f0 100644 --- a/unilabos/registry/resources/prcxi/tip_racks.yaml +++ b/unilabos/registry/resources/prcxi/tip_racks.yaml @@ -9,7 +9,6 @@ PRCXI_1000uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_10uL_Tips: category: @@ -22,7 +21,6 @@ PRCXI_10uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_10ul_eTips: category: @@ -35,7 +33,6 @@ PRCXI_10ul_eTips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_1250uL_Tips: category: @@ -48,7 +45,6 @@ PRCXI_1250uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_200uL_Tips: category: @@ -61,7 +57,6 @@ PRCXI_200uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_300ul_Tips: category: @@ -74,5 +69,4 @@ PRCXI_300ul_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/trash.yaml b/unilabos/registry/resources/prcxi/trash.yaml index f87a7624..952a832b 100644 --- a/unilabos/registry/resources/prcxi/trash.yaml +++ b/unilabos/registry/resources/prcxi/trash.yaml @@ -9,5 +9,4 @@ PRCXI_trash: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/tube_racks.yaml b/unilabos/registry/resources/prcxi/tube_racks.yaml index 0b1e07c6..6510c16c 100644 --- a/unilabos/registry/resources/prcxi/tube_racks.yaml +++ b/unilabos/registry/resources/prcxi/tube_racks.yaml @@ -9,5 +9,4 @@ PRCXI_EP_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/utils.py b/unilabos/registry/utils.py new file mode 100644 index 00000000..1ab7dd2c --- /dev/null +++ b/unilabos/registry/utils.py @@ -0,0 +1,724 @@ +""" +注册表工具函数 + +从 registry.py 中提取的纯工具函数,包括: +- docstring 解析 +- 类型字符串 → JSON Schema 转换 +- AST 类型节点解析 +- TypedDict / Slot / Handle 等辅助检测 +""" + +import inspect +import logging +import re +import typing +from typing import Any, Dict, List, Optional, Tuple, Union + +from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance + +from unilabos.utils.cls_creator import import_class + +_logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# 异常 +# --------------------------------------------------------------------------- + + +class ROSMsgNotFound(Exception): + pass + + +# --------------------------------------------------------------------------- +# Docstring 解析 (Google-style) +# --------------------------------------------------------------------------- + +_SECTION_RE = re.compile(r"^(\w[\w\s]*):\s*$") + + +def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]: + """ + 解析 Google-style docstring,提取描述和参数说明。 + + Returns: + {"description": "短描述", "params": {"param1": "参数1描述", ...}} + """ + result: Dict[str, Any] = {"description": "", "params": {}} + if not docstring: + return result + + lines = docstring.strip().splitlines() + if not lines: + return result + + result["description"] = lines[0].strip() + + in_args = False + current_param: Optional[str] = None + current_desc_parts: list = [] + + for line in lines[1:]: + stripped = line.strip() + section_match = _SECTION_RE.match(stripped) + if section_match: + if current_param is not None: + result["params"][current_param] = "\n".join(current_desc_parts).strip() + current_param = None + current_desc_parts = [] + section_name = section_match.group(1).lower() + in_args = section_name in ("args", "arguments", "parameters", "params") + continue + + if not in_args: + continue + + if ":" in stripped and not stripped.startswith(" "): + if current_param is not None: + result["params"][current_param] = "\n".join(current_desc_parts).strip() + param_part, _, desc_part = stripped.partition(":") + param_name = param_part.strip().split("(")[0].strip() + current_param = param_name + current_desc_parts = [desc_part.strip()] + elif current_param is not None: + aline = line + if aline.startswith(" "): + aline = aline[4:] + elif aline.startswith("\t"): + aline = aline[1:] + current_desc_parts.append(aline.strip()) + + if current_param is not None: + result["params"][current_param] = "\n".join(current_desc_parts).strip() + + return result + + +# --------------------------------------------------------------------------- +# 类型常量 +# --------------------------------------------------------------------------- + +SIMPLE_TYPE_MAP = { + "str": "string", + "string": "string", + "int": "integer", + "integer": "integer", + "float": "number", + "number": "number", + "bool": "boolean", + "boolean": "boolean", + "list": "array", + "array": "array", + "dict": "object", + "object": "object", +} + +ARRAY_TYPES = {"list", "List", "tuple", "Tuple", "set", "Set", "Sequence", "Iterable"} +OBJECT_TYPES = {"dict", "Dict", "Mapping"} +WRAPPER_TYPES = {"Optional"} +SLOT_TYPES = {"ResourceSlot", "DeviceSlot"} + + +# --------------------------------------------------------------------------- +# 简单类型映射 +# --------------------------------------------------------------------------- + + +def get_json_schema_type(type_str: str) -> str: + """简单类型名 -> JSON Schema type""" + return SIMPLE_TYPE_MAP.get(type_str.lower(), "string") + + +# --------------------------------------------------------------------------- +# AST 类型解析 +# --------------------------------------------------------------------------- + + +def parse_type_node(type_str: str): + """将类型注解字符串解析为 AST 节点,失败返回 None。""" + import ast as _ast + + try: + return _ast.parse(type_str.strip(), mode="eval").body + except Exception: + return None + + +def _collect_bitor(node, out: list): + """递归收集 X | Y | Z 的所有分支。""" + import ast as _ast + + if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr): + _collect_bitor(node.left, out) + _collect_bitor(node.right, out) + else: + out.append(node) + + +def type_node_to_schema( + node, + import_map: Optional[Dict[str, str]] = None, +) -> Dict[str, Any]: + """将 AST 类型注解节点递归转换为 JSON Schema dict。 + + 当提供 import_map 时,对于未知类名会尝试通过 import_map 解析模块路径, + 然后 import 真实类型对象来生成 schema (支持 TypedDict 等)。 + + 映射规则: + - Optional[X] → X 的 schema (剥掉 Optional) + - Union[X, Y] → {"anyOf": [X_schema, Y_schema]} + - List[X] / Tuple[X] / Set[X] → {"type": "array", "items": X_schema} + - Dict[K, V] → {"type": "object", "additionalProperties": V_schema} + - Literal["a", "b"] → {"type": "string", "enum": ["a", "b"]} + - TypedDict (via import_map) → {"type": "object", "properties": {...}} + - 基本类型 str/int/... → {"type": "string"/"integer"/...} + """ + import ast as _ast + + # --- Name 节点: str / int / dict / ResourceSlot / 自定义类 --- + if isinstance(node, _ast.Name): + name = node.id + if name in SLOT_TYPES: + return {"$slot": name} + json_type = SIMPLE_TYPE_MAP.get(name.lower()) + if json_type: + return {"type": json_type} + # 尝试通过 import_map 解析并 import 真实类型 + if import_map and name in import_map: + type_obj = resolve_type_object(import_map[name]) + if type_obj is not None: + return type_to_schema(type_obj) + # 未知类名 → 无法转 schema 的自定义类型默认当 object + return {"type": "object"} + + if isinstance(node, _ast.Constant): + if isinstance(node.value, str): + return {"type": SIMPLE_TYPE_MAP.get(node.value.lower(), "string")} + return {"type": "string"} + + # --- Subscript 节点: List[X], Dict[K,V], Optional[X], Literal[...] 等 --- + if isinstance(node, _ast.Subscript): + base_name = node.value.id if isinstance(node.value, _ast.Name) else "" + + # Optional[X] → 剥掉 + if base_name in WRAPPER_TYPES: + return type_node_to_schema(node.slice, import_map) + + # Union[X, None] → 剥掉 None; Union[X, Y] → anyOf + if base_name == "Union": + elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice] + non_none = [ + e + for e in elts + if not (isinstance(e, _ast.Constant) and e.value is None) + and not (isinstance(e, _ast.Name) and e.id == "None") + ] + if len(non_none) == 1: + return type_node_to_schema(non_none[0], import_map) + if len(non_none) > 1: + return {"anyOf": [type_node_to_schema(e, import_map) for e in non_none]} + return {"type": "string"} + + # Literal["a", "b", 1] → enum + if base_name == "Literal": + elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice] + values = [] + for e in elts: + if isinstance(e, _ast.Constant): + values.append(e.value) + elif isinstance(e, _ast.Name): + values.append(e.id) + if values: + return {"type": "string", "enum": values} + return {"type": "string"} + + # List / Tuple / Set → array + if base_name in ARRAY_TYPES: + if isinstance(node.slice, _ast.Tuple) and node.slice.elts: + inner_node = node.slice.elts[0] + else: + inner_node = node.slice + return {"type": "array", "items": type_node_to_schema(inner_node, import_map)} + + # Dict → object + if base_name in OBJECT_TYPES: + schema: Dict[str, Any] = {"type": "object"} + if isinstance(node.slice, _ast.Tuple) and len(node.slice.elts) >= 2: + val_node = node.slice.elts[1] + # Dict[str, Any] → 不加 additionalProperties (Any 等同于无约束) + is_any = (isinstance(val_node, _ast.Name) and val_node.id == "Any") or ( + isinstance(val_node, _ast.Constant) and val_node.value is None + ) + if not is_any: + val_schema = type_node_to_schema(val_node, import_map) + schema["additionalProperties"] = val_schema + return schema + + # --- BinOp: X | Y (Python 3.10+) → 当 Union 处理 --- + if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr): + parts: list = [] + _collect_bitor(node, parts) + non_none = [ + p + for p in parts + if not (isinstance(p, _ast.Constant) and p.value is None) + and not (isinstance(p, _ast.Name) and p.id == "None") + ] + if len(non_none) == 1: + return type_node_to_schema(non_none[0], import_map) + if len(non_none) > 1: + return {"anyOf": [type_node_to_schema(p, import_map) for p in non_none]} + return {"type": "string"} + + return {"type": "string"} + + +# --------------------------------------------------------------------------- +# 真实类型对象解析 (import-based) +# --------------------------------------------------------------------------- + + +def resolve_type_object(type_ref: str) -> Optional[Any]: + """通过 'module.path:ClassName' 格式的引用 import 并返回真实类型对象。 + + 对于 typing 内置名 (str, int, List 等) 直接返回 None (由 AST 路径处理)。 + import 失败时静默返回 None。 + """ + if ":" not in type_ref: + return None + try: + return import_class(type_ref) + except Exception: + return None + + +def is_typed_dict_class(obj: Any) -> bool: + """检查对象是否是 TypedDict 类。""" + if obj is None: + return False + try: + from typing_extensions import is_typeddict + + return is_typeddict(obj) + except ImportError: + if isinstance(obj, type): + return hasattr(obj, "__required_keys__") and hasattr(obj, "__optional_keys__") + return False + + +def type_to_schema(tp: Any) -> Dict[str, Any]: + """将真实 typing 对象递归转换为 JSON Schema dict。 + + 支持: + - 基本类型: str, int, float, bool → {"type": "string"/"integer"/...} + - typing 泛型: List[X], Dict[K,V], Optional[X], Union[X,Y], Literal[...] + - TypedDict → {"type": "object", "properties": {...}, "required": [...]} + - 自定义类 (ResourceSlot 等) → {"$slot": "..."} 或 {"type": "string"} + """ + origin = getattr(tp, "__origin__", None) + args = getattr(tp, "__args__", None) + + # --- None / NoneType --- + if tp is type(None): + return {"type": "null"} + + # --- 基本类型 --- + if tp is str: + return {"type": "string"} + if tp is int: + return {"type": "integer"} + if tp is float: + return {"type": "number"} + if tp is bool: + return {"type": "boolean"} + + # --- TypedDict --- + if is_typed_dict_class(tp): + try: + return TypedDictMessageInstance.get_json_schema_from_typed_dict(tp) + except Exception: + return {"type": "object"} + + # --- Literal --- + if origin is typing.Literal: + values = list(args) if args else [] + return {"type": "string", "enum": values} + + # --- Optional / Union --- + if origin is typing.Union: + non_none = [a for a in (args or ()) if a is not type(None)] + if len(non_none) == 1: + return type_to_schema(non_none[0]) + if len(non_none) > 1: + return {"anyOf": [type_to_schema(a) for a in non_none]} + return {"type": "string"} + + # --- List / Sequence / Set / Tuple / Iterable --- + if origin in (list, tuple, set, frozenset) or ( + origin is not None + and getattr(origin, "__name__", "") in ("Sequence", "Iterable", "Iterator", "MutableSequence") + ): + if args: + return {"type": "array", "items": type_to_schema(args[0])} + return {"type": "array"} + + # --- Dict / Mapping --- + if origin in (dict,) or (origin is not None and getattr(origin, "__name__", "") in ("Mapping", "MutableMapping")): + schema: Dict[str, Any] = {"type": "object"} + if args and len(args) >= 2: + schema["additionalProperties"] = type_to_schema(args[1]) + return schema + + # --- Slot 类型 --- + if isinstance(tp, type): + name = tp.__name__ + if name in SLOT_TYPES: + return {"$slot": name} + + # --- 其他未知类型 fallback --- + if isinstance(tp, type): + return {"type": "object"} + return {"type": "string"} + + +# --------------------------------------------------------------------------- +# Slot / Placeholder 检测 +# --------------------------------------------------------------------------- + + +def detect_slot_type(ptype) -> Tuple[Optional[str], bool]: + """检测参数类型是否为 ResourceSlot / DeviceSlot。 + + 兼容多种格式: + - runtime: "unilabos.registry.placeholder_type:ResourceSlot" + - runtime tuple: ("list", "unilabos.registry.placeholder_type:ResourceSlot") + - AST 裸名: "ResourceSlot", "List[ResourceSlot]", "Optional[ResourceSlot]" + + Returns: (slot_name | None, is_list) + """ + ptype_str = str(ptype) + + # 快速路径: 字符串里根本没有 Slot + if "ResourceSlot" not in ptype_str and "DeviceSlot" not in ptype_str: + return (None, False) + + # runtime 格式: 完整模块路径 + if isinstance(ptype, str): + if ptype.endswith(":ResourceSlot") or ptype == "ResourceSlot": + return ("ResourceSlot", False) + if ptype.endswith(":DeviceSlot") or ptype == "DeviceSlot": + return ("DeviceSlot", False) + # AST 复杂格式: List[ResourceSlot], Optional[ResourceSlot] 等 + if "[" in ptype: + node = parse_type_node(ptype) + if node is not None: + schema = type_node_to_schema(node) + # 直接是 slot + if "$slot" in schema: + return (schema["$slot"], False) + # array 包裹 slot: {"type": "array", "items": {"$slot": "..."}} + items = schema.get("items", {}) + if isinstance(items, dict) and "$slot" in items: + return (items["$slot"], True) + return (None, False) + + # runtime tuple 格式 + if isinstance(ptype, tuple) and len(ptype) == 2: + inner_str = str(ptype[1]) + if "ResourceSlot" in inner_str: + return ("ResourceSlot", True) + if "DeviceSlot" in inner_str: + return ("DeviceSlot", True) + + return (None, False) + + +def detect_placeholder_keys(params: list) -> Dict[str, str]: + """Detect parameters that reference ResourceSlot or DeviceSlot.""" + result: Dict[str, str] = {} + for p in params: + ptype = p.get("type", "") + if "ResourceSlot" in str(ptype): + result[p["name"]] = "unilabos_resources" + elif "DeviceSlot" in str(ptype): + result[p["name"]] = "unilabos_devices" + return result + + +# --------------------------------------------------------------------------- +# Handle 规范化 +# --------------------------------------------------------------------------- + + +def normalize_ast_handles(handles_raw: Any) -> List[Dict[str, Any]]: + """Convert AST-parsed handle structures to the standard registry format.""" + if not handles_raw: + return [] + + # handle_type → io_type 映射 (AST 内部类名 → YAML 标准字段值) + _HANDLE_TYPE_TO_IO_TYPE = { + "input": "target", + "output": "source", + "action_input": "action_target", + "action_output": "action_source", + } + + result: List[Dict[str, Any]] = [] + for h in handles_raw: + if isinstance(h, dict): + call = h.get("_call", "") + if "InputHandle" in call: + handle_type = "input" + elif "OutputHandle" in call: + handle_type = "output" + elif "ActionInputHandle" in call: + handle_type = "action_input" + elif "ActionOutputHandle" in call: + handle_type = "action_output" + else: + handle_type = h.get("handle_type", "unknown") + + io_type = _HANDLE_TYPE_TO_IO_TYPE.get(handle_type, handle_type) + + entry: Dict[str, Any] = { + "handler_key": h.get("key", ""), + "data_type": h.get("data_type", ""), + "io_type": io_type, + } + side = h.get("side") + if side: + if isinstance(side, str) and "." in side: + val = side.rsplit(".", 1)[-1] + side = val.lower() if val in ("LEFT", "RIGHT", "TOP", "BOTTOM") else val + entry["side"] = side + label = h.get("label") + if label: + entry["label"] = label + data_key = h.get("data_key") + if data_key: + entry["data_key"] = data_key + data_source = h.get("data_source") + if data_source: + if isinstance(data_source, str) and "." in data_source: + val = data_source.rsplit(".", 1)[-1] + data_source = val.lower() if val in ("HANDLE", "EXECUTOR") else val + entry["data_source"] = data_source + description = h.get("description") + if description: + entry["description"] = description + + result.append(entry) + return result + + +def normalize_ast_action_handles(handles_raw: Any) -> Dict[str, Any]: + """Convert AST-parsed action handle list to {"input": [...], "output": [...]}. + + Mirrors the runtime behavior of decorators._action_handles_to_dict: + - ActionInputHandle => grouped under "input" + - ActionOutputHandle => grouped under "output" + Field mapping: key -> handler_key (matches Pydantic serialization_alias). + """ + if not handles_raw or not isinstance(handles_raw, list): + return {} + + input_list: List[Dict[str, Any]] = [] + output_list: List[Dict[str, Any]] = [] + + for h in handles_raw: + if not isinstance(h, dict): + continue + call = h.get("_call", "") + is_input = "ActionInputHandle" in call or "InputHandle" in call + is_output = "ActionOutputHandle" in call or "OutputHandle" in call + + entry: Dict[str, Any] = { + "handler_key": h.get("key", ""), + "data_type": h.get("data_type", ""), + "label": h.get("label", ""), + } + for opt_key in ("side", "data_key", "data_source", "description", "io_type"): + val = h.get(opt_key) + if val is not None: + # Only resolve enum-style refs (e.g. DataSource.HANDLE -> handle) for data_source/side + # data_key values like "wells.@flatten", "@this.0@@@plate" must be preserved as-is + if ( + isinstance(val, str) + and "." in val + and opt_key not in ("io_type", "data_key") + ): + val = val.rsplit(".", 1)[-1].lower() + entry[opt_key] = val + + # io_type: only add when explicitly set; do not default output to "sink" (YAML convention omits it) + if "io_type" not in entry and is_input: + entry["io_type"] = "source" + + if is_input: + input_list.append(entry) + elif is_output: + output_list.append(entry) + + result: Dict[str, Any] = {} + if input_list: + result["input"] = input_list + # Always include output (empty list when no outputs) to match YAML + result["output"] = output_list + return result + + +# --------------------------------------------------------------------------- +# Schema 辅助 +# --------------------------------------------------------------------------- + + +def wrap_action_schema( + goal_schema: Dict[str, Any], + action_name: str, + description: str = "", + result_schema: Optional[Dict[str, Any]] = None, + feedback_schema: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + 将 goal 参数 schema 包装为标准的 action schema 格式: + { "properties": { "goal": ..., "feedback": ..., "result": ... }, ... } + """ + # 去掉 auto- 前缀用于 title/description,与 YAML 路径保持一致 + display_name = action_name.removeprefix("auto-") + return { + "title": f"{display_name}参数", + "description": description or f"{display_name}的参数schema", + "type": "object", + "properties": { + "goal": goal_schema, + "feedback": feedback_schema or {}, + "result": result_schema or {}, + }, + "required": ["goal"], + } + + +def preserve_field_descriptions(new_schema: Dict[str, Any], prev_schema: Dict[str, Any]): + """递归保留之前 schema 中各字段的 description / title。 + + 覆盖顶层以及嵌套 properties(如 goal.properties.xxx.description)。 + """ + if not prev_schema or not new_schema: + return + prev_props = prev_schema.get("properties", {}) + new_props = new_schema.get("properties", {}) + for field_name, prev_field in prev_props.items(): + if field_name not in new_props: + continue + new_field = new_props[field_name] + if not isinstance(prev_field, dict) or not isinstance(new_field, dict): + continue + if "title" in prev_field: + new_field.setdefault("title", prev_field["title"]) + if "description" in prev_field: + new_field.setdefault("description", prev_field["description"]) + if "properties" in prev_field and "properties" in new_field: + preserve_field_descriptions(new_field, prev_field) + + +def strip_ros_descriptions(schema: Any): + """递归清除 ROS schema 中自动生成的无意义 description(含 rosidl_parser 内存地址)。""" + if isinstance(schema, dict): + desc = schema.get("description", "") + if isinstance(desc, str) and "rosidl_parser" in desc: + del schema["description"] + for v in schema.values(): + strip_ros_descriptions(v) + elif isinstance(schema, list): + for item in schema: + strip_ros_descriptions(item) + + +# --------------------------------------------------------------------------- +# 深度对比 +# --------------------------------------------------------------------------- + + +def _short(val, limit=120): + """截断过长的值用于日志显示。""" + s = repr(val) + return s if len(s) <= limit else s[:limit] + "..." + + +def deep_diff(old, new, path="", max_depth=10) -> list: + """递归对比两个对象,返回所有差异的描述列表。""" + diffs = [] + if max_depth <= 0: + if old != new: + diffs.append(f"{path}: (达到最大深度) OLD≠NEW") + return diffs + + if type(old) != type(new): + diffs.append(f"{path}: 类型不同 OLD={type(old).__name__}({_short(old)}) NEW={type(new).__name__}({_short(new)})") + return diffs + + if isinstance(old, dict): + old_keys = set(old.keys()) + new_keys = set(new.keys()) + for k in sorted(new_keys - old_keys): + diffs.append(f"{path}.{k}: 新增字段 (AST有, YAML无) = {_short(new[k])}") + for k in sorted(old_keys - new_keys): + diffs.append(f"{path}.{k}: 缺失字段 (YAML有, AST无) = {_short(old[k])}") + for k in sorted(old_keys & new_keys): + diffs.extend(deep_diff(old[k], new[k], f"{path}.{k}", max_depth - 1)) + elif isinstance(old, (list, tuple)): + if len(old) != len(new): + diffs.append(f"{path}: 列表长度不同 OLD={len(old)} NEW={len(new)}") + for i in range(min(len(old), len(new))): + diffs.extend(deep_diff(old[i], new[i], f"{path}[{i}]", max_depth - 1)) + if len(new) > len(old): + for i in range(len(old), len(new)): + diffs.append(f"{path}[{i}]: 新增元素 = {_short(new[i])}") + elif len(old) > len(new): + for i in range(len(new), len(old)): + diffs.append(f"{path}[{i}]: 缺失元素 = {_short(old[i])}") + else: + if old != new: + diffs.append(f"{path}: OLD={_short(old)} NEW={_short(new)}") + return diffs + + +# --------------------------------------------------------------------------- +# MRO 方法参数解析 +# --------------------------------------------------------------------------- + + +def resolve_method_params_via_import(module_str: str, method_name: str) -> Dict[str, str]: + """当 AST 方法参数为空 (如 *args, **kwargs) 时, import class 并通过 MRO 获取真实方法参数. + + 返回 identity mapping {param_name: param_name}. + """ + if not module_str or ":" not in module_str: + return {} + try: + cls = import_class(module_str) + except Exception as e: + _logger.debug(f"[AST] resolve_method_params_via_import: import_class('{module_str}') failed: {e}") + return {} + + try: + for base_cls in cls.__mro__: + if method_name not in base_cls.__dict__: + continue + method = base_cls.__dict__[method_name] + actual = getattr(method, "__wrapped__", method) + if isinstance(actual, (staticmethod, classmethod)): + actual = actual.__func__ + if not callable(actual): + continue + sig = inspect.signature(actual, follow_wrapped=True) + params = [ + p.name for p in sig.parameters.values() + if p.name not in ("self", "cls") + and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) + ] + if params: + return {p: p for p in params} + except Exception as e: + _logger.debug(f"[AST] resolve_method_params_via_import: MRO walk for '{method_name}' failed: {e}") + return {} diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index ed3871d3..08d40af0 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -12,9 +12,11 @@ class RegularContainer(Container): kwargs["size_y"] = 0 if "size_z" not in kwargs: kwargs["size_z"] = 0 + if "category" not in kwargs: + kwargs["category"] = "container" self.kwargs = kwargs - super().__init__(*args, category="container", **kwargs) + super().__init__(*args, **kwargs) def load_state(self, state: Dict[str, Any]): super().load_state(state) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 17eaa68a..5a37c4c7 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -76,7 +76,7 @@ def canonicalize_nodes_data( if sample_id: logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}") for k in list(node.keys()): - if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]: + if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra", "machine_name"]: v = node.pop(k) node["config"][k] = v if outer_host_node_id is not None: @@ -288,6 +288,15 @@ def read_node_link_json( physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False) handle_communications(physical_setup_graph) + # Stamp machine_name on device trees only (resources are cloud-managed) + local_machine = BasicConfig.machine_name or "本地" + for tree in resource_tree_set.trees: + if tree.root_node.res_content.type != "device": + continue + for node in tree.get_all_nodes(): + if not node.res_content.machine_name: + node.res_content.machine_name = local_machine + return physical_setup_graph, resource_tree_set, standardized_links @@ -372,6 +381,15 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False) handle_communications(physical_setup_graph) + # Stamp machine_name on device trees only (resources are cloud-managed) + local_machine = BasicConfig.machine_name or "本地" + for tree in resource_tree_set.trees: + if tree.root_node.res_content.type != "device": + continue + for node in tree.get_all_nodes(): + if not node.res_content.machine_name: + node.res_content.machine_name = local_machine + return physical_setup_graph, resource_tree_set, standardized_links diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index baf0ed16..3fb945b6 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -75,14 +75,6 @@ class ResourceDictPositionObject(BaseModel): z: float = Field(description="Z coordinate", default=0.0) -class ResourceDictPoseExtraObjectType(BaseModel): - z_index: int - - -class ResourceDictPoseExtraObject(BaseModel): - z_index: Optional[int] = Field(alias="zIndex", default=None) - - class ResourceDictPositionType(TypedDict): size: ResourceDictPositionSizeType scale: ResourceDictPositionScaleType @@ -109,7 +101,7 @@ class ResourceDictPosition(BaseModel): cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field( description="Cross section type", default="rectangle" ) - extra: Optional[ResourceDictPoseExtraObject] = Field(description="Extra data", default=None) + extra: Optional[Dict[str, Any]] = Field(description="Extra data", default=None) class ResourceDictType(TypedDict): @@ -128,6 +120,7 @@ class ResourceDictType(TypedDict): config: Dict[str, Any] data: Dict[str, Any] extra: Dict[str, Any] + machine_name: str # 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化 @@ -147,8 +140,9 @@ class ResourceDict(BaseModel): klass: str = Field(alias="class", description="Resource class name") pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition) config: Dict[str, Any] = Field(description="Resource configuration") - data: Dict[str, Any] = Field(description="Resource data") - extra: Dict[str, Any] = Field(description="Extra data") + data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data") + extra: Dict[str, Any] = Field(description="Extra data, eg: slot index") + machine_name: str = Field(description="Machine this resource belongs to", default="") @field_serializer("parent_uuid") def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]): @@ -204,22 +198,30 @@ class ResourceDictInstance(object): self.typ = "dict" @classmethod - def get_resource_instance_from_dict(cls, content: Dict[str, Any]) -> "ResourceDictInstance": + def get_resource_instance_from_dict(cls, content: ResourceDictType) -> "ResourceDictInstance": """从字典创建资源实例""" if "id" not in content: content["id"] = content["name"] if "uuid" not in content: content["uuid"] = str(uuid.uuid4()) if "description" in content and content["description"] is None: + # noinspection PyTypedDict del content["description"] if "model" in content and content["model"] is None: + # noinspection PyTypedDict del content["model"] + # noinspection PyTypedDict if "schema" in content and content["schema"] is None: + # noinspection PyTypedDict del content["schema"] + # noinspection PyTypedDict if "x" in content.get("position", {}): # 说明是老版本的position格式,转换成新的 + # noinspection PyTypedDict content["position"] = {"position": content["position"]} + # noinspection PyTypedDict if not content.get("class"): + # noinspection PyTypedDict content["class"] = "" if not content.get("config"): # todo: 后续从后端保证字段非空 content["config"] = {} @@ -230,16 +232,18 @@ class ResourceDictInstance(object): if "position" in content: pose = content.get("pose", {}) if "position" not in pose: + # noinspection PyTypedDict if "position" in content["position"]: + # noinspection PyTypedDict pose["position"] = content["position"]["position"] else: - pose["position"] = {"x": 0, "y": 0, "z": 0} + pose["position"] = ResourceDictPositionObjectType(x=0, y=0, z=0) if "size" not in pose: - pose["size"] = { - "width": content["config"].get("size_x", 0), - "height": content["config"].get("size_y", 0), - "depth": content["config"].get("size_z", 0), - } + pose["size"] = ResourceDictPositionSizeType( + width= content["config"].get("size_x", 0), + height= content["config"].get("size_y", 0), + depth= content["config"].get("size_z", 0), + ) content["pose"] = pose try: res_dict = ResourceDict.model_validate(content) @@ -407,7 +411,7 @@ class ResourceTreeSet(object): ) @classmethod - def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False) -> "ResourceTreeSet": + def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False, old_size=False) -> "ResourceTreeSet": """ 从plr资源创建ResourceTreeSet """ @@ -430,13 +434,20 @@ class ResourceTreeSet(object): "resource_group": "resource_group", "trash": "trash", "plate_adapter": "plate_adapter", + "consumable": "consumable", + "tool": "tool", + "condenser": "condenser", + "crucible": "crucible", + "reagent_bottle": "reagent_bottle", + "flask": "flask", + "beaker": "beaker", } if source in replace_info: return replace_info[source] elif source is None: return "" else: - print("转换pylabrobot的时候,出现未知类型", source) + logger.trace(f"转换pylabrobot的时候,出现未知类型 {source}") return source def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None): @@ -491,7 +502,7 @@ class ResourceTreeSet(object): k: v for k, v in d.items() if k - not in [ + not in ([ "name", "children", "parent_name", @@ -502,7 +513,15 @@ class ResourceTreeSet(object): "size_z", "cross_section_type", "bottom_type", - ] + ] if not old_size else [ + "name", + "children", + "parent_name", + "location", + "rotation", + "cross_section_type", + "bottom_type", + ]) }, "data": states[d["name"]], "extra": extra, @@ -801,7 +820,8 @@ class ResourceTreeSet(object): if remote_root_type == "device": # 情况1: 一级是 device if remote_root_id not in local_device_map: - logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步") + if remote_root_id != "host_node": + logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步") continue local_device = local_device_map[remote_root_id] @@ -848,14 +868,27 @@ class ResourceTreeSet(object): f"从远端同步了 {added_count} 个物料子树" ) else: - # 情况2: 二级是物料(不是 device) - if remote_child_name not in local_children_map: - # 引入整个子树 - remote_child.res_content.parent = local_device.res_content - local_device.children.append(remote_child) - logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'") - else: - logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过") + # 二级物料已存在,比较三级子节点是否缺失 + local_material = local_children_map[remote_child_name] + local_material_children_map = {child.res_content.name: child for child in + local_material.children} + added_count = 0 + for remote_sub in remote_child.children: + remote_sub_name = remote_sub.res_content.name + if remote_sub_name not in local_material_children_map: + remote_sub.res_content.parent = local_material.res_content + local_material.children.append(remote_sub) + added_count += 1 + else: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}/{remote_sub_name}' " + f"已存在,跳过" + ) + if added_count > 0: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}': " + f"从远端同步了 {added_count} 个子物料" + ) else: # 情况1: 一级节点是物料(不是 device) # 检查是否已存在 @@ -878,7 +911,7 @@ class ResourceTreeSet(object): return self - def dump(self) -> List[List[Dict[str, Any]]]: + def dump(self, old_position=False) -> List[List[Dict[str, Any]]]: """ 将 ResourceTreeSet 序列化为嵌套列表格式 @@ -894,6 +927,10 @@ class ResourceTreeSet(object): # 获取树的所有节点并序列化 tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()] result.append(tree_nodes) + if old_position: + for r in result: + for rr in r: + rr["position"] = rr["pose"]["position"] return result @classmethod diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index b526d5f5..83e6f456 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -11,6 +11,7 @@ from io import StringIO from typing import Iterable, Any, Dict, Type, TypeVar, Union import yaml +from msgcenterpy.instances.ros2_instance import ROS2MessageInstance from pydantic import BaseModel from dataclasses import asdict, is_dataclass @@ -716,6 +717,19 @@ def ros_field_type_to_json_schema( # return {'type': 'object', 'description': f'未知类型: {field_type}'} +def _strip_rosidl_descriptions(schema: Any) -> None: + """递归清除 rosidl_parser 自动生成的无意义 description(含内存地址)。""" + if isinstance(schema, dict): + desc = schema.get("description", "") + if isinstance(desc, str) and "rosidl_parser" in desc: + del schema["description"] + for v in schema.values(): + _strip_rosidl_descriptions(v) + elif isinstance(schema, list): + for item in schema: + _strip_rosidl_descriptions(item) + + def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]: """ 将 ROS 消息类转换为 JSON Schema @@ -727,46 +741,10 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any Returns: 对应的 JSON Schema 定义 """ - schema = {"type": "object", "properties": {}, "required": []} - - # 优先使用字段名作为标题,否则使用类名 + schema = ROS2MessageInstance(msg_class()).get_json_schema() schema["title"] = field_name - - # 获取消息的字段和字段类型 - try: - for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()): - slot_name, slot_type = slot_info - type_info = msg_class.SLOT_TYPES[ind] - field_schema = ros_field_type_to_json_schema(type_info, slot_name) - schema["properties"][slot_name] = field_schema - schema["required"].append(slot_name) - # if hasattr(msg_class, 'get_fields_and_field_types'): - # fields_and_types = msg_class.get_fields_and_field_types() - # - # for field_name, field_type in fields_and_types.items(): - # # 将 ROS 字段类型转换为 JSON Schema - # field_schema = ros_field_type_to_json_schema(field_type) - # - # schema['properties'][field_name] = field_schema - # schema['required'].append(field_name) - # elif hasattr(msg_class, '__slots__') and hasattr(msg_class, '_fields_and_field_types'): - # # 直接从实例属性获取 - # for field_name in msg_class.__slots__: - # # 移除前导下划线(如果有) - # clean_name = field_name[1:] if field_name.startswith('_') else field_name - # - # # 从 _fields_and_field_types 获取类型 - # if clean_name in msg_class._fields_and_field_types: - # field_type = msg_class._fields_and_field_types[clean_name] - # field_schema = ros_field_type_to_json_schema(field_type) - # - # schema['properties'][clean_name] = field_schema - # schema['required'].append(clean_name) - except Exception as e: - # 如果获取字段类型失败,添加错误信息 - schema["description"] = f"解析消息字段时出错: {str(e)}" - logger.error(f"解析 {msg_class.__name__} 消息字段失败: {str(e)}") - + schema.pop("description", None) + _strip_rosidl_descriptions(schema) return schema @@ -813,6 +791,8 @@ def ros_action_to_json_schema( "required": ["goal"], } + _strip_rosidl_descriptions(schema) + # 保留之前 schema 中 goal/feedback/result 下一级字段的 description if previous_schema: _preserve_field_descriptions(schema, previous_schema) diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 6ff8cc57..ffc106c7 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -34,7 +34,8 @@ from unilabos_msgs.action import SendCmd from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos.config.config import BasicConfig -from unilabos.utils.decorator import get_topic_config, get_all_subscriptions +from unilabos.registry.decorators import get_topic_config +from unilabos.utils.decorator import get_all_subscriptions from unilabos.resources.container import RegularContainer from unilabos.resources.graphio import ( @@ -57,6 +58,7 @@ from unilabos_msgs.msg import Resource # type: ignore from unilabos.resources.resource_tracker import ( DeviceNodeResourceTracker, + ResourceDictType, ResourceTreeSet, ResourceTreeInstance, ResourceDictInstance, @@ -194,9 +196,9 @@ class PropertyPublisher: self._value = None try: self.publisher_ = node.create_publisher(msg_type, f"{name}", qos) - except AttributeError as ex: + except Exception as e: self.node.lab_logger().error( - f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}" + f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}" ) self.timer = node.create_timer(self.timer_period, self.publish_property) self.__loop = ROS2DeviceNode.get_asyncio_loop() @@ -569,9 +571,11 @@ class BaseROS2DeviceNode(Node, Generic[T]): future.add_done_callback(done_cb) except ImportError: self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot") + res.response = get_result_info_str(traceback.format_exc(), False, {}) except Exception as e: self.lab_logger().error("Host请求添加物料时出错") self.lab_logger().error(traceback.format_exc()) + res.response = get_result_info_str(traceback.format_exc(), False, {}) return res # noinspection PyTypeChecker @@ -594,6 +598,12 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.s2c_resource_tree, # type: ignore callback_group=self.callback_group, ), + "s2c_device_manage": self.create_service( + SerialCommand, + f"/srv{self.namespace}/s2c_device_manage", + self.s2c_device_manage, # type: ignore + callback_group=self.callback_group, + ), } # 向全局在线设备注册表添加设备信息 @@ -1062,6 +1072,48 @@ class BaseROS2DeviceNode(Node, Generic[T]): return res + async def s2c_device_manage(self, req: SerialCommand_Request, res: SerialCommand_Response): + """Handle add/remove device requests from HostNode via SerialCommand.""" + try: + cmd = json.loads(req.command) + action = cmd.get("action", "") + data = cmd.get("data", {}) + device_id = data.get("device_id", "") + + if not device_id: + res.response = json.dumps({"success": False, "error": "device_id required"}) + return res + + if action == "add": + result = self.create_device(device_id, data) + elif action == "remove": + result = self.destroy_device(device_id) + else: + result = {"success": False, "error": f"Unknown action: {action}"} + + res.response = json.dumps(result, ensure_ascii=False) + + except NotImplementedError as e: + self.lab_logger().warning(f"[DeviceManage] {e}") + res.response = json.dumps({"success": False, "error": str(e)}) + except Exception as e: + self.lab_logger().error(f"[DeviceManage] Error: {e}") + res.response = json.dumps({"success": False, "error": str(e)}) + + return res + + def create_device(self, device_id: str, config: "ResourceDictType") -> dict: + """Create a sub-device dynamically. Override in HostNode / WorkstationNode.""" + raise NotImplementedError( + f"{self.__class__.__name__} does not support dynamic device creation" + ) + + def destroy_device(self, device_id: str) -> dict: + """Destroy a sub-device dynamically. Override in HostNode / WorkstationNode.""" + raise NotImplementedError( + f"{self.__class__.__name__} does not support dynamic device removal" + ) + async def transfer_resource_to_another( self, plr_resources: List["ResourcePLR"], @@ -1204,22 +1256,40 @@ class BaseROS2DeviceNode(Node, Generic[T]): return self._lab_logger def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0): - """创建ROS发布者""" - # 检测装饰器配置(支持 get_{attr_name} 方法和 @property) + """创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建。""" + # 检测 @topic_config 装饰器配置 topic_config = {} + driver_class = type(self.driver_instance) - # 优先检测 get_{attr_name} 方法 - if hasattr(self.driver_instance, f"get_{attr_name}"): - getter_method = getattr(self.driver_instance, f"get_{attr_name}") - topic_config = get_topic_config(getter_method) + # 区分 @property 和普通方法两种情况 + is_prop = hasattr(driver_class, attr_name) and isinstance( + getattr(driver_class, attr_name), property + ) - # 如果没有配置,检测 @property 装饰的属性 + if is_prop: + # @property: 检测 fget 上的 @topic_config + class_attr = getattr(driver_class, attr_name) + if class_attr.fget is not None: + topic_config = get_topic_config(class_attr.fget) + else: + # 普通方法: 直接检测 attr_name 方法上的 @topic_config + if hasattr(self.driver_instance, attr_name): + method = getattr(self.driver_instance, attr_name) + if callable(method): + topic_config = get_topic_config(method) + + # 没有 @topic_config 装饰器则跳过发布 if not topic_config: - driver_class = type(self.driver_instance) - if hasattr(driver_class, attr_name): - class_attr = getattr(driver_class, attr_name) - if isinstance(class_attr, property) and class_attr.fget is not None: - topic_config = get_topic_config(class_attr.fget) + return + + # 发布名称优先级: @topic_config(name=...) > get_ 前缀去除 > attr_name + cfg_name = topic_config.get("name") + if cfg_name: + publish_name = cfg_name + elif attr_name.startswith("get_"): + publish_name = attr_name[4:] + else: + publish_name = attr_name # 使用装饰器配置或默认值 cfg_period = topic_config.get("period") @@ -1232,10 +1302,10 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 获取属性值的方法 def get_device_attr(): try: - if hasattr(self.driver_instance, f"get_{attr_name}"): - return getattr(self.driver_instance, f"get_{attr_name}")() - else: + if is_prop: return getattr(self.driver_instance, attr_name) + else: + return getattr(self.driver_instance, attr_name)() except AttributeError as ex: if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"): self.lab_logger().error( @@ -1247,8 +1317,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): ) self.lab_logger().error(traceback.format_exc()) - self._property_publishers[attr_name] = PropertyPublisher( - self, attr_name, get_device_attr, msg_type, period, print_publish, qos + self._property_publishers[publish_name] = PropertyPublisher( + self, publish_name, get_device_attr, msg_type, period, print_publish, qos ) def create_ros_action_server(self, action_name, action_value_mapping): @@ -1256,14 +1326,17 @@ class BaseROS2DeviceNode(Node, Generic[T]): action_type = action_value_mapping["type"] str_action_type = str(action_type)[8:-2] - self._action_servers[action_name] = ActionServer( - self, - action_type, - action_name, - execute_callback=self._create_execute_callback(action_name, action_value_mapping), - callback_group=self.callback_group, - ) - + try: + self._action_servers[action_name] = ActionServer( + self, + action_type, + action_name, + execute_callback=self._create_execute_callback(action_name, action_value_mapping), + callback_group=self.callback_group, + ) + except Exception as e: + self.lab_logger().error(f"创建ActionServer失败,Device: {self.device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}") + return self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") def _setup_decorated_subscribers(self): @@ -1811,7 +1884,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): continue # 处理单个 ResourceSlot - if arg_type == "unilabos.registry.placeholder_type:ResourceSlot": + _is_resource_slot = isinstance(arg_type, str) and arg_type.endswith(":ResourceSlot") + if _is_resource_slot: resource_data = function_args[arg_name] if isinstance(resource_data, dict) and "id" in resource_data: try: @@ -1825,8 +1899,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 处理 ResourceSlot 列表 elif isinstance(arg_type, tuple) and len(arg_type) == 2: - resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot" - if arg_type[0] == "list" and arg_type[1] == resource_slot_type: + if arg_type[0] == "list" and isinstance(arg_type[1], str) and arg_type[1].endswith(":ResourceSlot"): resource_list = function_args[arg_name] if isinstance(resource_list, list): try: diff --git a/unilabos/ros/nodes/presets/camera.py b/unilabos/ros/nodes/presets/camera.py index 2267f676..e94f001f 100644 --- a/unilabos/ros/nodes/presets/camera.py +++ b/unilabos/ros/nodes/presets/camera.py @@ -4,7 +4,14 @@ import cv2 from sensor_msgs.msg import Image from cv_bridge import CvBridge from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker +from unilabos.registry.decorators import device + +@device( + id="camera", + category=["camera"], + description="""VideoPublisher摄像头设备节点,用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头(如USB摄像头、内置摄像头等),定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。""", +) class VideoPublisher(BaseROS2DeviceNode): def __init__(self, device_id='video_publisher', registry_name="", device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None): # 初始化BaseROS2DeviceNode,使用自身作为driver_instance diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 4a868523..eb139f1f 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -12,6 +12,7 @@ from geometry_msgs.msg import Point from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.service import Service from typing_extensions import TypedDict +from unilabos_msgs.action import EmptyIn, StrSingleInput, ResourceCreateFromOuterEasy, ResourceCreateFromOuter from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv import ( ResourceAdd, @@ -23,6 +24,7 @@ from unilabos_msgs.srv import ( from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unique_identifier_msgs.msg import UUID +from unilabos.registry.decorators import device from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.registry.registry import lab_registry from unilabos.resources.container import RegularContainer @@ -30,6 +32,7 @@ from unilabos.resources.graphio import initialize_resource from unilabos.resources.registry import add_schema from unilabos.resources.resource_tracker import ( ResourceDict, + ResourceDictType, ResourceDictInstance, ResourceTreeSet, ResourceTreeInstance, @@ -65,7 +68,13 @@ class DeviceActionStatus: class TestResourceReturn(TypedDict): resources: List[List[ResourceDict]] devices: List[Dict[str, Any]] - unilabos_samples: List[LabSample] + # unilabos_samples: List[LabSample] + + +class CreateResourceReturn(TypedDict): + created_resource_tree: List[List[ResourceDict]] + liquid_input_resource_tree: List[Dict[str, Any]] + # unilabos_samples: List[LabSample] class TestLatencyReturn(TypedDict): @@ -80,6 +89,7 @@ class TestLatencyReturn(TypedDict): status: str +@device(id="host_node", category=[], description="Host Node", icon="icon_device.webp") class HostNode(BaseROS2DeviceNode): """ 主机节点类,负责管理设备、资源和控制器 @@ -268,44 +278,42 @@ class HostNode(BaseROS2DeviceNode): self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好 "/devices/host_node/create_resource": ActionClient( self, - lab_registry.ResourceCreateFromOuterEasy, + ResourceCreateFromOuterEasy, "/devices/host_node/create_resource", callback_group=self.callback_group, ), "/devices/host_node/create_resource_detailed": ActionClient( self, - lab_registry.ResourceCreateFromOuter, + ResourceCreateFromOuter, "/devices/host_node/create_resource_detailed", callback_group=self.callback_group, ), "/devices/host_node/test_latency": ActionClient( self, - lab_registry.EmptyIn, + EmptyIn, "/devices/host_node/test_latency", callback_group=self.callback_group, ), "/devices/host_node/test_resource": ActionClient( self, - lab_registry.EmptyIn, + EmptyIn, "/devices/host_node/test_resource", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command": ActionClient( self, - lab_registry.StrSingleInput, + StrSingleInput, "/devices/host_node/_execute_driver_command", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command_async": ActionClient( self, - lab_registry.StrSingleInput, + StrSingleInput, "/devices/host_node/_execute_driver_command_async", callback_group=self.callback_group, ), } # 用来存储多个ActionClient实例 - self._action_value_mappings: Dict[str, Dict] = ( - {} - ) # device_id -> action_value_mappings(本地+远程设备统一存储) + self._action_value_mappings: Dict[str, Dict] = {} # device_id -> action_value_mappings(本地+远程设备统一存储) self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings) self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 @@ -323,10 +331,18 @@ class HostNode(BaseROS2DeviceNode): self._discover_devices() # 初始化所有本机设备节点,多一次过滤,防止重复初始化 + local_machine = BasicConfig.machine_name for device_config in devices_config.root_nodes: device_id = device_config.res_content.id if device_config.res_content.type != "device": continue + dev_machine = device_config.res_content.machine_name + if dev_machine and local_machine and dev_machine != local_machine: + self.lab_logger().info( + f"[Host Node] Device {device_id} belongs to machine '{dev_machine}', " + f"local is '{local_machine}', skipping initialization." + ) + continue if device_id not in self.devices_names: self.initialize_device(device_id, device_config) else: @@ -556,7 +572,7 @@ class HostNode(BaseROS2DeviceNode): liquid_type: list[str] = [], liquid_volume: list[int] = [], slot_on_deck: str = "", - ): + ) -> CreateResourceReturn: # 暂不支持多对同名父子同时存在 res_creation_input = { "id": res_id.split("/")[-1], @@ -609,6 +625,8 @@ class HostNode(BaseROS2DeviceNode): assert len(response) == 1, "Create Resource应当只返回一个结果" for i in response: res = json.loads(i) + if "suc" in res: + raise ValueError(res.get("error")) return res except Exception as ex: pass @@ -650,7 +668,12 @@ class HostNode(BaseROS2DeviceNode): action_id = f"/devices/{device_id}/{action_name}" if action_id not in self._action_clients: action_type = action_value_mapping["type"] - self._action_clients[action_id] = ActionClient(self, action_type, action_id) + try: + self._action_clients[action_id] = ActionClient(self, action_type, action_id) + except Exception as e: + self.lab_logger().error( + f"创建ActionClient失败,Device: {device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}") + continue self.lab_logger().trace( f"[Host Node] Created ActionClient (Local): {action_id}" ) # 子设备再创建用的是Discover发现的 @@ -1250,9 +1273,9 @@ class HostNode(BaseROS2DeviceNode): # 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings if registry_name and registry_name in self._slave_registry_configs: - action_mappings = self._slave_registry_configs[registry_name].get( - "class", {} - ).get("action_value_mappings", {}) + action_mappings = ( + self._slave_registry_configs[registry_name].get("class", {}).get("action_value_mappings", {}) + ) if action_mappings: self._action_value_mappings[edge_device_id] = action_mappings self.lab_logger().info( @@ -1272,14 +1295,19 @@ class HostNode(BaseROS2DeviceNode): # 解析 devices_config,建立 device_id -> action_value_mappings 映射 if devices_config: + machine_name = info["machine_name"] + # Stamp machine_name on each device dict before parsing for device_tree in devices_config: for device_dict in device_tree: + device_dict["machine_name"] = machine_name device_id = device_dict.get("id", "") class_name = device_dict.get("class", "") if device_id and class_name and class_name in self._slave_registry_configs: - action_mappings = self._slave_registry_configs[class_name].get( - "class", {} - ).get("action_value_mappings", {}) + action_mappings = ( + self._slave_registry_configs[class_name] + .get("class", {}) + .get("action_value_mappings", {}) + ) if action_mappings: self._action_value_mappings[device_id] = action_mappings self.lab_logger().info( @@ -1287,6 +1315,18 @@ class HostNode(BaseROS2DeviceNode): f"for remote device {device_id} (class: {class_name})" ) + # Merge slave devices_config into self.devices_config tree + try: + slave_tree_set = ResourceTreeSet.load(devices_config) # slave一定是根节点的tree + for tree in slave_tree_set.trees: + self.devices_config.trees.append(tree) + self.lab_logger().info( + f"[Host Node] Merged {len(slave_tree_set.trees)} slave device trees " + f"(machine: {machine_name}) into devices_config" + ) + except Exception as e: + self.lab_logger().error(f"[Host Node] Failed to merge slave devices_config: {e}") + self.lab_logger().debug(f"[Host Node] Node info update: {info}") response.response = "OK" except Exception as e: @@ -1695,3 +1735,177 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}") self.lab_logger().error(traceback.format_exc()) return False + + # ------------------------------------------------------------------ + # Device lifecycle (add / remove) — pure forwarder + # ------------------------------------------------------------------ + + def notify_device_manage(self, target_node_id: str, action: str, config: ResourceDictType) -> bool: + """Forward an add/remove device command to the target node via ROS2 SerialCommand. + + The HostNode does NOT interpret the command; it simply resolves the + target namespace and forwards the request to ``s2c_device_manage``. + + If *target_node_id* equals the HostNode's own device_id (i.e. the + command targets the host itself), we call our local ``create_device`` + / ``destroy_device`` directly instead of going through ROS2. + """ + try: + # If the target is the host itself, handle locally + device_id = config["id"] + if target_node_id == self.device_id: + if action == "add": + return self.create_device(device_id, config).get("success", False) + elif action == "remove": + return self.destroy_device(device_id).get("success", False) + + if target_node_id not in self.devices_names: + self.lab_logger().error( + f"[Host Node-DeviceMgr] Target {target_node_id} not found in devices_names" + ) + return False + + namespace = self.devices_names[target_node_id] + device_key = f"{namespace}/{target_node_id}" + if device_key not in self._online_devices: + self.lab_logger().error(f"[Host Node-DeviceMgr] Target {device_key} is offline") + return False + + srv_address = f"/srv{namespace}/s2c_device_manage" + self.lab_logger().info( + f"[Host Node-DeviceMgr] Forwarding {action}_device to {target_node_id} ({srv_address})" + ) + + sclient = self.create_client(SerialCommand, srv_address) + if not sclient.wait_for_service(timeout_sec=5.0): + self.lab_logger().error(f"[Host Node-DeviceMgr] Service {srv_address} not available") + return False + + request = SerialCommand.Request() + request.command = json.dumps({"action": action, "data": config}, ensure_ascii=False) + + future = sclient.call_async(request) + timeout = 30.0 + start_time = time.time() + while not future.done(): + if time.time() - start_time > timeout: + self.lab_logger().error( + f"[Host Node-DeviceMgr] Timeout waiting for {action}_device on {target_node_id}" + ) + return False + time.sleep(0.05) + + response = future.result() + self.lab_logger().info( + f"[Host Node-DeviceMgr] {action}_device on {target_node_id} completed" + ) + return True + + except Exception as e: + self.lab_logger().error(f"[Host Node-DeviceMgr] Error: {e}") + self.lab_logger().error(traceback.format_exc()) + return False + + def create_device(self, device_id: str, config: ResourceDictType) -> dict: + """Dynamically create a root-level device on the host.""" + if not device_id: + return {"success": False, "error": "device_id required"} + + if device_id in self.devices_names: + return {"success": False, "error": f"Device {device_id} already exists"} + + try: + config.setdefault("id", device_id) + config.setdefault("type", "device") + config.setdefault("machine_name", BasicConfig.machine_name or "本地") + res_dict = ResourceDictInstance.get_resource_instance_from_dict(config) + + self.initialize_device(device_id, res_dict) + + if device_id not in self.devices_names: + return {"success": False, "error": f"initialize_device failed for {device_id}"} + + # Add to config tree (devices_config) + tree = ResourceTreeInstance(res_dict) + self.devices_config.trees.append(tree) + + # Add to resource tracker so s2c_resource_tree can find it + try: + for plr_resource in ResourceTreeSet([tree]).to_plr_resources(): + self._resource_tracker.add_resource(plr_resource) + except Exception as ex: + self.lab_logger().warning(f"[Host Node-DeviceMgr] PLR resource registration skipped: {ex}") + + self.lab_logger().info(f"[Host Node-DeviceMgr] Device {device_id} created successfully") + return {"success": True, "device_id": device_id} + + except Exception as e: + self.lab_logger().error(f"[Host Node-DeviceMgr] Failed to create {device_id}: {e}") + self.lab_logger().error(traceback.format_exc()) + return {"success": False, "error": str(e)} + + def destroy_device(self, device_id: str) -> dict: + """Remove a root-level device from the host.""" + if not device_id: + return {"success": False, "error": "device_id required"} + + if device_id not in self.devices_names: + return {"success": False, "error": f"Device {device_id} not found"} + + if device_id == self.device_id: + return {"success": False, "error": "Cannot destroy host_node itself"} + + try: + namespace = self.devices_names[device_id] + device_key = f"{namespace}/{device_id}" + + # Remove action clients + action_prefix = f"/devices/{device_id}/" + to_remove = [k for k in self._action_clients if k.startswith(action_prefix)] + for k in to_remove: + try: + self._action_clients[k].destroy() + except Exception: + pass + del self._action_clients[k] + + # Remove from config tree (devices_config) + self.devices_config.trees = [ + t for t in self.devices_config.trees + if t.root_node.res_content.id != device_id + ] + + # Remove from resource tracker + try: + tracked = self._resource_tracker.uuid_to_resources.copy() + for uid, res in tracked.items(): + res_id = res.get("id") if isinstance(res, dict) else getattr(res, "name", None) + if res_id == device_id: + self._resource_tracker.remove_resource(res) + except Exception as ex: + self.lab_logger().warning(f"[Host Node-DeviceMgr] Resource tracker cleanup: {ex}") + + # Clean internal state + self._online_devices.discard(device_key) + self.devices_names.pop(device_id, None) + self.device_machine_names.pop(device_id, None) + self._action_value_mappings.pop(device_id, None) + + # Destroy the ROS2 node of the device + instance = self.devices_instances.pop(device_id, None) + if instance is not None: + try: + # noinspection PyProtectedMember + ros_node = getattr(instance, "_ros_node", None) + if ros_node is not None: + ros_node.destroy_node() + except Exception as e: + self.lab_logger().warning(f"[Host Node-DeviceMgr] Error destroying ROS node for {device_id}: {e}") + + self.lab_logger().info(f"[Host Node-DeviceMgr] Device {device_id} destroyed") + return {"success": True, "device_id": device_id} + + except Exception as e: + self.lab_logger().error(f"[Host Node-DeviceMgr] Failed to destroy {device_id}: {e}") + self.lab_logger().error(traceback.format_exc()) + return {"success": False, "error": str(e)} diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index 902e2967..7f9f2aed 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -20,7 +20,7 @@ from unilabos.ros.msgs.message_converter import ( convert_from_ros_msg_with_mapping, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode -from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDictInstance +from unilabos.resources.resource_tracker import ResourceDictType, ResourceTreeSet, ResourceDictInstance from unilabos.utils.type_check import get_result_info_str if TYPE_CHECKING: @@ -177,6 +177,103 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}") return d + def create_device(self, device_id: str, config: ResourceDictType) -> dict: + """Dynamically add a sub-device to this workstation.""" + if not device_id: + return {"success": False, "error": "device_id required"} + + if device_id in self.sub_devices: + return {"success": False, "error": f"Sub-device {device_id} already exists"} + + try: + from unilabos.config.config import BasicConfig + config.setdefault("id", device_id) + config.setdefault("type", "device") + config.setdefault("machine_name", BasicConfig.machine_name or "本地") + res_dict = ResourceDictInstance.get_resource_instance_from_dict(config) + + d = self.initialize_device(device_id, res_dict) + if d is None: + return {"success": False, "error": f"initialize_device returned None for {device_id}"} + + # Add to children config list + self.children.append(res_dict) + + # Add to resource tracker + try: + from unilabos.resources.resource_tracker import ResourceTreeInstance + tree = ResourceTreeInstance(res_dict) + for plr_resource in ResourceTreeSet([tree]).to_plr_resources(): + self.resource_tracker.add_resource(plr_resource) + except Exception as ex: + self.lab_logger().warning(f"[Workstation-DeviceMgr] PLR resource registration skipped: {ex}") + + self.lab_logger().info(f"[Workstation-DeviceMgr] Sub-device {device_id} created") + return {"success": True, "device_id": device_id} + + except Exception as e: + self.lab_logger().error(f"[Workstation-DeviceMgr] Failed to create {device_id}: {e}") + self.lab_logger().error(traceback.format_exc()) + return {"success": False, "error": str(e)} + + def destroy_device(self, device_id: str) -> dict: + """Dynamically remove a sub-device from this workstation.""" + if not device_id: + return {"success": False, "error": "device_id required"} + + if device_id not in self.sub_devices: + return {"success": False, "error": f"Sub-device {device_id} not found"} + + try: + # Remove from children config list + self.children = [ + c for c in self.children + if c.res_content.id != device_id + ] + + # Remove from resource tracker + try: + tracked = self.resource_tracker.uuid_to_resources.copy() + for uid, res in tracked.items(): + res_id = res.get("id") if isinstance(res, dict) else getattr(res, "name", None) + if res_id == device_id: + self.resource_tracker.remove_resource(res) + except Exception as ex: + self.lab_logger().warning(f"[Workstation-DeviceMgr] Resource tracker cleanup: {ex}") + + # Remove action clients for this sub-device + action_prefix = f"/devices/{device_id}/" + to_remove = [k for k in self._action_clients if k.startswith(action_prefix)] + for k in to_remove: + try: + self._action_clients[k].destroy() + except Exception: + pass + del self._action_clients[k] + + # Destroy the ROS2 node + instance = self.sub_devices.pop(device_id, None) + if instance is not None: + ros_node = getattr(instance, "ros_node_instance", None) + if ros_node is not None: + try: + ros_node.destroy_node() + except Exception as e: + self.lab_logger().warning( + f"[Workstation-DeviceMgr] Error destroying ROS node for {device_id}: {e}" + ) + + # Remove from communication map if present + self.communication_node_id_to_instance.pop(device_id, None) + + self.lab_logger().info(f"[Workstation-DeviceMgr] Sub-device {device_id} destroyed") + return {"success": True, "device_id": device_id} + + except Exception as e: + self.lab_logger().error(f"[Workstation-DeviceMgr] Failed to destroy {device_id}: {e}") + self.lab_logger().error(traceback.format_exc()) + return {"success": False, "error": str(e)} + def create_ros_action_server(self, action_name, action_value_mapping): """创建ROS动作服务器""" if action_name not in self.protocol_names: diff --git a/unilabos/utils/decorator.py b/unilabos/utils/decorator.py index 22a90736..15793b14 100644 --- a/unilabos/utils/decorator.py +++ b/unilabos/utils/decorator.py @@ -19,74 +19,6 @@ def singleton(cls): return get_instance -def topic_config( - period: Optional[float] = None, - print_publish: Optional[bool] = None, - qos: Optional[int] = None, -) -> Callable[[F], F]: - """ - Topic发布配置装饰器 - - 用于装饰 get_{attr_name} 方法或 @property,控制对应属性的ROS topic发布行为。 - - Args: - period: 发布周期(秒)。None 表示使用默认值 5.0 - print_publish: 是否打印发布日志。None 表示使用节点默认配置 - qos: QoS深度配置。None 表示使用默认值 10 - - Example: - class MyDriver: - # 方式1: 装饰 get_{attr_name} 方法 - @topic_config(period=1.0, print_publish=False, qos=5) - def get_temperature(self): - return self._temperature - - # 方式2: 与 @property 连用(topic_config 放在下面) - @property - @topic_config(period=0.1) - def position(self): - return self._position - - Note: - 与 @property 连用时,@topic_config 必须放在 @property 下面, - 这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。 - """ - - def decorator(func: F) -> F: - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - # 在函数上附加配置属性 (type: ignore 用于动态属性) - wrapper._topic_period = period # type: ignore[attr-defined] - wrapper._topic_print_publish = print_publish # type: ignore[attr-defined] - wrapper._topic_qos = qos # type: ignore[attr-defined] - wrapper._has_topic_config = True # type: ignore[attr-defined] - - return wrapper # type: ignore[return-value] - - return decorator - - -def get_topic_config(func) -> dict: - """ - 获取函数上的topic配置 - - Args: - func: 被装饰的函数 - - Returns: - 包含 period, print_publish, qos 的配置字典 - """ - if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False): - return { - "period": getattr(func, "_topic_period", None), - "print_publish": getattr(func, "_topic_print_publish", None), - "qos": getattr(func, "_topic_qos", None), - } - return {} - - def subscribe( topic: str, msg_type: Optional[type] = None, @@ -104,24 +36,6 @@ def subscribe( - {namespace}: 完整命名空间 (如 "/devices/pump_1") msg_type: ROS 消息类型。如果为 None,需要在回调函数的类型注解中指定 qos: QoS 深度配置,默认为 10 - - Example: - from std_msgs.msg import String, Float64 - - class MyDriver: - @subscribe(topic="/devices/{device_id}/set_speed", msg_type=Float64) - def on_speed_update(self, msg: Float64): - self._speed = msg.data - print(f"Speed updated to: {self._speed}") - - @subscribe(topic="{namespace}/command") - def on_command(self, msg: String): - # msg_type 可从类型注解推断 - self.execute_command(msg.data) - - Note: - - 回调方法的第一个参数是 self,第二个参数是收到的 ROS 消息 - - topic 中的占位符会在创建订阅时被实际值替换 """ def decorator(func: F) -> F: @@ -129,7 +43,6 @@ def subscribe( def wrapper(*args, **kwargs): return func(*args, **kwargs) - # 在函数上附加订阅配置 wrapper._subscribe_topic = topic # type: ignore[attr-defined] wrapper._subscribe_msg_type = msg_type # type: ignore[attr-defined] wrapper._subscribe_qos = qos # type: ignore[attr-defined] @@ -141,15 +54,7 @@ def subscribe( def get_subscribe_config(func) -> dict: - """ - 获取函数上的订阅配置 - - Args: - func: 被装饰的函数 - - Returns: - 包含 topic, msg_type, qos 的配置字典 - """ + """获取函数上的订阅配置 (topic, msg_type, qos)""" if hasattr(func, "_has_subscribe") and getattr(func, "_has_subscribe", False): return { "topic": getattr(func, "_subscribe_topic", None), @@ -163,9 +68,6 @@ def get_all_subscriptions(instance) -> list: """ 扫描实例的所有方法,获取带有 @subscribe 装饰器的方法及其配置 - Args: - instance: 要扫描的实例 - Returns: 包含 (method_name, method, config) 元组的列表 """ @@ -184,92 +86,14 @@ def get_all_subscriptions(instance) -> list: return subscriptions -def always_free(func: F) -> F: - """ - 标记动作为永久闲置(不受busy队列限制)的装饰器 - - 被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制, - 任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。 - - Example: - class MyDriver: - @always_free - def query_status(self, param: str): - # 这个动作可以随时执行,不需要排队 - return self._status - - def transfer(self, volume: float): - # 这个动作会按正常排队逻辑执行 - pass - - Note: - - 可以与其他装饰器组合使用,@always_free 应放在最外层 - - 仅影响 WebSocket 调度层的 busy/free 判断,不影响 ROS2 层 - """ - - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - wrapper._is_always_free = True # type: ignore[attr-defined] - - return wrapper # type: ignore[return-value] - - -def is_always_free(func) -> bool: - """ - 检查函数是否被标记为永久闲置 - - Args: - func: 被检查的函数 - - Returns: - 如果函数被 @always_free 装饰则返回 True,否则返回 False - """ - return getattr(func, "_is_always_free", False) - - -def not_action(func: F) -> F: - """ - 标记方法为非动作的装饰器 - - 用于装饰 driver 类中的方法,使其在 complete_registry 时不被识别为动作。 - 适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。 - - Example: - class MyDriver: - @not_action - def helper_method(self): - # 这个方法不会被注册为动作 - pass - - def actual_action(self, param: str): - # 这个方法会被注册为动作 - self.helper_method() - - Note: - - 可以与其他装饰器组合使用,@not_action 应放在最外层 - - 仅影响 complete_registry 的动作识别,不影响方法的正常调用 - """ - - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - # 在函数上附加标记 - wrapper._is_not_action = True # type: ignore[attr-defined] - - return wrapper # type: ignore[return-value] - - -def is_not_action(func) -> bool: - """ - 检查函数是否被标记为非动作 - - Args: - func: 被检查的函数 - - Returns: - 如果函数被 @not_action 装饰则返回 True,否则返回 False - """ - return getattr(func, "_is_not_action", False) +# --------------------------------------------------------------------------- +# 向后兼容重导出 -- 已迁移到 unilabos.registry.decorators +# --------------------------------------------------------------------------- +from unilabos.registry.decorators import ( # noqa: E402, F401 + topic_config, + get_topic_config, + always_free, + is_always_free, + not_action, + is_not_action, +) diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index 73c0b10b..a2bbd262 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -22,6 +22,7 @@ class EnvironmentChecker: # "pymodbus.framer.FramerType": "pymodbus==3.9.2", "websockets": "websockets", "msgcenterpy": "msgcenterpy", + "orjson": "orjson", "opentrons_shared_data": "opentrons_shared_data", "typing_extensions": "typing_extensions", "crcmod": "crcmod-plus", @@ -32,7 +33,7 @@ class EnvironmentChecker: # 包版本要求(包名: 最低版本) self.version_requirements = { - "msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求 + "msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求 } self.missing_packages = [] diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index dabbe1a7..7fe2f501 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -21,15 +21,11 @@ __all__ = [ "get_class", "get_module", "init_from_list", - "get_class_info_static", - "get_registry_class_info", + "get_enhanced_class_info", ] -from ast import Constant - from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS from unilabos.utils import logger -from unilabos.utils.decorator import is_not_action, is_always_free class ImportManager: @@ -45,6 +41,7 @@ class ImportManager: self._modules: Dict[str, Any] = {} self._classes: Dict[str, Type] = {} self._functions: Dict[str, Callable] = {} + self._search_miss: set = set() if module_list: for module_path in module_list: @@ -159,193 +156,113 @@ class ImportManager: Returns: 找到的类对象,如果未找到则返回None """ - # 如果cls_name是builtins中的关键字,则返回对应类 if class_name in builtins.__dict__: return builtins.__dict__[class_name] - # 首先在已索引的类中查找 if class_name in self._classes: return self._classes[class_name] + cache_key = class_name.lower() if search_lower else class_name + if cache_key in self._search_miss: + return None + if search_lower: classes = {name.lower(): obj for name, obj in self._classes.items()} if class_name in classes: return classes[class_name] - # 遍历所有已加载的模块进行搜索 for module_path, module in self._modules.items(): for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and ( (name.lower() == class_name.lower()) if search_lower else (name == class_name) ): - # 将找到的类添加到索引中 self._classes[name] = obj self._classes[f"{module_path}:{name}"] = obj return obj + self._search_miss.add(cache_key) return None - def get_enhanced_class_info(self, module_path: str, use_dynamic: bool = True) -> Dict[str, Any]: - """ - 获取增强的类信息,支持动态导入和静态分析 + def get_enhanced_class_info(self, module_path: str, **_kwargs) -> Dict[str, Any]: + """通过 AST 分析获取类的增强信息。 + + 复用 ``ast_registry_scanner`` 的 ``_collect_imports`` / ``_extract_class_body``, + 与 AST 扫描注册表完全一致。 Args: - module_path: 模块路径,格式为 "module.path" 或 "module.path:ClassName" - use_dynamic: 是否优先使用动态导入 + module_path: 格式 ``"module.path:ClassName"`` Returns: - 包含详细类信息的字典 + ``{"module_path", "ast_analysis_success", "import_map", + "init_params", "status_methods", "action_methods"}`` """ - result = { + from unilabos.registry.ast_registry_scanner import ( + _collect_imports, + _extract_class_body, + _filepath_to_module, + ) + + result: Dict[str, Any] = { "module_path": module_path, - "dynamic_import_success": False, - "static_analysis_success": False, - "init_params": {}, - "status_methods": {}, # get_ 开头和 @property 方法 - "action_methods": {}, # set_ 开头和其他非_开头方法 - } - - # 尝试动态导入 - dynamic_info = None - static_info = None - if use_dynamic: - try: - dynamic_info = self._get_dynamic_class_info(module_path) - result["dynamic_import_success"] = True - logger.debug(f"[ImportManager] 动态导入类 {module_path} 成功") - except Exception as e: - logger.warning( - f"[UniLab Registry] 在补充注册表时,动态导入类 " - f"{module_path} 失败(将使用静态分析," - f"建议修复导入错误,以实现更好的注册表识别效果!): {e}" - ) - use_dynamic = False - if not use_dynamic: - # 尝试静态分析 - try: - static_info = self._get_static_class_info(module_path) - result["static_analysis_success"] = True - logger.debug(f"[ImportManager] 静态分析类 {module_path} 成功") - except Exception as e: - logger.warning(f"[ImportManager] 静态分析类 {module_path} 失败: {e}") - - # 合并信息(优先使用动态导入的信息) - if dynamic_info: - result.update(dynamic_info) - elif static_info: - result.update(static_info) - - return result - - def _get_dynamic_class_info(self, class_path: str) -> Dict[str, Any]: - """使用inspect模块动态获取类信息""" - cls = get_class(class_path) - class_name = cls.__name__ - - result = { - "class_name": class_name, - "init_params": self._analyze_method_signature(cls.__init__)["args"], + "ast_analysis_success": False, + "import_map": {}, + "init_params": [], "status_methods": {}, "action_methods": {}, } - # 分析类的所有成员 - for name, method in cls.__dict__.items(): - if name.startswith("_"): - continue - # 检查是否是property - if isinstance(method, property): - # @property 装饰的方法 - # noinspection PyTypeChecker - return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any" - prop_info = { - "name": name, - "return_type": return_type, - } - result["status_methods"][name] = prop_info - - # 检查是否有对应的setter - if method.fset: - setter_info = self._analyze_method_signature(method.fset) - result["action_methods"][name] = setter_info - - elif inspect.ismethod(method) or inspect.isfunction(method): - if name.startswith("get_"): - actual_name = name[4:] # 去掉get_前缀 - if actual_name in result["status_methods"]: - continue - # get_ 开头的方法归类为status - method_info = self._analyze_method_signature(method) - result["status_methods"][actual_name] = method_info - elif not name.startswith("_"): - # 检查是否被 @not_action 装饰器标记 - if is_not_action(method): - continue - # 其他非_开头的方法归类为action - method_info = self._analyze_method_signature(method) - # 检查是否被 @always_free 装饰器标记 - if is_always_free(method): - method_info["always_free"] = True - result["action_methods"][name] = method_info - - return result - - def _get_static_class_info(self, module_path: str) -> Dict[str, Any]: - """使用AST静态分析获取类信息""" module_name, class_name = module_path.rsplit(":", 1) - # 将模块路径转换为文件路径 file_path = self._module_path_to_file_path(module_name) if not file_path or not os.path.exists(file_path): - raise FileNotFoundError(f"找不到模块文件: {module_name} -> {file_path}") + logger.warning(f"[ImportManager] 找不到模块文件: {module_name} -> {file_path}") + return result - with open(file_path, "r", encoding="utf-8") as f: - source_code = f.read() + try: + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read(), filename=file_path) + except Exception as e: + logger.warning(f"[ImportManager] 解析文件 {file_path} 失败: {e}") + return result - tree = ast.parse(source_code) + # 推导 module dotted path → 构建 import_map + python_path = Path(file_path) + for sp in sorted(sys.path, key=len, reverse=True): + try: + Path(file_path).relative_to(sp) + python_path = Path(sp) + break + except ValueError: + continue + module_dotted = _filepath_to_module(Path(file_path), python_path) + import_map = _collect_imports(tree, module_dotted) + result["import_map"] = import_map - # 查找目标类 + # 定位目标类 AST 节点 target_class = None for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - if node.name == class_name: - target_class = node - break + if isinstance(node, ast.ClassDef) and node.name == class_name: + target_class = node + break if target_class is None: - raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}") + logger.warning(f"[ImportManager] 在文件 {file_path} 中找不到类 {class_name}") + return result - result = { - "class_name": class_name, - "init_params": {}, - "status_methods": {}, - "action_methods": {}, + body = _extract_class_body(target_class, import_map) + + # 映射到统一字段名(与 registry.py complete_registry 消费端一致) + result["init_params"] = body.get("init_params", []) + result["status_methods"] = body.get("status_properties", {}) + result["action_methods"] = { + k: { + "args": v.get("params", []), + "return_type": v.get("return_type", ""), + "is_async": v.get("is_async", False), + "always_free": v.get("always_free", False), + "docstring": v.get("docstring"), + } + for k, v in body.get("auto_methods", {}).items() } - - # 分析类的方法 - for node in target_class.body: - if isinstance(node, ast.FunctionDef): - method_info = self._analyze_method_node(node) - method_name = node.name - if method_name == "__init__": - result["init_params"] = method_info["args"] - elif method_name.startswith("_"): - continue - elif self._is_property_method(node): - # @property 装饰的方法 - result["status_methods"][method_name] = method_info - elif method_name.startswith("get_"): - # get_ 开头的方法归类为status - actual_name = method_name[4:] # 去掉get_前缀 - if actual_name not in result["status_methods"]: - result["status_methods"][actual_name] = method_info - else: - # 检查是否被 @not_action 装饰器标记 - if self._is_not_action_method(node): - continue - # 其他非_开头的方法归类为action - # 检查是否被 @always_free 装饰器标记 - if self._is_always_free_method(node): - method_info["always_free"] = True - result["action_methods"][method_name] = method_info + result["ast_analysis_success"] = True return result def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) -> Dict[str, Any]: @@ -401,23 +318,26 @@ class ImportManager: "name": method.__name__, "args": args, "return_type": self._get_type_string(signature.return_annotation), - "return_annotation": signature.return_annotation, # 保留原始类型注解,用于TypedDict等特殊处理 "is_async": inspect.iscoroutinefunction(method), } - def _get_return_type_from_method(self, method) -> str: + def _get_return_type_from_method(self, method) -> Union[str, Tuple[str, Any]]: """从方法中获取返回类型""" signature = inspect.signature(method) return self._get_type_string(signature.return_annotation) def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]: - """将类型注解转换为Class Library中可搜索的类名""" + """将类型注解转换为类型字符串。 + + 非内建类返回 ``module:ClassName`` 全路径(如 + ``"unilabos.registry.placeholder_type:ResourceSlot"``), + 避免短名冲突;内建类型直接返回短名(如 ``"str"``、``"int"``)。 + """ if annotation == inspect.Parameter.empty: - return "Any" # 如果没有注解,返回Any + return "Any" if annotation is None: - return "None" # 明确的None类型 + return "None" if hasattr(annotation, "__origin__"): - # 处理typing模块的类型 origin = annotation.__origin__ if origin in (list, set, tuple): if hasattr(annotation, "__args__") and annotation.__args__: @@ -432,126 +352,26 @@ class ImportManager: return "dict" elif origin is Optional: return "Unknown" - return f"Unknown" + return "Unknown" annotation_str = str(annotation) - # 处理typing模块的复杂类型 if "typing." in annotation_str: - # 简化typing类型显示 return ( annotation_str.replace("typing.", "") if getattr(annotation, "_name", None) is None else annotation._name.lower() ) - # 如果是类型对象 if hasattr(annotation, "__name__"): - # 如果是内置类型 - if annotation.__module__ == "builtins": - return annotation.__name__ - else: - # 如果是自定义类,返回完整路径 - return f"{annotation.__module__}:{annotation.__name__}" - # 如果是typing模块的类型 + module = getattr(annotation, "__module__", None) + if module and module != "builtins": + return f"{module}:{annotation.__name__}" + return annotation.__name__ elif hasattr(annotation, "_name"): return annotation._name - # 如果是字符串形式的类型注解 elif isinstance(annotation, str): return annotation else: return annotation_str - def _is_property_method(self, node: ast.FunctionDef) -> bool: - """检查是否是@property装饰的方法""" - for decorator in node.decorator_list: - if isinstance(decorator, ast.Name) and decorator.id == "property": - return True - return False - - def _is_setter_method(self, node: ast.FunctionDef) -> bool: - """检查是否是@xxx.setter装饰的方法""" - for decorator in node.decorator_list: - if isinstance(decorator, ast.Attribute) and decorator.attr == "setter": - return True - return False - - def _is_not_action_method(self, node: ast.FunctionDef) -> bool: - """检查是否是@not_action装饰的方法""" - for decorator in node.decorator_list: - if isinstance(decorator, ast.Name) and decorator.id == "not_action": - return True - return False - - def _is_always_free_method(self, node: ast.FunctionDef) -> bool: - """检查是否是@always_free装饰的方法""" - for decorator in node.decorator_list: - if isinstance(decorator, ast.Name) and decorator.id == "always_free": - return True - return False - - def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str: - """从setter装饰器中获取属性名""" - for decorator in node.decorator_list: - if isinstance(decorator, ast.Attribute) and decorator.attr == "setter": - if isinstance(decorator.value, ast.Name): - return decorator.value.id - return node.name - - def get_class_info_static(self, module_class_path: str) -> Dict[str, Any]: - """ - 静态分析获取类的方法信息,不需要实际导入模块 - - Args: - module_class_path: 格式为 "module.path:ClassName" 的字符串 - - Returns: - 包含类方法信息的字典 - """ - try: - if ":" not in module_class_path: - raise ValueError("module_class_path必须是 'module.path:ClassName' 格式") - - module_path, class_name = module_class_path.rsplit(":", 1) - - # 将模块路径转换为文件路径 - file_path = self._module_path_to_file_path(module_path) - if not file_path or not os.path.exists(file_path): - logger.warning(f"找不到模块文件: {module_path} -> {file_path}") - return {} - - # 解析源码 - with open(file_path, "r", encoding="utf-8") as f: - source_code = f.read() - - tree = ast.parse(source_code) - - # 查找目标类 - class_node = None - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == class_name: - class_node = node - break - - if not class_node: - logger.warning(f"在模块 {module_path} 中找不到类 {class_name}") - return {} - - # 分析类的方法 - methods_info = {} - for node in class_node.body: - if isinstance(node, ast.FunctionDef): - method_info = self._analyze_method_node(node) - methods_info[node.name] = method_info - - return { - "class_name": class_name, - "module_path": module_path, - "file_path": file_path, - "methods": methods_info, - } - - except Exception as e: - logger.error(f"静态分析类 {module_class_path} 时出错: {str(e)}") - return {} - def _module_path_to_file_path(self, module_path: str) -> Optional[str]: for path in sys.path: potential_path = Path(path) / module_path.replace(".", "/") @@ -566,222 +386,6 @@ class ImportManager: return None - def _analyze_method_node(self, node: ast.FunctionDef) -> Dict[str, Any]: - """分析方法节点,提取参数和返回类型信息""" - method_info = { - "name": node.name, - "args": [], - "return_type": None, - "is_async": isinstance(node, ast.AsyncFunctionDef), - } - # 获取默认值列表 - defaults = node.args.defaults - num_defaults = len(defaults) - - # 计算必需参数数量 - total_args = len(node.args.args) - num_required = total_args - num_defaults - - # 提取参数信息 - for i, arg in enumerate(node.args.args): - if arg.arg == "self": - continue - # 跳过 sample_uuids 参数(由系统自动注入) - if arg.arg == PARAM_SAMPLE_UUIDS: - continue - arg_info = { - "name": arg.arg, - "type": None, - "default": None, - "required": i < num_required, - } - - # 提取类型注解 - if arg.annotation: - arg_info["type"] = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else str(arg.annotation) - - # 提取默认值并推断类型 - if i >= num_required: - default_index = i - num_required - if default_index < len(defaults): - default_value: Constant = defaults[default_index] # type: ignore - assert isinstance(default_value, Constant), "暂不支持对非常量类型进行推断,可反馈开源仓库" - arg_info["default"] = default_value.value - # 如果没有类型注解,尝试从默认值推断类型 - if not arg_info["type"]: - arg_info["type"] = self._get_type_string(type(arg_info["default"])) - method_info["args"].append(arg_info) - - # 提取返回类型 - if node.returns: - method_info["return_type"] = ast.unparse(node.returns) if hasattr(ast, "unparse") else str(node.returns) - - return method_info - - def _infer_type_from_default(self, node: ast.AST) -> Optional[str]: - """从默认值推断参数类型""" - if isinstance(node, ast.Constant): - value = node.value - if isinstance(value, bool): - return "bool" - elif isinstance(value, int): - return "int" - elif isinstance(value, float): - return "float" - elif isinstance(value, str): - return "str" - elif value is None: - return "Optional[Any]" - elif isinstance(node, ast.List): - return "List" - elif isinstance(node, ast.Dict): - return "Dict" - elif isinstance(node, ast.Tuple): - return "Tuple" - elif isinstance(node, ast.Set): - return "Set" - elif isinstance(node, ast.Name): - # 常见的默认值模式 - if node.id in ["None"]: - return "Optional[Any]" - elif node.id in ["True", "False"]: - return "bool" - - return None - - def _infer_types_from_docstring(self, method_info: Dict[str, Any]) -> None: - """从docstring中推断参数类型""" - docstring = method_info.get("docstring", "") - if not docstring: - return - - lines = docstring.split("\n") - in_args_section = False - - for line in lines: - line = line.strip() - - # 检测Args或Arguments段落 - if line.lower().startswith(("args:", "arguments:")): - in_args_section = True - continue - elif line.startswith(("returns:", "return:", "yields:", "raises:")): - in_args_section = False - continue - elif not line or not in_args_section: - continue - - # 解析参数行,格式通常是: param_name (type): description 或 param_name: description - if ":" in line: - parts = line.split(":", 1) - param_part = parts[0].strip() - - # 提取参数名和类型 - param_name = None - param_type = None - - if "(" in param_part and ")" in param_part: - # 格式: param_name (type) - param_name = param_part.split("(")[0].strip() - type_part = param_part.split("(")[1].split(")")[0].strip() - param_type = type_part - else: - # 格式: param_name - param_name = param_part - - # 更新对应参数的类型信息 - if param_name: - for arg_info in method_info["args"]: - if arg_info["name"] == param_name and not arg_info["type"]: - if param_type: - arg_info["inferred_type"] = param_type - elif not arg_info["inferred_type"]: - # 从描述中推断类型 - description = parts[1].strip().lower() - if any(word in description for word in ["path", "file", "directory", "filename"]): - arg_info["inferred_type"] = "str" - elif any( - word in description for word in ["port", "number", "count", "size", "length"] - ): - arg_info["inferred_type"] = "int" - elif any( - word in description for word in ["rate", "ratio", "percentage", "temperature"] - ): - arg_info["inferred_type"] = "float" - elif any(word in description for word in ["flag", "enable", "disable", "option"]): - arg_info["inferred_type"] = "bool" - - def get_registry_class_info(self, module_class_path: str) -> Dict[str, Any]: - """ - 获取适用于注册表的类信息,包含完整的类型推断 - - Args: - module_class_path: 格式为 "module.path:ClassName" 的字符串 - - Returns: - 适用于注册表的类信息字典 - """ - class_info = self.get_class_info_static(module_class_path) - if not class_info: - return {} - - registry_info = { - "class_name": class_info["class_name"], - "module_path": class_info["module_path"], - "file_path": class_info["file_path"], - "methods": {}, - "properties": [], - "init_params": {}, - "action_methods": {}, - } - - for method_name, method_info in class_info["methods"].items(): - # 分类处理不同类型的方法 - if method_info["is_property"]: - registry_info["properties"].append( - { - "name": method_name, - "return_type": method_info.get("return_type"), - "docstring": method_info.get("docstring"), - } - ) - elif method_name == "__init__": - # 处理初始化参数 - init_params = {} - for arg in method_info["args"]: - if arg["name"] != "self": - param_info = { - "name": arg["name"], - "type": arg.get("type") or arg.get("inferred_type"), - "required": arg.get("is_required", True), - "default": arg.get("default"), - } - init_params[arg["name"]] = param_info - registry_info["init_params"] = init_params - elif not method_name.startswith("_"): - # 处理公共方法(可能的action方法) - action_info = { - "name": method_name, - "params": {}, - "return_type": method_info.get("return_type"), - "docstring": method_info.get("docstring"), - "num_required": method_info.get("num_required", 0) - 1, # 减去self - "num_defaults": method_info.get("num_defaults", 0), - } - - for arg in method_info["args"]: - if arg["name"] != "self": - param_info = { - "name": arg["name"], - "type": arg.get("type") or arg.get("inferred_type"), - "required": arg.get("is_required", True), - "default": arg.get("default"), - } - action_info["params"][arg["name"]] = param_info - - registry_info["action_methods"][method_name] = action_info - - return registry_info # 全局实例,便于直接使用 @@ -809,16 +413,6 @@ def init_from_list(module_list: List[str]) -> None: default_manager = ImportManager(module_list) -def get_class_info_static(module_class_path: str) -> Dict[str, Any]: - """静态分析获取类信息的便捷函数""" - return default_manager.get_class_info_static(module_class_path) - - -def get_registry_class_info(module_class_path: str) -> Dict[str, Any]: - """获取适用于注册表的类信息的便捷函数""" - return default_manager.get_registry_class_info(module_class_path) - - -def get_enhanced_class_info(module_path: str, use_dynamic: bool = True) -> Dict[str, Any]: +def get_enhanced_class_info(module_path: str, **kwargs) -> Dict[str, Any]: """获取增强的类信息的便捷函数""" - return default_manager.get_enhanced_class_info(module_path, use_dynamic) + return default_manager.get_enhanced_class_info(module_path, **kwargs) diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index be5d8c31..da085f14 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -217,7 +217,6 @@ def configure_logger(loglevel=None, working_dir=None): return log_filepath - # 配置日志系统 configure_logger() diff --git a/unilabos/utils/requirements.txt b/unilabos/utils/requirements.txt index 65d724fc..105d387d 100644 --- a/unilabos/utils/requirements.txt +++ b/unilabos/utils/requirements.txt @@ -1,7 +1,8 @@ networkx typing_extensions websockets -msgcenterpy>=0.1.5 +msgcenterpy>=0.1.8 +orjson>=3.11 opentrons_shared_data pint fastapi diff --git a/unilabos/utils/tools.py b/unilabos/utils/tools.py index 89195cbd..3c7b742e 100644 --- a/unilabos/utils/tools.py +++ b/unilabos/utils/tools.py @@ -1,4 +1,39 @@ +import json + +from unilabos.utils.type_check import TypeEncoder, json_default + +try: + import orjson + + def fast_dumps(obj, **kwargs) -> bytes: + """JSON 序列化为 bytes,优先使用 orjson。""" + return orjson.dumps(obj, option=orjson.OPT_NON_STR_KEYS, default=json_default) + + def fast_dumps_pretty(obj, **kwargs) -> bytes: + """JSON 序列化为 bytes(带缩进),优先使用 orjson。""" + return orjson.dumps( + obj, + option=orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2, + default=json_default, + ) + + def normalize_json(info: dict) -> dict: + """经 JSON 序列化/反序列化一轮来清理非标准类型。""" + return orjson.loads(orjson.dumps(info, default=json_default)) + +except ImportError: + + def fast_dumps(obj, **kwargs) -> bytes: # type: ignore[misc] + return json.dumps(obj, ensure_ascii=False, cls=TypeEncoder).encode("utf-8") + + def fast_dumps_pretty(obj, **kwargs) -> bytes: # type: ignore[misc] + return json.dumps(obj, indent=2, ensure_ascii=False, cls=TypeEncoder).encode("utf-8") + + def normalize_json(info: dict) -> dict: # type: ignore[misc] + return json.loads(json.dumps(info, ensure_ascii=False, cls=TypeEncoder)) + + # 辅助函数:将UUID数组转换为字符串 def uuid_to_str(uuid_array) -> str: """将UUID字节数组转换为十六进制字符串""" - return "".join(format(byte, "02x") for byte in uuid_array) \ No newline at end of file + return "".join(format(byte, "02x") for byte in uuid_array) diff --git a/unilabos/utils/type_check.py b/unilabos/utils/type_check.py index 64001e56..e3df2dc2 100644 --- a/unilabos/utils/type_check.py +++ b/unilabos/utils/type_check.py @@ -15,14 +15,21 @@ def get_type_class(type_hint): return final_type +def json_default(obj): + """将 type 对象序列化为类名,其余 fallback 到 str()。""" + if isinstance(obj, type): + return str(obj)[8:-2] + return str(obj) + + class TypeEncoder(json.JSONEncoder): """自定义JSON编码器处理特殊类型""" def default(self, obj): - # 优先处理类型对象 - if isinstance(obj, type): - return str(obj)[8:-2] - return super().default(obj) + try: + return json_default(obj) + except Exception: + return super().default(obj) class NoAliasDumper(yaml.SafeDumper): @@ -43,13 +50,10 @@ class ResultInfoEncoder(json.JSONEncoder): """专门用于处理任务执行结果信息的JSON编码器""" def default(self, obj): - # 优先处理类型对象 if isinstance(obj, type): - return str(obj)[8:-2] + return json_default(obj) - # 对于无法序列化的对象,统一转换为字符串 try: - # 尝试调用 __dict__ 或者其他序列化方法 if hasattr(obj, "__dict__"): return obj.__dict__ elif hasattr(obj, "_asdict"): # namedtuple @@ -59,10 +63,8 @@ class ResultInfoEncoder(json.JSONEncoder): elif hasattr(obj, "dict"): return obj.dict() else: - # 如果都不行,转换为字符串 return str(obj) except Exception: - # 如果转换失败,直接返回字符串表示 return str(obj) diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index 6957f7bf..ead5eded 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.18 + 0.10.19 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln