From 41bea8df9b82f0f669646dd7f01a58027de9f92d Mon Sep 17 00:00:00 2001 From: Skyzuo9 <3101065459@qq.com> Date: Tue, 14 Apr 2026 22:14:10 +0800 Subject: [PATCH] Sync Ubuntu dev branch to 3d_visual_20260413 base Key changes from Ubuntu dev environment: - Update device registry: expand bioyond_cell warehouse defs, add solenoid valve, add camera.yaml - Add workflow module (from_python_script.py, from_xdl.py) - Update app web layer (api.py, server.py, client.py, ws_client.py) - Update ROS nodes (base_device_node, host_node, camera, workstation) - Update resource tracker and graphio - Simplify ros2_controllers.yaml and resource_visalization.py - Update utils (decorator, environment_check, import_manager, log, tools, type_check) - Remove registry utils.py, ast_registry_scanner.py, decorators.py (consolidated) - Update docs, recipes, and CI config Co-Authored-By: Claude Opus 4.6 --- .conda/base/recipe.yaml | 4 +- .conda/environment/recipe.yaml | 2 +- .conda/full/recipe.yaml | 4 +- .github/workflows/ci-check.yml | 2 +- .gitignore | 1 - 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 +- scripts/workflow.py | 499 +-- setup.py | 2 +- unilabos/__init__.py | 2 +- unilabos/app/main.py | 150 +- unilabos/app/register.py | 56 +- unilabos/app/web/api.py | 2 +- unilabos/app/web/client.py | 47 +- unilabos/app/web/server.py | 4 +- unilabos/app/ws_client.py | 68 +- unilabos/config/config.py | 1 - unilabos/device_comms/universal_driver.py | 1 + unilabos/device_mesh/resource_visalization.py | 9 +- unilabos/device_mesh/ros2_controllers.yaml | 26 - unilabos/devices/ros_dev/moveit_interface.py | 274 +- 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 | 62 +- .../devices/characterization_optic.yaml | 15 +- unilabos/registry/devices/chinwe.yaml | 227 +- .../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 | 3742 +++++++++-------- .../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 | 170 +- unilabos/registry/devices/robot_gripper.yaml | 62 +- .../registry/devices/robot_linear_motion.yaml | 286 +- .../registry/devices/solid_dispenser.yaml | 130 +- unilabos/registry/devices/temperature.yaml | 130 +- unilabos/registry/devices/virtual_device.yaml | 1898 +++++---- 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 | 2511 +++-------- .../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/graphio.py | 24 +- unilabos/resources/resource_tracker.py | 64 +- unilabos/ros/main_slave_run.py | 2 +- unilabos/ros/msgs/message_converter.py | 58 +- unilabos/ros/nodes/base_device_node.py | 133 +- unilabos/ros/nodes/presets/camera.py | 7 - unilabos/ros/nodes/presets/host_node.py | 238 +- unilabos/ros/nodes/presets/workstation.py | 99 +- unilabos/utils/decorator.py | 200 +- unilabos/utils/environment_check.py | 3 +- unilabos/utils/import_manager.py | 572 ++- unilabos/utils/log.py | 1 + unilabos/utils/requirements.txt | 3 +- unilabos/utils/tools.py | 37 +- unilabos/utils/type_check.py | 22 +- unilabos/workflow/__init__.py | 0 unilabos/workflow/from_python_script.py | 241 ++ unilabos/workflow/from_xdl.py | 131 + unilabos_msgs/package.xml | 2 +- 100 files changed, 7764 insertions(+), 10883 deletions(-) mode change 100755 => 100644 unilabos/devices/ros_dev/moveit_interface.py delete mode 100644 unilabos/registry/ast_registry_scanner.py delete mode 100644 unilabos/registry/decorators.py create mode 100644 unilabos/registry/devices/camera.yaml mode change 100755 => 100644 unilabos/registry/devices/robot_arm.yaml mode change 100755 => 100644 unilabos/registry/devices/robot_linear_motion.yaml delete mode 100644 unilabos/registry/utils.py create mode 100644 unilabos/workflow/__init__.py create mode 100644 unilabos/workflow/from_python_script.py create mode 100644 unilabos/workflow/from_xdl.py diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index a63dda77a..cf908c9dc 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos - version: 0.10.19 + version: 0.10.18 source: path: ../../unilabos @@ -54,7 +54,7 @@ requirements: - pymodbus - matplotlib - pylibftdi - - uni-lab::unilabos-env ==0.10.19 + - uni-lab::unilabos-env ==0.10.18 about: repository: https://github.com/deepmodeling/Uni-Lab-OS diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index e9fd3e248..56ff44d47 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -2,7 +2,7 @@ package: name: unilabos-env - version: 0.10.19 + version: 0.10.18 build: noarch: generic diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index ab0e0c9fa..ff8b48242 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos-full - version: 0.10.19 + version: 0.10.18 build: noarch: generic @@ -11,7 +11,7 @@ build: requirements: run: # Base unilabos package (includes unilabos-env) - - uni-lab::unilabos ==0.10.19 + - uni-lab::unilabos ==0.10.18 # Documentation tools - sphinx - sphinx_rtd_theme diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 402edc26f..57245d94c 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 (AST registry validation) + - name: Run check mode (complete_registry) run: | call conda activate check-env echo Running check mode... diff --git a/.gitignore b/.gitignore index 12b344d63..838331e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ output/ unilabos_data/ pyrightconfig.json .cursorignore -device_package*/ ## Python # Byte-compiled / optimized / DLL files diff --git a/docs/developer_guide/add_device.md b/docs/developer_guide/add_device.md index 15ba4e087..dc95274fb 100644 --- a/docs/developer_guide/add_device.md +++ b/docs/developer_guide/add_device.md @@ -15,9 +15,6 @@ 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 @@ -26,23 +23,19 @@ 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 - # 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播 + # 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 @property - @topic_config(period=2.0) # 可自定义发布周期 def status(self) -> str: return self._status @@ -156,7 +149,7 @@ my_device: # 设备唯一标识符 系统会自动分析您的 Python 驱动类并生成: -- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性 +- `status_types`:从 `@property` 装饰的方法自动识别状态属性 - `action_value_mappings`:从类方法自动生成动作映射 - `init_param_schema`:从 `__init__` 方法分析初始化参数 - `schema`:前端显示用的属性类型定义 @@ -186,9 +179,7 @@ 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: """设备类文档字符串 @@ -207,9 +198,8 @@ 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]: @@ -227,61 +217,34 @@ class MyDevice: ## 状态属性 vs 动作方法 -### 状态属性(@property + @topic_config) +### 状态属性(@property) -状态属性需要同时使用 `@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_` 前缀去除 > 方法名 - **特点**: -- 必须使用 `@topic_config` 装饰器 -- 支持 `@property` 和普通方法 -- 添加到注册表的 `status_types` +- 使用`@property`装饰器 +- 只读,不能有参数 +- 自动添加到注册表的`status_types` - 定期发布到 ROS2 topic -> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。 - ### 动作方法 动作方法是设备可以执行的操作: @@ -534,7 +497,6 @@ class LiquidHandler: self._status = "idle" @property - @topic_config() def status(self) -> str: return self._status @@ -924,52 +886,7 @@ class MyDevice: ## 最佳实践 -### 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. 类型注解 +### 1. 类型注解 ```python from typing import Dict, Any, Optional, List @@ -984,7 +901,7 @@ def method( pass ``` -### 4. 文档字符串 +### 2. 文档字符串 ```python def method(self, param: float) -> Dict[str, Any]: @@ -1006,7 +923,7 @@ def method(self, param: float) -> Dict[str, Any]: pass ``` -### 5. 配置验证 +### 3. 配置验证 ```python def __init__(self, config: Dict[str, Any]): @@ -1020,7 +937,7 @@ def __init__(self, config: Dict[str, Any]): self.baudrate = config['baudrate'] ``` -### 6. 资源清理 +### 4. 资源清理 ```python def __del__(self): @@ -1029,7 +946,7 @@ def __del__(self): self.connection.close() ``` -### 7. 设计前端友好的返回值 +### 5. 设计前端友好的返回值 **记住:返回值会直接显示在 Web 界面** diff --git a/docs/developer_guide/add_registry.md b/docs/developer_guide/add_registry.md index 38d3f893f..36caa9435 100644 --- a/docs/developer_guide/add_registry.md +++ b/docs/developer_guide/add_registry.md @@ -422,20 +422,18 @@ placeholder_keys: ### status_types -系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分: +系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分: ```yaml status_types: - current_temperature: float # 从 @topic_config 装饰的 @property 或方法 - is_heating: bool - status: str + current_temperature: float # 从 get_current_temperature() 或 @property current_temperature + is_heating: bool # 从 get_is_heating() 或 @property is_heating + status: str # 从 get_status() 或 @property status ``` **注意事项**: -- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性 -- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播 -- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`) +- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 - 类型会自动转成相应的类型(如 `str`、`float`、`bool`) - 如果类型是 `Any`、`None` 或未知的,默认使用 `String` @@ -539,13 +537,11 @@ 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 @@ -813,23 +809,21 @@ my_temperature_controller: 你的设备类需要符合以下要求: ```python -from unilabos.registry.decorators import device, topic_config +from unilabos.common.device_base import DeviceBase -@device(id="my_device", category=["temperature"], description="My Device") -class MyDevice: +class MyDevice(DeviceBase): def __init__(self, config): """初始化,参数会自动分析到 init_param_schema.config""" + super().__init__(config) self.port = config.get('port', '/dev/ttyUSB0') - # 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播) + # 状态方法(会自动生成到 status_types) @property - @topic_config() def status(self): """返回设备状态""" return "idle" @property - @topic_config() def temperature(self): """返回当前温度""" return 25.0 @@ -1045,34 +1039,7 @@ resource.type # "resource" ### 代码规范 -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. **始终使用类型注解** +1. **始终使用类型注解** ```python # ✓ 好 @@ -1084,7 +1051,7 @@ def method(self, resource, device): pass ``` -4. **提供有意义的参数名** +2. **提供有意义的参数名** ```python # ✓ 好 - 清晰的参数名 @@ -1096,7 +1063,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot): pass ``` -5. **使用 Optional 表示可选参数** +3. **使用 Optional 表示可选参数** ```python from typing import Optional @@ -1109,7 +1076,7 @@ def method( pass ``` -6. **添加详细的文档字符串** +4. **添加详细的文档字符串** ```python def method( @@ -1129,13 +1096,13 @@ def method( pass ``` -7. **方法命名规范** +5. **方法命名规范** - - 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config` + - 状态方法使用 `@property` 装饰器或 `get_` 前缀 - 动作方法使用动词开头 - 保持命名清晰、一致 -8. **完善的错误处理** +6. **完善的错误处理** - 实现完善的错误处理 - 添加日志记录 - 提供有意义的错误信息 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index 19f163121..40b308d32 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 +unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource # 实验室B -unilab --ak your_ak --sk your_sk --upload_registry +unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource ``` --- diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 34caa5b90..402e39aa1 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -22,6 +22,7 @@ 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 @@ -84,7 +85,7 @@ Uni-Lab 的启动过程分为以下几个阶段: 支持两种方式: - **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式) -- **远程资源**:不指定本地文件即可 +- **远程资源**:使用 `--use_remote_resource` 从云端获取 ### 7. 注册表构建 @@ -195,7 +196,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 +unilab --ak your_ak --sk your_sk --use_remote_resource # 更新注册表 unilab --ak your_ak --sk your_sk --complete_registry diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index fc8a5ccff..a3c2d2bd7 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.10.19 + version: 0.10.18 source: path: ../../unilabos_msgs target_directory: src diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 91e07b242..aeb76a0cb 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.19" + version: "0.10.18" source: path: ../.. diff --git a/scripts/workflow.py b/scripts/workflow.py index be7bbd1e4..8bd896409 100644 --- a/scripts/workflow.py +++ b/scripts/workflow.py @@ -2,7 +2,6 @@ import logging import traceback import uuid -import xml.etree.ElementTree as ET from typing import Any, Dict, List import networkx as nx @@ -25,7 +24,15 @@ def add_node(self, node_id, **attrs): def add_edge(self, source, target, **attrs): """添加边""" - edge = {"source": source, "target": target, **attrs} + # edge = {"source": source, "target": target, **attrs} + edge = { + "source": source, "target": target, + "source_node_uuid": source, + "target_node_uuid": target, + "source_handle_io": "source", + "target_handle_io": "target", + **attrs + } self.edges.append(edge) def to_dict(self): @@ -42,6 +49,7 @@ def to_dict(self): "multigraph": False, "graph": {}, "nodes": nodes_list, + "edges": self.edges, "links": self.edges, } @@ -58,495 +66,8 @@ def extract_json_from_markdown(text: str) -> str: return text -def convert_to_type(val: str) -> Any: - """将字符串值转换为适当的数据类型""" - if val == "True": - return True - if val == "False": - return False - if val == "?": - return None - if val.endswith(" g"): - return float(val.split(" ")[0]) - if val.endswith("mg"): - return float(val.split("mg")[0]) - elif val.endswith("mmol"): - return float(val.split("mmol")[0]) / 1000 - elif val.endswith("mol"): - return float(val.split("mol")[0]) - elif val.endswith("ml"): - return float(val.split("ml")[0]) - elif val.endswith("RPM"): - return float(val.split("RPM")[0]) - elif val.endswith(" °C"): - return float(val.split(" ")[0]) - elif val.endswith(" %"): - return float(val.split(" ")[0]) - return val - - -def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """统一的数据重构函数,根据操作类型自动选择模板""" - refactored_data = [] - - # 定义操作映射,包含生物实验和有机化学的所有操作 - OPERATION_MAPPING = { - # 生物实验操作 - "transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid", - "transfer": "SynBioFactory-liquid_handler.biomek-transfer", - "incubation": "SynBioFactory-liquid_handler.biomek-incubation", - "move_labware": "SynBioFactory-liquid_handler.biomek-move_labware", - "oscillation": "SynBioFactory-liquid_handler.biomek-oscillation", - # 有机化学操作 - "HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol", - "StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol", - "StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol", - "HeatChill": "SynBioFactory-workstation-HeatChillProtocol", - "Dissolve": "SynBioFactory-workstation-DissolveProtocol", - "Transfer": "SynBioFactory-workstation-TransferProtocol", - "Evaporate": "SynBioFactory-workstation-EvaporateProtocol", - "Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol", - "Filter": "SynBioFactory-workstation-FilterProtocol", - "Dry": "SynBioFactory-workstation-DryProtocol", - "Add": "SynBioFactory-workstation-AddProtocol", - } - - UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"] - - for step in data: - operation = step.get("action") - if not operation or operation in UNSUPPORTED_OPERATIONS: - continue - - # 处理重复操作 - if operation == "Repeat": - times = step.get("times", step.get("parameters", {}).get("times", 1)) - sub_steps = step.get("steps", step.get("parameters", {}).get("steps", [])) - for i in range(int(times)): - sub_data = refactor_data(sub_steps) - refactored_data.extend(sub_data) - continue - - # 获取模板名称 - template = OPERATION_MAPPING.get(operation) - if not template: - # 自动推断模板类型 - if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]: - template = f"SynBioFactory-liquid_handler.biomek-{operation}" - else: - template = f"SynBioFactory-workstation-{operation}Protocol" - - # 创建步骤数据 - step_data = { - "template": template, - "description": step.get("description", step.get("purpose", f"{operation} operation")), - "lab_node_type": "Device", - "parameters": step.get("parameters", step.get("action_args", {})), - } - refactored_data.append(step_data) - - return refactored_data - - -def build_protocol_graph( - labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str -) -> SimpleGraph: - """统一的协议图构建函数,根据设备类型自动选择构建逻辑""" - G = SimpleGraph() - resource_last_writer = {} - LAB_NAME = "SynBioFactory" - - protocol_steps = refactor_data(protocol_steps) - - # 检查协议步骤中的模板来判断协议类型 - has_biomek_template = any( - ("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", "")) - for step in protocol_steps - ) - - if has_biomek_template: - # 生物实验协议图构建 - for labware_id, labware in labware_info.items(): - node_id = str(uuid.uuid4()) - - labware_attrs = labware.copy() - labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}")) - labware_attrs["description"] = labware_id - labware_attrs["lab_node_type"] = ( - "Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample" - ) - labware_attrs["device_id"] = workstation_name - - G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs) - resource_last_writer[labware_id] = f"{node_id}:labware" - - # 处理协议步骤 - prev_node = None - for i, step in enumerate(protocol_steps): - node_id = str(uuid.uuid4()) - G.add_node(node_id, **step) - - # 添加控制流边 - if prev_node is not None: - G.add_edge(prev_node, node_id, source_port="ready", target_port="ready") - prev_node = node_id - - # 处理物料流 - params = step.get("parameters", {}) - if "sources" in params and params["sources"] in resource_last_writer: - source_node, source_port = resource_last_writer[params["sources"]].split(":") - G.add_edge(source_node, node_id, source_port=source_port, target_port="labware") - - if "targets" in params: - resource_last_writer[params["targets"]] = f"{node_id}:labware" - - # 添加协议结束节点 - end_id = str(uuid.uuid4()) - G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol") - if prev_node is not None: - G.add_edge(prev_node, end_id, source_port="ready", target_port="ready") - - else: - # 有机化学协议图构建 - WORKSTATION_ID = workstation_name - - # 为所有labware创建资源节点 - for item_id, item in labware_info.items(): - # item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}") - node_id = str(uuid.uuid4()) - - # 判断节点类型 - if item.get("type") == "hardware" or "reactor" in str(item_id).lower(): - if "reactor" not in str(item_id).lower(): - continue - lab_node_type = "Sample" - description = f"Prepare Reactor: {item_id}" - liquid_type = [] - liquid_volume = [] - else: - lab_node_type = "Reagent" - description = f"Add Reagent to Flask: {item_id}" - liquid_type = [item_id] - liquid_volume = [1e5] - - G.add_node( - node_id, - template=f"{LAB_NAME}-host_node-create_resource", - description=description, - lab_node_type=lab_node_type, - res_id=item_id, - device_id=WORKSTATION_ID, - class_name="container", - parent=WORKSTATION_ID, - bind_locations={"x": 0.0, "y": 0.0, "z": 0.0}, - liquid_input_slot=[-1], - liquid_type=liquid_type, - liquid_volume=liquid_volume, - slot_on_deck="", - role=item.get("role", ""), - ) - resource_last_writer[item_id] = f"{node_id}:labware" - - last_control_node_id = None - - # 处理协议步骤 - for step in protocol_steps: - node_id = str(uuid.uuid4()) - G.add_node(node_id, **step) - - # 控制流 - if last_control_node_id is not None: - G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready") - last_control_node_id = node_id - - # 物料流 - params = step.get("parameters", {}) - input_resources = { - "Vessel": params.get("vessel"), - "ToVessel": params.get("to_vessel"), - "FromVessel": params.get("from_vessel"), - "reagent": params.get("reagent"), - "solvent": params.get("solvent"), - "compound": params.get("compound"), - "sources": params.get("sources"), - "targets": params.get("targets"), - } - - for target_port, resource_name in input_resources.items(): - if resource_name and resource_name in resource_last_writer: - source_node, source_port = resource_last_writer[resource_name].split(":") - G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port) - - output_resources = { - "VesselOut": params.get("vessel"), - "FromVesselOut": params.get("from_vessel"), - "ToVesselOut": params.get("to_vessel"), - "FiltrateOut": params.get("filtrate_vessel"), - "reagent": params.get("reagent"), - "solvent": params.get("solvent"), - "compound": params.get("compound"), - "sources_out": params.get("sources"), - "targets_out": params.get("targets"), - } - - for source_port, resource_name in output_resources.items(): - if resource_name: - resource_last_writer[resource_name] = f"{node_id}:{source_port}" - - return G - - -def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str): - """ - (辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。 - """ - if not protocol_graph: - print("Cannot draw graph: Graph object is empty.") - return - - G = nx.DiGraph() - - for node_id, attrs in protocol_graph.nodes.items(): - label = attrs.get("description", attrs.get("template", node_id[:8])) - G.add_node(node_id, label=label, **attrs) - - for edge in protocol_graph.edges: - G.add_edge(edge["source"], edge["target"]) - - plt.figure(figsize=(20, 15)) - try: - pos = nx.nx_agraph.graphviz_layout(G, prog="dot") - except Exception: - pos = nx.shell_layout(G) # Fallback layout - - node_labels = {node: data["label"] for node, data in G.nodes(data=True)} - nx.draw( - G, - pos, - with_labels=False, - node_size=2500, - node_color="skyblue", - node_shape="o", - edge_color="gray", - width=1.5, - arrowsize=15, - ) - nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold") - - plt.title("Chemical Protocol Workflow Graph", size=15) - plt.savefig(output_path, dpi=300, bbox_inches="tight") - plt.close() - print(f" - Visualization saved to '{output_path}'") - - -from networkx.drawing.nx_agraph import to_agraph -import re - -COMPASS = {"n","e","s","w","ne","nw","se","sw","c"} - -def _is_compass(port: str) -> bool: - return isinstance(port, str) and port.lower() in COMPASS - -def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"): - """ - 使用 Graphviz 端口语法绘制协议工作流图。 - - 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。 - - 否则自动为节点创建 record 形状并定义命名端口 。 - 最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。 - """ - if not protocol_graph: - print("Cannot draw graph: Graph object is empty.") - return - - # 1) 先用 networkx 搭建有向图,保留端口属性 - G = nx.DiGraph() - for node_id, attrs in protocol_graph.nodes.items(): - label = attrs.get("description", attrs.get("template", node_id[:8])) - # 保留一个干净的“中心标签”,用于放在 record 的中间槽 - G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)}) - - edges_data = [] - in_ports_by_node = {} # 收集命名输入端口 - out_ports_by_node = {} # 收集命名输出端口 - - for edge in protocol_graph.edges: - u = edge["source"] - v = edge["target"] - sp = edge.get("source_port") - tp = edge.get("target_port") - - # 记录到图里(保留原始端口信息) - G.add_edge(u, v, source_port=sp, target_port=tp) - edges_data.append((u, v, sp, tp)) - - # 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record - if sp and not _is_compass(sp): - out_ports_by_node.setdefault(u, set()).add(str(sp)) - if tp and not _is_compass(tp): - in_ports_by_node.setdefault(v, set()).add(str(tp)) - - # 2) 转为 AGraph,使用 Graphviz 渲染 - A = to_agraph(G) - A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10") - A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica") - A.edge_attr.update(arrowsize="0.8", color="#666666") - - # 3) 为需要命名端口的节点设置 record 形状与 label - # 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口 - for n in A.nodes(): - node = A.get_node(n) - core = G.nodes[n].get("_core_label", n) - - in_ports = sorted(in_ports_by_node.get(n, [])) - out_ports = sorted(out_ports_by_node.get(n, [])) - - # 如果该节点涉及命名端口,则用 record;否则保留原 box - if in_ports or out_ports: - def port_fields(ports): - if not ports: - return " " # 必须留一个空槽占位 - # 每个端口一个小格子,

name - return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports) - - left = port_fields(in_ports) - right = port_fields(out_ports) - - # 三栏:左(入) | 中(节点名) | 右(出) - record_label = f"{{ {left} | {core} | {right} }}" - node.attr.update(shape="record", label=record_label) - else: - # 没有命名端口:普通盒子,显示核心标签 - node.attr.update(label=str(core)) - - # 4) 给边设置 headport / tailport - # - 若端口为 compass:直接用 compass(e.g., headport="e") - # - 若端口为命名端口:使用在 record 中定义的 名(同名即可) - for (u, v, sp, tp) in edges_data: - e = A.get_edge(u, v) - - # Graphviz 属性:tail 是源,head 是目标 - if sp: - if _is_compass(sp): - e.attr["tailport"] = sp.lower() - else: - # 与 record label 中 名一致;特殊字符已在 label 中做了清洗 - e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp)) - - if tp: - if _is_compass(tp): - e.attr["headport"] = tp.lower() - else: - e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp)) - - # 可选:若想让边更贴边缘,可设置 constraint/spline 等 - # e.attr["arrowhead"] = "vee" - - # 5) 输出 - A.draw(output_path, prog="dot") - print(f" - Port-aware workflow rendered to '{output_path}'") - - -def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]: - """展平嵌套的XDL程序结构""" - flattened_operations = [] - TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"] - - def extract_operations(element: ET.Element): - if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]: - if element.tag not in TEMP_UNSUPPORTED_PROTOCOL: - flattened_operations.append(element) - - for child in element: - extract_operations(child) - - for child in procedure_elem: - extract_operations(child) - - return flattened_operations - - -def parse_xdl_content(xdl_content: str) -> tuple: - """解析XDL内容""" - try: - xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable()) - root = ET.fromstring(xdl_content_cleaned) - - synthesis_elem = root.find("Synthesis") - if synthesis_elem is None: - return None, None, None - - # 解析硬件组件 - hardware_elem = synthesis_elem.find("Hardware") - hardware = [] - if hardware_elem is not None: - hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")] - - # 解析试剂 - reagents_elem = synthesis_elem.find("Reagents") - reagents = [] - if reagents_elem is not None: - reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")] - - # 解析程序 - procedure_elem = synthesis_elem.find("Procedure") - if procedure_elem is None: - return None, None, None - flattened_operations = flatten_xdl_procedure(procedure_elem) - return hardware, reagents, flattened_operations - - except ET.ParseError as e: - raise ValueError(f"Invalid XDL format: {e}") - - -def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]: - """ - 将XDL XML格式转换为标准的字典格式 - - Args: - xdl_content: XDL XML内容 - Returns: - 转换结果,包含步骤和器材信息 - """ - try: - hardware, reagents, flattened_operations = parse_xdl_content(xdl_content) - if hardware is None: - return {"error": "Failed to parse XDL content", "success": False} - - # 将XDL元素转换为字典格式 - steps_data = [] - for elem in flattened_operations: - # 转换参数类型 - parameters = {} - for key, val in elem.attrib.items(): - converted_val = convert_to_type(val) - if converted_val is not None: - parameters[key] = converted_val - - step_dict = { - "operation": elem.tag, - "parameters": parameters, - "description": elem.get("purpose", f"Operation: {elem.tag}"), - } - steps_data.append(step_dict) - - # 合并硬件和试剂为统一的labware_info格式 - labware_data = [] - labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware) - labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents) - - return { - "success": True, - "steps": steps_data, - "labware": labware_data, - "message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.", - } - - except Exception as e: - error_msg = f"XDL conversion failed: {str(e)}" - logger.error(error_msg) - return {"error": error_msg, "success": False} def create_workflow( diff --git a/setup.py b/setup.py index 7ca06f2e2..dc7bbc735 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name=package_name, - version='0.10.19', + version='0.10.18', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index eebdd7577..63e3face9 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.10.19" +__version__ = "0.10.18" diff --git a/unilabos/app/main.py b/unilabos/app/main.py index fa7bc35da..937512627 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -4,7 +4,6 @@ import platform import shutil import signal -import subprocess import sys import threading import time @@ -26,84 +25,6 @@ _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: @@ -145,13 +66,6 @@ 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, @@ -242,16 +156,16 @@ def parse_args(): help="Skip environment dependency check on startup", ) parser.add_argument( - "--check_mode", + "--complete_registry", action="store_true", default=False, - help="Run in check mode for CI: validates registry imports and ensures no file changes", + help="Complete registry information", ) parser.add_argument( - "--complete_registry", + "--check_mode", action="store_true", default=False, - help="Complete and rewrite YAML registry files using AST analysis results", + help="Run in check mode for CI: validates registry imports and ensures no file changes", ) parser.add_argument( "--no_update_feedback", @@ -264,24 +178,6 @@ 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", @@ -332,11 +228,6 @@ 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) @@ -467,9 +358,6 @@ 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]) @@ -494,32 +382,22 @@ def main(): # 显示启动横幅 print_unilab_banner(args_dict) - # Step 0: AST 分析优先 + YAML 注册表加载 - # check_mode 和 upload_registry 都会执行实际 import 验证 - devices_dirs = args_dict.get("devices", None) + # 注册表 - check_mode 时强制启用 complete_registry complete_registry = args_dict.get("complete_registry", False) or check_mode - 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, - ) + lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry) - # Check mode: 注册表验证完成后直接退出 + # Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行 if check_mode: - 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") + print_status("Check mode: complete_registry 完成,退出", "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: @@ -604,7 +482,7 @@ def main(): continue # 如果从远端获取了物料信息,则与本地物料进行同步 - if file_path is not None and request_startup_json and "nodes" in request_startup_json: + if 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) @@ -701,10 +579,6 @@ def _exit(signum, frame): 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 5940364ed..5918b43a3 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -1,8 +1,9 @@ +import json import time -from typing import Any, Dict, Optional, Tuple +from typing import Optional, Tuple, Dict, Any from unilabos.utils.log import logger -from unilabos.utils.tools import normalize_json as _normalize_device +from unilabos.utils.type_check import TypeEncoder def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]: @@ -10,63 +11,50 @@ 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"]] = _normalize_device(device_info) - logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}") + 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']}") resources_to_register = {} for resource_info in lab_registry.obtain_registry_resource_info(): resources_to_register[resource_info["id"]] = resource_info - logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}") + logger.debug(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())}, - tag="device_registry", - ) + response = http_client.resource_registry({"resources": list(devices_to_register.values())}) cost_time = time.time() - start_time - 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") + if response.status_code in [200, 201]: + logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s") else: - logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s") + logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}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())}, - tag="resource_registry", - ) + response = http_client.resource_registry({"resources": list(resources_to_register.values())}) cost_time = time.time() - start_time - 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") + if response.status_code in [200, 201]: + logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s") else: - logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s") + logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}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 99981f776..a67d09d20 100644 --- a/unilabos/app/web/api.py +++ b/unilabos/app/web/api.py @@ -1052,7 +1052,7 @@ async def send_result(result_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() diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b1cc67ebe..75b9e3436 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -8,8 +8,6 @@ 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 @@ -282,54 +280,29 @@ def upload_file(self, file_path: str, scene: str = "models") -> requests.Respons ) return response - def resource_registry( - self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry", - ) -> requests.Response: + def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> 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", - } + compressed_body = gzip.compress( + json.dumps(registry_data, ensure_ascii=False, default=str).encode("utf-8") + ) response = requests.post( f"{self.remote_addr}/lab/resource", data=compressed_body, - headers=headers, + headers={ + "Authorization": f"Lab {self.auth}", + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, 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 981edeca1..8d0901626 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 cbbb58efe..faaa30750 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -26,7 +26,6 @@ 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 @@ -409,7 +408,6 @@ def __init__(self, websocket_url: str, send_queue: Queue, device_manager: Device # 线程控制 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}") @@ -436,31 +434,22 @@ def start(self) -> None: 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): """运行消息处理主循环""" - self._loop = asyncio.new_event_loop() + loop = asyncio.new_event_loop() try: - asyncio.set_event_loop(self._loop) - self._loop.run_until_complete(self._connection_handler()) + asyncio.set_event_loop(loop) + 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 self._loop: - self._loop.close() - self._loop = None + if loop: + loop.close() async def _connection_handler(self): """处理WebSocket连接和重连逻辑""" @@ -659,10 +648,6 @@ async def _process_message(self, message_type: str, message_data: Dict[str, Any] # 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: @@ -999,37 +984,6 @@ def _notify_resource_tree(dev_id, act, item_list): ) 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]): """ 处理重启请求 @@ -1041,9 +995,10 @@ async def _handle_request_restart(self, data: Dict[str, Any]): logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") # 发送确认消息 - self.send_message( - {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} - ) + if self.websocket_client: + await self.websocket_client.send_message( + {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} + ) # 设置全局重启标志 import unilabos.app.main as main_module @@ -1145,7 +1100,6 @@ def start(self) -> None: 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") @@ -1399,8 +1353,8 @@ def stop(self) -> None: 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}") - # send_handler 每100ms检查一次队列,等300ms足以让消息发出 - time.sleep(0.3) + # 给一点时间让消息发送出去 + time.sleep(1) 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 b80d3b60d..d66b399d4 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -24,7 +24,6 @@ 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" diff --git a/unilabos/device_comms/universal_driver.py b/unilabos/device_comms/universal_driver.py index 0ff418051..281e0cd96 100644 --- a/unilabos/device_comms/universal_driver.py +++ b/unilabos/device_comms/universal_driver.py @@ -1,3 +1,4 @@ + from abc import abstractmethod from functools import wraps import inspect diff --git a/unilabos/device_mesh/resource_visalization.py b/unilabos/device_mesh/resource_visalization.py index f42202c12..ee58d67ec 100644 --- a/unilabos/device_mesh/resource_visalization.py +++ b/unilabos/device_mesh/resource_visalization.py @@ -132,7 +132,10 @@ def __init__(self, device: dict, resource: dict, enable_rviz: bool = True): new_dev.set("x",str(float(node["position"]["position"]["x"])/1000)) new_dev.set("y",str(float(node["position"]["position"]["y"])/1000)) new_dev.set("z",str(float(node["position"]["position"]["z"])/1000)) - + if "rotation" in node["config"]: + new_dev.set("rx",str(float(node["config"]["rotation"]["x"]))) + new_dev.set("ry",str(float(node["config"]["rotation"]["y"]))) + new_dev.set("r",str(float(node["config"]["rotation"]["z"]))) if "pose" in node: new_dev.set("x",str(float(node["pose"]["position"]["x"])/1000)) new_dev.set("y",str(float(node["pose"]["position"]["y"])/1000)) @@ -140,10 +143,6 @@ def __init__(self, device: dict, resource: dict, enable_rviz: bool = True): new_dev.set("rx",str(float(node["pose"]["rotation"]["x"]))) new_dev.set("ry",str(float(node["pose"]["rotation"]["y"]))) new_dev.set("r",str(float(node["pose"]["rotation"]["z"]))) - if "rotation" in node["config"]: - new_dev.set("rx",str(float(node["config"]["rotation"]["x"]))) - new_dev.set("ry",str(float(node["config"]["rotation"]["y"]))) - new_dev.set("r",str(float(node["config"]["rotation"]["z"]))) if "device_config" in node["config"]: for key, value in node["config"]["device_config"].items(): new_dev.set(key, str(value)) diff --git a/unilabos/device_mesh/ros2_controllers.yaml b/unilabos/device_mesh/ros2_controllers.yaml index 7dbad85c2..230fcd421 100644 --- a/unilabos/device_mesh/ros2_controllers.yaml +++ b/unilabos/device_mesh/ros2_controllers.yaml @@ -1,31 +1,5 @@ -arm_slider_arm_controller: - ros__parameters: - command_interfaces: - - position - joints: - - arm_slider_arm_base_joint - - arm_slider_arm_link_1_joint - - arm_slider_arm_link_2_joint - - arm_slider_arm_link_3_joint - - arm_slider_gripper_base_joint - state_interfaces: - - position - - velocity -arm_slider_gripper_controller: - ros__parameters: - command_interfaces: - - position - joints: - - arm_slider_gripper_right_joint - state_interfaces: - - position - - velocity controller_manager: ros__parameters: - arm_slider_arm_controller: - type: joint_trajectory_controller/JointTrajectoryController - arm_slider_gripper_controller: - type: joint_trajectory_controller/JointTrajectoryController joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster update_rate: 100 diff --git a/unilabos/devices/ros_dev/moveit_interface.py b/unilabos/devices/ros_dev/moveit_interface.py old mode 100755 new mode 100644 index 66cba2fed..81c2d112b --- a/unilabos/devices/ros_dev/moveit_interface.py +++ b/unilabos/devices/ros_dev/moveit_interface.py @@ -2,7 +2,6 @@ import time from copy import deepcopy from pathlib import Path -from typing import Optional, Sequence from moveit_msgs.msg import JointConstraint, Constraints from rclpy.action import ActionClient @@ -172,160 +171,173 @@ def resource_manager(self, resource, parent_link): return True - def pick_and_place( - self, - option: str, - move_group: str, - status: str, - resource: Optional[str] = None, - x_distance: Optional[float] = None, - y_distance: Optional[float] = None, - lift_height: Optional[float] = None, - retry: Optional[int] = None, - speed: Optional[float] = None, - target: Optional[str] = None, - constraints: Optional[Sequence[float]] = None, - ) -> None: + def pick_and_place(self, command: str): """ - 使用 MoveIt 完成抓取/放置等序列(pick/place/side_pick/side_place)。 + Using MoveIt to make the robotic arm pick or place materials to a target point. - 必选:option, move_group, status。 - 可选:resource, x_distance, y_distance, lift_height, retry, speed, target, constraints。 - 无返回值;失败时提前 return 或打印异常。 + Args: + command: A JSON-formatted string that includes option, target, speed, lift_height, mt_height + + *option (string) : Action type: pick/place/side_pick/side_place + *move_group (string): The move group moveit will plan + *status(string) : Target pose + resource(string) : The target resource + x_distance (float) : The distance to the target in x direction(meters) + y_distance (float) : The distance to the target in y direction(meters) + lift_height (float) : The height at which the material should be lifted(meters) + retry (float) : Retry times when moveit plan fails + speed (float) : The speed of the movement, speed > 0 + Returns: + None """ + result = SendCmd.Result() + try: - if option not in self.move_option: - raise ValueError(f"Invalid option: {option}") + cmd_str = str(command).replace("'", '"') + cmd_dict = json.loads(cmd_str) - option_index = self.move_option.index(option) - place_flag = option_index % 2 + if cmd_dict["option"] in self.move_option: + option_index = self.move_option.index(cmd_dict["option"]) + place_flag = option_index % 2 - config: dict = {"move_group": move_group} - if speed is not None: - config["speed"] = speed - if retry is not None: - config["retry"] = retry + config = {} + function_list = [] - function_list = [] - joint_positions_ = self.joint_poses[move_group][status] + status = cmd_dict["status"] + joint_positions_ = self.joint_poses[cmd_dict["move_group"]][status] - # 夹取 / 放置:绑定 resource 与 parent - if not place_flag: - if target is not None: - function_list.append(lambda r=resource, t=target: self.resource_manager(r, t)) - else: - ee = self.moveit2[move_group].end_effector_name - function_list.append(lambda r=resource: self.resource_manager(r, ee)) - else: - function_list.append(lambda r=resource: self.resource_manager(r, "world")) - - joint_constraint_msgs: list = [] - if constraints is not None: - for i, c in enumerate(constraints): - v = float(c) - if v > 0: - joint_constraint_msgs.append( - JointConstraint( - joint_name=self.moveit2[move_group].joint_names[i], - position=joint_positions_[i], - tolerance_above=v, - tolerance_below=v, - weight=1.0, + config.update({k: cmd_dict[k] for k in ["speed", "retry", "move_group"] if k in cmd_dict}) + + # 夹取 + if not place_flag: + if "target" in cmd_dict.keys(): + function_list.append(lambda: self.resource_manager(cmd_dict["resource"], cmd_dict["target"])) + else: + function_list.append( + lambda: self.resource_manager( + cmd_dict["resource"], self.moveit2[cmd_dict["move_group"]].end_effector_name ) ) + else: + function_list.append(lambda: self.resource_manager(cmd_dict["resource"], "world")) + + constraints = [] + if "constraints" in cmd_dict.keys(): + + for i in range(len(cmd_dict["constraints"])): + v = float(cmd_dict["constraints"][i]) + if v > 0: + constraints.append( + JointConstraint( + joint_name=self.moveit2[cmd_dict["move_group"]].joint_names[i], + position=joint_positions_[i], + tolerance_above=v, + tolerance_below=v, + weight=1.0, + ) + ) - if lift_height is not None: - retval = None - attempts = config.get("retry", 10) - while retval is None and attempts > 0: - retval = self.moveit2[move_group].compute_fk(joint_positions_) - time.sleep(0.1) - attempts -= 1 - if retval is None: - raise ValueError("Failed to compute forward kinematics") - pose = [retval.pose.position.x, retval.pose.position.y, retval.pose.position.z] - quaternion = [ - retval.pose.orientation.x, - retval.pose.orientation.y, - retval.pose.orientation.z, - retval.pose.orientation.w, - ] - - function_list = [ - lambda: self.moveit_task( - position=[retval.pose.position.x, retval.pose.position.y, retval.pose.position.z], - quaternion=quaternion, - **config, - cartesian=self.cartesian_flag, - ) - ] + function_list - - pose[2] += float(lift_height) - function_list.append( - lambda p=pose.copy(), q=quaternion, cfg=config: self.moveit_task( - position=p, quaternion=q, **cfg, cartesian=self.cartesian_flag - ) - ) - end_pose = list(pose) - - if x_distance is not None or y_distance is not None: - if x_distance is not None: - deep_pose = deepcopy(pose) - deep_pose[0] += float(x_distance) - elif y_distance is not None: - deep_pose = deepcopy(pose) - deep_pose[1] += float(y_distance) + if "lift_height" in cmd_dict.keys(): + retval = None + retry = config.get("retry", 10) + while retval is None and retry > 0: + retval = self.moveit2[cmd_dict["move_group"]].compute_fk(joint_positions_) + time.sleep(0.1) + retry -= 1 + if retval is None: + result.success = False + return result + pose = [retval.pose.position.x, retval.pose.position.y, retval.pose.position.z] + quaternion = [ + retval.pose.orientation.x, + retval.pose.orientation.y, + retval.pose.orientation.z, + retval.pose.orientation.w, + ] function_list = [ - lambda p=pose.copy(), q=quaternion, cfg=config: self.moveit_task( - position=p, quaternion=q, **cfg, cartesian=self.cartesian_flag + lambda: self.moveit_task( + position=[retval.pose.position.x, retval.pose.position.y, retval.pose.position.z], + quaternion=quaternion, + **config, + cartesian=self.cartesian_flag, ) ] + function_list + + pose[2] += float(cmd_dict["lift_height"]) function_list.append( - lambda dp=deep_pose.copy(), q=quaternion, cfg=config: self.moveit_task( - position=dp, quaternion=q, **cfg, cartesian=self.cartesian_flag + lambda: self.moveit_task( + position=pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag ) ) - end_pose = list(deep_pose) - - retval_ik = None - attempts_ik = config.get("retry", 10) - while retval_ik is None and attempts_ik > 0: - retval_ik = self.moveit2[move_group].compute_ik( - position=end_pose, - quat_xyzw=quaternion, - constraints=Constraints(joint_constraints=joint_constraint_msgs), - ) - time.sleep(0.1) - attempts_ik -= 1 - if retval_ik is None: - raise ValueError("Failed to compute inverse kinematics") - position_ = [ - retval_ik.position[retval_ik.name.index(i)] for i in self.moveit2[move_group].joint_names - ] - jn = self.moveit2[move_group].joint_names - function_list = [ - lambda pos=position_, names=jn, cfg=config: self.moveit_joint_task( - joint_positions=pos, joint_names=names, **cfg - ) - ] + function_list - else: - function_list = [lambda cfg=config, jp=joint_positions_: self.moveit_joint_task(**cfg, joint_positions=jp)] + function_list + end_pose = pose + + if "x_distance" in cmd_dict.keys() or "y_distance" in cmd_dict.keys(): + if "x_distance" in cmd_dict.keys(): + deep_pose = deepcopy(pose) + deep_pose[0] += float(cmd_dict["x_distance"]) + elif "y_distance" in cmd_dict.keys(): + deep_pose = deepcopy(pose) + deep_pose[1] += float(cmd_dict["y_distance"]) + + function_list = [ + lambda: self.moveit_task( + position=pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag + ) + ] + function_list + function_list.append( + lambda: self.moveit_task( + position=deep_pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag + ) + ) + end_pose = deep_pose - for i in range(len(function_list)): - if i == 0: - self.cartesian_flag = False + retval_ik = None + retry = config.get("retry", 10) + while retval_ik is None and retry > 0: + retval_ik = self.moveit2[cmd_dict["move_group"]].compute_ik( + position=end_pose, quat_xyzw=quaternion, constraints=Constraints(joint_constraints=constraints) + ) + time.sleep(0.1) + retry -= 1 + if retval_ik is None: + result.success = False + return result + position_ = [ + retval_ik.position[retval_ik.name.index(i)] + for i in self.moveit2[cmd_dict["move_group"]].joint_names + ] + function_list = [ + lambda: self.moveit_joint_task( + joint_positions=position_, + joint_names=self.moveit2[cmd_dict["move_group"]].joint_names, + **config, + ) + ] + function_list else: - self.cartesian_flag = True + function_list = [ + lambda: self.moveit_joint_task(**config, joint_positions=joint_positions_) + ] + function_list + + for i in range(len(function_list)): + if i == 0: + self.cartesian_flag = False + else: + self.cartesian_flag = True - re = function_list[i]() - if not re: - print(i, re) - raise ValueError(f"Failed to execute moveit task: {i}") + re = function_list[i]() + if not re: + print(i, re) + result.success = False + return result + result.success = True except Exception as e: + print(e) self.cartesian_flag = False - raise e + result.success = False + + return result def set_status(self, command: str): """ diff --git a/unilabos/devices/virtual/workbench.py b/unilabos/devices/virtual/workbench.py index d67db3985..f5fae47ee 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,11 +21,9 @@ 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.resources.resource_tracker import SampleUUIDsType, LabSample +from unilabos.utils.decorator import not_action, always_free +from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES # ============ TypedDict 返回类型定义 ============ @@ -59,8 +57,6 @@ class MoveToOutputResult(TypedDict): success: bool station_id: int material_id: str - output_position: str - message: str unilabos_samples: List[LabSample] @@ -85,9 +81,9 @@ class HeatingStationState(Enum): """加热台状态枚举""" IDLE = "idle" # 空闲 - OCCUPIED = "occupied" # 已放置物料, 等待加热 + OCCUPIED = "occupied" # 已放置物料,等待加热 HEATING = "heating" # 加热中 - COMPLETED = "completed" # 加热完成, 等待取走 + COMPLETED = "completed" # 加热完成,等待取走 class ArmState(Enum): @@ -109,24 +105,19 @@ 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 @@ -154,19 +145,19 @@ def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, A 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]] = {} + self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info self._tasks_lock = Lock() # 处理其他kwargs参数 @@ -192,6 +183,7 @@ def initialize(self) -> bool: """初始化虚拟工作台""" self.logger.info(f"初始化虚拟工作台 {self.device_id}") + # 重置加热台状态 (已在__init__中创建,这里重置为初始状态) with self._stations_lock: for station in self._heating_stations.values(): station.state = HeatingStationState.IDLE @@ -199,6 +191,7 @@ def initialize(self) -> bool: station.material_number = None station.heating_progress = 0.0 + # 初始化状态 self.data.update( { "status": "Ready", @@ -264,7 +257,11 @@ def _update_data_status(self, message: Optional[str] = None): 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: @@ -272,12 +269,23 @@ def _find_available_heating_station(self) -> Optional[int]: 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 @@ -290,22 +298,6 @@ def _release_arm(self): 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, @@ -314,14 +306,19 @@ def prepare_materials( """ 批量准备物料 - 虚拟起始节点 - 作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。 - 输出5个handle (material_1 ~ material_5), 分别对应实验1~5。 + 作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。 + 输出5个handle (material_1 ~ material_5),分别对应实验1~5。 + + Args: + count: 待生成的物料数量,默认5 (生成 A1-A5) + + Returns: + PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station """ + # 生成物料列表 A1 - A{count} materials = [i for i in range(1, count + 1)] - self.logger.info( - f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}" - ) + self.logger.info(f"[准备物料] 生成 {count} 个物料: " f"A1-A{count} -> material_1~material_{count}") return { "success": True, @@ -332,28 +329,9 @@ def prepare_materials( "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() if content else {}), - ) - 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()) 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, @@ -362,12 +340,20 @@ def move_to_heating_station( """ 将物料从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", @@ -375,27 +361,33 @@ def move_to_heating_station( } 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 @@ -403,11 +395,11 @@ def move_to_heating_station( 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: @@ -420,16 +412,8 @@ def move_to_heating_station( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } except Exception as e: @@ -443,33 +427,11 @@ def move_to_heating_station( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } - @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), - ], - ) + @always_free def start_heating( self, sample_uuids: SampleUUIDsType, @@ -478,6 +440,13 @@ def start_heating( ) -> 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}] 开始加热") @@ -489,16 +458,8 @@ def start_heating( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } with self._stations_lock: @@ -512,16 +473,8 @@ def start_heating( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } if station.state == HeatingStationState.HEATING: @@ -532,20 +485,13 @@ def start_heating( "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() if content else {}) - ), - ) - 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()) 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 @@ -556,6 +502,7 @@ def start_heating( self._update_data_status(f"加热台{station_id}开始加热{material_id}") + # 打印当前所有正在加热的台位 with self._stations_lock: heating_list = [ f"加热台{sid}:{s.current_material}" @@ -564,6 +511,7 @@ def start_heating( ] self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}") + # 模拟加热过程 start_time = time.time() last_countdown_log = start_time while True: @@ -576,6 +524,7 @@ def start_heating( 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() @@ -585,6 +534,7 @@ def start_heating( 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 @@ -603,28 +553,10 @@ def start_heating( "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() if content else {}) - ), - ) - 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()) 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, @@ -633,8 +565,15 @@ def move_to_output( ) -> 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 { @@ -644,16 +583,8 @@ def move_to_output( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } with self._stations_lock: @@ -668,16 +599,8 @@ def move_to_output( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } if station.state != HeatingStationState.COMPLETED: @@ -688,16 +611,8 @@ def move_to_output( "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() if content else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } output_position = f"C{output_number}" @@ -709,17 +624,18 @@ def move_to_output( 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" - self.logger.info( - f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..." - ) + # 模拟机械臂操作 (3秒) + 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 @@ -727,17 +643,17 @@ def move_to_output( 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, @@ -746,17 +662,8 @@ def move_to_output( "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() if content is not None else {}) - ), - ) - 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()) for + sample_uuid, content in sample_uuids.items()] } except Exception as e: @@ -770,105 +677,83 @@ def move_to_output( "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() if content else {}) - ), - ) - 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()) 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 deleted file mode 100644 index 86c3602ec..000000000 --- a/unilabos/registry/ast_registry_scanner.py +++ /dev/null @@ -1,1037 +0,0 @@ -""" -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 deleted file mode 100644 index e8c65ac88..000000000 --- a/unilabos/registry/decorators.py +++ /dev/null @@ -1,614 +0,0 @@ -""" -装饰器注册表系统 - -通过 @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 5c5f1f8a9..fa182c777 100644 --- a/unilabos/registry/devices/Qone_nmr.yaml +++ b/unilabos/registry/devices/Qone_nmr.yaml @@ -13,18 +13,21 @@ Qone_nmr: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -68,6 +71,31 @@ 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: {} @@ -110,18 +138,21 @@ Qone_nmr: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -136,31 +167,32 @@ Qone_nmr: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 f57cd35c5..fc4b75cb2 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -22,8 +22,7 @@ bioyond_cell: required: - xlsx_path type: object - result: - type: object + result: {} required: - goal title: auto_batch_outbound_from_xlsx参数 @@ -491,9 +490,7 @@ bioyond_cell: goal: properties: material_names: - items: - type: string - type: array + type: string type_id: default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 type: string @@ -502,8 +499,7 @@ bioyond_cell: type: string required: [] type: object - result: - type: object + result: {} required: - goal title: create_and_inbound_materials参数 @@ -539,8 +535,7 @@ bioyond_cell: - type_id - warehouse_name type: object - result: - type: object + result: {} required: - goal title: create_material参数 @@ -561,16 +556,11 @@ bioyond_cell: goal: properties: mappings: - additionalProperties: - type: object type: object required: - mappings type: object - result: - items: - type: object - type: array + result: {} required: - goal title: create_materials参数 @@ -602,8 +592,7 @@ bioyond_cell: required: - xlsx_path type: object - result: - type: object + result: {} required: - goal title: create_orders参数 @@ -635,8 +624,7 @@ bioyond_cell: required: - xlsx_path type: object - result: - type: object + result: {} required: - goal title: create_orders_v2参数 @@ -677,8 +665,7 @@ bioyond_cell: - bottle_type - location_code type: object - result: - type: object + result: {} required: - goal title: create_sample参数 @@ -731,8 +718,7 @@ bioyond_cell: type: string required: [] type: object - result: - type: object + result: {} required: - goal title: order_list_v2参数 @@ -835,8 +821,7 @@ bioyond_cell: required: - material_obj type: object - result: - type: object + result: {} required: - goal title: report_material_change参数 @@ -890,8 +875,7 @@ bioyond_cell: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_continue参数 @@ -912,8 +896,7 @@ bioyond_cell: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_reset参数 @@ -934,8 +917,7 @@ bioyond_cell: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_start参数 @@ -1380,8 +1362,7 @@ bioyond_cell: type: string required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_start_and_auto_feeding参数 @@ -1826,8 +1807,7 @@ bioyond_cell: type: string required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_start_and_auto_feeding_v2参数 @@ -1848,8 +1828,7 @@ bioyond_cell: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: scheduler_stop参数 @@ -1871,15 +1850,12 @@ bioyond_cell: properties: items: items: - additionalProperties: - type: string type: object type: array required: - items type: object - result: - type: object + result: {} required: - goal title: storage_batch_inbound参数 @@ -1908,8 +1884,7 @@ bioyond_cell: - material_id - location_id type: object - result: - type: object + result: {} required: - goal title: storage_inbound参数 @@ -1930,8 +1905,7 @@ bioyond_cell: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: transfer_1_to_2参数 @@ -1972,8 +1946,7 @@ bioyond_cell: type: integer required: [] type: object - result: - type: object + result: {} required: - goal title: transfer_3_to_2参数 @@ -2010,8 +1983,7 @@ bioyond_cell: type: integer required: [] type: object - result: - type: object + result: {} required: - goal title: transfer_3_to_2_to_1参数 @@ -2035,11 +2007,10 @@ bioyond_cell: ip: type: string port: - type: integer + type: string required: [] type: object - result: - type: object + result: {} required: - goal title: update_push_ip参数 @@ -2068,8 +2039,7 @@ bioyond_cell: required: - order_code type: object - result: - type: object + result: {} required: - goal title: wait_for_order_finish参数 @@ -2102,8 +2072,7 @@ bioyond_cell: required: - order_code type: object - result: - type: object + result: {} required: - goal title: wait_for_order_finish_polling参数 @@ -2135,8 +2104,7 @@ bioyond_cell: type: integer required: [] type: object - result: - type: boolean + result: {} required: - goal title: wait_for_transfer_task参数 @@ -2144,7 +2112,8 @@ bioyond_cell: type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation status_types: - device_id: '' + device_id: String + material_info: dict type: python config_info: [] description: '' @@ -2165,7 +2134,11 @@ 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 547b54ffb..7b9ebc901 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -24,8 +24,7 @@ bioyond_dispensing_station: required: - data type: object - result: - type: object + result: {} required: - goal title: brief_step_parameters参数 @@ -54,8 +53,7 @@ bioyond_dispensing_station: - report_request - used_materials type: object - result: - type: object + result: {} required: - goal title: process_order_finish_report参数 @@ -80,8 +78,7 @@ bioyond_dispensing_station: required: - order_id type: object - result: - type: object + result: {} required: - goal title: project_order_report参数 @@ -131,8 +128,7 @@ bioyond_dispensing_station: required: - workflow_id type: object - result: - type: object + result: {} required: - goal title: workflow_sample_locations参数 @@ -148,12 +144,12 @@ bioyond_dispensing_station: temperature: temperature titration: titration goal_default: - delay_time: null - hold_m_name: null + delay_time: '600' + hold_m_name: '' liquid_material_name: NMP - speed: null - temperature: null - titration: null + speed: '400' + temperature: '40' + titration: '' handles: input: - data_key: titration @@ -169,16 +165,20 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - placeholder_keys: {} - result: {} + result: + return_info: return_info 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,9 +189,11 @@ bioyond_dispensing_station: description: 10%物料的液体物料名称,默认为"NMP" type: string speed: + default: '400' description: 搅拌速度,默认400 type: string temperature: + default: '40' description: 温度(℃),默认40 type: string titration: @@ -200,14 +202,21 @@ 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: string + type: object required: - goal - title: batch_create_90_10_vial_feeding_tasks参数 + title: BatchCreate9010VialFeedingTasks type: object type: UniLabJsonCommand batch_create_diamine_solution_tasks: @@ -219,11 +228,11 @@ bioyond_dispensing_station: speed: speed temperature: temperature goal_default: - delay_time: null + delay_time: '600' liquid_material_name: NMP - solutions: null - speed: null - temperature: null + solutions: '' + speed: '400' + temperature: '20' handles: input: - data_key: solutions @@ -239,16 +248,20 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - placeholder_keys: {} - result: {} + result: + return_info: return_info 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: @@ -262,9 +275,11 @@ 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: @@ -272,11 +287,17 @@ 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: string + type: object required: - goal - title: batch_create_diamine_solution_tasks参数 + title: BatchCreateDiamineSolutionTasks type: object type: UniLabJsonCommand compute_experiment_design: @@ -288,7 +309,7 @@ bioyond_dispensing_station: wt_percent: wt_percent goal_default: m_tot: '70' - ratio: null + ratio: '' titration_percent: '0.03' wt_percent: '0.25' handles: @@ -317,8 +338,12 @@ bioyond_dispensing_station: handler_key: feeding_order io_type: sink label: Feeding Order Data From Calculation Node - placeholder_keys: {} - result: {} + result: + feeding_order: feeding_order + return_info: return_info + solutions: solutions + solvents: solvents + titration: titration schema: description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。 properties: @@ -331,7 +356,7 @@ bioyond_dispensing_station: type: string ratio: description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} - type: object + type: string titration_percent: default: '0.03' description: 滴定比例(10%部分) @@ -346,23 +371,14 @@ 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 @@ -370,11 +386,11 @@ bioyond_dispensing_station: - solvents - feeding_order - return_info - title: ComputeExperimentDesignReturn + title: ComputeExperimentDesign_Result type: object required: - goal - title: compute_experiment_design参数 + title: ComputeExperimentDesign type: object type: UniLabJsonCommand create_90_10_vial_feeding_task: @@ -428,18 +444,17 @@ bioyond_dispensing_station: speed: '' temperature: '' handles: {} - placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: DispenStationVialFeed_Feedback type: object goal: - additionalProperties: false properties: delay_time: type: string @@ -487,13 +502,38 @@ 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: @@ -524,18 +564,17 @@ bioyond_dispensing_station: temperature: '' volume: '' handles: {} - placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: DispenStationSolnPrep_Feedback type: object goal: - additionalProperties: false properties: delay_time: type: string @@ -555,13 +594,24 @@ 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: @@ -574,8 +624,8 @@ bioyond_dispensing_station: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info schema: description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务 properties: @@ -585,6 +635,12 @@ bioyond_dispensing_station: required: [] type: object result: + properties: + return_info: + description: 调度器启动结果,成功返回1,失败返回0 + type: integer + required: + - return_info title: scheduler_start结果 type: object required: @@ -598,8 +654,8 @@ bioyond_dispensing_station: target_device_id: target_device_id transfer_groups: transfer_groups goal_default: - target_device_id: null - transfer_groups: null + target_device_id: '' + transfer_groups: '' handles: {} placeholder_keys: target_device_id: unilabos_devices @@ -615,13 +671,32 @@ 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: - type: object + result: {} required: - goal title: transfer_materials_to_reaction_station参数 @@ -634,9 +709,9 @@ bioyond_dispensing_station: check_interval: check_interval timeout: timeout goal_default: - batch_create_result: null - check_interval: 10 - timeout: 7200 + batch_create_result: '' + check_interval: '10' + timeout: '7200' handles: input: - data_key: batch_create_result @@ -652,35 +727,47 @@ bioyond_dispensing_station: handler_key: batch_reports_result io_type: sink label: Batch Order Completion Reports - placeholder_keys: {} - result: {} + result: + return_info: return_info 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: integer + type: string timeout: - default: 7200 + default: '7200' description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout - type: integer - required: [] + type: string + required: + - batch_create_result 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: wait_for_multiple_orders_and_get_reports参数 + title: WaitForMultipleOrdersAndGetReports 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 new file mode 100644 index 000000000..c8b9d9443 --- /dev/null +++ b/unilabos/registry/devices/camera.yaml @@ -0,0 +1,81 @@ +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 446357d07..ad2df9559 100644 --- a/unilabos/registry/devices/cameraSII.yaml +++ b/unilabos/registry/devices/cameraSII.yaml @@ -18,7 +18,7 @@ cameracontroller_device: goal: properties: config: - type: object + type: string required: [] type: object result: {} @@ -42,8 +42,7 @@ cameracontroller_device: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: stop参数 @@ -51,7 +50,7 @@ cameracontroller_device: type: UniLabJsonCommand module: unilabos.devices.cameraSII.cameraUSB:CameraController status_types: - status: Dict[str, Any] + status: dict type: python config_info: [] description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ) @@ -104,4 +103,5 @@ 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 1b33b9e27..f3059b583 100644 --- a/unilabos/registry/devices/characterization_chromatic.yaml +++ b/unilabos/registry/devices/characterization_chromatic.yaml @@ -141,26 +141,30 @@ 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: @@ -171,6 +175,7 @@ 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 @@ -194,6 +199,10 @@ hplc.agilent: properties: could_run: type: boolean + data_file: + items: + type: string + type: array device_status: type: string driver_init_ok: @@ -207,13 +216,14 @@ hplc.agilent: success: type: boolean required: - - could_run + - status_text - device_status + - could_run - driver_init_ok - - finish_status - is_running - - status_text - success + - finish_status + - data_file type: object version: 1.0.0 hplc.agilent-zhida: @@ -226,25 +236,26 @@ hplc.agilent-zhida: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -304,18 +315,21 @@ hplc.agilent-zhida: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -327,35 +341,35 @@ hplc.agilent-zhida: feedback: {} goal: string: string - text: text goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -393,7 +407,7 @@ hplc.agilent-zhida: status: type: object required: - - methods - status + - methods type: object version: 1.0.0 diff --git a/unilabos/registry/devices/characterization_optic.yaml b/unilabos/registry/devices/characterization_optic.yaml index a7c0e98d1..80dcf93dc 100644 --- a/unilabos/registry/devices/characterization_optic.yaml +++ b/unilabos/registry/devices/characterization_optic.yaml @@ -120,41 +120,42 @@ raman.home_made: type: object type: UniLabJsonCommand raman_cmd: - feedback: - status: status + feedback: {} 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 ac4d00bb0..2078d0f0b 100644 --- a/unilabos/registry/devices/chinwe.yaml +++ b/unilabos/registry/devices/chinwe.yaml @@ -19,8 +19,7 @@ separator.chinwe: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: connect参数 @@ -66,145 +65,135 @@ separator.chinwe: required: - command_dict type: object - result: - type: boolean + result: {} 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:旋钮) - type: integer + enum: + - '4' + - '5' + type: string 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:旋钮) - type: integer + enum: + - '4' + - '5' + type: string 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: integer + type: string 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: 选择泵 - type: integer + enum: + - '1' + - '2' + - '3' + type: string valve_port: + default: '1' description: 阀门端口 - type: integer + enum: + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + type: string volume: + default: 1000 description: 吸液步数 type: integer required: @@ -212,38 +201,41 @@ 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: 选择泵 - type: integer + enum: + - '1' + - '2' + - '3' + type: string valve_port: + default: '1' description: 阀门端口 - type: integer + enum: + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + type: string volume: + default: 1000 description: 排液步数 type: integer required: @@ -251,152 +243,121 @@ 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: string + type: integer output_port: default: 0 description: 输出口索引 - type: string + type: integer pump_id: + default: '1' description: 选择泵 + enum: + - '1' + - '2' + - '3' title: '注: 1号泵, 2号泵, 3号泵' - type: integer + type: string speed: default: 10 description: 运动速度 - type: string + type: integer 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) - type: integer + enum: + - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + type: string pump_id: + default: '1' description: 选择泵 - type: integer + enum: + - '1' + - '2' + - '3' + type: string 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: [] + required: + - target_state 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: @@ -445,8 +406,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 df5a35085..2e9f60739 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -64,8 +64,7 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: fun_wuliao_test参数 @@ -110,8 +109,7 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: - type: object + result: {} required: - goal title: func_allpack_cmd参数 @@ -222,8 +220,7 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: - type: object + result: {} required: - goal title: func_allpack_cmd_simp参数 @@ -312,8 +309,7 @@ coincellassemblyworkstation_device: type: boolean required: [] type: object - result: - type: boolean + result: {} required: - goal title: func_pack_device_init_auto_start_combined参数 @@ -355,8 +351,7 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: func_pack_device_stop参数 @@ -381,8 +376,7 @@ coincellassemblyworkstation_device: type: string required: [] type: object - result: - type: boolean + result: {} required: - goal title: func_pack_get_msg_cmd参数 @@ -436,8 +430,7 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: func_pack_send_finished_cmd参数 @@ -474,8 +467,7 @@ coincellassemblyworkstation_device: - assembly_type - assembly_pressure type: object - result: - type: boolean + result: {} required: - goal title: func_pack_send_msg_cmd参数 @@ -619,8 +611,7 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: - type: object + result: {} required: - goal title: func_sendbottle_allpack_multi参数 @@ -672,6 +663,31 @@ 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: {} @@ -719,8 +735,7 @@ coincellassemblyworkstation_device: required: - fujipian_panshu type: object - result: - type: boolean + result: {} required: - goal title: qiming_coin_cell_code参数 @@ -811,24 +826,25 @@ 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_coin_cell_code + - data_pole_weight + - data_assembly_pressure + - data_electrolyte_volume - data_coin_num + - data_coin_cell_code - data_electrolyte_code - - data_electrolyte_volume - - data_glove_box_o2_content - data_glove_box_pressure + - data_glove_box_o2_content - 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 ad2125758..65218619e 100644 --- a/unilabos/registry/devices/gas_handler.yaml +++ b/unilabos/registry/devices/gas_handler.yaml @@ -50,25 +50,26 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -81,25 +82,26 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -114,31 +116,32 @@ gas_source.mock: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -229,25 +232,26 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -260,25 +264,26 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -293,31 +298,32 @@ vacuum_pump.mock: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 fdcc89dd0..3fd0ea5b1 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: '' + rotation: String 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 6d87f4291..dcaa98187 100644 --- a/unilabos/registry/devices/laiyu_liquid_test.yaml +++ b/unilabos/registry/devices/laiyu_liquid_test.yaml @@ -22,8 +22,7 @@ xyz_stepper_controller: required: - degrees type: object - result: - type: integer + result: {} required: - goal title: degrees_to_steps参数 @@ -48,8 +47,7 @@ xyz_stepper_controller: required: - axis type: object - result: - type: boolean + result: {} required: - goal title: emergency_stop参数 @@ -74,10 +72,7 @@ xyz_stepper_controller: type: boolean required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: enable_all_axes参数 @@ -106,8 +101,7 @@ xyz_stepper_controller: required: - axis type: object - result: - type: boolean + result: {} required: - goal title: enable_motor参数 @@ -128,10 +122,7 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: home_all_axes参数 @@ -156,8 +147,7 @@ xyz_stepper_controller: required: - axis type: object - result: - type: boolean + result: {} required: - goal title: home_axis参数 @@ -198,8 +188,7 @@ xyz_stepper_controller: - axis - position type: object - result: - type: boolean + result: {} required: - goal title: move_to_position参数 @@ -240,8 +229,7 @@ xyz_stepper_controller: - axis - degrees type: object - result: - type: boolean + result: {} required: - goal title: move_to_position_degrees参数 @@ -282,8 +270,7 @@ xyz_stepper_controller: - axis - revolutions type: object - result: - type: boolean + result: {} required: - goal title: move_to_position_revolutions参数 @@ -314,17 +301,14 @@ xyz_stepper_controller: default: 5000 type: integer x: - type: integer + type: string y: - type: integer + type: string z: - type: integer + type: string required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: move_xyz参数 @@ -355,17 +339,14 @@ xyz_stepper_controller: default: 5000 type: integer x_deg: - type: number + type: string y_deg: - type: number + type: string z_deg: - type: number + type: string required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: move_xyz_degrees参数 @@ -396,17 +377,14 @@ xyz_stepper_controller: default: 5000 type: integer x_rev: - type: number + type: string y_rev: - type: number + type: string z_rev: - type: number + type: string required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: move_xyz_revolutions参数 @@ -431,8 +409,7 @@ xyz_stepper_controller: required: - revolutions type: object - result: - type: integer + result: {} required: - goal title: revolutions_to_steps参数 @@ -465,8 +442,7 @@ xyz_stepper_controller: - axis - speed type: object - result: - type: boolean + result: {} required: - goal title: set_speed_mode参数 @@ -491,8 +467,7 @@ xyz_stepper_controller: required: - steps type: object - result: - type: number + result: {} required: - goal title: steps_to_degrees参数 @@ -517,8 +492,7 @@ xyz_stepper_controller: required: - steps type: object - result: - type: number + result: {} required: - goal title: steps_to_revolutions参数 @@ -539,10 +513,7 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: - additionalProperties: - type: boolean - type: object + result: {} required: - goal title: stop_all_axes参数 @@ -571,8 +542,7 @@ xyz_stepper_controller: required: - axis type: object - result: - type: boolean + result: {} required: - goal title: wait_for_completion参数 @@ -580,7 +550,8 @@ xyz_stepper_controller: type: UniLabJsonCommand module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController status_types: - all_positions: Dict[MotorAxis, MotorPosition] + all_positions: dict + motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition type: python config_info: [] description: 新XYZ控制器 @@ -603,10 +574,12 @@ xyz_stepper_controller: data: properties: all_positions: - additionalProperties: - type: object + type: object + motor_status: 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 4d2f72884..b04d63173 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -8,7 +8,6 @@ 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 @@ -24,38 +23,84 @@ liquid_handler: targets: targets use_channels: use_channels goal_default: - asp_vols: [] - blow_out_air_volume: [] - dis_vols: [] - flow_rates: [] + asp_vols: + - 0.0 + blow_out_air_volume: + - 0.0 + dis_vols: + - 0.0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - reagent_sources: [] + 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: '' spread: '' - targets: [] - use_channels: [] + 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 handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAdd_Feedback type: object goal: - additionalProperties: false properties: asp_vols: items: @@ -80,8 +125,6 @@ liquid_handler: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -112,6 +155,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array reagent_sources: @@ -186,6 +230,7 @@ liquid_handler: - pose - config - data + title: reagent_sources type: object type: array spread: @@ -262,21 +307,43 @@ 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: @@ -295,14 +362,41 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: [] - flow_rates: [] - liquid_height: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} result: name: name @@ -310,11 +404,11 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAspirate_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -341,6 +435,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array resources: @@ -415,27 +510,41 @@ 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: @@ -465,9 +574,7 @@ liquid_handler: properties: none_keys: default: [] - items: - type: string - type: array + type: string protocol_author: type: string protocol_date: @@ -484,701 +591,159 @@ liquid_handler: - protocol_name - protocol_description - protocol_version - - protocol_author - - protocol_date - - protocol_type - type: object - result: {} - required: - - goal - title: create_protocol参数 - type: object - type: UniLabJsonCommandAsync - auto-custom_delay: - feedback: {} - goal: {} - goal_default: - msg: null - seconds: 0 - handles: {} - placeholder_keys: {} - result: {} - schema: - description: 自定义延时函数。在实验流程中插入可配置的等待时间,用于满足特定的反应时间、孵育时间或设备稳定时间要求。支持自定义延时消息和秒数设置,提供流程控制和时间管理功能。适用于酶反应等待、温度平衡、样品孵育等需要时间控制的实验步骤。 - properties: - feedback: {} - goal: - properties: - msg: - type: string - seconds: - default: 0 - type: string - required: [] - type: object - result: {} - required: - - goal - title: custom_delay参数 - type: object - type: UniLabJsonCommandAsync - auto-iter_tips: - feedback: {} - goal: {} - goal_default: - tip_racks: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 - properties: - feedback: {} - goal: - properties: - tip_racks: - items: - type: object - type: array - required: - - tip_racks - type: object - result: - type: string - required: - - goal - title: iter_tips参数 - type: object - type: UniLabJsonCommand - auto-set_group: - feedback: {} - goal: {} - goal_default: - group_name: null - volumes: null - wells: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - group_name: - type: string - volumes: - items: - type: number - type: array - wells: - items: - type: object - type: array - required: - - group_name - - wells - - volumes - type: object - result: {} - required: - - goal - 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: {} - goal_default: - liquid_names: null - plate: null - volumes: null - well_names: null - handles: {} - placeholder_keys: - plate: unilabos_resources - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - liquid_names: - items: - type: string - type: array - plate: - 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: - items: - type: number - type: array - well_names: - items: - type: string - type: array - required: - - plate - - well_names - - 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 + - protocol_author + - protocol_date + - protocol_type + type: object + result: {} + required: + - goal + title: create_protocol参数 + type: object + type: UniLabJsonCommandAsync + auto-custom_delay: + feedback: {} + goal: {} + goal_default: + msg: null + seconds: 0 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 自定义延时函数。在实验流程中插入可配置的等待时间,用于满足特定的反应时间、孵育时间或设备稳定时间要求。支持自定义延时消息和秒数设置,提供流程控制和时间管理功能。适用于酶反应等待、温度平衡、样品孵育等需要时间控制的实验步骤。 + properties: + feedback: {} + goal: properties: - plate: - items: - items: - $ref: '#/$defs/ResourceDict' - type: array - title: Plate - type: array + msg: + type: string + seconds: + default: 0 + type: string + required: [] + type: object + result: {} + required: + - goal + title: custom_delay参数 + type: object + type: UniLabJsonCommandAsync + auto-iter_tips: + feedback: {} + goal: {} + goal_default: + tip_racks: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 + properties: + feedback: {} + goal: + properties: + tip_racks: + type: string + required: + - tip_racks + type: object + result: {} + 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: {} + goal_default: + group_name: null + volumes: null + wells: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + group_name: + type: string volumes: - items: - type: number - title: Volumes - type: array + type: string wells: - items: - items: - $ref: '#/$defs/ResourceDict' - type: array - title: Wells - type: array + type: string required: - - plate + - group_name - wells - volumes - title: SetLiquidFromPlateReturn type: object + result: {} + required: + - goal + title: set_group参数 + type: object + type: UniLabJsonCommand + auto-set_liquid_from_plate: + feedback: {} + goal: {} + goal_default: + liquid_names: null + plate: null + volumes: null + well_names: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + liquid_names: + type: string + plate: + type: string + volumes: + type: string + well_names: + type: string + required: + - plate + - well_names + - liquid_names + - volumes + type: object + result: {} required: - goal title: set_liquid_from_plate参数 @@ -1199,9 +764,7 @@ liquid_handler: goal: properties: tip_racks: - items: - type: object - type: array + type: string required: - tip_racks type: object @@ -1226,9 +789,7 @@ liquid_handler: goal: properties: targets: - items: - type: object - type: array + type: string required: - targets type: object @@ -1276,7 +837,8 @@ liquid_handler: goal: use_channels: use_channels goal_default: - use_channels: [] + use_channels: + - 0 handles: {} result: name: name @@ -1284,25 +846,31 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -1321,13 +889,39 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: [] - flow_rates: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} result: name: name @@ -1335,14 +929,16 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDispense_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -1362,6 +958,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array resources: @@ -1436,27 +1033,40 @@ 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: @@ -1473,9 +1083,32 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} placeholder_keys: tip_spots: unilabos_resources @@ -1485,11 +1118,11 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDropTips_Feedback type: object goal: - additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -1506,6 +1139,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array tip_spots: @@ -1580,21 +1214,31 @@ 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: @@ -1641,28 +1285,21 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -1671,7 +1308,6 @@ liquid_handler: title: offset type: object tip_rack: - additionalProperties: false properties: category: type: string @@ -1690,26 +1326,16 @@ 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 @@ -1719,19 +1345,12 @@ 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 @@ -1761,15 +1380,21 @@ 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: @@ -1792,31 +1417,47 @@ liquid_handler: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - targets: [] + 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: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -1843,6 +1484,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array targets: @@ -1917,17 +1559,28 @@ 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: @@ -1955,7 +1608,10 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: [] + intermediate_locations: + - x: 0.0 + y: 0.0 + z: 0.0 lid: category: '' children: [] @@ -2010,26 +1666,19 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -2054,10 +1703,10 @@ liquid_handler: - x - y - z + title: intermediate_locations type: object type: array lid: - additionalProperties: false properties: category: type: string @@ -2076,26 +1725,16 @@ 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 @@ -2105,19 +1744,12 @@ 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 @@ -2150,25 +1782,16 @@ 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 @@ -2177,7 +1800,6 @@ liquid_handler: title: resource_offset type: object to: - additionalProperties: false properties: category: type: string @@ -2196,26 +1818,16 @@ 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 @@ -2225,19 +1837,12 @@ 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 @@ -2267,15 +1872,28 @@ 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: @@ -2303,7 +1921,10 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: [] + intermediate_locations: + - x: 0.0 + y: 0.0 + z: 0.0 pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -2362,26 +1983,19 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -2406,28 +2020,20 @@ 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 @@ -2436,7 +2042,6 @@ liquid_handler: title: pickup_offset type: object plate: - additionalProperties: false properties: category: type: string @@ -2455,26 +2060,16 @@ 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 @@ -2484,19 +2079,12 @@ 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 @@ -2529,19 +2117,12 @@ 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 @@ -2550,7 +2131,6 @@ liquid_handler: title: resource_offset type: object to: - additionalProperties: false properties: category: type: string @@ -2569,26 +2149,16 @@ 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 @@ -2598,19 +2168,12 @@ 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 @@ -2640,15 +2203,29 @@ 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: @@ -2676,7 +2253,10 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: [] + intermediate_locations: + - x: 0.0 + y: 0.0 + z: 0.0 pickup_direction: '' pickup_distance_from_top: 0.0 put_direction: '' @@ -2715,26 +2295,19 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -2759,18 +2332,16 @@ 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 @@ -2789,26 +2360,16 @@ 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 @@ -2818,19 +2379,12 @@ 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 @@ -2861,19 +2415,12 @@ 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 @@ -2882,19 +2429,12 @@ 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 @@ -2902,15 +2442,28 @@ 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: @@ -2948,30 +2501,24 @@ liquid_handler: sample_id: '' type: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -2990,26 +2537,16 @@ 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 @@ -3019,19 +2556,12 @@ 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 @@ -3061,15 +2591,21 @@ 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: @@ -3084,9 +2620,32 @@ liquid_handler: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} result: name: name @@ -3094,11 +2653,11 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerPickUpTips_Feedback type: object goal: - additionalProperties: false properties: offsets: items: @@ -3113,6 +2672,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array tip_spots: @@ -3187,21 +2747,30 @@ 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: @@ -3246,26 +2815,19 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -3274,7 +2836,6 @@ liquid_handler: title: offset type: object tip_rack: - additionalProperties: false properties: category: type: string @@ -3293,26 +2854,16 @@ 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 @@ -3322,19 +2873,12 @@ 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 @@ -3364,15 +2908,20 @@ 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: @@ -3397,18 +2946,48 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: [] - delays: [] - flow_rates: [] + blow_out_air_volume: + - 0.0 + delays: + - 0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - top: [] - use_channels: [] - vols: [] + top: + - 0.0 + use_channels: + - 0 + vols: + - 0.0 waste_liquid: category: '' children: [] @@ -3435,11 +3014,11 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerRemove_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -3447,6 +3026,8 @@ liquid_handler: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -3476,6 +3057,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array sources: @@ -3550,6 +3132,7 @@ liquid_handler: - pose - config - data + title: sources type: object type: array spread: @@ -3560,6 +3143,8 @@ liquid_handler: type: array use_channels: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array vols: @@ -3567,7 +3152,6 @@ liquid_handler: type: number type: array waste_liquid: - additionalProperties: false properties: category: type: string @@ -3586,26 +3170,16 @@ 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 @@ -3615,19 +3189,12 @@ 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 @@ -3657,15 +3224,31 @@ 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: @@ -3690,18 +3273,48 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: [] - delays: [] - flow_rates: [] + blow_out_air_volume: + - 0.0 + delays: + - 0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - top: [] - use_channels: [] - vols: [] + top: + - 0.0 + use_channels: + - 0 + vols: + - 0.0 waste_liquid: category: '' children: [] @@ -3726,18 +3339,16 @@ liquid_handler: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerRemove_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -3745,6 +3356,8 @@ liquid_handler: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -3774,6 +3387,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array sources: @@ -3848,6 +3462,7 @@ liquid_handler: - pose - config - data + title: sources type: object type: array spread: @@ -3858,6 +3473,8 @@ liquid_handler: type: array use_channels: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array vols: @@ -3865,7 +3482,6 @@ liquid_handler: type: number type: array waste_liquid: - additionalProperties: false properties: category: type: string @@ -3884,26 +3500,16 @@ 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 @@ -3913,19 +3519,12 @@ 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 @@ -3955,15 +3554,31 @@ 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: @@ -3978,7 +3593,8 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - use_channels: [] + use_channels: + - 0 handles: {} result: name: name @@ -3986,27 +3602,34 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -4027,23 +3650,27 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -4110,22 +3737,17 @@ liquid_handler: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -4144,26 +3766,16 @@ 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 @@ -4173,19 +3785,12 @@ 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 @@ -4216,7 +3821,6 @@ liquid_handler: title: source type: object target: - additionalProperties: false properties: category: type: string @@ -4235,26 +3839,16 @@ 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 @@ -4264,19 +3858,12 @@ 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 @@ -4307,18 +3894,24 @@ 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: @@ -4351,22 +3944,20 @@ 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 @@ -4379,27 +3970,31 @@ 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 @@ -4407,6 +4002,10 @@ liquid_handler: type: string success: type: boolean + required: + - success + - message + - return_info title: Transfer_Result type: object required: @@ -4439,27 +4038,96 @@ liquid_handler: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: [] - asp_vols: [] - blow_out_air_volume: [] - delays: [] - dis_flow_rates: [] - dis_vols: [] + 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 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - targets: [] - tip_racks: [] + 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: '' touch_tip: false - use_channels: [] + use_channels: + - 0 handles: input: - data_key: sources @@ -4492,18 +4160,16 @@ liquid_handler: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerTransfer_Feedback type: object goal: - additionalProperties: false properties: asp_flow_rates: items: @@ -4519,6 +4185,8 @@ liquid_handler: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -4536,8 +4204,6 @@ liquid_handler: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -4570,6 +4236,7 @@ liquid_handler: - x - y - z + title: offsets type: object type: array sources: @@ -4644,6 +4311,7 @@ liquid_handler: - pose - config - data + title: sources type: object type: array spread: @@ -4720,6 +4388,7 @@ liquid_handler: - pose - config - data + title: targets type: object type: array tip_racks: @@ -4794,23 +4463,50 @@ 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: @@ -4829,12 +4525,12 @@ liquid_handler: config: properties: backend: - type: object + type: string channel_num: default: 8 type: integer deck: - type: object + type: string simulator: default: false type: boolean @@ -4877,8 +4573,6 @@ liquid_handler.biomek: goal: properties: bind_location: - additionalProperties: - type: number type: object bind_parent_id: type: string @@ -4918,36 +4612,6 @@ 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: {} @@ -5014,7 +4678,8 @@ liquid_handler.biomek: protocol_type: protocol_type protocol_version: protocol_version goal_default: - none_keys: [] + none_keys: + - '' protocol_author: '' protocol_date: '' protocol_description: '' @@ -5022,18 +4687,16 @@ liquid_handler.biomek: protocol_type: '' protocol_version: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerProtocolCreation_Feedback type: object goal: - additionalProperties: false properties: none_keys: items: @@ -5051,13 +4714,22 @@ 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: @@ -5084,33 +4756,34 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -5121,10 +4794,8 @@ liquid_handler.biomek: move_biomek: feedback: {} goal: - source: source - sources: sources - target: target - targets: targets + source: sources + target: targets goal_default: sources: '' targets: '' @@ -5141,33 +4812,36 @@ liquid_handler.biomek: data_type: resource handler_key: targets label: targets - placeholder_keys: {} result: - return_info: return_info - success: success + name: name schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -5196,19 +4870,16 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerOscillateBiomek_Feedback type: object goal: - additionalProperties: false properties: rpm: maximum: 2147483647 @@ -5218,15 +4889,20 @@ 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: @@ -5239,25 +4915,26 @@ liquid_handler.biomek: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -5268,13 +4945,9 @@ 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 @@ -5313,19 +4986,16 @@ liquid_handler.biomek: data_type: resource handler_key: targets_out label: targets - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerTransferBiomek_Feedback type: object goal: - additionalProperties: false properties: aspirate_technique: type: string @@ -5338,18 +5008,25 @@ 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: @@ -5382,27 +5059,96 @@ liquid_handler.biomek: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: [] - asp_vols: [] - blow_out_air_volume: [] - delays: [] - dis_flow_rates: [] - dis_vols: [] + 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 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - targets: [] - tip_racks: [] + 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: '' touch_tip: false - use_channels: [] + use_channels: + - 0 handles: input: - data_key: sources @@ -5437,18 +5183,16 @@ liquid_handler.biomek: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerTransfer_Feedback type: object goal: - additionalProperties: false properties: asp_flow_rates: items: @@ -5464,6 +5208,8 @@ liquid_handler.biomek: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -5481,8 +5227,6 @@ liquid_handler.biomek: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5515,6 +5259,7 @@ liquid_handler.biomek: - x - y - z + title: offsets type: object type: array sources: @@ -5589,6 +5334,7 @@ liquid_handler.biomek: - pose - config - data + title: sources type: object type: array spread: @@ -5665,6 +5411,7 @@ liquid_handler.biomek: - pose - config - data + title: targets type: object type: array tip_racks: @@ -5739,23 +5486,50 @@ 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: @@ -5765,7 +5539,7 @@ liquid_handler.biomek: type: LiquidHandlerTransfer module: unilabos.devices.liquid_handling.biomek:LiquidHandlerBiomek status_types: - success: '' + success: String type: python config_info: [] description: Biomek液体处理器设备,基于pylabrobot控制 @@ -5794,7 +5568,6 @@ 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 @@ -5810,38 +5583,84 @@ liquid_handler.laiyu: targets: targets use_channels: use_channels goal_default: - asp_vols: [] - blow_out_air_volume: [] - dis_vols: [] - flow_rates: [] + asp_vols: + - 0.0 + blow_out_air_volume: + - 0.0 + dis_vols: + - 0.0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - reagent_sources: [] + 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: '' spread: '' - targets: [] - use_channels: [] + 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 handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAdd_Feedback type: object goal: - additionalProperties: false properties: asp_vols: items: @@ -5866,8 +5685,6 @@ liquid_handler.laiyu: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5898,6 +5715,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array reagent_sources: @@ -5972,6 +5790,7 @@ liquid_handler.laiyu: - pose - config - data + title: reagent_sources type: object type: array spread: @@ -6048,21 +5867,43 @@ 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: @@ -6078,33 +5919,57 @@ 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: [] - flow_rates: [] - liquid_height: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} placeholder_keys: resources: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAspirate_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -6131,6 +5996,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array resources: @@ -6205,27 +6071,41 @@ 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: @@ -6268,93 +6148,54 @@ liquid_handler.laiyu: goal: properties: asp_flow_rates: - items: - type: number - type: array + type: string asp_vols: - anyOf: - - items: - type: number - type: array - - type: number + type: string blow_out_air_volume: - items: - type: number - type: array + type: string delays: - items: - type: integer - type: array + type: string dis_flow_rates: - items: - type: number - type: array + type: string dis_vols: - anyOf: - - items: - type: number - type: array - - type: number + type: string is_96_well: default: false type: boolean liquid_height: - items: - type: number - type: array + type: string mix_liquid_height: - type: number + type: string mix_rate: - type: integer + type: string mix_stage: default: none - enum: - - none - - before - - after - - both type: string mix_times: - items: - type: integer - type: array + type: string mix_vol: - type: integer + type: string none_keys: default: [] items: type: string type: array offsets: - items: - type: object - type: array + type: string sources: - items: - type: object - type: array + type: string spread: default: wide - enum: - - wide - - tight - - custom type: string targets: - items: - type: object - type: array + type: string tip_racks: - items: - type: object - type: array + type: string touch_tip: default: false type: boolean use_channels: - items: - type: integer - type: array + type: string required: - sources - targets @@ -6376,35 +6217,60 @@ 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: [] - flow_rates: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} placeholder_keys: resources: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDispense_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -6424,6 +6290,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array resources: @@ -6498,27 +6365,40 @@ 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: @@ -6535,24 +6415,45 @@ liquid_handler.laiyu: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} placeholder_keys: tip_spots: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDropTips_Feedback type: object goal: - additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -6569,6 +6470,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array tip_spots: @@ -6643,21 +6545,31 @@ 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: @@ -6680,32 +6592,49 @@ liquid_handler.laiyu: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - targets: [] + 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: '' handles: {} placeholder_keys: targets: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -6732,6 +6661,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array targets: @@ -6806,17 +6736,28 @@ 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: @@ -6831,24 +6772,45 @@ liquid_handler.laiyu: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} placeholder_keys: tip_spots: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerPickUpTips_Feedback type: object goal: - additionalProperties: false properties: offsets: items: @@ -6863,6 +6825,7 @@ liquid_handler.laiyu: - x - y - z + title: offsets type: object type: array tip_spots: @@ -6937,21 +6900,30 @@ 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: @@ -7006,7 +6978,6 @@ 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 @@ -7022,38 +6993,84 @@ liquid_handler.prcxi: targets: targets use_channels: use_channels goal_default: - asp_vols: [] - blow_out_air_volume: [] - dis_vols: [] - flow_rates: [] + asp_vols: + - 0.0 + blow_out_air_volume: + - 0.0 + dis_vols: + - 0.0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - reagent_sources: [] + 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: '' spread: '' - targets: [] - use_channels: [] + 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 handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAdd_Feedback type: object goal: - additionalProperties: false properties: asp_vols: items: @@ -7078,8 +7095,6 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -7110,6 +7125,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array reagent_sources: @@ -7184,6 +7200,7 @@ liquid_handler.prcxi: - pose - config - data + title: reagent_sources type: object type: array spread: @@ -7260,21 +7277,43 @@ 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: @@ -7290,33 +7329,57 @@ 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: [] - flow_rates: [] - liquid_height: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} placeholder_keys: resources: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerAspirate_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -7343,6 +7406,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array resources: @@ -7417,27 +7481,41 @@ 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: @@ -7568,14 +7646,11 @@ liquid_handler.prcxi: goal: properties: tip_racks: - items: - type: object - type: array + type: string required: - tip_racks type: object - result: - type: string + result: {} required: - goal title: iter_tips参数 @@ -7614,6 +7689,31 @@ 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: {} @@ -7724,9 +7824,7 @@ liquid_handler.prcxi: goal: properties: targets: - items: - type: object - type: array + type: string required: - targets type: object @@ -7772,39 +7870,41 @@ liquid_handler.prcxi: discard_tips: feedback: {} goal: - allow_nonzero_volume: allow_nonzero_volume - offsets: offsets use_channels: use_channels goal_default: - use_channels: [] + use_channels: + - 0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -7817,38 +7917,63 @@ 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: [] - flow_rates: [] - offsets: [] - resources: [] + 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: '' spread: '' - use_channels: [] - vols: [] + use_channels: + - 0 + vols: + - 0.0 handles: {} placeholder_keys: resources: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDispense_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -7868,6 +7993,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array resources: @@ -7942,27 +8068,40 @@ 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: @@ -7979,24 +8118,45 @@ liquid_handler.prcxi: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} placeholder_keys: tip_spots: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerDropTips_Feedback type: object goal: - additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -8013,6 +8173,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array tip_spots: @@ -8087,21 +8248,31 @@ 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: @@ -8124,32 +8295,49 @@ liquid_handler.prcxi: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: [] - offsets: [] - targets: [] + 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: '' handles: {} placeholder_keys: targets: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -8176,6 +8364,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array targets: @@ -8250,17 +8439,28 @@ 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: @@ -8276,7 +8476,6 @@ 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 @@ -8289,7 +8488,10 @@ liquid_handler.prcxi: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: [] + intermediate_locations: + - x: 0.0 + y: 0.0 + z: 0.0 pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -8346,32 +8548,24 @@ liquid_handler.prcxi: plate: unilabos_resources to: unilabos_resources result: - return_info: return_info - success: success + name: name schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -8396,28 +8590,20 @@ 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 @@ -8426,7 +8612,6 @@ liquid_handler.prcxi: title: pickup_offset type: object plate: - additionalProperties: false properties: category: type: string @@ -8445,26 +8630,16 @@ 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 @@ -8474,19 +8649,12 @@ 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 @@ -8519,19 +8687,12 @@ 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 @@ -8540,7 +8701,6 @@ liquid_handler.prcxi: title: resource_offset type: object to: - additionalProperties: false properties: category: type: string @@ -8559,26 +8719,16 @@ 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 @@ -8588,19 +8738,12 @@ 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 @@ -8630,15 +8773,29 @@ 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: @@ -8653,24 +8810,45 @@ liquid_handler.prcxi: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: [] - tip_spots: [] - use_channels: [] + 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 handles: {} placeholder_keys: tip_spots: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerPickUpTips_Feedback type: object goal: - additionalProperties: false properties: offsets: items: @@ -8685,6 +8863,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array tip_spots: @@ -8759,21 +8938,30 @@ 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: @@ -8798,18 +8986,48 @@ liquid_handler.prcxi: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: [] - delays: [] - flow_rates: [] + blow_out_air_volume: + - 0.0 + delays: + - 0 + flow_rates: + - 0.0 is_96_well: false - liquid_height: [] - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - top: [] - use_channels: [] - vols: [] + top: + - 0.0 + use_channels: + - 0 + vols: + - 0.0 waste_liquid: category: '' children: [] @@ -8834,18 +9052,16 @@ liquid_handler.prcxi: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerRemove_Feedback type: object goal: - additionalProperties: false properties: blow_out_air_volume: items: @@ -8853,6 +9069,8 @@ liquid_handler.prcxi: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array flow_rates: @@ -8882,6 +9100,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array sources: @@ -8956,6 +9175,7 @@ liquid_handler.prcxi: - pose - config - data + title: sources type: object type: array spread: @@ -8966,6 +9186,8 @@ liquid_handler.prcxi: type: array use_channels: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array vols: @@ -8973,7 +9195,6 @@ liquid_handler.prcxi: type: number type: array waste_liquid: - additionalProperties: false properties: category: type: string @@ -8992,26 +9213,16 @@ 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 @@ -9021,19 +9232,12 @@ 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 @@ -9063,15 +9267,31 @@ 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: @@ -9086,9 +9306,30 @@ liquid_handler.prcxi: volumes: volumes wells: wells goal_default: - liquid_names: [] - volumes: [] - wells: [] + 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: '' handles: input: - data_key: wells @@ -9104,17 +9345,16 @@ liquid_handler.prcxi: label: 已设定液体孔 placeholder_keys: wells: unilabos_resources - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerSetLiquid_Feedback type: object goal: - additionalProperties: false properties: liquid_names: items: @@ -9196,15 +9436,21 @@ 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: @@ -9257,7 +9503,6 @@ liquid_handler.prcxi: type: string type: array plate: - additionalProperties: false properties: category: type: string @@ -9276,26 +9521,16 @@ 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 @@ -9305,19 +9540,12 @@ 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 @@ -9334,6 +9562,17 @@ 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: @@ -9387,11 +9626,6 @@ 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 @@ -9455,14 +9689,6 @@ 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 @@ -9583,22 +9809,39 @@ liquid_handler.prcxi: goal: tip_racks: tip_racks goal_default: - tip_racks: [] + 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: '' handles: {} placeholder_keys: tip_racks: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerSetTipRack_Feedback type: object goal: - additionalProperties: false properties: tip_racks: items: @@ -9672,17 +9915,22 @@ 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: @@ -9715,22 +9963,20 @@ 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 @@ -9743,27 +9989,31 @@ 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 @@ -9771,6 +10021,10 @@ liquid_handler.prcxi: type: string success: type: boolean + required: + - success + - message + - return_info title: Transfer_Result type: object required: @@ -9803,27 +10057,96 @@ liquid_handler.prcxi: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: [] - asp_vols: [] - blow_out_air_volume: [] - delays: [] - dis_flow_rates: [] - dis_vols: [] + 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 is_96_well: false - liquid_height: [] + liquid_height: + - 0.0 mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: [] - offsets: [] - sources: [] + 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: '' spread: '' - targets: [] - tip_racks: [] + 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: '' touch_tip: false - use_channels: [] + use_channels: + - 0 handles: input: - data_key: sources @@ -9856,18 +10179,16 @@ liquid_handler.prcxi: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: LiquidHandlerTransfer_Feedback type: object goal: - additionalProperties: false properties: asp_flow_rates: items: @@ -9883,6 +10204,8 @@ liquid_handler.prcxi: type: array delays: items: + maximum: 2147483647 + minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -9900,8 +10223,6 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -9934,6 +10255,7 @@ liquid_handler.prcxi: - x - y - z + title: offsets type: object type: array sources: @@ -10008,6 +10330,7 @@ liquid_handler.prcxi: - pose - config - data + title: sources type: object type: array spread: @@ -10084,6 +10407,7 @@ liquid_handler.prcxi: - pose - config - data + title: targets type: object type: array tip_racks: @@ -10158,23 +10482,50 @@ 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: @@ -10246,13 +10597,11 @@ liquid_handler.revvity: action_value_mappings: run: feedback: - gantt: gantt status: status goal: - file_path: file_path params: params resource: resource - wf_name: wf_name + wf_name: file_path goal_default: params: '' resource: @@ -10277,29 +10626,27 @@ 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 @@ -10318,26 +10665,16 @@ 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 @@ -10347,19 +10684,12 @@ 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 @@ -10391,15 +10721,21 @@ 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: @@ -10428,7 +10764,7 @@ liquid_handler.revvity: success: type: boolean required: - - status - success + - status 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 4f3b972ad..ea6bedc4c 100644 --- a/unilabos/registry/devices/neware_battery_test_system.yaml +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -5,6 +5,31 @@ 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: {} @@ -41,8 +66,7 @@ neware_battery_test_system: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: test_connection参数 @@ -53,8 +77,9 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info + success: success schema: description: 调试方法:显示所有资源的实际名称 properties: @@ -64,10 +89,19 @@ 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: @@ -77,8 +111,9 @@ neware_battery_test_system: goal_default: filepath: bts_status.json handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info + success: success schema: description: 导出当前状态数据到JSON文件 properties: @@ -92,10 +127,19 @@ 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: @@ -137,8 +181,10 @@ neware_battery_test_system: goal_default: plate_num: null handles: {} - placeholder_keys: {} - result: {} + result: + plate_data: plate_data + return_info: return_info + success: success schema: description: 获取指定盘或所有盘的状态信息 properties: @@ -147,14 +193,29 @@ 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: @@ -162,8 +223,9 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info + success: success schema: description: 打印通道状态摘要信息到控制台 properties: @@ -173,21 +235,28 @@ 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: - plate_id: plate_id - string: string + string: plate_id goal_default: string: '' handles: {} - placeholder_keys: {} result: return_info: return_info success: success @@ -195,23 +264,27 @@ neware_battery_test_system: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -225,11 +298,13 @@ neware_battery_test_system: csv_path: string output_dir: string goal_default: - csv_path: null + csv_path: '' output_dir: . handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info + submitted_count: submitted_count + success: success schema: description: 从CSV文件批量提交Neware测试任务 properties: @@ -240,17 +315,31 @@ 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: @@ -258,8 +347,9 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info + success: success schema: description: 测试与电池测试系统的TCP连接 properties: @@ -269,10 +359,19 @@ 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: @@ -293,8 +392,12 @@ neware_battery_test_system: handler_key: uploaded_files io_type: sink label: Uploaded Files (with standard flow info) - placeholder_keys: {} - result: {} + result: + failed_files: failed_files + return_info: return_info + success: success + total_count: total_count + uploaded_count: uploaded_count schema: description: 上传备份文件到阿里云OSS properties: @@ -314,17 +417,65 @@ 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[int, Dict] - connection_info: Dict[str, str] + channel_status: dict + connection_info: dict device_summary: dict + plate_status: dict status: str total_channels: int type: python @@ -366,24 +517,23 @@ 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 - - device_summary - - status - total_channels + - plate_status + - device_summary type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opcua_example.yaml b/unilabos/registry/devices/opcua_example.yaml index 271fd6826..a7e6b4e31 100644 --- a/unilabos/registry/devices/opcua_example.yaml +++ b/unilabos/registry/devices/opcua_example.yaml @@ -142,7 +142,8 @@ opcua_example: type: object type: UniLabJsonCommand module: unilabos.device_comms.opcua_client.client:OpcUaClient - status_types: {} + status_types: + node_value: String type: python config_info: [] description: null @@ -166,7 +167,10 @@ opcua_example: - url type: object data: - properties: {} - required: [] + properties: + node_value: + type: string + required: + - node_value type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opsky_ATR30007.yaml b/unilabos/registry/devices/opsky_ATR30007.yaml index a3fa7df8b..ee8b88719 100644 --- a/unilabos/registry/devices/opsky_ATR30007.yaml +++ b/unilabos/registry/devices/opsky_ATR30007.yaml @@ -80,8 +80,7 @@ opsky_ATR30007: type: string required: [] type: object - result: - type: object + result: {} required: - goal title: run_once参数 diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index c1290beae..3085c8230 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -100,41 +100,42 @@ rotavap.one: type: object type: UniLabJsonCommand set_timer: - feedback: - status: status + feedback: {} 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: @@ -249,13 +250,9 @@ separator.homemade: 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 + stir_time: stir_time, goal_default: event: '' settling_time: '' @@ -284,42 +281,34 @@ 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 @@ -338,26 +327,16 @@ 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 @@ -367,19 +346,12 @@ 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 @@ -409,10 +381,17 @@ 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 @@ -420,6 +399,10 @@ separator.homemade: type: string success: type: boolean + required: + - success + - message + - return_info title: Stir_Result type: object required: @@ -435,34 +418,36 @@ 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 1614a2c3a..be42bad48 100644 --- a/unilabos/registry/devices/post_process_station.yaml +++ b/unilabos/registry/devices/post_process_station.yaml @@ -28,6 +28,31 @@ 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: {} @@ -79,41 +104,42 @@ post_process_station: type: object type: UniLabJsonCommand disconnect: - feedback: - status: status + feedback: {} 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: @@ -123,41 +149,42 @@ post_process_station: type: SendCmd read_node: feedback: - status: status + result: result goal: - command: command - node_name: node_name + command: 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: @@ -256,19 +283,17 @@ post_process_station: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -279,8 +304,6 @@ 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 @@ -299,8 +322,6 @@ 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 @@ -327,8 +348,6 @@ 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 @@ -339,8 +358,6 @@ 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 @@ -359,8 +376,6 @@ 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 @@ -379,8 +394,6 @@ 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 @@ -391,8 +404,6 @@ 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 @@ -411,8 +422,6 @@ 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 @@ -422,13 +431,55 @@ 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: @@ -451,11 +502,11 @@ post_process_station: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: PostProcessGrab_Feedback type: object goal: - additionalProperties: false properties: raw_tank_number: maximum: 2147483647 @@ -465,13 +516,17 @@ 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: @@ -518,15 +573,13 @@ post_process_station: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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 @@ -541,12 +594,8 @@ 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 @@ -573,20 +622,32 @@ 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: @@ -608,26 +669,30 @@ 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: @@ -637,7 +702,8 @@ post_process_station: type: SendCmd module: unilabos.devices.workstation.post_process.post_process:OpcUaClient status_types: - cache_stats: Dict[str, Any] + cache_stats: dict + node_value: String type: python config_info: [] description: 后处理站 @@ -652,9 +718,7 @@ post_process_station: config_path: type: string deck: - anyOf: - - type: object - - type: object + type: string password: type: string subscription_interval: @@ -674,7 +738,10 @@ 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 95a082d53..40fd9d3e8 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: - position: position - string: string + string: position goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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,25 +278,26 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -309,25 +310,26 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -420,27 +422,6 @@ 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: {} @@ -714,10 +695,7 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: position: - anyOf: - - type: integer - - type: string - - type: number + type: string required: - position type: object @@ -742,9 +720,7 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: velocity: - anyOf: - - type: integer - - type: string + type: string required: - velocity type: object @@ -804,13 +780,13 @@ syringe_pump_with_valve.runze.SY03B-T06: status_types: max_velocity: float mode: int - plunger_position: '' + plunger_position: String position: float status: str valve_position: str - velocity_end: '' - velocity_grade: '' - velocity_init: '' + velocity_end: String + velocity_grade: String + velocity_init: String type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -909,15 +885,15 @@ syringe_pump_with_valve.runze.SY03B-T06: velocity_init: type: string required: - - max_velocity - - mode - - plunger_position - - position - status - - valve_position - - velocity_end + - mode + - max_velocity - velocity_grade - velocity_init + - velocity_end + - valve_position + - position + - plunger_position type: object version: 1.0.0 syringe_pump_with_valve.runze.SY03B-T08: @@ -967,27 +943,6 @@ 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: {} @@ -1261,10 +1216,7 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: position: - anyOf: - - type: integer - - type: string - - type: number + type: string required: - position type: object @@ -1289,9 +1241,7 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: velocity: - anyOf: - - type: integer - - type: string + type: string required: - velocity type: object @@ -1351,13 +1301,13 @@ syringe_pump_with_valve.runze.SY03B-T08: status_types: max_velocity: float mode: int - plunger_position: '' + plunger_position: String position: float status: str valve_position: str - velocity_end: '' - velocity_grade: '' - velocity_init: '' + velocity_end: String + velocity_grade: String + velocity_init: String type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -1472,14 +1422,14 @@ syringe_pump_with_valve.runze.SY03B-T08: velocity_init: type: string required: - - max_velocity - - mode - - plunger_position - - position - status - - valve_position - - velocity_end + - mode + - max_velocity - 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 1372140d2..8b4622dc9 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -13,13 +13,12 @@ reaction_station.bioyond: start_point: start_point start_step_key: start_step_key goal_default: - duration: null + duration: 0 end_point: 0 end_step_key: '' start_point: 0 start_step_key: '' handles: {} - placeholder_keys: {} result: {} schema: description: 添加时间约束 - 在两个工作流之间添加时间约束 @@ -31,19 +30,23 @@ reaction_station.bioyond: description: 时间(秒) type: integer end_point: - default: 0 + default: Start description: 终点计时点 (Start=开始前, End=结束后) - type: integer + enum: + - Start + - End + type: string end_step_key: - default: '' description: 终点步骤Key (可选, 默认为空则自动选择) type: string start_point: - default: 0 + default: Start description: 起点计时点 (Start=开始前, End=结束后) - type: integer + enum: + - Start + - End + type: string start_step_key: - default: '' description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) type: string required: @@ -95,8 +98,7 @@ reaction_station.bioyond: required: - json_str type: object - result: - type: object + result: {} required: - goal title: create_order参数 @@ -123,8 +125,7 @@ reaction_station.bioyond: required: - workflow_ids type: object - result: - type: object + result: {} required: - goal title: hard_delete_merged_workflows参数 @@ -149,8 +150,7 @@ reaction_station.bioyond: required: - json_str type: object - result: - type: object + result: {} required: - goal title: merge_workflow_with_parameters参数 @@ -175,8 +175,7 @@ reaction_station.bioyond: required: - report_request type: object - result: - type: object + result: {} required: - goal title: process_temperature_cutoff_report参数 @@ -201,12 +200,7 @@ reaction_station.bioyond: required: - web_workflow_json type: object - result: - items: - additionalProperties: - type: string - type: object - type: array + result: {} required: - goal title: process_web_workflows参数 @@ -235,8 +229,7 @@ reaction_station.bioyond: - reactor_id - temperature type: object - result: - type: string + result: {} required: - goal title: set_reactor_temperature参数 @@ -261,8 +254,7 @@ reaction_station.bioyond: required: - preintake_id type: object - result: - type: object + result: {} required: - goal title: skip_titration_steps参数 @@ -283,8 +275,7 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: - type: object + result: {} required: - goal title: sync_workflow_sequence_from_bioyond参数 @@ -316,8 +307,7 @@ reaction_station.bioyond: type: integer required: [] type: object - result: - type: object + result: {} required: - goal title: wait_for_multiple_orders_and_get_reports参数 @@ -369,8 +359,7 @@ reaction_station.bioyond: required: - workflow_id type: object - result: - type: object + result: {} required: - goal title: workflow_step_query参数 @@ -381,8 +370,9 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + code: code + message: message schema: description: 清空服务端所有非核心工作流 (保留核心流程) properties: @@ -392,6 +382,13 @@ reaction_station.bioyond: required: [] type: object result: + properties: + code: + description: 操作结果代码(1表示成功) + type: integer + message: + description: 结果描述 + type: string type: object required: - goal @@ -408,14 +405,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: null - temperature: 25.0 - time: '90' - titration_type: '1' - torque_variation: 2 - volume: null + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' handles: {} - placeholder_keys: {} result: {} schema: description: 滴回去 @@ -427,27 +423,33 @@ reaction_station.bioyond: description: 物料名称(不能为空) type: string temperature: - default: 25.0 description: 温度设定(°C) - type: number + type: string 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=是) - type: integer + enum: + - 'NO' + - 'YES' + type: string volume: description: 分液公式(mL) type: string required: - - assign_material_name - volume + - assign_material_name + - time + - torque_variation + - titration_type + - temperature type: object result: {} required: @@ -460,7 +462,7 @@ reaction_station.bioyond: goal: batch_reports_result: batch_reports_result goal_default: - batch_reports_result: null + batch_reports_result: '' handles: input: - data_key: batch_reports_result @@ -476,8 +478,8 @@ reaction_station.bioyond: handler_key: ACTUALS_EXTRACTED io_type: sink label: Extracted Actuals - placeholder_keys: {} - result: {} + result: + return_info: return_info schema: description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。 properties: @@ -491,6 +493,13 @@ 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: @@ -508,14 +517,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: BAPP - temperature: 25.0 - time: '0' - titration_type: '1' - torque_variation: 1 - volume: '350' + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' handles: {} - placeholder_keys: {} result: {} schema: description: 液体进料烧杯 @@ -524,30 +532,36 @@ reaction_station.bioyond: goal: properties: assign_material_name: - default: BAPP description: 物料名称 type: string temperature: - default: 25.0 description: 温度设定(°C) - type: number + type: string 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=是) - type: integer + enum: + - 'NO' + - 'YES' + type: string volume: - default: '350' description: 分液公式(mL) type: string - required: [] + required: + - volume + - assign_material_name + - time + - torque_variation + - titration_type + - temperature type: object result: {} required: @@ -566,13 +580,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: null - solvents: null - temperature: 25.0 + assign_material_name: '' + solvents: '' + temperature: '25.00' time: '360' titration_type: '1' - torque_variation: 2 - volume: null + torque_variation: '2' + volume: '' handles: input: - data_key: solvents @@ -581,7 +595,6 @@ 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。 @@ -596,21 +609,27 @@ reaction_station.bioyond: description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' type: string temperature: - default: 25.0 + default: '25.00' description: 温度设定(°C),默认25.00 - type: number + type: string time: default: '360' description: 观察时间(分钟),默认360 type: string titration_type: - default: '1' + default: 'NO' description: 是否滴定(NO=否, YES=是),默认NO + enum: + - 'NO' + - 'YES' type: string torque_variation: - default: 2 + default: 'YES' description: 是否观察 (NO=否, YES=是),默认YES - type: integer + enum: + - 'NO' + - 'YES' + type: string volume: description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 type: string @@ -636,15 +655,15 @@ reaction_station.bioyond: volume_formula: volume_formula x_value: x_value goal_default: - assign_material_name: null - extracted_actuals: null - feeding_order_data: null - temperature: 25.0 + assign_material_name: '' + extracted_actuals: '' + feeding_order_data: '' + temperature: '25.00' time: '90' titration_type: '2' - torque_variation: 2 - volume_formula: null - x_value: null + torque_variation: '2' + volume_formula: '' + x_value: '' handles: input: - data_key: extracted_actuals @@ -659,7 +678,6 @@ 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二酐滴定" @@ -678,21 +696,27 @@ reaction_station.bioyond: {"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}' type: string temperature: - default: 25.0 + default: '25.00' description: 温度设定(°C),默认25.00 - type: number + type: string time: default: '90' description: 观察时间(分钟),默认90 type: string titration_type: - default: '2' + default: 'YES' description: 是否滴定(NO=否, YES=是),默认YES + enum: + - 'NO' + - 'YES' type: string torque_variation: - default: 2 + default: 'YES' description: 是否观察 (NO=否, YES=是),默认YES - type: integer + enum: + - 'NO' + - 'YES' + type: string volume_formula: description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 type: string @@ -718,14 +742,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume_formula: volume_formula goal_default: - assign_material_name: null - temperature: 25.0 - time: '0' - titration_type: '1' - torque_variation: 1 - volume_formula: null + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume_formula: '' handles: {} - placeholder_keys: {} result: {} schema: description: 液体进料小瓶(非滴定) @@ -737,27 +760,33 @@ reaction_station.bioyond: description: 物料名称 type: string temperature: - default: 25.0 description: 温度设定(°C) - type: number + type: string 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=是) - type: integer + enum: + - 'NO' + - 'YES' + type: string volume_formula: description: 分液公式(mL) type: string required: - volume_formula - assign_material_name + - time + - torque_variation + - titration_type + - temperature type: object result: {} required: @@ -771,10 +800,9 @@ reaction_station.bioyond: task_name: task_name workflow_name: workflow_name goal_default: - task_name: null - workflow_name: null + task_name: '' + workflow_name: '' handles: {} - placeholder_keys: {} result: {} schema: description: 处理并执行工作流 @@ -792,8 +820,7 @@ reaction_station.bioyond: - workflow_name - task_name type: object - result: - type: object + result: {} required: - goal title: process_and_execute_workflow参数 @@ -806,11 +833,10 @@ reaction_station.bioyond: cutoff: cutoff temperature: temperature goal_default: - assign_material_name: null - cutoff: '900000' - temperature: -10.0 + assign_material_name: '' + cutoff: '' + temperature: '' handles: {} - placeholder_keys: {} result: {} schema: description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 @@ -822,14 +848,14 @@ reaction_station.bioyond: description: 物料名称 type: string cutoff: - default: '900000' description: 粘度上限 type: string temperature: - default: -10.0 description: 温度设定(°C) - type: number + type: string required: + - cutoff + - temperature - assign_material_name type: object result: {} @@ -843,7 +869,6 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - placeholder_keys: {} result: {} schema: description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 @@ -853,7 +878,15 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: {} + result: + properties: + code: + description: 操作结果代码(1表示成功,0表示失败) + type: integer + return_info: + description: 操作结果详细信息 + type: string + type: object required: - goal title: reactor_taken_out参数 @@ -864,8 +897,8 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + return_info: return_info schema: description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 properties: @@ -875,6 +908,12 @@ reaction_station.bioyond: required: [] type: object result: + properties: + return_info: + description: 调度器启动结果,成功返回1,失败返回0 + type: integer + required: + - return_info title: scheduler_start结果 type: object required: @@ -891,13 +930,12 @@ reaction_station.bioyond: time: time torque_variation: torque_variation goal_default: - assign_material_name: null - material_id: null - temperature: 25.0 - time: '0' - torque_variation: 1 + assign_material_name: '' + material_id: '' + temperature: '' + time: '' + torque_variation: '' handles: {} - placeholder_keys: {} result: {} schema: description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) @@ -910,21 +948,29 @@ 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: number + type: string time: - default: '0' description: 观察时间(分钟) type: string torque_variation: - default: 1 description: 是否观察 (NO=否, YES=是) - type: integer + enum: + - 'NO' + - 'YES' + type: string 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 b37a0c46f..9f45bd5e0 100644 --- a/unilabos/registry/devices/robot_agv.yaml +++ b/unilabos/registry/devices/robot_agv.yaml @@ -37,41 +37,42 @@ agv.SEER: type: object type: UniLabJsonCommand send_nav_task: - feedback: - status: status + feedback: {} 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 old mode 100755 new mode 100644 index a2d60573e..147eab4d4 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -122,6 +122,31 @@ 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: {} @@ -174,99 +199,83 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: UniLabJsonCommand pick_and_place: feedback: {} - goal: {} + goal: + command: command goal_default: - constraints: null - lift_height: null - move_group: null - option: null - resource: null - retry: null - speed: null - status: null - target: null - x_distance: null - y_distance: null + command: '' handles: {} - placeholder_keys: {} result: {} schema: - description: pick_and_place 显式参数(UniLabJsonCommand) + description: '' properties: - feedback: {} - goal: + feedback: properties: - constraints: - items: - type: number - type: array - lift_height: - type: string - move_group: - type: string - option: - type: string - resource: - type: string - retry: - type: string - speed: - type: string status: type: string - target: - type: string - x_distance: + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: type: string - y_distance: + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: type: string + success: + type: boolean required: - - option - - move_group - - status + - return_info + - success + title: SendCmd_Result type: object - result: {} required: - goal - title: pick_and_place参数 + title: SendCmd type: object - type: UniLabJsonCommand + type: SendCmd set_position: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} 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: @@ -275,41 +284,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: SendCmd set_status: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} 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: @@ -446,41 +455,42 @@ robotic_arm.UR: type: object type: UniLabJsonCommand move_pos_task: - feedback: - status: status + feedback: {} 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: @@ -522,8 +532,8 @@ robotic_arm.UR: type: string required: - arm_pose - - arm_status - gripper_pose + - arm_status - gripper_status type: object version: 1.0.0 @@ -716,41 +726,41 @@ robotic_arm.elite: type: object type: UniLabJsonCommand modbus_task_cmd: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} 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: @@ -760,8 +770,8 @@ robotic_arm.elite: type: SendCmd module: unilabos.devices.arm.elite_robot:EliteRobot status_types: - actual_joint_positions: '' - arm_pose: list[float] + actual_joint_positions: String + arm_pose: String type: python config_info: [] description: Elite robot arm @@ -787,8 +797,8 @@ robotic_arm.elite: type: number type: array required: - - actual_joint_positions - arm_pose + - actual_joint_positions type: object model: mesh: elite_robot diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 4f579e24a..295c48a0c 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -114,12 +114,11 @@ gripper.misumi_rz: goal: properties: data: - type: object + type: string required: - data type: object - result: - type: object + result: {} required: - goal title: modbus_crc参数 @@ -399,26 +398,30 @@ 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: @@ -501,82 +504,71 @@ gripper.mock: type: UniLabJsonCommand push_to: feedback: - effort: effort + effort: torque position: position - reached_goal: reached_goal - stalled: stalled goal: - command: command - position: position - torque: torque - velocity: velocity + command.max_effort: torque + command.position: position goal_default: command: max_effort: 0.0 position: 0.0 handles: {} - placeholder_keys: {} result: - effort: effort + effort: torque 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: @@ -612,8 +604,8 @@ gripper.mock: type: number required: - position - - status - - torque - velocity + - torque + - status type: object version: 1.0.0 diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml old mode 100755 new mode 100644 index 6a36dae03..0f8506e9e --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -24,27 +24,6 @@ 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: {} @@ -114,39 +93,44 @@ linear_motion.grbl: type: UniLabJsonCommandAsync move_through_points: feedback: - 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 + current_pose.pose.position: position + estimated_time_remaining.sec: time_remaining + navigation_time.sec: time_spent + number_of_poses_remaining: pose_number_remaining goal: - behavior_tree: behavior_tree - poses: poses - positions: positions + poses[].pose.position: positions[] goal_default: behavior_tree: '' - poses: [] + 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 handles: {} - placeholder_keys: {} - result: - 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 @@ -167,26 +151,16 @@ 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 @@ -196,19 +170,12 @@ 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 @@ -227,11 +194,8 @@ 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 @@ -247,7 +211,6 @@ linear_motion.grbl: title: estimated_time_remaining type: object navigation_time: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -270,10 +233,16 @@ 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 @@ -287,8 +256,12 @@ linear_motion.grbl: stamp: properties: nanosec: + maximum: 4294967295 + minimum: 0 type: integer sec: + maximum: 2147483647 + minimum: -2147483648 type: integer required: - sec @@ -341,17 +314,23 @@ 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: - additionalProperties: true + properties: {} + required: [] title: result type: object + required: + - result title: NavigateThroughPoses_Result type: object required: @@ -361,15 +340,9 @@ linear_motion.grbl: type: NavigateThroughPoses set_spindle_speed: feedback: - error: error - header: header - position: position - velocity: velocity + position: spindle_speed goal: - max_velocity: max_velocity - min_duration: min_duration - position: position - spindle_speed: spindle_speed + position: spindle_speed goal_default: max_velocity: 0.0 min_duration: @@ -377,25 +350,19 @@ 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 @@ -416,24 +383,21 @@ 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 @@ -449,13 +413,16 @@ 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: - additionalProperties: true + properties: {} + required: [] title: SingleJointPosition_Result type: object required: @@ -465,7 +432,7 @@ linear_motion.grbl: type: SingleJointPosition module: unilabos.devices.cnc.grbl_sync:GrblCNC status_types: - position: Point3D + position: unilabos.messages:Point3D spindle_speed: float status: str type: python @@ -504,9 +471,9 @@ linear_motion.grbl: status: type: string required: + - status - position - spindle_speed - - status type: object version: 1.0.0 linear_motion.toyo_xyz.sim: @@ -633,6 +600,31 @@ 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: {} @@ -685,99 +677,83 @@ linear_motion.toyo_xyz.sim: type: UniLabJsonCommand pick_and_place: feedback: {} - goal: {} + goal: + command: command goal_default: - constraints: null - lift_height: null - move_group: null - option: null - resource: null - retry: null - speed: null - status: null - target: null - x_distance: null - y_distance: null + command: '' handles: {} - placeholder_keys: {} result: {} schema: - description: pick_and_place 显式参数(UniLabJsonCommand) + description: '' properties: - feedback: {} - goal: + feedback: properties: - constraints: - items: - type: number - type: array - lift_height: - type: string - move_group: - type: string - option: - type: string - resource: - type: string - retry: - type: string - speed: - type: string status: type: string - target: - type: string - x_distance: + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: type: string - y_distance: + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: type: string + success: + type: boolean required: - - option - - move_group - - status + - return_info + - success + title: SendCmd_Result type: object - result: {} required: - goal - title: pick_and_place参数 + title: SendCmd type: object - type: UniLabJsonCommand + type: SendCmd set_position: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} 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: @@ -786,41 +762,41 @@ linear_motion.toyo_xyz.sim: type: object type: SendCmd set_status: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} 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: @@ -963,26 +939,30 @@ 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: @@ -1020,8 +1000,8 @@ motor.iCL42: success: type: boolean required: - - is_executing_run - motor_position + - is_executing_run - success type: object version: 1.0.0 diff --git a/unilabos/registry/devices/solid_dispenser.yaml b/unilabos/registry/devices/solid_dispenser.yaml index 462806313..9bceb54b8 100644 --- a/unilabos/registry/devices/solid_dispenser.yaml +++ b/unilabos/registry/devices/solid_dispenser.yaml @@ -14,24 +14,19 @@ 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: - additionalProperties: true + properties: {} + required: [] 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 @@ -39,19 +34,24 @@ 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,12 +74,11 @@ solid_dispenser.laiyu: goal: properties: data: - type: object + type: string required: - data type: object - result: - type: object + result: {} required: - goal title: calculate_crc参数 @@ -100,12 +99,11 @@ solid_dispenser.laiyu: goal: properties: command: - type: object + type: string required: - command type: object - result: - type: object + result: {} required: - goal title: send_command参数 @@ -114,37 +112,36 @@ solid_dispenser.laiyu: discharge: feedback: {} goal: - float_in: float_in + float_input: float_input goal_default: float_in: 0.0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -159,31 +156,32 @@ solid_dispenser.laiyu: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -202,41 +200,38 @@ solid_dispenser.laiyu: y: 0.0 z: 0.0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -251,33 +246,34 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -292,33 +288,34 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -331,25 +328,26 @@ solid_dispenser.laiyu: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] 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 9e60adb1f..874fe517b 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -34,8 +34,7 @@ chiller: - register_address - value type: object - result: - type: object + result: {} required: - goal title: build_modbus_frame参数 @@ -64,8 +63,7 @@ chiller: required: - temperature type: object - result: - type: integer + result: {} required: - goal title: convert_temperature_to_modbus_value参数 @@ -86,12 +84,11 @@ chiller: goal: properties: data: - type: object + type: string required: - data type: object - result: - type: object + result: {} required: - goal title: modbus_crc参数 @@ -119,41 +116,42 @@ chiller: type: object type: UniLabJsonCommand set_temperature: - feedback: - status: status + feedback: {} 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: @@ -268,15 +266,9 @@ 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: '' @@ -309,23 +301,20 @@ 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 @@ -336,12 +325,8 @@ 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 @@ -350,7 +335,6 @@ heaterstirrer.dalong: time_spec: type: string vessel: - additionalProperties: false properties: category: type: string @@ -369,26 +353,16 @@ 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 @@ -398,19 +372,12 @@ 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 @@ -440,10 +407,20 @@ 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 @@ -451,6 +428,10 @@ heaterstirrer.dalong: type: string success: type: boolean + required: + - success + - message + - return_info title: HeatChill_Result type: object required: @@ -459,42 +440,42 @@ heaterstirrer.dalong: type: object type: HeatChill set_temp_target: - feedback: - status: status + feedback: {} goal: - command: command - temp: temp + command: 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: @@ -503,42 +484,42 @@ heaterstirrer.dalong: type: object type: SendCmd set_temp_warning: - feedback: - status: status + feedback: {} goal: - command: command - temp: temp + command: 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: @@ -588,8 +569,8 @@ heaterstirrer.dalong: - status - stir_speed - temp - - temp_target - temp_warning + - temp_target type: object version: 1.0.0 tempsensor: @@ -710,41 +691,42 @@ tempsensor: type: object type: UniLabJsonCommand set_warning: - feedback: - status: status + feedback: {} 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 67560f2f1..f0635755e 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -18,8 +18,7 @@ virtual_centrifuge: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -40,17 +39,41 @@ virtual_centrifuge: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: current_status + current_status: status current_temp: current_temp progress: progress goal: @@ -83,50 +106,38 @@ 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 @@ -145,26 +156,16 @@ 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 @@ -174,19 +175,12 @@ 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 @@ -216,10 +210,14 @@ virtual_centrifuge: - data title: vessel type: object + required: + - vessel + - speed + - time + - temp title: Centrifuge_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -227,6 +225,10 @@ virtual_centrifuge: type: string success: type: boolean + required: + - success + - message + - return_info title: Centrifuge_Result type: object required: @@ -265,7 +267,7 @@ virtual_centrifuge: config: properties: config: - type: object + type: string device_id: type: string required: [] @@ -297,18 +299,18 @@ virtual_centrifuge: time_remaining: type: number required: + - status - centrifuge_state - current_speed + - target_speed - current_temp + - target_temp - max_speed - max_temp - - message - min_temp - - progress - - status - - target_speed - - target_temp - time_remaining + - progress + - message type: object version: 1.0.0 virtual_column: @@ -331,8 +333,7 @@ virtual_column: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -353,26 +354,45 @@ virtual_column: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: '' @@ -423,32 +443,29 @@ virtual_column: sample_id: '' type: '' handles: {} - placeholder_keys: {} result: - message: message - return_info: return_info + message: current_status + return_info: current_status 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 @@ -467,26 +484,16 @@ 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 @@ -496,19 +503,12 @@ 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,7 +551,6 @@ virtual_column: solvent2: type: string to_vessel: - additionalProperties: false properties: category: type: string @@ -570,26 +569,16 @@ 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 @@ -599,19 +588,12 @@ 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 @@ -641,10 +623,19 @@ 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 @@ -652,6 +643,10 @@ virtual_column: type: string success: type: boolean + required: + - success + - message + - return_info title: RunColumn_Result type: object required: @@ -727,17 +722,17 @@ virtual_column: status: type: string required: - - column_diameter - - column_length + - status - column_state - current_flow_rate - - current_phase - - current_status - - final_volume - max_flow_rate + - column_length + - column_diameter - processed_volume - progress - - status + - current_status + - current_phase + - final_volume type: object version: 1.0.0 virtual_filter: @@ -760,8 +755,7 @@ virtual_filter: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -782,13 +776,37 @@ virtual_filter: properties: {} required: [] type: object - result: - type: boolean + result: {} 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 @@ -850,40 +868,35 @@ virtual_filter: type: '' volume: 0.0 handles: {} - placeholder_keys: {} result: message: message - return_info: return_info + return_info: message 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 @@ -902,26 +915,16 @@ 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 @@ -931,19 +934,12 @@ 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 @@ -976,15 +972,10 @@ 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 @@ -1003,26 +994,16 @@ 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 @@ -1032,19 +1013,12 @@ 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 @@ -1075,13 +1049,18 @@ 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 @@ -1089,6 +1068,10 @@ virtual_filter: type: string success: type: boolean + required: + - success + - message + - return_info title: Filter_Result type: object required: @@ -1140,7 +1123,7 @@ virtual_filter: config: properties: config: - type: object + type: string device_id: type: string required: [] @@ -1166,15 +1149,15 @@ virtual_filter: status: type: string required: - - current_status + - status + - progress - current_temp + - current_status - filtered_volume - - max_stir_speed + - message - max_temp + - max_stir_speed - max_volume - - message - - progress - - status type: object version: 1.0.0 virtual_gas_source: @@ -1197,8 +1180,7 @@ virtual_gas_source: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -1219,8 +1201,7 @@ virtual_gas_source: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 @@ -1273,25 +1254,26 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -1304,25 +1286,26 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -1337,31 +1320,32 @@ virtual_gas_source: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -1389,7 +1373,7 @@ virtual_gas_source: config: properties: config: - type: object + type: string device_id: type: string required: [] @@ -1422,8 +1406,7 @@ virtual_heatchill: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -1444,26 +1427,46 @@ virtual_heatchill: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: '' @@ -1496,23 +1499,20 @@ 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,12 +1523,8 @@ 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 @@ -1537,7 +1533,6 @@ virtual_heatchill: time_spec: type: string vessel: - additionalProperties: false properties: category: type: string @@ -1556,26 +1551,16 @@ 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 @@ -1585,19 +1570,12 @@ 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 @@ -1627,10 +1605,20 @@ 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 @@ -1638,6 +1626,10 @@ virtual_heatchill: type: string success: type: boolean + required: + - success + - message + - return_info title: HeatChill_Result type: object required: @@ -1676,31 +1668,26 @@ 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 @@ -1719,26 +1706,16 @@ 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 @@ -1748,19 +1725,12 @@ 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 @@ -1790,15 +1760,21 @@ 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: @@ -1833,25 +1809,22 @@ 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 @@ -1870,26 +1843,16 @@ 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 @@ -1899,19 +1862,12 @@ 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 @@ -1941,15 +1897,19 @@ 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: @@ -2011,15 +1971,15 @@ virtual_heatchill: stir_speed: type: number required: + - status + - operation_mode - is_stirring - - max_stir_speed + - stir_speed + - remaining_time + - progress - max_temp - min_temp - - operation_mode - - progress - - remaining_time - - status - - stir_speed + - max_stir_speed type: object version: 1.0.0 virtual_multiway_valve: @@ -2067,8 +2027,7 @@ virtual_multiway_valve: required: - port_number type: object - result: - type: boolean + result: {} required: - goal title: is_at_port参数 @@ -2093,8 +2052,7 @@ virtual_multiway_valve: required: - position type: object - result: - type: boolean + result: {} required: - goal title: is_at_position参数 @@ -2115,8 +2073,7 @@ virtual_multiway_valve: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: is_at_pump_position参数 @@ -2236,41 +2193,42 @@ virtual_multiway_valve: type: object type: UniLabJsonCommand set_position: - feedback: - status: status + feedback: {} 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: @@ -2279,41 +2237,42 @@ virtual_multiway_valve: type: object type: SendCmd set_valve_position: - feedback: - status: status + feedback: {} 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: @@ -2443,13 +2402,13 @@ virtual_multiway_valve: valve_state: type: string required: - - current_port - - current_position - - flow_path - status + - valve_state + - current_position - target_position + - current_port - valve_position - - valve_state + - flow_path type: object version: 1.0.0 virtual_rotavap: @@ -2472,8 +2431,7 @@ virtual_rotavap: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -2494,22 +2452,43 @@ virtual_rotavap: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - evaporate: - feedback: - current_device: current_device - status: status - time_remaining: time_remaining - time_spent: time_spent - goal: - pressure: pressure - solvent: solvent + 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 + goal: + pressure: pressure stir_speed: stir_speed temp: temp time: time @@ -2541,22 +2520,19 @@ virtual_rotavap: sample_id: '' type: '' handles: {} - placeholder_keys: {} result: - return_info: return_info + message: message success: success schema: description: '' properties: feedback: - additionalProperties: false properties: current_device: type: string status: type: string time_remaining: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2572,7 +2548,6 @@ virtual_rotavap: title: time_remaining type: object time_spent: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2587,29 +2562,26 @@ 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 @@ -2628,26 +2600,16 @@ 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 @@ -2657,19 +2619,12 @@ 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 @@ -2699,15 +2654,24 @@ 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: @@ -2761,7 +2725,7 @@ virtual_rotavap: config: properties: config: - type: object + type: string device_id: type: string required: [] @@ -2791,17 +2755,17 @@ virtual_rotavap: vacuum_pressure: type: number required: + - status + - rotavap_state - current_temp + - rotation_speed + - vacuum_pressure - evaporated_volume - - max_rotation_speed - - max_temp - - message - progress + - message + - max_temp + - max_rotation_speed - remaining_time - - rotation_speed - - rotavap_state - - status - - vacuum_pressure type: object version: 1.0.0 virtual_separator: @@ -2824,8 +2788,7 @@ virtual_separator: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -2846,21 +2809,44 @@ virtual_separator: properties: {} required: [] type: object - result: - type: boolean + result: {} 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 @@ -2871,10 +2857,7 @@ 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: '' @@ -3027,30 +3010,26 @@ 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 @@ -3069,26 +3048,16 @@ 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 @@ -3098,19 +3067,12 @@ 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 @@ -3143,7 +3105,6 @@ virtual_separator: product_phase: type: string product_vessel: - additionalProperties: false properties: category: type: string @@ -3162,26 +3123,16 @@ 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 @@ -3191,19 +3142,12 @@ 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 @@ -3240,7 +3184,6 @@ virtual_separator: minimum: -2147483648 type: integer separation_vessel: - additionalProperties: false properties: category: type: string @@ -3259,26 +3202,16 @@ 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 @@ -3288,19 +3221,12 @@ 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 @@ -3331,25 +3257,18 @@ 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 @@ -3368,26 +3287,16 @@ 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 @@ -3397,19 +3306,12 @@ 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 @@ -3440,7 +3342,6 @@ virtual_separator: title: to_vessel type: object vessel: - additionalProperties: false properties: category: type: string @@ -3459,26 +3360,16 @@ 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 @@ -3488,19 +3379,12 @@ 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 @@ -3533,7 +3417,6 @@ virtual_separator: volume: type: string waste_phase_to_vessel: - additionalProperties: false properties: category: type: string @@ -3552,26 +3435,16 @@ 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 @@ -3581,19 +3454,12 @@ 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 @@ -3624,7 +3490,6 @@ virtual_separator: title: waste_phase_to_vessel type: object waste_vessel: - additionalProperties: false properties: category: type: string @@ -3643,26 +3508,16 @@ 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 @@ -3672,19 +3527,12 @@ 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 @@ -3714,10 +3562,27 @@ 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 @@ -3725,6 +3590,10 @@ virtual_separator: type: string success: type: boolean + required: + - success + - message + - return_info title: Separate_Result type: object required: @@ -3776,7 +3645,7 @@ virtual_separator: config: properties: config: - type: object + type: string device_id: type: string required: [] @@ -3802,15 +3671,15 @@ virtual_separator: volume: type: number required: + - status + - separator_state + - volume - has_phases - - message - phase_separation - - progress - - separator_state - - settling_time - - status - stir_speed - - volume + - settling_time + - progress + - message type: object version: 1.0.0 virtual_solenoid_valve: @@ -3833,8 +3702,7 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -3855,8 +3723,7 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 @@ -3877,13 +3744,37 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: {} @@ -3928,28 +3819,31 @@ virtual_solenoid_valve: type: UniLabJsonCommand close: feedback: {} - goal: {} + goal: + command: CLOSED goal_default: {} handles: {} - placeholder_keys: {} result: - return_info: return_info + success: success schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -3962,25 +3856,26 @@ virtual_solenoid_valve: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -3995,31 +3890,32 @@ virtual_solenoid_valve: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -4028,41 +3924,44 @@ virtual_solenoid_valve: type: object type: StrSingleInput set_valve_position: - feedback: - status: status + feedback: {} goal: command: command goal_default: command: '' handles: {} - placeholder_keys: {} result: - return_info: return_info + message: message 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: @@ -4117,10 +4016,10 @@ virtual_solenoid_valve: valve_state: type: string required: - - is_open - status - - valve_position - valve_state + - is_open + - valve_position type: object version: 1.0.0 virtual_solid_dispenser: @@ -4130,10 +4029,9 @@ virtual_solid_dispenser: action_value_mappings: add_solid: feedback: - current_status: current_status + current_status: status progress: progress goal: - amount: amount equiv: equiv event: event mass: mass @@ -4142,12 +4040,7 @@ 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: '' @@ -4184,7 +4077,6 @@ virtual_solid_dispenser: viscous: false volume: '' handles: {} - placeholder_keys: {} result: message: message return_info: return_info @@ -4193,18 +4085,17 @@ 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 @@ -4227,13 +4118,10 @@ 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 @@ -4252,26 +4140,16 @@ 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 @@ -4281,19 +4159,12 @@ 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 @@ -4327,10 +4198,25 @@ 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 @@ -4338,6 +4224,10 @@ virtual_solid_dispenser: type: string success: type: boolean + required: + - success + - message + - return_info title: Add_Result type: object required: @@ -4360,8 +4250,7 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -4386,8 +4275,7 @@ virtual_solid_dispenser: required: - reagent_name type: object - result: - type: string + result: {} required: - goal title: find_solid_reagent_bottle参数 @@ -4408,8 +4296,7 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 @@ -4434,8 +4321,7 @@ virtual_solid_dispenser: required: - mass_str type: object - result: - type: number + result: {} required: - goal title: parse_mass_string参数 @@ -4460,13 +4346,37 @@ virtual_solid_dispenser: required: - mol_str type: object - result: - type: number + result: {} 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 @@ -4515,9 +4425,9 @@ virtual_solid_dispenser: total_operations: type: integer required: + - status - current_reagent - dispensed_amount - - status - total_operations type: object version: 1.0.0 @@ -4541,8 +4451,7 @@ virtual_stirrer: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -4563,18 +4472,40 @@ virtual_stirrer: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: - current_speed: current_speed - current_status: current_status - progress: progress + status: status goal: purpose: purpose stir_speed: stir_speed @@ -4603,40 +4534,32 @@ 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 @@ -4655,26 +4578,16 @@ 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 @@ -4684,19 +4597,12 @@ 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 @@ -4726,10 +4632,13 @@ virtual_stirrer: - data title: vessel type: object + required: + - vessel + - stir_speed + - purpose title: StartStir_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -4737,6 +4646,10 @@ virtual_stirrer: type: string success: type: boolean + required: + - success + - message + - return_info title: StartStir_Result type: object required: @@ -4748,13 +4661,9 @@ 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: '' @@ -4783,42 +4692,34 @@ 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 @@ -4837,26 +4738,16 @@ 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 @@ -4866,19 +4757,12 @@ 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 @@ -4908,10 +4792,17 @@ 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 @@ -4919,6 +4810,10 @@ virtual_stirrer: type: string success: type: boolean + required: + - success + - message + - return_info title: Stir_Result type: object required: @@ -4928,8 +4823,7 @@ virtual_stirrer: type: Stir stop_stir: feedback: - current_status: current_status - progress: progress + status: status goal: vessel: vessel goal_default: @@ -4954,30 +4848,25 @@ 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 @@ -4996,26 +4885,16 @@ 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 @@ -5025,19 +4904,12 @@ 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 @@ -5067,10 +4939,11 @@ virtual_stirrer: - data title: vessel type: object + required: + - vessel title: StopStir_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -5078,6 +4951,10 @@ virtual_stirrer: type: string success: type: boolean + required: + - success + - message + - return_info title: StopStir_Result type: object required: @@ -5089,7 +4966,7 @@ virtual_stirrer: status_types: current_speed: float current_vessel: str - device_info: Dict[str, Any] + device_info: dict is_stirring: bool max_speed: float min_speed: float @@ -5139,15 +5016,15 @@ virtual_stirrer: status: type: string required: - - current_speed + - status + - operation_mode - current_vessel - - device_info + - current_speed - is_stirring + - remaining_time - max_speed - min_speed - - operation_mode - - remaining_time - - status + - device_info type: object version: 1.0.0 virtual_transfer_pump: @@ -5198,8 +5075,7 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -5296,8 +5172,7 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 @@ -5318,8 +5193,7 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: is_empty参数 @@ -5340,13 +5214,37 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} 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: {} @@ -5461,44 +5359,38 @@ 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 @@ -5506,6 +5398,10 @@ virtual_transfer_pump: type: string success: type: boolean + required: + - return_info + - success + - message title: SetPumpPosition_Result type: object required: @@ -5520,8 +5416,6 @@ 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 @@ -5543,31 +5437,27 @@ 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 @@ -5580,27 +5470,31 @@ 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 @@ -5608,6 +5502,10 @@ virtual_transfer_pump: type: string success: type: boolean + required: + - success + - message + - return_info title: Transfer_Result type: object required: @@ -5660,12 +5558,12 @@ virtual_transfer_pump: transfer_rate: type: number required: + - status + - position - current_volume - max_velocity - - position - - remaining_capacity - - status - transfer_rate + - remaining_capacity type: object version: 1.0.0 virtual_vacuum_pump: @@ -5688,8 +5586,7 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: cleanup参数 @@ -5710,8 +5607,7 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: - type: boolean + result: {} required: - goal title: initialize参数 @@ -5764,25 +5660,26 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -5795,25 +5692,26 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -5828,31 +5726,32 @@ virtual_vacuum_pump: goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -5880,16 +5779,495 @@ virtual_vacuum_pump: config: properties: config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + status: + type: string + required: + - 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 87a2fabef..e1be7f3d9 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -59,17 +59,16 @@ 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 @@ -88,26 +87,16 @@ 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 @@ -117,19 +106,12 @@ 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 @@ -162,7 +144,6 @@ workstation: from_repo_position: type: string to_repo: - additionalProperties: false properties: category: type: string @@ -181,26 +162,16 @@ 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 @@ -210,19 +181,12 @@ 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 @@ -254,15 +218,22 @@ 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: @@ -348,18 +319,17 @@ 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 @@ -382,13 +352,10 @@ 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 @@ -407,26 +374,16 @@ 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 @@ -436,19 +393,12 @@ 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 @@ -482,10 +432,25 @@ 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 @@ -493,6 +458,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Add_Result type: object required: @@ -559,27 +528,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: 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 @@ -598,26 +563,16 @@ 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 @@ -627,19 +582,12 @@ 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 @@ -669,10 +617,13 @@ workstation: - data title: vessel type: object + required: + - vessel + - ph_value + - reagent title: AdjustPH_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -680,6 +631,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: AdjustPH_Result type: object required: @@ -738,41 +693,31 @@ 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 @@ -791,26 +736,16 @@ 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 @@ -820,19 +755,12 @@ 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 @@ -862,10 +790,14 @@ workstation: - data title: vessel type: object + required: + - vessel + - speed + - time + - temp title: Centrifuge_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -873,6 +805,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Centrifuge_Result type: object required: @@ -938,14 +874,12 @@ workstation: description: '' properties: feedback: - additionalProperties: false properties: current_device: type: string status: type: string time_remaining: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -961,7 +895,6 @@ workstation: title: time_remaining type: object time_spent: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -976,10 +909,14 @@ 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 @@ -988,11 +925,8 @@ workstation: solvent: type: string temp: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number vessel: - additionalProperties: false properties: category: type: string @@ -1011,26 +945,16 @@ 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 @@ -1040,19 +964,12 @@ 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 @@ -1083,18 +1000,24 @@ 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: @@ -1160,18 +1083,17 @@ 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 @@ -1180,11 +1102,8 @@ workstation: solvent: type: string temp: - maximum: 1.7976931348623157e+308 - minimum: -1.7976931348623157e+308 type: number vessel: - additionalProperties: false properties: category: type: string @@ -1203,26 +1122,16 @@ 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 @@ -1232,19 +1141,12 @@ 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 @@ -1275,13 +1177,16 @@ 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 @@ -1289,6 +1194,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: CleanVessel_Result type: object required: @@ -1371,18 +1280,17 @@ 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 @@ -1397,15 +1305,12 @@ 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 @@ -1424,26 +1329,16 @@ 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 @@ -1453,19 +1348,12 @@ 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 @@ -1497,10 +1385,21 @@ 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 @@ -1508,6 +1407,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Dissolve_Result type: object required: @@ -1562,23 +1465,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: Dry_Feedback type: object goal: - additionalProperties: false properties: compound: type: string vessel: - additionalProperties: false properties: category: type: string @@ -1597,26 +1498,16 @@ 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 @@ -1626,19 +1517,12 @@ 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 @@ -1668,10 +1552,12 @@ workstation: - data title: vessel type: object + required: + - compound + - vessel title: Dry_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -1679,6 +1565,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Dry_Result type: object required: @@ -1733,14 +1623,12 @@ workstation: description: '' properties: feedback: - additionalProperties: false properties: current_device: type: string status: type: string time_remaining: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1756,7 +1644,6 @@ workstation: title: time_remaining type: object time_spent: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1771,15 +1658,18 @@ 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 @@ -1798,26 +1688,16 @@ 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 @@ -1827,19 +1707,12 @@ 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 @@ -1869,15 +1742,20 @@ 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: @@ -1945,14 +1823,12 @@ workstation: description: '' properties: feedback: - additionalProperties: false properties: current_device: type: string status: type: string time_remaining: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1968,7 +1844,6 @@ workstation: title: time_remaining type: object time_spent: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1983,29 +1858,26 @@ 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 @@ -2024,26 +1896,16 @@ 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 @@ -2053,19 +1915,12 @@ 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 @@ -2095,15 +1950,24 @@ 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: @@ -2198,31 +2062,27 @@ 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 @@ -2241,26 +2101,16 @@ 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 @@ -2270,19 +2120,12 @@ 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 @@ -2315,15 +2158,10 @@ 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 @@ -2342,26 +2180,16 @@ 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 @@ -2371,19 +2199,12 @@ 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 @@ -2414,13 +2235,18 @@ 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 @@ -2428,6 +2254,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Filter_Result type: object required: @@ -2546,18 +2376,17 @@ 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 @@ -2566,11 +2395,8 @@ 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 @@ -2589,26 +2415,16 @@ 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 @@ -2618,19 +2434,12 @@ 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 @@ -2661,7 +2470,6 @@ workstation: title: filter_through type: object from_vessel: - additionalProperties: false properties: category: type: string @@ -2680,26 +2488,16 @@ 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 @@ -2709,19 +2507,12 @@ 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 @@ -2752,11 +2543,8 @@ 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 @@ -2775,26 +2563,16 @@ 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 @@ -2804,19 +2582,12 @@ 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 @@ -2846,10 +2617,17 @@ 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 @@ -2857,6 +2635,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: FilterThrough_Result type: object required: @@ -2927,14 +2709,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 @@ -2945,12 +2727,8 @@ 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 @@ -2959,7 +2737,6 @@ workstation: time_spec: type: string vessel: - additionalProperties: false properties: category: type: string @@ -2978,26 +2755,16 @@ 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 @@ -3007,19 +2774,12 @@ 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 @@ -3049,10 +2809,20 @@ 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 @@ -3060,6 +2830,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: HeatChill_Result type: object required: @@ -3116,23 +2890,20 @@ 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 @@ -3151,26 +2922,16 @@ 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 @@ -3180,19 +2941,12 @@ 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 @@ -3222,15 +2976,21 @@ 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: @@ -3283,17 +3043,16 @@ 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 @@ -3312,26 +3071,16 @@ 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 @@ -3341,19 +3090,12 @@ 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 @@ -3383,15 +3125,19 @@ 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: @@ -3448,25 +3194,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: Hydrogenate_Feedback type: object goal: - additionalProperties: false properties: temp: type: string time: type: string vessel: - additionalProperties: false properties: category: type: string @@ -3485,26 +3229,16 @@ 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 @@ -3514,19 +3248,12 @@ 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 @@ -3556,10 +3283,13 @@ workstation: - data title: vessel type: object + required: + - temp + - time + - vessel title: Hydrogenate_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -3567,6 +3297,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Hydrogenate_Result type: object required: @@ -3682,14 +3416,12 @@ workstation: description: '' properties: feedback: - additionalProperties: false properties: current_device: type: string status: type: string time_remaining: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3705,7 +3437,6 @@ workstation: title: time_remaining type: object time_spent: - additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3720,21 +3451,22 @@ 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 @@ -3753,26 +3485,16 @@ 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 @@ -3782,19 +3504,12 @@ 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 @@ -3833,19 +3548,14 @@ 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 @@ -3864,26 +3574,16 @@ 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 @@ -3893,19 +3593,12 @@ 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 @@ -3936,24 +3629,38 @@ 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: @@ -4024,18 +3731,17 @@ 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 @@ -4044,7 +3750,6 @@ workstation: solvent2: type: string vessel: - additionalProperties: false properties: category: type: string @@ -4063,26 +3768,16 @@ 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 @@ -4092,19 +3787,12 @@ 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 @@ -4136,10 +3824,15 @@ workstation: type: object volume: type: string + required: + - ratio + - solvent1 + - solvent2 + - vessel + - volume title: Recrystallize_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -4147,6 +3840,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Recrystallize_Result type: object required: @@ -4193,23 +3890,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: ResetHandling_Feedback type: object goal: - additionalProperties: false properties: solvent: type: string vessel: - additionalProperties: false properties: category: type: string @@ -4228,26 +3923,16 @@ 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 @@ -4257,19 +3942,12 @@ 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 @@ -4299,10 +3977,12 @@ workstation: - data title: vessel type: object + required: + - solvent + - vessel title: ResetHandling_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -4310,6 +3990,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: ResetHandling_Result type: object required: @@ -4403,23 +4087,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: RunColumn_Feedback type: object goal: - additionalProperties: false properties: column: type: string from_vessel: - additionalProperties: false properties: category: type: string @@ -4438,26 +4120,16 @@ 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 @@ -4467,19 +4139,12 @@ 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 @@ -4522,7 +4187,6 @@ workstation: solvent2: type: string to_vessel: - additionalProperties: false properties: category: type: string @@ -4541,26 +4205,16 @@ 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 @@ -4570,19 +4224,12 @@ 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 @@ -4612,10 +4259,19 @@ 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 @@ -4623,6 +4279,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: RunColumn_Result type: object required: @@ -4835,21 +4495,19 @@ 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 @@ -4868,26 +4526,16 @@ 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 @@ -4897,19 +4545,12 @@ 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 @@ -4942,7 +4583,6 @@ workstation: product_phase: type: string product_vessel: - additionalProperties: false properties: category: type: string @@ -4961,26 +4601,16 @@ 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 @@ -4990,19 +4620,12 @@ 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 @@ -5039,7 +4662,6 @@ workstation: minimum: -2147483648 type: integer separation_vessel: - additionalProperties: false properties: category: type: string @@ -5058,26 +4680,16 @@ 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 @@ -5087,19 +4699,12 @@ 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 @@ -5130,25 +4735,18 @@ 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 @@ -5167,26 +4765,16 @@ 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 @@ -5196,19 +4784,12 @@ 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 @@ -5239,7 +4820,6 @@ workstation: title: to_vessel type: object vessel: - additionalProperties: false properties: category: type: string @@ -5258,26 +4838,16 @@ 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 @@ -5287,19 +4857,12 @@ 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 @@ -5332,7 +4895,6 @@ workstation: volume: type: string waste_phase_to_vessel: - additionalProperties: false properties: category: type: string @@ -5351,26 +4913,16 @@ 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 @@ -5380,19 +4932,12 @@ 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 @@ -5423,7 +4968,6 @@ workstation: title: waste_phase_to_vessel type: object waste_vessel: - additionalProperties: false properties: category: type: string @@ -5442,26 +4986,16 @@ 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 @@ -5471,19 +5005,12 @@ 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 @@ -5513,10 +5040,27 @@ 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 @@ -5524,6 +5068,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Separate_Result type: object required: @@ -5580,31 +5128,26 @@ 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 @@ -5623,26 +5166,16 @@ 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 @@ -5652,19 +5185,12 @@ 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 @@ -5694,10 +5220,13 @@ workstation: - data title: vessel type: object + required: + - vessel + - stir_speed + - purpose title: StartStir_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -5705,6 +5234,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: StartStir_Result type: object required: @@ -5769,33 +5302,28 @@ 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 @@ -5814,26 +5342,16 @@ 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 @@ -5843,19 +5361,12 @@ 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 @@ -5885,10 +5396,17 @@ 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 @@ -5896,6 +5414,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Stir_Result type: object required: @@ -5948,21 +5470,19 @@ 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 @@ -5981,26 +5501,16 @@ 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 @@ -6010,19 +5520,12 @@ 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 @@ -6052,10 +5555,11 @@ workstation: - data title: vessel type: object + required: + - vessel title: StopStir_Goal type: object result: - additionalProperties: false properties: message: type: string @@ -6063,6 +5567,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: StopStir_Result type: object required: @@ -6130,22 +5638,20 @@ 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 @@ -6158,27 +5664,31 @@ 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 @@ -6186,6 +5696,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: Transfer_Result type: object required: @@ -6293,23 +5807,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: WashSolid_Feedback type: object goal: - additionalProperties: false properties: event: type: string filtrate_vessel: - additionalProperties: false properties: category: type: string @@ -6328,26 +5840,16 @@ 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 @@ -6357,19 +5859,12 @@ 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 @@ -6412,17 +5907,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 time: type: string vessel: - additionalProperties: false properties: category: type: string @@ -6441,26 +5931,16 @@ 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 @@ -6470,19 +5950,12 @@ 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 @@ -6516,10 +5989,23 @@ 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 @@ -6527,6 +6013,10 @@ workstation: type: string success: type: boolean + required: + - success + - message + - return_info title: WashSolid_Result type: object required: @@ -6545,7 +6035,7 @@ workstation: config: properties: deck: - type: object + type: string protocol_type: items: type: string diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml index 2b49ae552..cbdf8aa83 100644 --- a/unilabos/registry/devices/xrd_d7mate.yaml +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -45,6 +45,31 @@ 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: {} @@ -60,14 +85,11 @@ xrd_d7mate: goal: properties: params: - anyOf: - - type: string - - type: object + type: string required: - params type: object - result: - type: object + result: {} required: - goal title: start_from_string参数 @@ -83,18 +105,21 @@ xrd_d7mate: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -105,38 +130,38 @@ xrd_d7mate: get_sample_down: feedback: {} goal: - int_input: int_input - sample_station: sample_station + sample_station: 1 goal_default: int_input: 0 handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -154,18 +179,21 @@ xrd_d7mate: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -183,18 +211,21 @@ xrd_d7mate: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -207,25 +238,26 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -242,35 +274,42 @@ xrd_d7mate: sample_id: '' start_theta: 10.0 goal_default: - end_theta: null - exp_time: null - increment: null - sample_id: null - start_theta: null + end_theta: 80.0 + exp_time: 0.5 + increment: 0.02 + sample_id: Sample001 + start_theta: 10.0 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 @@ -281,11 +320,19 @@ 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: send_sample_ready参数 + title: SampleReadyInput type: object type: UniLabJsonCommand set_power_off: @@ -293,25 +340,26 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -324,25 +372,26 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -356,16 +405,18 @@ xrd_d7mate: current: 30.0 voltage: 40.0 goal_default: - current: null - voltage: null + current: 30.0 + voltage: 40.0 handles: {} - placeholder_keys: {} result: {} schema: description: 设置高压电源电压和电流 properties: feedback: + properties: {} + required: [] title: VoltageCurrentInput_Feedback + type: object goal: properties: current: @@ -380,11 +431,19 @@ 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: set_voltage_current参数 + title: VoltageCurrentInput type: object type: UniLabJsonCommand start: @@ -394,12 +453,11 @@ 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: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。 @@ -408,42 +466,54 @@ xrd_d7mate: goal: properties: end_theta: - default: 80.0 description: 结束角度(≥5.5°,且必须大于start_theta) - type: number + minimum: 5.5 + type: string exp_time: - default: 0.1 description: 曝光时间(0.1-5.0秒) - type: number + maximum: 5.0 + minimum: 0.1 + type: string increment: - default: 0.05 description: 角度增量(≥0.005) - type: number + minimum: 0.005 + type: string sample_id: - default: '' description: 样品标识符 type: string start_theta: - default: 10.0 description: 起始角度(≥5°) - type: number + minimum: 5.0 + type: string string: - default: '' description: 字符串格式的参数输入,如果提供则优先解析使用 type: string wait_minutes: - default: 3.0 description: 允许上样后等待分钟数 + minimum: 0.0 type: number - required: [] + required: + - sample_id + - start_theta + - end_theta + - increment + - exp_time 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: start参数 + title: StartWorkflow type: object type: UniLabJsonCommand start_auto_mode: @@ -451,15 +521,17 @@ xrd_d7mate: goal: status: true goal_default: - status: null + status: true handles: {} - placeholder_keys: {} result: {} schema: description: 启动或停止自动模式 properties: feedback: + properties: {} + required: [] title: BoolSingleInput_Feedback + type: object goal: properties: status: @@ -470,16 +542,25 @@ 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: start_auto_mode参数 + title: BoolSingleInput 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 @@ -505,13 +586,16 @@ xrd_d7mate: properties: current_acquire_data: type: object + sample_down: + type: object sample_request: type: object sample_status: type: object required: - - current_acquire_data - sample_request + - current_acquire_data - 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 37adbd795..607af9b9b 100644 --- a/unilabos/registry/devices/zhida_gcms.yaml +++ b/unilabos/registry/devices/zhida_gcms.yaml @@ -8,25 +8,26 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -76,6 +77,31 @@ 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: {} @@ -86,18 +112,21 @@ zhida_gcms: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -115,18 +144,21 @@ zhida_gcms: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -144,18 +176,21 @@ zhida_gcms: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -168,25 +203,26 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - placeholder_keys: {} - result: - return_info: return_info + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Feedback type: object goal: - additionalProperties: true + properties: {} + required: [] title: EmptyIn_Goal type: object result: - additionalProperties: false properties: return_info: type: string + required: + - return_info title: EmptyIn_Result type: object required: @@ -198,35 +234,35 @@ zhida_gcms: feedback: {} goal: string: string - text: text goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -237,36 +273,36 @@ zhida_gcms: start_with_csv_file: feedback: {} goal: - csv_file_path: csv_file_path string: string goal_default: string: '' handles: {} - placeholder_keys: {} - result: - return_info: return_info - success: success + result: {} schema: description: '' properties: feedback: - additionalProperties: true + properties: {} + required: [] 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: @@ -307,8 +343,8 @@ zhida_gcms: version: type: object required: - - methods - status + - methods - version type: object version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 8841764cc..2a277664a 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,58 +1,20 @@ -""" -统一注册表系统 - -合并了原 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, Optional, Tuple, Union +from typing import Any, Dict, List, Union, Tuple 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, @@ -61,401 +23,426 @@ ) from unilabos.utils import logger from unilabos.utils.decorator import singleton -from unilabos.utils.cls_creator import import_class -from unilabos.utils.import_manager import get_enhanced_class_info +from unilabos.utils.import_manager import get_enhanced_class_info, get_class from unilabos.utils.type_check import NoAliasDumper -from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance -from msgcenterpy.instances.ros2_instance import ROS2MessageInstance -_module_hash_cache: Dict[str, Optional[str]] = {} +DEFAULT_PATHS = [Path(__file__).absolute().parent] -@singleton -class Registry: - """ - 统一注册表。 +class ROSMsgNotFound(Exception): + pass - 核心流程: - 1. AST 静态扫描 @device/@resource 装饰器 (快速, 无需 import) - 2. 加载 YAML 注册表 (兼容旧格式) - 3. 设置 host_node 内置设备 - 4. verify & resolve (实际 import 验证 + 类型解析) - """ +@singleton +class Registry: 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: + except OSError as e: pass - self.registry_paths = [Path(__file__).absolute().parent] + self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 if registry_paths: self.registry_paths.extend(registry_paths) - logger.debug(f"[UniLab Registry] registry_paths: {self.registry_paths}") - - 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): - """统一构建注册表入口。""" + 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中 + + def setup(self, complete_registry=False, upload_registry=False): + # 检查是否已调用过setup if self._setup_called: logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用") return - self._startup_executor = ThreadPoolExecutor( - max_workers=8, thread_name_prefix="RegistryStartup" - ) + from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - # 1. AST 静态扫描 (快速, 无需 import) - self._run_ast_scan(devices_dirs, upload_registry=upload_registry) + # 获取 HostNode 类的增强信息,用于自动生成 action schema + host_node_enhanced_info = get_enhanced_class_info( + "unilabos.ros.nodes.presets.host_node:HostNode", use_dynamic=True + ) - # 2. Host node 内置设备 - self._setup_host_node() + # 为 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"] = "用于测试延迟的动作,返回延迟时间和时间差。" - # 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=complete_registry) - if BasicConfig.enable_resource_load: - self.load_resource_types(path, upload_registry, complete_registry=complete_registry) - else: - 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", - }, - ] - } + 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"] = "用于测试物料、设备和样本。" - create_resource_action = ast_actions.get("auto-create_resource", {}) + create_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("create_resource", {}) + create_resource_schema = self._generate_unilab_json_command_schema( + create_resource_method_info.get("args", []), + "create_resource", + create_resource_method_info.get("return_annotation"), + ) + create_resource_schema["description"] = "用于创建物料" raw_create_resource_schema = ros_action_to_json_schema( - ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。" + self.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", + raw_create_resource_schema["properties"]["result"] = create_resource_schema["properties"]["result"] + + 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", }, - { - "handler_key": "liquid_slots", - "data_type": "resource", - "label": "LiquidSlots", - "data_source": "executor", - "data_key": "liquid_input_resource_tree.@flatten", + "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", }, - { - "handler_key": "materials", - "data_type": "resource", - "label": "AllMaterials", - "data_source": "executor", - "data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten", + "feedback": {}, + "result": {"success": "success"}, + "schema": raw_create_resource_schema, + "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", - "device_id": "unilabos_devices", - "parent": "unilabos_nodes", - "class_name": "unilabos_class", + "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", # 不为空 + }, + ] + }, + }, }, }, - "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": "/", - } + "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----------") + 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) + if BasicConfig.enable_resource_load: + self.load_resource_types(path, complete_registry, upload_registry) + else: + logger.warning("跳过了资源注册表加载!") + logger.info("[UniLab Registry] 注册表设置完成") + # 标记setup已被调用 + self._setup_called = True - # ------------------------------------------------------------------ - # AST 静态扫描 - # ------------------------------------------------------------------ - - def _run_ast_scan(self, devices_dirs=None, upload_registry=False): + def _load_single_resource_file( + self, file: Path, complete_registry: bool, upload_registry: bool + ) -> Tuple[Dict[str, Any], Dict[str, Any], bool]: """ - 执行 AST 静态扫描,从 Python 代码中提取 @device / @resource 装饰器元数据。 - 无需 import 任何驱动模块,速度极快。 + 加载单个资源文件 (线程安全) - 所有缓存(AST 扫描 / build 结果 / config_info)统一存放在 - registry_cache.pkl 一个文件中,删除即可完全重置。 + Returns: + (data, complete_data, is_valid): 资源数据, 完整数据, 是否有效 """ - import time as _time - from unilabos.registry.ast_registry_scanner import scan_directory + try: + with open(file, encoding="utf-8", mode="r") as f: + data = yaml.safe_load(io.StringIO(f.read())) + except Exception as e: + logger.warning(f"[UniLab Registry] 读取资源文件失败: {file}, 错误: {e}") + return {}, {}, False - scan_t0 = _time.perf_counter() + if not data: + return {}, {}, False - # 确保 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 启用加载)" - ) + complete_data = {} + for resource_id, resource_info in data.items(): + if "version" not in resource_info: + resource_info["version"] = "1.0.0" + if "category" not in resource_info: + resource_info["category"] = [file.stem] + elif file.stem not in resource_info["category"]: + resource_info["category"].append(file.stem) + elif not isinstance(resource_info.get("category"), list): + resource_info["category"] = [resource_info["category"]] + if "config_info" not in resource_info: + resource_info["config_info"] = [] + if "icon" not in resource_info: + resource_info["icon"] = "" + if "handles" not in resource_info: + resource_info["handles"] = [] + if "init_param_schema" not in resource_info: + resource_info["init_param_schema"] = {} + if "config_info" in resource_info: + del resource_info["config_info"] + 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("\\", "/") - # 合并缓存统计 - total_stats = scan_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0}) + complete_data = dict(sorted(complete_data.items())) + complete_data = copy.deepcopy(complete_data) - # 额外目录逐个扫描并合并 - 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']} 重新解析" - ) + if complete_registry: + 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}") + + return data, complete_data, True + + def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool): + 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)}") - 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 构建" + if not files: + return + + # 使用线程池并行加载 + max_workers = min(8, len(files)) + results = [] + + 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}") + + # 线程安全地更新注册表 + 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())}" ) - else: - cached_build = None + current_resource_number += 1 - if not cached_build: - build_t0 = _time.perf_counter() + # 记录无效文件 + 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()}") - 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 + def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]: + """ + 从模块字符串中提取类和方法的docstring信息 - 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 + Args: + module_string: 模块字符串,格式为 "module.path:ClassName" - build_elapsed = _time.perf_counter() - build_t0 - logger.info(f"[UniLab Registry] entry 构建耗时: {build_elapsed:.2f}s") + Returns: + 包含类和方法docstring信息的字典 + """ + docstrings = {"class_docstring": "", "methods": {}} - 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}, - } + if not module_string or ":" not in module_string: + return docstrings - # upload 模式下,利用线程池并行 import pylabrobot 资源并生成 config_info - if upload_registry: - self._populate_resource_config_info(config_cache=unified_cache) + try: + module_path, class_name = module_string.split(":", 1) - # 统一保存一次 - self._save_config_cache(unified_cache) + # 动态导入模块 + module = importlib.import_module(module_path) - 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 hasattr(module, class_name): + cls = getattr(module, class_name) - if own_executor: - self._startup_executor.shutdown(wait=False) - self._startup_executor = None + # 获取类的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: - """将类型名称替换为实际的 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 + Args: + type_name: 类型名称 + device_id: 设备ID,用于错误信息 + field_name: 字段名称,用于错误信息 + + Returns: + 找到的类对象或原始字符串 - convert_manager = { + 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) + type_name = convert_manager.get(type_name, type_name) # 替换为ROS2类型 if ":" in type_name: type_class = msg_converter_manager.get_class(type_name) else: @@ -463,89 +450,118 @@ def _resolve_type_uncached(self, type_name: str, device_id: str, field_name: str if type_class: return type_class else: - logger.trace( - f"[Registry] 类型 '{type_name}' 非 ROS2 消息类型 (设备 {device_id} {field_name}),映射为 String" - ) - return String + logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id} 的 {field_name}") + raise ROSMsgNotFound(f"类型 '{type_name}' 未找到,用于设备 {device_id} 的 {field_name}") - # ---- 类型字符串 -> JSON Schema type ---- - # (常量和工具函数已移至 unilabos.registry.utils) + 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", + } + + # 遍历映射找到匹配的类型 + for type_variants, json_type in type_mapping.items(): + if type_lower in type_variants: + return json_type + + # 特殊处理包含冒号的类型(如ROS消息类型) + if ":" in type_lower: + return "object" + + # 默认返回字符串类型 + return "string" 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, + self, + param_name: str, + param_type: Union[str, Tuple[str]], + param_default: Any, ) -> 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): + """ + 根据参数信息生成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 = get_json_schema_type(outer_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 - # 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} + + # 根据外层类型设置内层类型信息 + 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_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 + ) + status_schema["required"].append(status_name) + return status_schema + def _generate_unilab_json_command_schema( - self, method_args: list, docstring: Optional[str] = None, - import_map: Optional[Dict[str, str]] = None, + self, + method_args: List[Dict[str, Any]], + method_name: str, + return_annotation: Any = None, + previous_schema: Dict[str, Any] | None = None, ) -> Dict[str, Any]: - """根据方法参数和 docstring 生成 UniLabJsonCommand schema""" - doc_info = parse_docstring(docstring) - param_descs = doc_info.get("params", {}) + """ + 根据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": {}, @@ -556,1113 +572,155 @@ def _generate_unilab_json_command_schema( 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"} + 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, import_map=import_map + param_name, param_type, param_default ) - - 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 + # 生成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) - def _generate_status_types_schema(self, status_methods: Dict[str, Any]) -> Dict[str, Any]: - """根据 status 方法信息生成 status_types schema""" - status_schema: Dict[str, Any] = { + final_schema = { + "title": f"{method_name}参数", + "description": f"", "type": "object", - "properties": {}, - "required": [], + "properties": {"goal": schema, "feedback": {}, "result": result_schema}, + "required": ["goal"], } - 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) + # 保留之前 schema 中 goal/feedback/result 下一级字段的 description + if previous_schema: + self._preserve_field_descriptions(final_schema, previous_schema) - # ------------------------------------------------------------------ - # 动态类信息提取 (import-based) - # ------------------------------------------------------------------ + return final_schema - def _extract_class_info(self, cls: type) -> Dict[str, Any]: - """ - 从类中提取 init 参数、状态方法和动作方法信息。 + def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]) -> None: """ - 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": {}, - } + 保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title - # ------------------------------------------------------------------ - # 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). + Args: + new_schema: 新生成的 schema(会被修改) + previous_schema: 之前的 schema """ - 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 - ) + for section in ["goal", "feedback", "result"]: + new_section = new_schema.get("properties", {}).get(section, {}) + prev_section = previous_schema.get("properties", {}).get(section, {}) - 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"): + if not new_section or not prev_section: 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 + new_props = new_section.get("properties", {}) + prev_props = prev_section.get("properties", {}) - action_value_mappings = dict(sorted(action_value_mappings.items())) + 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"] - # --- 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 _is_typed_dict(self, annotation: Any) -> bool: + """ + 检查类型注解是否是TypedDict - def _ast_scan_module(self, module_str: str) -> Optional[Dict[str, Any]]: - """对单个 module_str 做定向 AST 扫描,返回 ast_meta 或 None。 + Args: + annotation: 类型注解对象 - 用于 complete_registry 模式下 YAML 中存在但 AST 全量扫描未覆盖的设备/资源。 - 仅做文件定位 + AST 解析,不实例化类。 + Returns: + 是否为TypedDict """ - 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 + if annotation is None or annotation == inspect.Parameter.empty: + return False - 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 {} + # 使用 typing_extensions.is_typeddict 进行检查(Python < 3.12 兼容) 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 {} + from typing_extensions import is_typeddict - 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 + return is_typeddict(annotation) + except ImportError: + # 回退方案:检查 TypedDict 特有的属性 + if isinstance(annotation, type): + return hasattr(annotation, "__required_keys__") and hasattr(annotation, "__optional_keys__") + return False - def _populate_resource_config_info(self, config_cache: Optional[dict] = None): + def _generate_typed_dict_result_schema(self, return_annotation: Any) -> Dict[str, Any]: """ - 利用线程池并行 import pylabrobot 资源类,生成 config_info。 - 仅在 upload_registry=True 时调用。 - - 启用缓存:以 module_str 为 key,记录源文件 MD5。若源文件未变则 - 直接复用上次的 config_info,跳过 import + 实例化 + dump。 + 根据TypedDict类型生成result的JSON Schema Args: - config_cache: 共享的缓存 dict。未提供时自行加载/保存; - 由 load_resource_types 传入时由调用方统一保存。 - """ - import time as _time + return_annotation: TypedDict类型注解 - 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 验证(使用共享线程池并行)。 + Returns: + JSON Schema格式的result schema """ - 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}" + if not self._is_typed_dict(return_annotation): + return {} - 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)" - ) + from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance - # ------------------------------------------------------------------ - # YAML 注册表加载 (兼容旧格式) - # ------------------------------------------------------------------ + 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 _load_single_resource_file( - self, file: Path, complete_registry: bool - ) -> Tuple[Dict[str, Any], Dict[str, Any], bool]: + def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): """ - 加载单个资源文件 (线程安全) + 为设备配置添加内置的执行驱动命令动作 - Returns: - (data, complete_data, is_valid): 资源数据, 完整数据, 是否有效 + Args: + device_config: 设备配置字典 + device_id: 设备ID """ - try: - with open(file, encoding="utf-8", mode="r") as f: - data = yaml.safe_load(io.StringIO(f.read())) - except Exception as e: - logger.warning(f"[UniLab Registry] 读取资源文件失败: {file}, 错误: {e}") - return {}, {}, False - - if not data: - 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: - resource_info["category"] = [file.stem] - elif file.stem not in resource_info["category"]: - resource_info["category"].append(file.stem) - elif not isinstance(resource_info.get("category"), list): - resource_info["category"] = [resource_info["category"]] - if "config_info" not in resource_info: - resource_info["config_info"] = [] - if "icon" not in resource_info: - resource_info["icon"] = "" - if "handles" not in resource_info: - resource_info["handles"] = [] - if "init_param_schema" not in resource_info: - resource_info["init_param_schema"] = {} - if "config_info" in resource_info: - del resource_info["config_info"] - if "file_path" in resource_info: - del resource_info["file_path"] - complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items()))) - 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())) - - 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(write_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) - except Exception as e: - logger.warning(f"[UniLab Registry] 写入资源文件失败: {file}, 错误: {e}") + from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - return data, complete_data, True - - def load_resource_types(self, path: os.PathLike, upload_registry: bool, complete_registry: bool = False): - abs_path = Path(path).absolute() - 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: + if "class" not in device_config: return - import hashlib as _hl - - # --- 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]] = {} + if "action_value_mappings" not in device_config["class"]: + device_config["class"]["action_value_mappings"] = {} - if complete_registry: - uncached_files = files - yaml_cache_misses = len(files) - else: - 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 - - # 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 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}") - - # upload 模式下,统一利用线程池为 pylabrobot 资源生成 config_info - if upload_registry: - self._populate_resource_config_info(config_cache=config_cache) - - # 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) - - 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} 重新加载" - ) + 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 + self, file: Path, complete_registry: bool, get_yaml_from_goal_type ) -> Tuple[Dict[str, Any], Dict[str, Any], bool, List[str]]: """ 加载单个设备文件 (线程安全) @@ -1688,12 +746,7 @@ def _load_single_device_file( 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: @@ -1710,18 +763,7 @@ def _load_single_device_file( 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 ( @@ -1729,21 +771,15 @@ def _load_single_device_file( 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"]) - if not enhanced_info.get("ast_analysis_success", False): + enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True) + if not enhanced_info.get("dynamic_import_success", False): continue - 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 映射 --- + device_config["class"]["status_types"].update( + {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()} + ) 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" @@ -1756,137 +792,68 @@ def _load_single_device_file( 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 = dict(device_config["class"]["action_value_mappings"]) + old_action_configs = {} + for action_name, action_config in device_config["class"]["action_value_mappings"].items(): + old_action_configs[action_name] = action_config device_config["class"]["action_value_mappings"] = { k: v for k, v in device_config["class"]["action_value_mappings"].items() if not k.startswith("auto-") } - 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, + 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"] } - if v.get("always_free"): - entry["always_free"] = True - device_config["class"]["action_value_mappings"][action_key] = entry - + ) + 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"] = {} - init_schema = self._generate_unilab_json_command_schema( - enhanced_info["init_params"], "__init__", - import_map=enhanced_import_map, + 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"] ) - 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()) @@ -1911,82 +878,37 @@ def _load_single_device_file( continue action_str_type_mapping[action_type_str] = target_type if target_type is not None: - 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["goal_default"] = yaml.safe_load( + io.StringIO(get_yaml_from_goal_type(target_type.Goal)) + ) 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(): - if status_type in status_str_type_mapping: - device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] + 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.get("type") in action_str_type_mapping: - action_config["type"] = action_str_type_mapping[action_config["type"]] - + if action_config["type"] not in action_str_type_mapping: + continue + 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) - 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}") + 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}") return data, complete_data, True, device_ids - 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() + def load_device_types(self, path: os.PathLike, complete_registry: bool): abs_path = Path(path).absolute() devices_path = abs_path / "devices" device_comms_path = abs_path / "device_comms" @@ -1999,80 +921,44 @@ def load_device_types(self, path: os.PathLike, complete_registry: bool = False): if not files: return - config_cache = self._load_config_cache() - yaml_dev_cache: dict = config_cache.get("_yaml_devices", {}) - cache_hits = 0 - uncached_files: list[Path] = [] + from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - if complete_registry: - uncached_files = files - else: - for file in files: - file_key = str(file.absolute()).replace("\\", "/") - try: - 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) - - executor = self._startup_executor - future_to_file = { - executor.submit( - self._load_single_device_file, file, complete_registry - ): file - for file in uncached_files - } + # 使用线程池并行加载 + max_workers = min(8, len(files)) + results = [] - 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}" - ) + 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] + 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}") + + # 线程安全地更新注册表 + 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 - # ------------------------------------------------------------------ - # 注册表信息输出 - # ------------------------------------------------------------------ + # 记录无效文件 + 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()}") def obtain_registry_device_info(self): devices = [] @@ -2080,6 +966,7 @@ def obtain_registry_device_info(self): 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 @@ -2089,9 +976,6 @@ def obtain_registry_device_info(self): 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结构存在 @@ -2115,10 +999,6 @@ def obtain_registry_device_info(self): 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) @@ -2131,76 +1011,35 @@ def obtain_registry_resource_info(self): 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, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False): +def build_registry(registry_paths=None, complete_registry=False, upload_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(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 + # 初始化注册表 + lab_registry.setup(complete_registry, upload_registry) return lab_registry diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index 199173723..f8e172612 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -9,6 +9,7 @@ YB_20ml_fenyeping: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_5ml_fenyeping: category: @@ -21,6 +22,7 @@ YB_5ml_fenyeping: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_jia_yang_tou_da: category: @@ -33,6 +35,7 @@ YB_jia_yang_tou_da: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_pei_ye_da_Bottle: category: @@ -45,6 +48,7 @@ YB_pei_ye_da_Bottle: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_pei_ye_xiao_Bottle: category: @@ -57,6 +61,7 @@ YB_pei_ye_xiao_Bottle: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_qiang_tou: category: @@ -69,6 +74,7 @@ YB_qiang_tou: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_ye_Bottle: category: @@ -82,4 +88,5 @@ 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 76b6b9387..4698a2664 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -9,6 +9,7 @@ YB_100ml_yeti: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_20ml_fenyepingban: category: @@ -21,6 +22,7 @@ YB_20ml_fenyepingban: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_5ml_fenyepingban: category: @@ -33,6 +35,7 @@ YB_5ml_fenyepingban: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_6StockCarrier: category: @@ -45,6 +48,7 @@ YB_6StockCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_6VialCarrier: category: @@ -57,6 +61,7 @@ YB_6VialCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_gao_nian_ye_Bottle: category: @@ -69,6 +74,7 @@ YB_gao_nian_ye_Bottle: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_gaonianye: category: @@ -81,6 +87,7 @@ YB_gaonianye: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_jia_yang_tou_da_Carrier: category: @@ -93,6 +100,7 @@ YB_jia_yang_tou_da_Carrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_peiyepingdaban: category: @@ -105,6 +113,7 @@ YB_peiyepingdaban: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_peiyepingxiaoban: category: @@ -117,6 +126,7 @@ YB_peiyepingxiaoban: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_qiang_tou_he: category: @@ -129,6 +139,7 @@ YB_qiang_tou_he: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_shi_pei_qi_kuai: category: @@ -141,6 +152,7 @@ YB_shi_pei_qi_kuai: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_ye: category: @@ -153,6 +165,7 @@ YB_ye: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 YB_ye_100ml_Bottle: category: @@ -165,4 +178,5 @@ 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 f72cc10d8..764a8aa5c 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -8,6 +8,7 @@ BIOYOND_PolymerStation_1BottleCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_1FlaskCarrier: category: @@ -19,6 +20,7 @@ BIOYOND_PolymerStation_1FlaskCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_6StockCarrier: category: @@ -30,6 +32,7 @@ BIOYOND_PolymerStation_6StockCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_8StockCarrier: category: @@ -41,4 +44,5 @@ 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 5770a2d12..8d6993b17 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -8,6 +8,7 @@ BIOYOND_PolymerPreparationStation_Deck: handles: [] icon: 配液站.webp init_param_schema: {} + registry_type: resource version: 1.0.0 BIOYOND_PolymerReactionStation_Deck: category: @@ -19,6 +20,7 @@ BIOYOND_PolymerReactionStation_Deck: handles: [] icon: 反应站.webp init_param_schema: {} + registry_type: resource version: 1.0.0 BIOYOND_YB_Deck: category: @@ -30,6 +32,7 @@ BIOYOND_YB_Deck: handles: [] icon: 配液站.webp init_param_schema: {} + registry_type: resource version: 1.0.0 CoincellDeck: category: @@ -41,4 +44,5 @@ 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 3f0aa9d2a..48dcab595 100644 --- a/unilabos/registry/resources/common/resource_container.yaml +++ b/unilabos/registry/resources/common/resource_container.yaml @@ -1,3 +1,24 @@ +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 @@ -19,6 +40,56 @@ 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: @@ -41,6 +112,7 @@ 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: @@ -63,6 +135,35 @@ 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: @@ -94,6 +195,7 @@ 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: @@ -125,4 +227,5 @@ 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 586e3cfeb..1652956e0 100644 --- a/unilabos/registry/resources/laiyu/container.yaml +++ b/unilabos/registry/resources/laiyu/container.yaml @@ -29,6 +29,7 @@ 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: @@ -61,4 +62,5 @@ 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 85da0ca7c..e6d930a58 100644 --- a/unilabos/registry/resources/laiyu/deck.yaml +++ b/unilabos/registry/resources/laiyu/deck.yaml @@ -12,4 +12,5 @@ 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 10e91cef3..8fa35ee51 100644 --- a/unilabos/registry/resources/opentrons/deck.yaml +++ b/unilabos/registry/resources/opentrons/deck.yaml @@ -12,6 +12,7 @@ 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: @@ -27,4 +28,5 @@ 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 d09bf7844..d2942d466 100644 --- a/unilabos/registry/resources/opentrons/plate_adapters.yaml +++ b/unilabos/registry/resources/opentrons/plate_adapters.yaml @@ -8,4 +8,5 @@ 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 20a71995a..02267ae08 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -8,6 +8,7 @@ appliedbiosystemsmicroamp_384_wellplate_40ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 biorad_384_wellplate_50ul: category: @@ -19,6 +20,7 @@ biorad_384_wellplate_50ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 biorad_96_wellplate_200ul_pcr: category: @@ -30,6 +32,7 @@ biorad_96_wellplate_200ul_pcr: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_12_wellplate_6point9ml_flat: category: @@ -41,6 +44,7 @@ corning_12_wellplate_6point9ml_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_24_wellplate_3point4ml_flat: category: @@ -52,6 +56,7 @@ corning_24_wellplate_3point4ml_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_384_wellplate_112ul_flat: category: @@ -63,6 +68,7 @@ corning_384_wellplate_112ul_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_48_wellplate_1point6ml_flat: category: @@ -74,6 +80,7 @@ corning_48_wellplate_1point6ml_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_6_wellplate_16point8ml_flat: category: @@ -85,6 +92,7 @@ corning_6_wellplate_16point8ml_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 corning_96_wellplate_360ul_flat: category: @@ -96,6 +104,7 @@ 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: @@ -127,6 +136,7 @@ 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: @@ -138,6 +148,7 @@ nest_96_wellplate_200ul_flat: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 nest_96_wellplate_2ml_deep: category: @@ -160,6 +171,7 @@ 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: @@ -171,6 +183,7 @@ thermoscientificnunc_96_wellplate_1300ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 thermoscientificnunc_96_wellplate_2000ul: category: @@ -182,6 +195,7 @@ thermoscientificnunc_96_wellplate_2000ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 usascientific_96_wellplate_2point4ml_deep: category: @@ -193,4 +207,5 @@ 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 6b2033d93..b2f7857bc 100644 --- a/unilabos/registry/resources/opentrons/reservoirs.yaml +++ b/unilabos/registry/resources/opentrons/reservoirs.yaml @@ -8,6 +8,7 @@ agilent_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 axygen_1_reservoir_90ml: category: @@ -19,6 +20,7 @@ axygen_1_reservoir_90ml: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 nest_12_reservoir_15ml: category: @@ -30,6 +32,7 @@ nest_12_reservoir_15ml: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 nest_1_reservoir_195ml: category: @@ -41,6 +44,7 @@ nest_1_reservoir_195ml: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 nest_1_reservoir_290ml: category: @@ -52,6 +56,7 @@ nest_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 usascientific_12_reservoir_22ml: category: @@ -63,4 +68,5 @@ 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 d1682b2af..cbc7d6f1c 100644 --- a/unilabos/registry/resources/opentrons/tip_racks.yaml +++ b/unilabos/registry/resources/opentrons/tip_racks.yaml @@ -8,6 +8,7 @@ eppendorf_96_tiprack_1000ul_eptips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 eppendorf_96_tiprack_10ul_eptips: category: @@ -19,6 +20,7 @@ eppendorf_96_tiprack_10ul_eptips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 geb_96_tiprack_1000ul: category: @@ -30,6 +32,7 @@ geb_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 geb_96_tiprack_10ul: category: @@ -41,6 +44,7 @@ geb_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_1000ul: category: @@ -71,6 +75,7 @@ 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: @@ -82,6 +87,7 @@ opentrons_96_filtertiprack_10ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_200ul: category: @@ -93,6 +99,7 @@ opentrons_96_filtertiprack_200ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_20ul: category: @@ -104,6 +111,7 @@ opentrons_96_filtertiprack_20ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_tiprack_1000ul: category: @@ -115,6 +123,7 @@ opentrons_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_tiprack_10ul: category: @@ -126,6 +135,7 @@ opentrons_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_tiprack_20ul: category: @@ -137,6 +147,7 @@ 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 33ec5dc92..32bf3e360 100644 --- a/unilabos/registry/resources/opentrons/tube_racks.yaml +++ b/unilabos/registry/resources/opentrons/tube_racks.yaml @@ -8,6 +8,7 @@ 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: @@ -19,6 +20,7 @@ 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: @@ -30,6 +32,7 @@ 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: @@ -41,6 +44,7 @@ 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: @@ -52,6 +56,7 @@ 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: @@ -63,6 +68,7 @@ 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: @@ -74,6 +80,7 @@ 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: @@ -85,6 +92,7 @@ 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: @@ -96,6 +104,7 @@ 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: @@ -107,6 +116,7 @@ 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: @@ -118,6 +128,7 @@ 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: @@ -129,6 +140,7 @@ 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: @@ -140,6 +152,7 @@ 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: @@ -151,6 +164,7 @@ 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: @@ -162,6 +176,7 @@ 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: @@ -173,6 +188,7 @@ 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: @@ -184,6 +200,7 @@ 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: @@ -195,6 +212,7 @@ 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: @@ -206,6 +224,7 @@ opentrons_6_tuberack_nest_50ml_conical: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 opentrons_96_well_aluminum_block: category: @@ -217,4 +236,5 @@ 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 240cdf799..a8fb9b6c8 100644 --- a/unilabos/registry/resources/organic/container.yaml +++ b/unilabos/registry/resources/organic/container.yaml @@ -29,4 +29,5 @@ 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 45054311a..ea30cb7df 100644 --- a/unilabos/registry/resources/post_process/bottle_carriers.yaml +++ b/unilabos/registry/resources/post_process/bottle_carriers.yaml @@ -8,6 +8,7 @@ POST_PROCESS_Raw_1BottleCarrier: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 POST_PROCESS_Reaction_1BottleCarrier: category: @@ -19,4 +20,5 @@ 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 e5d4cc8db..621cafc6c 100644 --- a/unilabos/registry/resources/post_process/deck.yaml +++ b/unilabos/registry/resources/post_process/deck.yaml @@ -9,4 +9,5 @@ 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 3e960f2ee..a769fee3d 100644 --- a/unilabos/registry/resources/prcxi/plate_adapters.yaml +++ b/unilabos/registry/resources/prcxi/plate_adapters.yaml @@ -9,6 +9,7 @@ PRCXI_30mm_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Adapter: category: @@ -21,6 +22,7 @@ PRCXI_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Deep10_Adapter: category: @@ -33,6 +35,7 @@ PRCXI_Deep10_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Deep300_Adapter: category: @@ -45,6 +48,7 @@ PRCXI_Deep300_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_PCR_Adapter: category: @@ -57,6 +61,7 @@ PRCXI_PCR_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Reservoir_Adapter: category: @@ -69,6 +74,7 @@ PRCXI_Reservoir_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Tip10_Adapter: category: @@ -81,6 +87,7 @@ PRCXI_Tip10_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Tip1250_Adapter: category: @@ -93,6 +100,7 @@ PRCXI_Tip1250_Adapter: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_Tip300_Adapter: category: @@ -105,4 +113,5 @@ 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 b8527dbfe..81e2ae967 100644 --- a/unilabos/registry/resources/prcxi/plates.yaml +++ b/unilabos/registry/resources/prcxi/plates.yaml @@ -9,6 +9,7 @@ PRCXI_48_DeepWell: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_96_DeepWell: category: @@ -21,6 +22,7 @@ PRCXI_96_DeepWell: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_AGenBio_4_troughplate: category: @@ -33,6 +35,7 @@ PRCXI_AGenBio_4_troughplate: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_BioER_96_wellplate: category: @@ -45,6 +48,7 @@ PRCXI_BioER_96_wellplate: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_BioRad_384_wellplate: category: @@ -57,6 +61,7 @@ PRCXI_BioRad_384_wellplate: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_CellTreat_96_wellplate: category: @@ -69,6 +74,7 @@ PRCXI_CellTreat_96_wellplate: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_nonskirted: category: @@ -81,6 +87,7 @@ PRCXI_PCR_Plate_200uL_nonskirted: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_semiskirted: category: @@ -93,6 +100,7 @@ PRCXI_PCR_Plate_200uL_semiskirted: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_skirted: category: @@ -105,6 +113,7 @@ PRCXI_PCR_Plate_200uL_skirted: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_nest_12_troughplate: category: @@ -117,6 +126,7 @@ PRCXI_nest_12_troughplate: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_nest_1_troughplate: category: @@ -129,4 +139,5 @@ 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 f6d2e7f05..56a16db83 100644 --- a/unilabos/registry/resources/prcxi/tip_racks.yaml +++ b/unilabos/registry/resources/prcxi/tip_racks.yaml @@ -9,6 +9,7 @@ PRCXI_1000uL_Tips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_10uL_Tips: category: @@ -21,6 +22,7 @@ PRCXI_10uL_Tips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_10ul_eTips: category: @@ -33,6 +35,7 @@ PRCXI_10ul_eTips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_1250uL_Tips: category: @@ -45,6 +48,7 @@ PRCXI_1250uL_Tips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_200uL_Tips: category: @@ -57,6 +61,7 @@ PRCXI_200uL_Tips: handles: [] icon: '' init_param_schema: {} + registry_type: resource version: 1.0.0 PRCXI_300ul_Tips: category: @@ -69,4 +74,5 @@ 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 952a832b4..f87a76249 100644 --- a/unilabos/registry/resources/prcxi/trash.yaml +++ b/unilabos/registry/resources/prcxi/trash.yaml @@ -9,4 +9,5 @@ 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 6510c16cd..0b1e07c62 100644 --- a/unilabos/registry/resources/prcxi/tube_racks.yaml +++ b/unilabos/registry/resources/prcxi/tube_racks.yaml @@ -9,4 +9,5 @@ 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 deleted file mode 100644 index 1ab7dd2cb..000000000 --- a/unilabos/registry/utils.py +++ /dev/null @@ -1,724 +0,0 @@ -""" -注册表工具函数 - -从 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/graphio.py b/unilabos/resources/graphio.py index 603c4c1df..38f969685 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -42,7 +42,7 @@ def canonicalize_nodes_data( Returns: ResourceTreeSet: 标准化后的资源树集合 """ - print_status(f"{len(nodes)} Resources loaded:", "info") + print_status(f"{len(nodes)} Resources loaded", "info") # 第一步:基本预处理(处理graphml的label字段) outer_host_node_id = None @@ -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", "machine_name"]: + if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]: v = node.pop(k) node["config"][k] = v if outer_host_node_id is not None: @@ -288,15 +288,6 @@ 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 @@ -381,15 +372,6 @@ 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 @@ -593,7 +575,7 @@ def resource_ulab_to_plr_inner(resource: dict): "size_y": resource["config"].get("size_y", 0), "size_z": resource["config"].get("size_z", 0), "location": {**resource["position"], "type": "Coordinate"}, - "rotation": {resource["config"].get("rotation", {"x": 0, "y": 0, "z": 0, "type": "Rotation"})}, # Resource如果没有rotation,是plr版本太低 + "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"}, # Resource如果没有rotation,是plr版本太低 "category": resource["type"], "model": resource["config"].get("model", None), # resource中deck没有model "children": ( diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 8b305bce1..b34d10cc0 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -120,7 +120,6 @@ class ResourceDictType(TypedDict): config: Dict[str, Any] data: Dict[str, Any] extra: Dict[str, Any] - machine_name: str # 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化 @@ -142,7 +141,6 @@ class ResourceDict(BaseModel): config: Dict[str, Any] = Field(description="Resource configuration") 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"]): @@ -198,30 +196,22 @@ def __init__(self, res_content: "ResourceDict"): self.typ = "dict" @classmethod - def get_resource_instance_from_dict(cls, content: ResourceDictType) -> "ResourceDictInstance": + def get_resource_instance_from_dict(cls, content: Dict[str, Any]) -> "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"] = {} @@ -232,24 +222,16 @@ def get_resource_instance_from_dict(cls, content: ResourceDictType) -> "Resource 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"] = ResourceDictPositionObjectType(x=0, y=0, z=0) + pose["position"] = {"x": 0, "y": 0, "z": 0} if "size" not in pose: - pose["size"] = ResourceDictPositionSizeType( - width= content["config"].get("size_x", 0), - height= content["config"].get("size_y", 0), - depth= content["config"].get("size_z", 0), - ) - if "rotation" not in pose: - pose["rotation"] = ResourceDictPositionObjectType( - x=content["config"].get("rotation", {}).get("x", 0), - y=content["config"].get("rotation", {}).get("y", 0), - z=content["config"].get("rotation", {}).get("z", 0), - ) + pose["size"] = { + "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) @@ -417,7 +399,7 @@ def __init__(self, resource_list: List[List[ResourceDictInstance]] | List[Resour ) @classmethod - def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False, old_size=False) -> "ResourceTreeSet": + def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False) -> "ResourceTreeSet": """ 从plr资源创建ResourceTreeSet """ @@ -440,20 +422,13 @@ def replace_plr_type(source: str): "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: - logger.trace(f"转换pylabrobot的时候,出现未知类型 {source}") + print("转换pylabrobot的时候,出现未知类型", source) return source def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None): @@ -508,7 +483,7 @@ def resource_plr_inner( k: v for k, v in d.items() if k - not in ([ + not in [ "name", "children", "parent_name", @@ -519,15 +494,7 @@ def resource_plr_inner( "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, @@ -826,8 +793,7 @@ def merge_remote_resources(self, remote_tree_set: "ResourceTreeSet") -> "Resourc if remote_root_type == "device": # 情况1: 一级是 device if remote_root_id not in local_device_map: - if remote_root_id != "host_node": - logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步") + logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步") continue local_device = local_device_map[remote_root_id] @@ -917,7 +883,7 @@ def merge_remote_resources(self, remote_tree_set: "ResourceTreeSet") -> "Resourc return self - def dump(self, old_position=False) -> List[List[Dict[str, Any]]]: + def dump(self) -> List[List[Dict[str, Any]]]: """ 将 ResourceTreeSet 序列化为嵌套列表格式 @@ -933,10 +899,6 @@ def dump(self, old_position=False) -> List[List[Dict[str, Any]]]: # 获取树的所有节点并序列化 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/main_slave_run.py b/unilabos/ros/main_slave_run.py index afb8f88cb..c24f9e8ee 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -90,7 +90,7 @@ def main( device_id="resource_mesh_manager", device_uuid=str(uuid.uuid4()), ) - joint_republisher = JointRepublisher("joint_republisher","", host_node.resource_tracker) + joint_republisher = JointRepublisher("joint_republisher", host_node.resource_tracker) # lh_joint_pub = LiquidHandlerJointPublisher( # resources_config=resources_list, resource_tracker=host_node.resource_tracker # ) diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 83e6f4568..b526d5f54 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -11,7 +11,6 @@ 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 @@ -717,19 +716,6 @@ 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 @@ -741,10 +727,46 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any Returns: 对应的 JSON Schema 定义 """ - schema = ROS2MessageInstance(msg_class()).get_json_schema() + schema = {"type": "object", "properties": {}, "required": []} + + # 优先使用字段名作为标题,否则使用类名 schema["title"] = field_name - schema.pop("description", None) - _strip_rosidl_descriptions(schema) + + # 获取消息的字段和字段类型 + 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)}") + return schema @@ -791,8 +813,6 @@ 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 ffc106c71..772e667b9 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -34,8 +34,7 @@ from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos.config.config import BasicConfig -from unilabos.registry.decorators import get_topic_config -from unilabos.utils.decorator import get_all_subscriptions +from unilabos.utils.decorator import get_topic_config, get_all_subscriptions from unilabos.resources.container import RegularContainer from unilabos.resources.graphio import ( @@ -58,7 +57,6 @@ from unilabos.resources.resource_tracker import ( DeviceNodeResourceTracker, - ResourceDictType, ResourceTreeSet, ResourceTreeInstance, ResourceDictInstance, @@ -196,9 +194,9 @@ def __init__( self._value = None try: self.publisher_ = node.create_publisher(msg_type, f"{name}", qos) - except Exception as e: + except AttributeError as ex: self.node.lab_logger().error( - f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}" + f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}" ) self.timer = node.create_timer(self.timer_period, self.publish_property) self.__loop = ROS2DeviceNode.get_asyncio_loop() @@ -598,12 +596,6 @@ def done_cb(*args): 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, - ), } # 向全局在线设备注册表添加设备信息 @@ -1072,48 +1064,6 @@ def _handle_update( 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"], @@ -1256,40 +1206,22 @@ def lab_logger(self): return self._lab_logger def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0): - """创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建。""" - # 检测 @topic_config 装饰器配置 + """创建ROS发布者""" + # 检测装饰器配置(支持 get_{attr_name} 方法和 @property) topic_config = {} - driver_class = type(self.driver_instance) - # 区分 @property 和普通方法两种情况 - is_prop = hasattr(driver_class, attr_name) and isinstance( - getattr(driver_class, attr_name), property - ) + # 优先检测 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) - 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 装饰器则跳过发布 + # 如果没有配置,检测 @property 装饰的属性 if not topic_config: - 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 + 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) # 使用装饰器配置或默认值 cfg_period = topic_config.get("period") @@ -1302,10 +1234,10 @@ def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0): # 获取属性值的方法 def get_device_attr(): try: - if is_prop: - return getattr(self.driver_instance, attr_name) + if hasattr(self.driver_instance, f"get_{attr_name}"): + return getattr(self.driver_instance, f"get_{attr_name}")() else: - return getattr(self.driver_instance, attr_name)() + 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( @@ -1317,8 +1249,8 @@ def get_device_attr(): ) self.lab_logger().error(traceback.format_exc()) - self._property_publishers[publish_name] = PropertyPublisher( - self, publish_name, get_device_attr, msg_type, period, print_publish, qos + self._property_publishers[attr_name] = PropertyPublisher( + self, attr_name, get_device_attr, msg_type, period, print_publish, qos ) def create_ros_action_server(self, action_name, action_value_mapping): @@ -1326,17 +1258,14 @@ def create_ros_action_server(self, action_name, action_value_mapping): action_type = action_value_mapping["type"] str_action_type = str(action_type)[8:-2] - 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._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, + ) + self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") def _setup_decorated_subscribers(self): @@ -1884,8 +1813,7 @@ async def _execute_driver_command_async(self, string: str): continue # 处理单个 ResourceSlot - _is_resource_slot = isinstance(arg_type, str) and arg_type.endswith(":ResourceSlot") - if _is_resource_slot: + if arg_type == "unilabos.registry.placeholder_type:ResourceSlot": resource_data = function_args[arg_name] if isinstance(resource_data, dict) and "id" in resource_data: try: @@ -1899,7 +1827,8 @@ async def _execute_driver_command_async(self, string: str): # 处理 ResourceSlot 列表 elif isinstance(arg_type, tuple) and len(arg_type) == 2: - if arg_type[0] == "list" and isinstance(arg_type[1], str) and arg_type[1].endswith(":ResourceSlot"): + resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot" + if arg_type[0] == "list" and arg_type[1] == resource_slot_type: 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 e94f001fe..2267f6760 100644 --- a/unilabos/ros/nodes/presets/camera.py +++ b/unilabos/ros/nodes/presets/camera.py @@ -4,14 +4,7 @@ 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 eb139f1fd..aa8b813fb 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -12,7 +12,6 @@ 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, @@ -24,7 +23,6 @@ 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 @@ -32,7 +30,6 @@ from unilabos.resources.registry import add_schema from unilabos.resources.resource_tracker import ( ResourceDict, - ResourceDictType, ResourceDictInstance, ResourceTreeSet, ResourceTreeInstance, @@ -89,7 +86,6 @@ class TestLatencyReturn(TypedDict): status: str -@device(id="host_node", category=[], description="Host Node", icon="icon_device.webp") class HostNode(BaseROS2DeviceNode): """ 主机节点类,负责管理设备、资源和控制器 @@ -278,42 +274,44 @@ def __init__( self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好 "/devices/host_node/create_resource": ActionClient( self, - ResourceCreateFromOuterEasy, + lab_registry.ResourceCreateFromOuterEasy, "/devices/host_node/create_resource", callback_group=self.callback_group, ), "/devices/host_node/create_resource_detailed": ActionClient( self, - ResourceCreateFromOuter, + lab_registry.ResourceCreateFromOuter, "/devices/host_node/create_resource_detailed", callback_group=self.callback_group, ), "/devices/host_node/test_latency": ActionClient( self, - EmptyIn, + lab_registry.EmptyIn, "/devices/host_node/test_latency", callback_group=self.callback_group, ), "/devices/host_node/test_resource": ActionClient( self, - EmptyIn, + lab_registry.EmptyIn, "/devices/host_node/test_resource", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command": ActionClient( self, - StrSingleInput, + lab_registry.StrSingleInput, "/devices/host_node/_execute_driver_command", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command_async": ActionClient( self, - StrSingleInput, + lab_registry.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}"} # 用于跟踪在线设备 @@ -331,18 +329,10 @@ def __init__( 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: @@ -668,12 +658,7 @@ def initialize_device(self, device_id: str, device_config: ResourceDictInstance) action_id = f"/devices/{device_id}/{action_name}" if action_id not in self._action_clients: action_type = action_value_mapping["type"] - 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._action_clients[action_id] = ActionClient(self, action_type, action_id) self.lab_logger().trace( f"[Host Node] Created ActionClient (Local): {action_id}" ) # 子设备再创建用的是Discover发现的 @@ -1273,9 +1258,9 @@ def _node_info_update_callback(self, request, response): # 用 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( @@ -1295,19 +1280,14 @@ def _node_info_update_callback(self, request, response): # 解析 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( @@ -1315,18 +1295,6 @@ def _node_info_update_callback(self, request, response): 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: @@ -1735,177 +1703,3 @@ def notify_resource_tree_update(self, device_id: str, action: str, resource_uuid 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 7f9f2aedc..902e29679 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -20,7 +20,7 @@ convert_from_ros_msg_with_mapping, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode -from unilabos.resources.resource_tracker import ResourceDictType, ResourceTreeSet, ResourceDictInstance +from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDictInstance from unilabos.utils.type_check import get_result_info_str if TYPE_CHECKING: @@ -177,103 +177,6 @@ def initialize_device(self, device_id, device_config): 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 15793b148..22a90736d 100644 --- a/unilabos/utils/decorator.py +++ b/unilabos/utils/decorator.py @@ -19,6 +19,74 @@ def get_instance(*args, **kwargs): 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, @@ -36,6 +104,24 @@ 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: @@ -43,6 +129,7 @@ def decorator(func: F) -> F: 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] @@ -54,7 +141,15 @@ def wrapper(*args, **kwargs): def get_subscribe_config(func) -> dict: - """获取函数上的订阅配置 (topic, msg_type, qos)""" + """ + 获取函数上的订阅配置 + + Args: + func: 被装饰的函数 + + Returns: + 包含 topic, msg_type, qos 的配置字典 + """ if hasattr(func, "_has_subscribe") and getattr(func, "_has_subscribe", False): return { "topic": getattr(func, "_subscribe_topic", None), @@ -68,6 +163,9 @@ def get_all_subscriptions(instance) -> list: """ 扫描实例的所有方法,获取带有 @subscribe 装饰器的方法及其配置 + Args: + instance: 要扫描的实例 + Returns: 包含 (method_name, method, config) 元组的列表 """ @@ -86,14 +184,92 @@ def get_all_subscriptions(instance) -> list: return subscriptions -# --------------------------------------------------------------------------- -# 向后兼容重导出 -- 已迁移到 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, -) +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) diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index a2bbd2621..73c0b10bf 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -22,7 +22,6 @@ def __init__(self): # "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", @@ -33,7 +32,7 @@ def __init__(self): # 包版本要求(包名: 最低版本) self.version_requirements = { - "msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求 + "msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求 } self.missing_packages = [] diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index 7fe2f501e..dabbe1a7d 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -21,11 +21,15 @@ "get_class", "get_module", "init_from_list", - "get_enhanced_class_info", + "get_class_info_static", + "get_registry_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: @@ -41,7 +45,6 @@ def __init__(self, module_list: Optional[List[str]] = None): 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: @@ -156,113 +159,193 @@ def search_class(self, class_name: str, search_lower=False) -> Optional[Type]: 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, **_kwargs) -> Dict[str, Any]: - """通过 AST 分析获取类的增强信息。 - - 复用 ``ast_registry_scanner`` 的 ``_collect_imports`` / ``_extract_class_body``, - 与 AST 扫描注册表完全一致。 + def get_enhanced_class_info(self, module_path: str, use_dynamic: bool = True) -> Dict[str, Any]: + """ + 获取增强的类信息,支持动态导入和静态分析 Args: - module_path: 格式 ``"module.path:ClassName"`` + module_path: 模块路径,格式为 "module.path" 或 "module.path:ClassName" + use_dynamic: 是否优先使用动态导入 Returns: - ``{"module_path", "ast_analysis_success", "import_map", - "init_params", "status_methods", "action_methods"}`` + 包含详细类信息的字典 """ - from unilabos.registry.ast_registry_scanner import ( - _collect_imports, - _extract_class_body, - _filepath_to_module, - ) - - result: Dict[str, Any] = { + result = { "module_path": module_path, - "ast_analysis_success": False, - "import_map": {}, - "init_params": [], + "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"], "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): - logger.warning(f"[ImportManager] 找不到模块文件: {module_name} -> {file_path}") - return result + raise FileNotFoundError(f"找不到模块文件: {module_name} -> {file_path}") - 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 + with open(file_path, "r", encoding="utf-8") as f: + source_code = f.read() - # 推导 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 + tree = ast.parse(source_code) - # 定位目标类 AST 节点 + # 查找目标类 target_class = None for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == class_name: - target_class = node - break + if isinstance(node, ast.ClassDef): + if node.name == class_name: + target_class = node + break if target_class is None: - logger.warning(f"[ImportManager] 在文件 {file_path} 中找不到类 {class_name}") - return result - - 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() + raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}") + + result = { + "class_name": class_name, + "init_params": {}, + "status_methods": {}, + "action_methods": {}, } - result["ast_analysis_success"] = True + + # 分析类的方法 + 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 return result def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) -> Dict[str, Any]: @@ -318,26 +401,23 @@ def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) - "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) -> Union[str, Tuple[str, Any]]: + def _get_return_type_from_method(self, method) -> str: """从方法中获取返回类型""" signature = inspect.signature(method) return self._get_type_string(signature.return_annotation) def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]: - """将类型注解转换为类型字符串。 - - 非内建类返回 ``module:ClassName`` 全路径(如 - ``"unilabos.registry.placeholder_type:ResourceSlot"``), - 避免短名冲突;内建类型直接返回短名(如 ``"str"``、``"int"``)。 - """ + """将类型注解转换为Class Library中可搜索的类名""" if annotation == inspect.Parameter.empty: - return "Any" + return "Any" # 如果没有注解,返回Any if annotation is None: - return "None" + return "None" # 明确的None类型 if hasattr(annotation, "__origin__"): + # 处理typing模块的类型 origin = annotation.__origin__ if origin in (list, set, tuple): if hasattr(annotation, "__args__") and annotation.__args__: @@ -352,26 +432,126 @@ def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]: return "dict" elif origin is Optional: return "Unknown" - return "Unknown" + return f"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__"): - module = getattr(annotation, "__module__", None) - if module and module != "builtins": - return f"{module}:{annotation.__name__}" - return annotation.__name__ + # 如果是内置类型 + if annotation.__module__ == "builtins": + return annotation.__name__ + else: + # 如果是自定义类,返回完整路径 + return f"{annotation.__module__}:{annotation.__name__}" + # 如果是typing模块的类型 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(".", "/") @@ -386,6 +566,222 @@ def _module_path_to_file_path(self, module_path: str) -> Optional[str]: 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 # 全局实例,便于直接使用 @@ -413,6 +809,16 @@ def init_from_list(module_list: List[str]) -> None: default_manager = ImportManager(module_list) -def get_enhanced_class_info(module_path: str, **kwargs) -> Dict[str, Any]: +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]: """获取增强的类信息的便捷函数""" - return default_manager.get_enhanced_class_info(module_path, **kwargs) + return default_manager.get_enhanced_class_info(module_path, use_dynamic) diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index da085f147..be5d8c312 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -217,6 +217,7 @@ 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 105d387d7..65d724fc1 100644 --- a/unilabos/utils/requirements.txt +++ b/unilabos/utils/requirements.txt @@ -1,8 +1,7 @@ networkx typing_extensions websockets -msgcenterpy>=0.1.8 -orjson>=3.11 +msgcenterpy>=0.1.5 opentrons_shared_data pint fastapi diff --git a/unilabos/utils/tools.py b/unilabos/utils/tools.py index 3c7b742ed..89195cbd2 100644 --- a/unilabos/utils/tools.py +++ b/unilabos/utils/tools.py @@ -1,39 +1,4 @@ -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) + return "".join(format(byte, "02x") for byte in uuid_array) \ No newline at end of file diff --git a/unilabos/utils/type_check.py b/unilabos/utils/type_check.py index e3df2dc2c..64001e561 100644 --- a/unilabos/utils/type_check.py +++ b/unilabos/utils/type_check.py @@ -15,21 +15,14 @@ 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): - try: - return json_default(obj) - except Exception: - return super().default(obj) + # 优先处理类型对象 + if isinstance(obj, type): + return str(obj)[8:-2] + return super().default(obj) class NoAliasDumper(yaml.SafeDumper): @@ -50,10 +43,13 @@ class ResultInfoEncoder(json.JSONEncoder): """专门用于处理任务执行结果信息的JSON编码器""" def default(self, obj): + # 优先处理类型对象 if isinstance(obj, type): - return json_default(obj) + return str(obj)[8:-2] + # 对于无法序列化的对象,统一转换为字符串 try: + # 尝试调用 __dict__ 或者其他序列化方法 if hasattr(obj, "__dict__"): return obj.__dict__ elif hasattr(obj, "_asdict"): # namedtuple @@ -63,8 +59,10 @@ def default(self, obj): elif hasattr(obj, "dict"): return obj.dict() else: + # 如果都不行,转换为字符串 return str(obj) except Exception: + # 如果转换失败,直接返回字符串表示 return str(obj) diff --git a/unilabos/workflow/__init__.py b/unilabos/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/unilabos/workflow/from_python_script.py b/unilabos/workflow/from_python_script.py new file mode 100644 index 000000000..5a8ce38ea --- /dev/null +++ b/unilabos/workflow/from_python_script.py @@ -0,0 +1,241 @@ +import ast +import json +from typing import Dict, List, Any, Tuple, Optional + +from .common import WorkflowGraph, RegistryAdapter + +Json = Dict[str, Any] + +# ---------------- Converter ---------------- + +class DeviceMethodConverter: + """ + - 字段统一:resource_name(原 device_class)、template_name(原 action_key) + - params 单层;inputs 使用 'params.' 前缀 + - SimpleGraph.add_workflow_node 负责变量连线与边 + """ + def __init__(self, device_registry: Optional[Dict[str, Any]] = None): + self.graph = WorkflowGraph() + self.variable_sources: Dict[str, Dict[str, Any]] = {} # var -> {node_id, output_name} + self.instance_to_resource: Dict[str, Optional[str]] = {} # 实例名 -> resource_name + self.node_id_counter: int = 0 + self.registry = RegistryAdapter(device_registry or {}) + + # ---- helpers ---- + def _new_node_id(self) -> int: + nid = self.node_id_counter + self.node_id_counter += 1 + return nid + + def _assign_targets(self, targets) -> List[str]: + names: List[str] = [] + import ast + if isinstance(targets, ast.Tuple): + for elt in targets.elts: + if isinstance(elt, ast.Name): + names.append(elt.id) + elif isinstance(targets, ast.Name): + names.append(targets.id) + return names + + def _extract_device_instantiation(self, node) -> Optional[Tuple[str, str]]: + import ast + if not isinstance(node.value, ast.Call): + return None + callee = node.value.func + if isinstance(callee, ast.Name): + class_name = callee.id + elif isinstance(callee, ast.Attribute) and isinstance(callee.value, ast.Name): + class_name = callee.attr + else: + return None + if isinstance(node.targets[0], ast.Name): + instance = node.targets[0].id + return instance, class_name + return None + + def _extract_call(self, call) -> Tuple[str, str, Dict[str, Any], str]: + import ast + owner_name, method_name, call_kind = "", "", "func" + if isinstance(call.func, ast.Attribute): + method_name = call.func.attr + if isinstance(call.func.value, ast.Name): + owner_name = call.func.value.id + call_kind = "instance" if owner_name in self.instance_to_resource else "class_or_module" + elif isinstance(call.func.value, ast.Attribute) and isinstance(call.func.value.value, ast.Name): + owner_name = call.func.value.attr + call_kind = "class_or_module" + elif isinstance(call.func, ast.Name): + method_name = call.func.id + call_kind = "func" + + def pack(node): + if isinstance(node, ast.Name): + return {"type": "variable", "value": node.id} + if isinstance(node, ast.Constant): + return {"type": "constant", "value": node.value} + if isinstance(node, ast.Dict): + return {"type": "dict", "value": self._parse_dict(node)} + if isinstance(node, ast.List): + return {"type": "list", "value": self._parse_list(node)} + return {"type": "raw", "value": ast.unparse(node) if hasattr(ast, "unparse") else str(node)} + + args: Dict[str, Any] = {} + pos: List[Any] = [] + for a in call.args: + pos.append(pack(a)) + for kw in call.keywords: + args[kw.arg] = pack(kw.value) + if pos: + args["_positional"] = pos + return owner_name, method_name, args, call_kind + + def _parse_dict(self, node) -> Dict[str, Any]: + import ast + out: Dict[str, Any] = {} + for k, v in zip(node.keys, node.values): + if isinstance(k, ast.Constant): + key = str(k.value) + if isinstance(v, ast.Name): + out[key] = f"var:{v.id}" + elif isinstance(v, ast.Constant): + out[key] = v.value + elif isinstance(v, ast.Dict): + out[key] = self._parse_dict(v) + elif isinstance(v, ast.List): + out[key] = self._parse_list(v) + return out + + def _parse_list(self, node) -> List[Any]: + import ast + out: List[Any] = [] + for elt in node.elts: + if isinstance(elt, ast.Name): + out.append(f"var:{elt.id}") + elif isinstance(elt, ast.Constant): + out.append(elt.value) + elif isinstance(elt, ast.Dict): + out.append(self._parse_dict(elt)) + elif isinstance(elt, ast.List): + out.append(self._parse_list(elt)) + return out + + def _normalize_var_tokens(self, x: Any) -> Any: + if isinstance(x, str) and x.startswith("var:"): + return {"__var__": x[4:]} + if isinstance(x, list): + return [self._normalize_var_tokens(i) for i in x] + if isinstance(x, dict): + return {k: self._normalize_var_tokens(v) for k, v in x.items()} + return x + + def _make_params_payload(self, resource_name: Optional[str], template_name: str, call_args: Dict[str, Any]) -> Dict[str, Any]: + input_keys = self.registry.get_action_input_keys(resource_name, template_name) if resource_name else [] + defaults = self.registry.get_action_goal_default(resource_name, template_name) if resource_name else {} + params: Dict[str, Any] = dict(defaults) + + def unpack(p): + t, v = p.get("type"), p.get("value") + if t == "variable": + return {"__var__": v} + if t == "dict": + return self._normalize_var_tokens(v) + if t == "list": + return self._normalize_var_tokens(v) + return v + + for k, p in call_args.items(): + if k == "_positional": + continue + params[k] = unpack(p) + + pos = call_args.get("_positional", []) + if pos: + if input_keys: + for i, p in enumerate(pos): + if i >= len(input_keys): + break + name = input_keys[i] + if name in params: + continue + params[name] = unpack(p) + else: + for i, p in enumerate(pos): + params[f"arg_{i}"] = unpack(p) + return params + + # ---- handlers ---- + def _on_assign(self, stmt): + import ast + inst = self._extract_device_instantiation(stmt) + if inst: + instance, code_class = inst + resource_name = self.registry.resolve_resource_by_classname(code_class) + self.instance_to_resource[instance] = resource_name + return + + if isinstance(stmt.value, ast.Call): + owner, method, call_args, kind = self._extract_call(stmt.value) + if kind == "instance": + device_key = owner + resource_name = self.instance_to_resource.get(owner) + else: + device_key = owner + resource_name = self.registry.resolve_resource_by_classname(owner) + + module = self.registry.get_device_module(resource_name) + params = self._make_params_payload(resource_name, method, call_args) + + nid = self._new_node_id() + self.graph.add_workflow_node( + nid, + device_key=device_key, + resource_name=resource_name, # ✅ + module=module, + template_name=method, # ✅ + params=params, + variable_sources=self.variable_sources, + add_ready_if_no_vars=True, + prev_node_id=(nid - 1) if nid > 0 else None, + ) + + out_vars = self._assign_targets(stmt.targets[0]) + for var in out_vars: + self.variable_sources[var] = {"node_id": nid, "output_name": "result"} + + def _on_expr(self, stmt): + import ast + if not isinstance(stmt.value, ast.Call): + return + owner, method, call_args, kind = self._extract_call(stmt.value) + if kind == "instance": + device_key = owner + resource_name = self.instance_to_resource.get(owner) + else: + device_key = owner + resource_name = self.registry.resolve_resource_by_classname(owner) + + module = self.registry.get_device_module(resource_name) + params = self._make_params_payload(resource_name, method, call_args) + + nid = self._new_node_id() + self.graph.add_workflow_node( + nid, + device_key=device_key, + resource_name=resource_name, # ✅ + module=module, + template_name=method, # ✅ + params=params, + variable_sources=self.variable_sources, + add_ready_if_no_vars=True, + prev_node_id=(nid - 1) if nid > 0 else None, + ) + + def convert(self, python_code: str): + tree = ast.parse(python_code) + for stmt in tree.body: + if isinstance(stmt, ast.Assign): + self._on_assign(stmt) + elif isinstance(stmt, ast.Expr): + self._on_expr(stmt) + return self diff --git a/unilabos/workflow/from_xdl.py b/unilabos/workflow/from_xdl.py new file mode 100644 index 000000000..1041f9adc --- /dev/null +++ b/unilabos/workflow/from_xdl.py @@ -0,0 +1,131 @@ +from typing import List, Any, Dict +import xml.etree.ElementTree as ET + + +def convert_to_type(val: str) -> Any: + """将字符串值转换为适当的数据类型""" + if val == "True": + return True + if val == "False": + return False + if val == "?": + return None + if val.endswith(" g"): + return float(val.split(" ")[0]) + if val.endswith("mg"): + return float(val.split("mg")[0]) + elif val.endswith("mmol"): + return float(val.split("mmol")[0]) / 1000 + elif val.endswith("mol"): + return float(val.split("mol")[0]) + elif val.endswith("ml"): + return float(val.split("ml")[0]) + elif val.endswith("RPM"): + return float(val.split("RPM")[0]) + elif val.endswith(" °C"): + return float(val.split(" ")[0]) + elif val.endswith(" %"): + return float(val.split(" ")[0]) + return val + + +def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]: + """展平嵌套的XDL程序结构""" + flattened_operations = [] + TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"] + + def extract_operations(element: ET.Element): + if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]: + if element.tag not in TEMP_UNSUPPORTED_PROTOCOL: + flattened_operations.append(element) + + for child in element: + extract_operations(child) + + for child in procedure_elem: + extract_operations(child) + + return flattened_operations + + +def parse_xdl_content(xdl_content: str) -> tuple: + """解析XDL内容""" + try: + xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable()) + root = ET.fromstring(xdl_content_cleaned) + + synthesis_elem = root.find("Synthesis") + if synthesis_elem is None: + return None, None, None + + # 解析硬件组件 + hardware_elem = synthesis_elem.find("Hardware") + hardware = [] + if hardware_elem is not None: + hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")] + + # 解析试剂 + reagents_elem = synthesis_elem.find("Reagents") + reagents = [] + if reagents_elem is not None: + reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")] + + # 解析程序 + procedure_elem = synthesis_elem.find("Procedure") + if procedure_elem is None: + return None, None, None + + flattened_operations = flatten_xdl_procedure(procedure_elem) + return hardware, reagents, flattened_operations + + except ET.ParseError as e: + raise ValueError(f"Invalid XDL format: {e}") + + +def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]: + """ + 将XDL XML格式转换为标准的字典格式 + + Args: + xdl_content: XDL XML内容 + + Returns: + 转换结果,包含步骤和器材信息 + """ + try: + hardware, reagents, flattened_operations = parse_xdl_content(xdl_content) + if hardware is None: + return {"error": "Failed to parse XDL content", "success": False} + + # 将XDL元素转换为字典格式 + steps_data = [] + for elem in flattened_operations: + # 转换参数类型 + parameters = {} + for key, val in elem.attrib.items(): + converted_val = convert_to_type(val) + if converted_val is not None: + parameters[key] = converted_val + + step_dict = { + "operation": elem.tag, + "parameters": parameters, + "description": elem.get("purpose", f"Operation: {elem.tag}"), + } + steps_data.append(step_dict) + + # 合并硬件和试剂为统一的labware_info格式 + labware_data = [] + labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware) + labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents) + + return { + "success": True, + "steps": steps_data, + "labware": labware_data, + "message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.", + } + + except Exception as e: + error_msg = f"XDL conversion failed: {str(e)}" + return {"error": error_msg, "success": False} diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index ead5eded3..6957f7bfc 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.19 + 0.10.18 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln