mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 19:27:35 +00:00
Compare commits
12 Commits
f6d46e669d
...
prcix9320
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68029217de | ||
|
|
792504e08c | ||
|
|
41be9e4e19 | ||
|
|
f38f3dfc89 | ||
|
|
699a0b3ce7 | ||
|
|
cf3a20ae79 | ||
|
|
cdf0652020 | ||
|
|
60073ff139 | ||
|
|
d3f59913b0 | ||
|
|
a9053b822f | ||
|
|
d238c2ab8b | ||
|
|
9a7d5c7c82 |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: 0.10.17
|
version: 0.10.19
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos
|
path: ../../unilabos
|
||||||
@@ -46,13 +46,15 @@ requirements:
|
|||||||
- jinja2
|
- jinja2
|
||||||
- requests
|
- requests
|
||||||
- uvicorn
|
- uvicorn
|
||||||
- opcua # [not osx]
|
- if: not osx
|
||||||
|
then:
|
||||||
|
- opcua
|
||||||
- pyserial
|
- pyserial
|
||||||
- pandas
|
- pandas
|
||||||
- pymodbus
|
- pymodbus
|
||||||
- matplotlib
|
- matplotlib
|
||||||
- pylibftdi
|
- pylibftdi
|
||||||
- uni-lab::unilabos-env ==0.10.17
|
- uni-lab::unilabos-env ==0.10.19
|
||||||
|
|
||||||
about:
|
about:
|
||||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-full
|
name: unilabos-full
|
||||||
version: 0.10.17
|
version: 0.10.19
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
@@ -11,7 +11,7 @@ build:
|
|||||||
requirements:
|
requirements:
|
||||||
run:
|
run:
|
||||||
# Base unilabos package (includes unilabos-env)
|
# Base unilabos package (includes unilabos-env)
|
||||||
- uni-lab::unilabos ==0.10.17
|
- uni-lab::unilabos ==0.10.19
|
||||||
# Documentation tools
|
# Documentation tools
|
||||||
- sphinx
|
- sphinx
|
||||||
- sphinx_rtd_theme
|
- sphinx_rtd_theme
|
||||||
|
|||||||
2
.github/workflows/ci-check.yml
vendored
2
.github/workflows/ci-check.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
uv pip uninstall enum34 || echo enum34 not installed, skipping
|
uv pip uninstall enum34 || echo enum34 not installed, skipping
|
||||||
uv pip install .
|
uv pip install .
|
||||||
|
|
||||||
- name: Run check mode (complete_registry)
|
- name: Run check mode (AST registry validation)
|
||||||
run: |
|
run: |
|
||||||
call conda activate check-env
|
call conda activate check-env
|
||||||
echo Running check mode...
|
echo Running check mode...
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -4,6 +4,8 @@ temp/
|
|||||||
output/
|
output/
|
||||||
unilabos_data/
|
unilabos_data/
|
||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
|
.cursorignore
|
||||||
|
device_package*/
|
||||||
## Python
|
## Python
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
@@ -250,3 +252,9 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
|||||||
test_config.py
|
test_config.py
|
||||||
|
|
||||||
|
|
||||||
|
/.claude
|
||||||
|
/.conda
|
||||||
|
/.cursor
|
||||||
|
/.github
|
||||||
|
/.conda/base
|
||||||
|
.conda/base/recipe.yaml
|
||||||
|
|||||||
87
AGENTS.md
Normal file
87
AGENTS.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
Also follow the monorepo-level rules in `../AGENTS.md`.
|
||||||
|
|
||||||
|
## Build & Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install in editable mode (requires mamba env with python 3.11)
|
||||||
|
pip install -e .
|
||||||
|
uv pip install -r unilabos/utils/requirements.txt
|
||||||
|
|
||||||
|
# Run with a device graph
|
||||||
|
unilab --graph <graph.json> --config <config.py> --backend ros
|
||||||
|
unilab --graph <graph.json> --config <config.py> --backend simple # no ROS2 needed
|
||||||
|
|
||||||
|
# Common CLI flags
|
||||||
|
unilab --app_bridges websocket fastapi # communication bridges
|
||||||
|
unilab --test_mode # simulate hardware, no real execution
|
||||||
|
unilab --check_mode # CI validation of registry imports
|
||||||
|
unilab --skip_env_check # skip auto-install of dependencies
|
||||||
|
unilab --visual rviz|web|disable # visualization mode
|
||||||
|
unilab --is_slave # run as slave node
|
||||||
|
|
||||||
|
# Workflow upload subcommand
|
||||||
|
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
pytest tests/ # all tests
|
||||||
|
pytest tests/resources/test_resourcetreeset.py # single test file
|
||||||
|
pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Startup Flow
|
||||||
|
|
||||||
|
`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client.
|
||||||
|
|
||||||
|
### Core Layers
|
||||||
|
|
||||||
|
**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`.
|
||||||
|
|
||||||
|
**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict` → `ResourceDictInstance` → `ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`).
|
||||||
|
|
||||||
|
**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers.
|
||||||
|
|
||||||
|
**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases).
|
||||||
|
|
||||||
|
**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences.
|
||||||
|
|
||||||
|
**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud.
|
||||||
|
|
||||||
|
**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default.
|
||||||
|
|
||||||
|
### Configuration System
|
||||||
|
|
||||||
|
- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files
|
||||||
|
- Config files are `.py` files with matching class names (see `config/example_config.py`)
|
||||||
|
- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`)
|
||||||
|
- Device topology defined in graph files (JSON with node-link format, or GraphML)
|
||||||
|
|
||||||
|
### Key Data Flow
|
||||||
|
|
||||||
|
1. Graph file → `graphio.read_node_link_json()` → `(nx.Graph, ResourceTreeSet, resource_links)`
|
||||||
|
2. `ResourceTreeSet` + `Registry` → `initialize_device.initialize_device_from_dict()` → `ROS2DeviceNode` instances
|
||||||
|
3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend)
|
||||||
|
4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`)
|
||||||
|
|
||||||
|
### Test Data
|
||||||
|
|
||||||
|
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
- Code comments and log messages in simplified Chinese
|
||||||
|
- Python 3.11+, type hints expected
|
||||||
|
- Pydantic models for data validation (`resource_tracker.py`)
|
||||||
|
- Singleton pattern via `@singleton` decorator (`utils/decorator.py`)
|
||||||
|
- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths
|
||||||
|
- CLI argument dashes auto-converted to underscores for consistency
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
- Framework code: GPL-3.0
|
||||||
|
- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute
|
||||||
@@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,
|
|||||||
**示例:**
|
**示例:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from unilabos.registry.decorators import device, topic_config
|
||||||
|
|
||||||
|
@device(id="mock_gripper", category=["gripper"], description="Mock Gripper")
|
||||||
class MockGripper:
|
class MockGripper:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._position: float = 0.0
|
self._position: float = 0.0
|
||||||
@@ -23,19 +26,23 @@ class MockGripper:
|
|||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config() # 添加 @topic_config 才会定时广播
|
||||||
def position(self) -> float:
|
def position(self) -> float:
|
||||||
return self._position
|
return self._position
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def velocity(self) -> float:
|
def velocity(self) -> float:
|
||||||
return self._velocity
|
return self._velocity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def torque(self) -> float:
|
def torque(self) -> float:
|
||||||
return self._torque
|
return self._torque
|
||||||
|
|
||||||
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
# 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播
|
||||||
@property
|
@property
|
||||||
|
@topic_config(period=2.0) # 可自定义发布周期
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@@ -149,7 +156,7 @@ my_device: # 设备唯一标识符
|
|||||||
|
|
||||||
系统会自动分析您的 Python 驱动类并生成:
|
系统会自动分析您的 Python 驱动类并生成:
|
||||||
|
|
||||||
- `status_types`:从 `@property` 装饰的方法自动识别状态属性
|
- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性
|
||||||
- `action_value_mappings`:从类方法自动生成动作映射
|
- `action_value_mappings`:从类方法自动生成动作映射
|
||||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||||
- `schema`:前端显示用的属性类型定义
|
- `schema`:前端显示用的属性类型定义
|
||||||
@@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
from unilabos.registry.decorators import device, topic_config
|
||||||
|
|
||||||
|
@device(id="my_device", category=["general"], description="My Device")
|
||||||
class MyDevice:
|
class MyDevice:
|
||||||
"""设备类文档字符串
|
"""设备类文档字符串
|
||||||
|
|
||||||
@@ -198,8 +207,9 @@ class MyDevice:
|
|||||||
# 初始化硬件连接
|
# 初始化硬件连接
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config() # 必须添加 @topic_config 才会广播
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态(会自动广播)"""
|
"""设备状态(通过 @topic_config 广播)"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
def my_action(self, param: float) -> Dict[str, Any]:
|
def my_action(self, param: float) -> Dict[str, Any]:
|
||||||
@@ -217,34 +227,61 @@ class MyDevice:
|
|||||||
|
|
||||||
## 状态属性 vs 动作方法
|
## 状态属性 vs 动作方法
|
||||||
|
|
||||||
### 状态属性(@property)
|
### 状态属性(@property + @topic_config)
|
||||||
|
|
||||||
状态属性会被自动识别并定期广播:
|
状态属性需要同时使用 `@property` 和 `@topic_config` 装饰器才会被识别并定期广播:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from unilabos.registry.decorators import topic_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config() # 必须添加,否则不会广播
|
||||||
def temperature(self) -> float:
|
def temperature(self) -> float:
|
||||||
"""当前温度"""
|
"""当前温度"""
|
||||||
return self._read_temperature()
|
return self._read_temperature()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config(period=2.0) # 可自定义发布周期(秒)
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态: idle, running, error"""
|
"""设备状态: idle, running, error"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config(name="ready") # 可自定义发布名称
|
||||||
def is_ready(self) -> bool:
|
def is_ready(self) -> bool:
|
||||||
"""设备是否就绪"""
|
"""设备是否就绪"""
|
||||||
return self._status == "idle"
|
return self._status == "idle"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
也可以使用普通方法(非 @property)配合 `@topic_config`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@topic_config(period=10.0)
|
||||||
|
def get_sensor_data(self) -> Dict[str, float]:
|
||||||
|
"""获取传感器数据(get_ 前缀会自动去除,发布名为 sensor_data)"""
|
||||||
|
return {"temp": self._temp, "humidity": self._humidity}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`@topic_config` 参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `period` | float | 5.0 | 发布周期(秒) |
|
||||||
|
| `print_publish` | bool | 节点默认 | 是否打印发布日志 |
|
||||||
|
| `qos` | int | 10 | QoS 深度 |
|
||||||
|
| `name` | str | None | 自定义发布名称 |
|
||||||
|
|
||||||
|
**发布名称优先级**:`@topic_config(name=...)` > `get_` 前缀去除 > 方法名
|
||||||
|
|
||||||
**特点**:
|
**特点**:
|
||||||
|
|
||||||
- 使用`@property`装饰器
|
- 必须使用 `@topic_config` 装饰器
|
||||||
- 只读,不能有参数
|
- 支持 `@property` 和普通方法
|
||||||
- 自动添加到注册表的`status_types`
|
- 添加到注册表的 `status_types`
|
||||||
- 定期发布到 ROS2 topic
|
- 定期发布到 ROS2 topic
|
||||||
|
|
||||||
|
> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。
|
||||||
|
|
||||||
### 动作方法
|
### 动作方法
|
||||||
|
|
||||||
动作方法是设备可以执行的操作:
|
动作方法是设备可以执行的操作:
|
||||||
@@ -497,6 +534,7 @@ class LiquidHandler:
|
|||||||
self._status = "idle"
|
self._status = "idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@@ -886,7 +924,52 @@ class MyDevice:
|
|||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
### 1. 类型注解
|
### 1. 使用 `@device` 装饰器标识设备类
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.registry.decorators import device
|
||||||
|
|
||||||
|
@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp")
|
||||||
|
class MyDevice:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `id`:设备唯一标识符,用于注册表匹配
|
||||||
|
- `category`:分类列表,前端用于分组显示
|
||||||
|
- `description`:设备描述
|
||||||
|
- `icon`:图标文件名(可选)
|
||||||
|
|
||||||
|
### 2. 使用 `@topic_config` 声明需要广播的状态
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.registry.decorators import topic_config
|
||||||
|
|
||||||
|
# ✓ @property + @topic_config → 会广播
|
||||||
|
@property
|
||||||
|
@topic_config(period=2.0)
|
||||||
|
def temperature(self) -> float:
|
||||||
|
return self._temp
|
||||||
|
|
||||||
|
# ✓ 普通方法 + @topic_config → 会广播(get_ 前缀自动去除)
|
||||||
|
@topic_config(period=10.0)
|
||||||
|
def get_sensor_data(self) -> Dict[str, float]:
|
||||||
|
return {"temp": self._temp}
|
||||||
|
|
||||||
|
# ✓ 使用 name 参数自定义发布名称
|
||||||
|
@property
|
||||||
|
@topic_config(name="ready")
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
return self._status == "idle"
|
||||||
|
|
||||||
|
# ✗ 仅有 @property,没有 @topic_config → 不会广播
|
||||||
|
@property
|
||||||
|
def internal_state(self) -> str:
|
||||||
|
return self._state
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。
|
||||||
|
|
||||||
|
### 3. 类型注解
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
@@ -901,7 +984,7 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 文档字符串
|
### 4. 文档字符串
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def method(self, param: float) -> Dict[str, Any]:
|
def method(self, param: float) -> Dict[str, Any]:
|
||||||
@@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]:
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置验证
|
### 5. 配置验证
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def __init__(self, config: Dict[str, Any]):
|
def __init__(self, config: Dict[str, Any]):
|
||||||
@@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]):
|
|||||||
self.baudrate = config['baudrate']
|
self.baudrate = config['baudrate']
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 资源清理
|
### 6. 资源清理
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@@ -946,7 +1029,7 @@ def __del__(self):
|
|||||||
self.connection.close()
|
self.connection.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 设计前端友好的返回值
|
### 7. 设计前端友好的返回值
|
||||||
|
|
||||||
**记住:返回值会直接显示在 Web 界面**
|
**记住:返回值会直接显示在 Web 界面**
|
||||||
|
|
||||||
|
|||||||
@@ -422,18 +422,20 @@ placeholder_keys:
|
|||||||
|
|
||||||
### status_types
|
### status_types
|
||||||
|
|
||||||
系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分:
|
系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
status_types:
|
status_types:
|
||||||
current_temperature: float # 从 get_current_temperature() 或 @property current_temperature
|
current_temperature: float # 从 @topic_config 装饰的 @property 或方法
|
||||||
is_heating: bool # 从 get_is_heating() 或 @property is_heating
|
is_heating: bool
|
||||||
status: str # 从 get_status() 或 @property status
|
status: str
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意事项**:
|
**注意事项**:
|
||||||
|
|
||||||
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性
|
- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性
|
||||||
|
- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播
|
||||||
|
- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`)
|
||||||
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
||||||
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
||||||
|
|
||||||
@@ -537,11 +539,13 @@ class AdvancedLiquidHandler:
|
|||||||
self._temperature = 25.0
|
self._temperature = 25.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态"""
|
"""设备状态"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def temperature(self) -> float:
|
def temperature(self) -> float:
|
||||||
"""当前温度"""
|
"""当前温度"""
|
||||||
return self._temperature
|
return self._temperature
|
||||||
@@ -809,21 +813,23 @@ my_temperature_controller:
|
|||||||
你的设备类需要符合以下要求:
|
你的设备类需要符合以下要求:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from unilabos.common.device_base import DeviceBase
|
from unilabos.registry.decorators import device, topic_config
|
||||||
|
|
||||||
class MyDevice(DeviceBase):
|
@device(id="my_device", category=["temperature"], description="My Device")
|
||||||
|
class MyDevice:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""初始化,参数会自动分析到 init_param_schema.config"""
|
"""初始化,参数会自动分析到 init_param_schema.config"""
|
||||||
super().__init__(config)
|
|
||||||
self.port = config.get('port', '/dev/ttyUSB0')
|
self.port = config.get('port', '/dev/ttyUSB0')
|
||||||
|
|
||||||
# 状态方法(会自动生成到 status_types)
|
# 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播)
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def status(self):
|
def status(self):
|
||||||
"""返回设备状态"""
|
"""返回设备状态"""
|
||||||
return "idle"
|
return "idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def temperature(self):
|
def temperature(self):
|
||||||
"""返回当前温度"""
|
"""返回当前温度"""
|
||||||
return 25.0
|
return 25.0
|
||||||
@@ -1039,7 +1045,34 @@ resource.type # "resource"
|
|||||||
|
|
||||||
### 代码规范
|
### 代码规范
|
||||||
|
|
||||||
1. **始终使用类型注解**
|
1. **使用 `@device` 装饰器标识设备类**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.registry.decorators import device
|
||||||
|
|
||||||
|
@device(id="my_device", category=["heating"], description="My Device")
|
||||||
|
class MyDevice:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用 `@topic_config` 声明广播属性**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.registry.decorators import topic_config
|
||||||
|
|
||||||
|
# ✓ 需要广播的状态属性
|
||||||
|
@property
|
||||||
|
@topic_config(period=2.0)
|
||||||
|
def temperature(self) -> float:
|
||||||
|
return self._temp
|
||||||
|
|
||||||
|
# ✗ 仅有 @property 不会广播
|
||||||
|
@property
|
||||||
|
def internal_counter(self) -> int:
|
||||||
|
return self._counter
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **始终使用类型注解**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ✓ 好
|
# ✓ 好
|
||||||
@@ -1051,7 +1084,7 @@ def method(self, resource, device):
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **提供有意义的参数名**
|
4. **提供有意义的参数名**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ✓ 好 - 清晰的参数名
|
# ✓ 好 - 清晰的参数名
|
||||||
@@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot):
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **使用 Optional 表示可选参数**
|
5. **使用 Optional 表示可选参数**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -1076,7 +1109,7 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **添加详细的文档字符串**
|
6. **添加详细的文档字符串**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def method(
|
def method(
|
||||||
@@ -1096,13 +1129,13 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **方法命名规范**
|
7. **方法命名规范**
|
||||||
|
|
||||||
- 状态方法使用 `@property` 装饰器或 `get_` 前缀
|
- 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config`
|
||||||
- 动作方法使用动词开头
|
- 动作方法使用动词开头
|
||||||
- 保持命名清晰、一致
|
- 保持命名清晰、一致
|
||||||
|
|
||||||
6. **完善的错误处理**
|
8. **完善的错误处理**
|
||||||
- 实现完善的错误处理
|
- 实现完善的错误处理
|
||||||
- 添加日志记录
|
- 添加日志记录
|
||||||
- 提供有意义的错误信息
|
- 提供有意义的错误信息
|
||||||
|
|||||||
@@ -221,10 +221,10 @@ Laboratory A Laboratory B
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 实验室A
|
# 实验室A
|
||||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
unilab --ak your_ak --sk your_sk --upload_registry
|
||||||
|
|
||||||
# 实验室B
|
# 实验室B
|
||||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
unilab --ak your_ak --sk your_sk --upload_registry
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ options:
|
|||||||
--is_slave Run the backend as slave node (without host privileges).
|
--is_slave Run the backend as slave node (without host privileges).
|
||||||
--slave_no_host Skip waiting for host service in slave mode
|
--slave_no_host Skip waiting for host service in slave mode
|
||||||
--upload_registry Upload registry information when starting unilab
|
--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
|
--config CONFIG Configuration file path, supports .py format Python config files
|
||||||
--port PORT Port for web service information page
|
--port PORT Port for web service information page
|
||||||
--disable_browser Disable opening information page on startup
|
--disable_browser Disable opening information page on startup
|
||||||
@@ -85,7 +84,7 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
|||||||
支持两种方式:
|
支持两种方式:
|
||||||
|
|
||||||
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
||||||
- **远程资源**:使用 `--use_remote_resource` 从云端获取
|
- **远程资源**:不指定本地文件即可
|
||||||
|
|
||||||
### 7. 注册表构建
|
### 7. 注册表构建
|
||||||
|
|
||||||
@@ -196,7 +195,7 @@ unilab --config path/to/your/config.py
|
|||||||
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
||||||
|
|
||||||
# 使用远程资源启动
|
# 使用远程资源启动
|
||||||
unilab --ak your_ak --sk your_sk --use_remote_resource
|
unilab --ak your_ak --sk your_sk
|
||||||
|
|
||||||
# 更新注册表
|
# 更新注册表
|
||||||
unilab --ak your_ak --sk your_sk --complete_registry
|
unilab --ak your_ak --sk your_sk --complete_registry
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.10.17
|
version: 0.10.19
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
target_directory: src
|
target_directory: src
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.10.17"
|
version: "0.10.19"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.10.17',
|
version='0.10.19',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.10.17"
|
__version__ = "0.10.19"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -24,6 +26,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
|||||||
_restart_requested: bool = False
|
_restart_requested: bool = False
|
||||||
_restart_reason: str = ""
|
_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):
|
def load_config_from_file(config_path):
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
@@ -65,6 +145,13 @@ def parse_args():
|
|||||||
action="append",
|
action="append",
|
||||||
help="Path to the registry directory",
|
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(
|
parser.add_argument(
|
||||||
"--working_dir",
|
"--working_dir",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -154,23 +241,53 @@ def parse_args():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Skip environment dependency check on startup",
|
help="Skip environment dependency check on startup",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--complete_registry",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Complete registry information",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--check_mode",
|
"--check_mode",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Run in check mode for CI: validates registry imports and ensures no file changes",
|
help="Run in check mode for CI: validates registry imports and ensures no file changes",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--complete_registry",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Complete and rewrite YAML registry files using AST analysis results",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no_update_feedback",
|
"--no_update_feedback",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Disable sending update feedback to server",
|
help="Disable sending update feedback to server",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--test_mode",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--external_devices_only",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Only load external device packages (--devices), skip built-in unilabos/devices/ scanning and YAML device registry",
|
||||||
|
)
|
||||||
|
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 upload subcommand
|
||||||
workflow_parser = subparsers.add_parser(
|
workflow_parser = subparsers.add_parser(
|
||||||
"workflow_upload",
|
"workflow_upload",
|
||||||
@@ -204,6 +321,12 @@ def parse_args():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Whether to publish the workflow (default: False)",
|
help="Whether to publish the workflow (default: False)",
|
||||||
)
|
)
|
||||||
|
workflow_parser.add_argument(
|
||||||
|
"--description",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="Workflow description, used when publishing the workflow",
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -215,68 +338,88 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args_dict = vars(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)
|
skip_env_check = args_dict.get("skip_env_check", False)
|
||||||
check_mode = args_dict.get("check_mode", False)
|
check_mode = args_dict.get("check_mode", False)
|
||||||
|
|
||||||
if not skip_env_check:
|
if not skip_env_check:
|
||||||
from unilabos.utils.environment_check import check_environment
|
from unilabos.utils.environment_check import check_environment, check_device_package_requirements
|
||||||
|
|
||||||
if not check_environment(auto_install=True):
|
if not check_environment(auto_install=True):
|
||||||
print_status("环境检查失败,程序退出", "error")
|
print_status("环境检查失败,程序退出", "error")
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
|
# 第一次设备包依赖检查:build_registry 之前,确保 import map 可用
|
||||||
|
devices_dirs_for_req = args_dict.get("devices", None)
|
||||||
|
if devices_dirs_for_req:
|
||||||
|
if not check_device_package_requirements(devices_dirs_for_req):
|
||||||
|
print_status("设备包依赖检查失败,程序退出", "error")
|
||||||
|
os._exit(1)
|
||||||
else:
|
else:
|
||||||
print_status("跳过环境依赖检查", "warning")
|
print_status("跳过环境依赖检查", "warning")
|
||||||
|
|
||||||
# 加载配置文件,优先加载config,然后从env读取
|
# 加载配置文件,优先加载config,然后从env读取
|
||||||
config_path = args_dict.get("config")
|
config_path = args_dict.get("config")
|
||||||
|
|
||||||
if check_mode:
|
# === 解析 working_dir ===
|
||||||
args_dict["working_dir"] = os.path.abspath(os.getcwd())
|
# 规则1: working_dir 传入 → 检测 unilabos_data 子目录,已是则不修改
|
||||||
# 当 skip_env_check 时,默认使用当前目录作为 working_dir
|
# 规则2: 仅 config_path 传入 → 用其父目录作为 working_dir
|
||||||
if skip_env_check and not args_dict.get("working_dir") and not config_path:
|
# 规则4: 两者都传入 → 各用各的,但 working_dir 仍做 unilabos_data 子目录检测
|
||||||
|
raw_working_dir = args_dict.get("working_dir")
|
||||||
|
if raw_working_dir:
|
||||||
|
working_dir = os.path.abspath(raw_working_dir)
|
||||||
|
elif config_path and os.path.exists(config_path):
|
||||||
|
working_dir = os.path.dirname(os.path.abspath(config_path))
|
||||||
|
else:
|
||||||
working_dir = os.path.abspath(os.getcwd())
|
working_dir = os.path.abspath(os.getcwd())
|
||||||
print_status(f"跳过环境检查模式:使用当前目录作为工作目录 {working_dir}", "info")
|
|
||||||
# 检查当前目录是否有 local_config.py
|
# unilabos_data 子目录自动检测
|
||||||
local_config_in_cwd = os.path.join(working_dir, "local_config.py")
|
if os.path.basename(working_dir) != "unilabos_data":
|
||||||
if os.path.exists(local_config_in_cwd):
|
unilabos_data_sub = os.path.join(working_dir, "unilabos_data")
|
||||||
config_path = local_config_in_cwd
|
if os.path.isdir(unilabos_data_sub):
|
||||||
|
working_dir = unilabos_data_sub
|
||||||
|
elif not raw_working_dir and not (config_path and os.path.exists(config_path)):
|
||||||
|
# 未显式指定路径,默认使用 cwd/unilabos_data
|
||||||
|
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||||
|
|
||||||
|
# === 解析 config_path ===
|
||||||
|
if config_path and not os.path.exists(config_path):
|
||||||
|
# config_path 传入但不存在,尝试在 working_dir 中查找
|
||||||
|
candidate = os.path.join(working_dir, "local_config.py")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
config_path = candidate
|
||||||
|
print_status(f"在工作目录中发现配置文件: {config_path}", "info")
|
||||||
|
else:
|
||||||
|
print_status(
|
||||||
|
f"配置文件 {config_path} 不存在,工作目录 {working_dir} 中也未找到 local_config.py,"
|
||||||
|
f"请通过 --config 传入 local_config.py 文件路径",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
os._exit(1)
|
||||||
|
elif not config_path:
|
||||||
|
# 规则3: 未传入 config_path,尝试 working_dir/local_config.py
|
||||||
|
candidate = os.path.join(working_dir, "local_config.py")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
config_path = candidate
|
||||||
print_status(f"发现本地配置文件: {config_path}", "info")
|
print_status(f"发现本地配置文件: {config_path}", "info")
|
||||||
else:
|
else:
|
||||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||||
elif os.getcwd().endswith("unilabos_data"):
|
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||||
working_dir = os.path.abspath(os.getcwd())
|
if check_mode or input() != "n":
|
||||||
else:
|
os.makedirs(working_dir, exist_ok=True)
|
||||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
shutil.copy(
|
||||||
if args_dict.get("working_dir"):
|
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"),
|
||||||
working_dir = args_dict.get("working_dir", "")
|
config_path,
|
||||||
if config_path and not os.path.exists(config_path):
|
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
print_status(
|
|
||||||
f"当前工作目录 {working_dir} 未找到local_config.py,请通过 --config 传入 local_config.py 文件路径",
|
|
||||||
"error",
|
|
||||||
)
|
)
|
||||||
|
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||||
|
else:
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
elif config_path and os.path.exists(config_path):
|
|
||||||
working_dir = os.path.dirname(config_path)
|
|
||||||
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
|
||||||
elif not skip_env_check and not config_path and (
|
|
||||||
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
|
|
||||||
):
|
|
||||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
|
||||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
|
||||||
if input() != "n":
|
|
||||||
os.makedirs(working_dir, exist_ok=True)
|
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
|
||||||
shutil.copy(
|
|
||||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
|
||||||
)
|
|
||||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
os._exit(1)
|
|
||||||
|
|
||||||
# 加载配置文件 (check_mode 跳过)
|
# 加载配置文件 (check_mode 跳过)
|
||||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||||
@@ -288,7 +431,9 @@ def main():
|
|||||||
|
|
||||||
if hasattr(BasicConfig, "log_level"):
|
if hasattr(BasicConfig, "log_level"):
|
||||||
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||||
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
file_path = configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||||
|
if file_path is not None:
|
||||||
|
logger.info(f"[LOG_FILE] {file_path}")
|
||||||
|
|
||||||
if args.addr != parser.get_default("addr"):
|
if args.addr != parser.get_default("addr"):
|
||||||
if args.addr == "test":
|
if args.addr == "test":
|
||||||
@@ -332,46 +477,66 @@ def main():
|
|||||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||||
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
||||||
|
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"
|
BasicConfig.communication_protocol = "websocket"
|
||||||
machine_name = os.popen("hostname").read().strip()
|
machine_name = platform.node()
|
||||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||||
BasicConfig.machine_name = machine_name
|
BasicConfig.machine_name = machine_name
|
||||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
BasicConfig.check_mode = check_mode
|
BasicConfig.check_mode = check_mode
|
||||||
|
|
||||||
from unilabos.resources.graphio import (
|
|
||||||
read_node_link_json,
|
|
||||||
read_graphml,
|
|
||||||
dict_from_graph,
|
|
||||||
)
|
|
||||||
from unilabos.app.communication import get_communication_client
|
|
||||||
from unilabos.registry.registry import build_registry
|
from unilabos.registry.registry import build_registry
|
||||||
from unilabos.app.backend import start_backend
|
|
||||||
from unilabos.app.web import http_client
|
|
||||||
from unilabos.app.web import start_server
|
|
||||||
from unilabos.app.register import register_devices_and_resources
|
|
||||||
from unilabos.resources.graphio import modify_to_backend_format
|
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
|
||||||
|
|
||||||
# 显示启动横幅
|
# 显示启动横幅
|
||||||
print_unilab_banner(args_dict)
|
print_unilab_banner(args_dict)
|
||||||
|
|
||||||
# 注册表 - check_mode 时强制启用 complete_registry
|
# Step 0: AST 分析优先 + YAML 注册表加载
|
||||||
|
# check_mode 和 upload_registry 都会执行实际 import 验证
|
||||||
|
devices_dirs = args_dict.get("devices", None)
|
||||||
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
||||||
lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry)
|
external_only = args_dict.get("external_devices_only", False)
|
||||||
|
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,
|
||||||
|
external_only=external_only,
|
||||||
|
)
|
||||||
|
|
||||||
# Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行
|
# Check mode: 注册表验证完成后直接退出
|
||||||
if check_mode:
|
if check_mode:
|
||||||
print_status("Check mode: complete_registry 完成,退出", "info")
|
device_count = len(lab_registry.device_type_registry)
|
||||||
|
resource_count = len(lab_registry.resource_type_registry)
|
||||||
|
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
# 以下导入依赖 ROS2 环境,check_mode 已退出不需要
|
||||||
|
from unilabos.resources.graphio import (
|
||||||
|
read_node_link_json,
|
||||||
|
read_graphml,
|
||||||
|
dict_from_graph,
|
||||||
|
modify_to_backend_format,
|
||||||
|
)
|
||||||
|
from unilabos.app.communication import get_communication_client
|
||||||
|
from unilabos.app.backend import start_backend
|
||||||
|
from unilabos.app.web import http_client
|
||||||
|
from unilabos.app.web import start_server
|
||||||
|
from unilabos.app.register import register_devices_and_resources
|
||||||
|
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
||||||
|
|
||||||
|
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
|
||||||
if BasicConfig.upload_registry:
|
if BasicConfig.upload_registry:
|
||||||
# 设备注册到服务端 - 需要 ak 和 sk
|
|
||||||
if BasicConfig.ak and BasicConfig.sk:
|
if BasicConfig.ak and BasicConfig.sk:
|
||||||
print_status("开始注册设备到服务端...", "info")
|
# print_status("开始注册设备到服务端...", "info")
|
||||||
try:
|
try:
|
||||||
register_devices_and_resources(lab_registry)
|
register_devices_and_resources(lab_registry)
|
||||||
print_status("设备注册完成", "info")
|
# print_status("设备注册完成", "info")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print_status(f"设备注册失败: {e}", "error")
|
print_status(f"设备注册失败: {e}", "error")
|
||||||
else:
|
else:
|
||||||
@@ -456,12 +621,16 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 如果从远端获取了物料信息,则与本地物料进行同步
|
# 如果从远端获取了物料信息,则与本地物料进行同步
|
||||||
if request_startup_json and "nodes" in request_startup_json:
|
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
|
||||||
print_status("开始同步远端物料到本地...", "info")
|
print_status("开始同步远端物料到本地...", "info")
|
||||||
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
||||||
resource_tree_set.merge_remote_resources(remote_tree_set)
|
resource_tree_set.merge_remote_resources(remote_tree_set)
|
||||||
print_status("远端物料同步完成", "info")
|
print_status("远端物料同步完成", "info")
|
||||||
|
|
||||||
|
# 第二次设备包依赖检查:云端物料同步后,community 包可能引入新的 requirements
|
||||||
|
# TODO: 当 community device package 功能上线后,在这里调用
|
||||||
|
# install_requirements_txt(community_pkg_path / "requirements.txt", label="community.xxx")
|
||||||
|
|
||||||
# 使用 ResourceTreeSet 代替 list
|
# 使用 ResourceTreeSet 代替 list
|
||||||
args_dict["resources_config"] = resource_tree_set
|
args_dict["resources_config"] = resource_tree_set
|
||||||
args_dict["devices_config"] = resource_tree_set
|
args_dict["devices_config"] = resource_tree_set
|
||||||
@@ -553,6 +722,10 @@ def main():
|
|||||||
open_browser=not args_dict["disable_browser"],
|
open_browser=not args_dict["disable_browser"],
|
||||||
port=BasicConfig.port,
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Tuple, Dict, Any
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.tools import normalize_json as _normalize_device
|
||||||
|
|
||||||
|
|
||||||
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
||||||
@@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
|
|||||||
注册设备和资源到服务器(仅支持HTTP)
|
注册设备和资源到服务器(仅支持HTTP)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 注册资源信息 - 使用HTTP方式
|
|
||||||
from unilabos.app.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
|
|
||||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||||
|
|
||||||
# 注册设备信息
|
|
||||||
devices_to_register = {}
|
devices_to_register = {}
|
||||||
for device_info in lab_registry.obtain_registry_device_info():
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
devices_to_register[device_info["id"]] = json.loads(
|
devices_to_register[device_info["id"]] = _normalize_device(device_info)
|
||||||
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
|
logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}")
|
||||||
)
|
|
||||||
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
|
|
||||||
|
|
||||||
resources_to_register = {}
|
resources_to_register = {}
|
||||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
resources_to_register[resource_info["id"]] = resource_info
|
resources_to_register[resource_info["id"]] = resource_info
|
||||||
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||||
|
|
||||||
if gather_only:
|
if gather_only:
|
||||||
return devices_to_register, resources_to_register
|
return devices_to_register, resources_to_register
|
||||||
# 注册设备
|
|
||||||
if devices_to_register:
|
if devices_to_register:
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
response = http_client.resource_registry(
|
||||||
|
{"resources": list(devices_to_register.values())},
|
||||||
|
tag="device_registry",
|
||||||
|
)
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
if response.status_code in [200, 201]:
|
res_data = response.json() if response.status_code == 200 else {}
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
skipped = res_data.get("data", {}).get("skipped", False)
|
||||||
|
if skipped:
|
||||||
|
logger.info(
|
||||||
|
f"[UniLab Register] 设备注册跳过(内容未变化)"
|
||||||
|
f" {len(devices_to_register)} 个 {cost_time:.3f}s"
|
||||||
|
)
|
||||||
|
elif response.status_code in [200, 201]:
|
||||||
|
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||||
|
|
||||||
# 注册资源
|
|
||||||
if resources_to_register:
|
if resources_to_register:
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
response = http_client.resource_registry(
|
||||||
|
{"resources": list(resources_to_register.values())},
|
||||||
|
tag="resource_registry",
|
||||||
|
)
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
if response.status_code in [200, 201]:
|
res_data = response.json() if response.status_code == 200 else {}
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
skipped = res_data.get("data", {}).get("skipped", False)
|
||||||
|
if skipped:
|
||||||
|
logger.info(
|
||||||
|
f"[UniLab Register] 资源注册跳过(内容未变化)"
|
||||||
|
f" {len(resources_to_register)} 个 {cost_time:.3f}s"
|
||||||
|
)
|
||||||
|
elif response.status_code in [200, 201]:
|
||||||
|
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||||
|
|
||||||
logger.info("[UniLab Register] 设备和资源注册完成.")
|
|
||||||
|
|||||||
@@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
"result": {},
|
"result": {},
|
||||||
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
||||||
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||||
"handles": [],
|
"handles": {},
|
||||||
}
|
}
|
||||||
# 不生成已配置action的动作
|
# 不生成已配置action的动作
|
||||||
for k, v in enhanced_info["action_methods"].items()
|
for k, v in enhanced_info["action_methods"].items()
|
||||||
@@ -1340,5 +1340,5 @@ def setup_api_routes(app):
|
|||||||
# 启动广播任务
|
# 启动广播任务
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
asyncio.create_task(broadcast_device_status())
|
asyncio.create_task(broadcast_device_status(), name="web-api-startup-device")
|
||||||
asyncio.create_task(broadcast_status_page_data())
|
asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status")
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ HTTP客户端模块
|
|||||||
|
|
||||||
提供与远程服务器通信的客户端功能,只有host需要用
|
提供与远程服务器通信的客户端功能,只有host需要用
|
||||||
"""
|
"""
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Any, Optional
|
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
|
import requests
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.utils.log import info
|
from unilabos.utils.log import info
|
||||||
@@ -280,22 +282,54 @@ class HTTPClient:
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
def resource_registry(
|
||||||
|
self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry",
|
||||||
|
) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
注册资源到服务器
|
注册资源到服务器,同步保存请求/响应到 unilabos_data
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||||
|
tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
|
# 序列化一次,同时用于保存和发送
|
||||||
|
json_bytes = _fast_dumps(registry_data)
|
||||||
|
|
||||||
|
# 保存请求数据到 unilabos_data
|
||||||
|
req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json")
|
||||||
|
try:
|
||||||
|
os.makedirs(BasicConfig.working_dir, exist_ok=True)
|
||||||
|
with open(req_path, "wb") as f:
|
||||||
|
f.write(_fast_dumps_pretty(registry_data))
|
||||||
|
logger.trace(f"注册表请求数据已保存: {req_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"保存注册表请求数据失败: {e}")
|
||||||
|
|
||||||
|
compressed_body = gzip.compress(json_bytes)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Lab {self.auth}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Encoding": "gzip",
|
||||||
|
}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource",
|
f"{self.remote_addr}/lab/resource",
|
||||||
json=registry_data,
|
data=compressed_body,
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers=headers,
|
||||||
timeout=30,
|
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]:
|
if response.status_code not in [200, 201]:
|
||||||
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -343,9 +377,10 @@ class HTTPClient:
|
|||||||
edges: List[Dict[str, Any]],
|
edges: List[Dict[str, Any]],
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
published: bool = False,
|
published: bool = False,
|
||||||
|
description: str = "",
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
导入工作流到服务器
|
导入工作流到服务器,如果 published 为 True,则额外发起发布请求
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: 工作流名称(顶层)
|
name: 工作流名称(顶层)
|
||||||
@@ -355,6 +390,7 @@ class HTTPClient:
|
|||||||
edges: 工作流边列表
|
edges: 工作流边列表
|
||||||
tags: 工作流标签列表,默认为空列表
|
tags: 工作流标签列表,默认为空列表
|
||||||
published: 是否发布工作流,默认为False
|
published: 是否发布工作流,默认为False
|
||||||
|
description: 工作流描述,发布时使用
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
Dict: API响应数据,包含 code 和 data (uuid, name)
|
||||||
@@ -367,7 +403,6 @@ class HTTPClient:
|
|||||||
"nodes": nodes,
|
"nodes": nodes,
|
||||||
"edges": edges,
|
"edges": edges,
|
||||||
"tags": tags if tags is not None else [],
|
"tags": tags if tags is not None else [],
|
||||||
"published": published,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
# 保存请求到文件
|
# 保存请求到文件
|
||||||
@@ -388,11 +423,51 @@ class HTTPClient:
|
|||||||
res = response.json()
|
res = response.json()
|
||||||
if "code" in res and res["code"] != 0:
|
if "code" in res and res["code"] != 0:
|
||||||
logger.error(f"导入工作流失败: {response.text}")
|
logger.error(f"导入工作流失败: {response.text}")
|
||||||
|
return res
|
||||||
|
# 导入成功后,如果需要发布则额外发起发布请求
|
||||||
|
if published:
|
||||||
|
imported_uuid = res.get("data", {}).get("uuid", workflow_uuid)
|
||||||
|
publish_res = self.workflow_publish(imported_uuid, description)
|
||||||
|
res["publish_result"] = publish_res
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
||||||
return {"code": response.status_code, "message": response.text}
|
return {"code": response.status_code, "message": response.text}
|
||||||
|
|
||||||
|
def workflow_publish(self, workflow_uuid: str, description: str = "") -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
发布工作流
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workflow_uuid: 工作流UUID
|
||||||
|
description: 工作流描述
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: API响应数据
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"uuid": workflow_uuid,
|
||||||
|
"description": description,
|
||||||
|
"published": True,
|
||||||
|
}
|
||||||
|
logger.info(f"正在发布工作流: {workflow_uuid}")
|
||||||
|
response = requests.patch(
|
||||||
|
f"{self.remote_addr}/lab/workflow/owner",
|
||||||
|
json=payload,
|
||||||
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
res = response.json()
|
||||||
|
if "code" in res and res["code"] != 0:
|
||||||
|
logger.error(f"发布工作流失败: {response.text}")
|
||||||
|
else:
|
||||||
|
logger.info(f"工作流发布成功: {workflow_uuid}")
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
logger.error(f"发布工作流失败: {response.status_code}, {response.text}")
|
||||||
|
return {"code": response.status_code, "message": response.text}
|
||||||
|
|
||||||
|
|
||||||
# 创建默认客户端实例
|
# 创建默认客户端实例
|
||||||
http_client = HTTPClient()
|
http_client = HTTPClient()
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def setup_server() -> FastAPI:
|
|||||||
# 设置页面路由
|
# 设置页面路由
|
||||||
try:
|
try:
|
||||||
setup_web_pages(pages)
|
setup_web_pages(pages)
|
||||||
info("[Web] 已加载Web UI模块")
|
# info("[Web] 已加载Web UI模块")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
info(f"[Web] 未找到Web页面模块: {str(e)}")
|
info(f"[Web] 未找到Web页面模块: {str(e)}")
|
||||||
except Exception as 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 = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
|
||||||
server_thread.start()
|
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
|
import unilabos.app.main as main_module
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ from typing import Optional, Dict, Any, List
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from jedi.inference.gradual.typing import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from unilabos.app.model import JobAddReq
|
from unilabos.app.model import JobAddReq
|
||||||
|
from unilabos.resources.resource_tracker import ResourceDictType
|
||||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
from unilabos.utils.type_check import serialize_result_info
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
from unilabos.app.communication import BaseCommunicationClient
|
from unilabos.app.communication import BaseCommunicationClient
|
||||||
@@ -76,6 +77,7 @@ class JobInfo:
|
|||||||
start_time: float
|
start_time: float
|
||||||
last_update_time: float = field(default_factory=time.time)
|
last_update_time: float = field(default_factory=time.time)
|
||||||
ready_timeout: Optional[float] = None # READY状态的超时时间
|
ready_timeout: Optional[float] = None # READY状态的超时时间
|
||||||
|
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
|
||||||
|
|
||||||
def update_timestamp(self):
|
def update_timestamp(self):
|
||||||
"""更新最后更新时间"""
|
"""更新最后更新时间"""
|
||||||
@@ -127,6 +129,15 @@ class DeviceActionManager:
|
|||||||
# 总是将job添加到all_jobs中
|
# 总是将job添加到all_jobs中
|
||||||
self.all_jobs[job_info.job_id] = job_info
|
self.all_jobs[job_info.job_id] = job_info
|
||||||
|
|
||||||
|
# always_free的动作不受排队限制,直接设为READY
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.READY
|
||||||
|
job_info.update_timestamp()
|
||||||
|
job_info.set_ready_timeout(10)
|
||||||
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
|
logger.trace(f"[DeviceActionManager] Job {job_log} always_free, start immediately")
|
||||||
|
return True
|
||||||
|
|
||||||
# 检查是否有正在执行或准备执行的任务
|
# 检查是否有正在执行或准备执行的任务
|
||||||
if device_key in self.active_jobs:
|
if device_key in self.active_jobs:
|
||||||
# 有正在执行或准备执行的任务,加入队列
|
# 有正在执行或准备执行的任务,加入队列
|
||||||
@@ -154,7 +165,7 @@ class DeviceActionManager:
|
|||||||
job_info.set_ready_timeout(10) # 设置10秒超时
|
job_info.set_ready_timeout(10) # 设置10秒超时
|
||||||
self.active_jobs[device_key] = job_info
|
self.active_jobs[device_key] = job_info
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.info(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
logger.trace(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def start_job(self, job_id: str) -> bool:
|
def start_job(self, job_id: str) -> bool:
|
||||||
@@ -176,11 +187,15 @@ class DeviceActionManager:
|
|||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查设备上是否是这个job
|
# always_free的job不需要检查active_jobs
|
||||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
if not job_info.always_free:
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
# 检查设备上是否是这个job
|
||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||||
return False
|
job_log = format_job_log(
|
||||||
|
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||||
|
)
|
||||||
|
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||||
|
return False
|
||||||
|
|
||||||
# 开始执行任务,将状态从READY转换为STARTED
|
# 开始执行任务,将状态从READY转换为STARTED
|
||||||
job_info.status = JobStatus.STARTED
|
job_info.status = JobStatus.STARTED
|
||||||
@@ -203,6 +218,13 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
|
# always_free的job直接清理,不影响队列
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.ENDED
|
||||||
|
job_info.update_timestamp()
|
||||||
|
del self.all_jobs[job_id]
|
||||||
|
return None
|
||||||
|
|
||||||
# 移除活跃任务
|
# 移除活跃任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
del self.active_jobs[device_key]
|
del self.active_jobs[device_key]
|
||||||
@@ -210,8 +232,9 @@ class DeviceActionManager:
|
|||||||
job_info.update_timestamp()
|
job_info.update_timestamp()
|
||||||
# 从all_jobs中移除已结束的job
|
# 从all_jobs中移除已结束的job
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.info(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
# logger.debug(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
||||||
@@ -227,15 +250,20 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
||||||
return next_job
|
return next_job
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_active_jobs(self) -> List[JobInfo]:
|
def get_active_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有正在执行的任务"""
|
"""获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return list(self.active_jobs.values())
|
jobs = list(self.active_jobs.values())
|
||||||
|
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
|
||||||
|
for job in self.all_jobs.values():
|
||||||
|
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
|
||||||
|
jobs.append(job)
|
||||||
|
return jobs
|
||||||
|
|
||||||
def get_queued_jobs(self) -> List[JobInfo]:
|
def get_queued_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有排队中的任务"""
|
"""获取所有排队中的任务"""
|
||||||
@@ -260,6 +288,14 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
|
# always_free的job直接清理
|
||||||
|
if job_info.always_free:
|
||||||
|
job_info.status = JobStatus.ENDED
|
||||||
|
del self.all_jobs[job_id]
|
||||||
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
|
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
|
||||||
|
return True
|
||||||
|
|
||||||
# 如果是正在执行的任务
|
# 如果是正在执行的任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
# 清理active job状态
|
# 清理active job状态
|
||||||
@@ -268,7 +304,7 @@ class DeviceActionManager:
|
|||||||
# 从all_jobs中移除
|
# 从all_jobs中移除
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
logger.trace(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
||||||
|
|
||||||
# 启动下一个任务
|
# 启动下一个任务
|
||||||
if device_key in self.device_queues and self.device_queues[device_key]:
|
if device_key in self.device_queues and self.device_queues[device_key]:
|
||||||
@@ -281,7 +317,7 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 如果是排队中的任务
|
# 如果是排队中的任务
|
||||||
@@ -295,7 +331,7 @@ class DeviceActionManager:
|
|||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||||
)
|
)
|
||||||
logger.info(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
logger.trace(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
@@ -333,13 +369,18 @@ class DeviceActionManager:
|
|||||||
timeout_jobs = []
|
timeout_jobs = []
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# 统计READY状态的任务数量
|
# 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
|
||||||
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY)
|
ready_candidates = list(self.active_jobs.values())
|
||||||
|
for job in self.all_jobs.values():
|
||||||
|
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
|
||||||
|
ready_candidates.append(job)
|
||||||
|
|
||||||
|
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
|
||||||
if ready_jobs_count > 0:
|
if ready_jobs_count > 0:
|
||||||
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
||||||
|
|
||||||
# 找到所有超时的READY任务(只检测,不处理)
|
# 找到所有超时的READY任务(只检测,不处理)
|
||||||
for job_info in self.active_jobs.values():
|
for job_info in ready_candidates:
|
||||||
if job_info.is_ready_timeout():
|
if job_info.is_ready_timeout():
|
||||||
timeout_jobs.append(job_info)
|
timeout_jobs.append(job_info)
|
||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
@@ -368,6 +409,7 @@ class MessageProcessor:
|
|||||||
# 线程控制
|
# 线程控制
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
self._loop = None # asyncio event loop引用,用于外部关闭websocket
|
||||||
self.reconnect_count = 0
|
self.reconnect_count = 0
|
||||||
|
|
||||||
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
|
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
|
||||||
@@ -394,22 +436,31 @@ class MessageProcessor:
|
|||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止消息处理线程"""
|
"""停止消息处理线程"""
|
||||||
self.is_running = False
|
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():
|
if self.thread and self.thread.is_alive():
|
||||||
self.thread.join(timeout=2)
|
self.thread.join(timeout=2)
|
||||||
logger.info("[MessageProcessor] Stopped")
|
logger.info("[MessageProcessor] Stopped")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""运行消息处理主循环"""
|
"""运行消息处理主循环"""
|
||||||
loop = asyncio.new_event_loop()
|
self._loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(self._loop)
|
||||||
loop.run_until_complete(self._connection_handler())
|
self._loop.run_until_complete(self._connection_handler())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Thread error: {str(e)}")
|
logger.error(f"[MessageProcessor] Thread error: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
if loop:
|
if self._loop:
|
||||||
loop.close()
|
self._loop.close()
|
||||||
|
self._loop = None
|
||||||
|
|
||||||
async def _connection_handler(self):
|
async def _connection_handler(self):
|
||||||
"""处理WebSocket连接和重连逻辑"""
|
"""处理WebSocket连接和重连逻辑"""
|
||||||
@@ -426,8 +477,10 @@ class MessageProcessor:
|
|||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
self.websocket_url,
|
self.websocket_url,
|
||||||
ssl=ssl_context,
|
ssl=ssl_context,
|
||||||
|
open_timeout=20,
|
||||||
ping_interval=WSConfig.ping_interval,
|
ping_interval=WSConfig.ping_interval,
|
||||||
ping_timeout=10,
|
ping_timeout=10,
|
||||||
|
close_timeout=5,
|
||||||
additional_headers={
|
additional_headers={
|
||||||
"Authorization": f"Lab {BasicConfig.auth_secret()}",
|
"Authorization": f"Lab {BasicConfig.auth_secret()}",
|
||||||
"EdgeSession": f"{self.session_id}",
|
"EdgeSession": f"{self.session_id}",
|
||||||
@@ -438,77 +491,94 @@ class MessageProcessor:
|
|||||||
self.connected = True
|
self.connected = True
|
||||||
self.reconnect_count = 0
|
self.reconnect_count = 0
|
||||||
|
|
||||||
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}")
|
logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}")
|
||||||
|
|
||||||
# 启动发送协程
|
# 启动发送协程
|
||||||
send_task = asyncio.create_task(self._send_handler())
|
send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task")
|
||||||
|
|
||||||
|
# 每次连接(含重连)后重新向服务端注册,
|
||||||
|
# 否则服务端不知道客户端已上线,不会推送消息。
|
||||||
|
if self.websocket_client:
|
||||||
|
self.websocket_client.publish_host_ready()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 接收消息循环
|
# 接收消息循环
|
||||||
await self._message_handler()
|
await self._message_handler()
|
||||||
finally:
|
finally:
|
||||||
|
# 必须在 async with __aexit__ 之前停止 send_task,
|
||||||
|
# 否则 send_task 会在关闭握手期间继续发送数据,
|
||||||
|
# 干扰 websockets 库的内部清理,导致 task 泄漏。
|
||||||
|
self.connected = False
|
||||||
send_task.cancel()
|
send_task.cancel()
|
||||||
try:
|
try:
|
||||||
await send_task
|
await send_task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
logger.warning("[MessageProcessor] Connection closed")
|
logger.warning("[MessageProcessor] 与服务端连接中断")
|
||||||
self.connected = False
|
except TimeoutError:
|
||||||
|
logger.warning(
|
||||||
|
f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况"
|
||||||
|
)
|
||||||
|
except websockets.exceptions.InvalidStatus as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Connection error: {str(e)}")
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
self.connected = False
|
logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
|
self.connected = False
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
|
|
||||||
# 重连逻辑
|
# 重连逻辑
|
||||||
if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts:
|
if not self.is_running:
|
||||||
|
break
|
||||||
|
if self.reconnect_count < WSConfig.max_reconnect_attempts:
|
||||||
self.reconnect_count += 1
|
self.reconnect_count += 1
|
||||||
|
backoff = WSConfig.reconnect_interval
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s "
|
f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
||||||
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
|
||||||
)
|
)
|
||||||
await asyncio.sleep(WSConfig.reconnect_interval)
|
await asyncio.sleep(backoff)
|
||||||
elif self.reconnect_count >= WSConfig.max_reconnect_attempts:
|
else:
|
||||||
logger.error("[MessageProcessor] Max reconnection attempts reached")
|
logger.error("[MessageProcessor] Max reconnection attempts reached")
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
self.reconnect_count -= 1
|
|
||||||
|
|
||||||
async def _message_handler(self):
|
async def _message_handler(self):
|
||||||
"""处理接收到的消息"""
|
"""处理接收到的消息。
|
||||||
|
|
||||||
|
ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler,
|
||||||
|
以便 async with websockets.connect() 的 __aexit__ 能感知连接已断,
|
||||||
|
正确清理内部 task,避免 task 泄漏。
|
||||||
|
"""
|
||||||
if not self.websocket:
|
if not self.websocket:
|
||||||
logger.error("[MessageProcessor] WebSocket connection is None")
|
logger.error("[MessageProcessor] WebSocket connection is None")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
async for message in self.websocket:
|
||||||
async for message in self.websocket:
|
try:
|
||||||
try:
|
data = json.loads(message)
|
||||||
data = json.loads(message)
|
message_type = data.get("action", "")
|
||||||
message_type = data.get("action", "")
|
message_data = data.get("data")
|
||||||
message_data = data.get("data")
|
if self.session_id and self.session_id == data.get("edge_session"):
|
||||||
if self.session_id and self.session_id == data.get("edge_session"):
|
await self._process_message(message_type, message_data)
|
||||||
await self._process_message(message_type, message_data)
|
else:
|
||||||
|
if message_type.endswith("_material"):
|
||||||
|
logger.trace(
|
||||||
|
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}"
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if message_type.endswith("_material"):
|
await self._process_message(message_type, message_data)
|
||||||
logger.trace(f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}")
|
except json.JSONDecodeError:
|
||||||
logger.debug(f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}")
|
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
||||||
else:
|
except Exception as e:
|
||||||
await self._process_message(message_type, message_data)
|
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
||||||
except json.JSONDecodeError:
|
logger.error(traceback.format_exc())
|
||||||
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
|
||||||
logger.info("[MessageProcessor] Message handler stopped - connection closed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[MessageProcessor] Message handler error: {str(e)}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
async def _send_handler(self):
|
async def _send_handler(self):
|
||||||
"""处理发送队列中的消息"""
|
"""处理发送队列中的消息"""
|
||||||
@@ -557,6 +627,7 @@ class MessageProcessor:
|
|||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug("[MessageProcessor] Send handler cancelled")
|
logger.debug("[MessageProcessor] Send handler cancelled")
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
|
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -565,7 +636,7 @@ class MessageProcessor:
|
|||||||
|
|
||||||
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
||||||
"""处理收到的消息"""
|
"""处理收到的消息"""
|
||||||
logger.debug(f"[MessageProcessor] Processing message: {message_type}")
|
logger.trace(f"[MessageProcessor] Processing message: {message_type}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if message_type == "pong":
|
if message_type == "pong":
|
||||||
@@ -588,6 +659,10 @@ class MessageProcessor:
|
|||||||
# elif message_type == "session_id":
|
# elif message_type == "session_id":
|
||||||
# self.session_id = message_data.get("session_id")
|
# self.session_id = message_data.get("session_id")
|
||||||
# logger.info(f"[MessageProcessor] Session ID: {self.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":
|
elif message_type == "request_restart":
|
||||||
await self._handle_request_restart(message_data)
|
await self._handle_request_restart(message_data)
|
||||||
else:
|
else:
|
||||||
@@ -603,6 +678,24 @@ class MessageProcessor:
|
|||||||
if host_node:
|
if host_node:
|
||||||
host_node.handle_pong_response(pong_data)
|
host_node.handle_pong_response(pong_data)
|
||||||
|
|
||||||
|
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
|
||||||
|
"""检查该action是否标记为always_free,通过HostNode统一的_action_value_mappings查找"""
|
||||||
|
try:
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
return False
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
action_mappings = host_node._action_value_mappings.get(device_id)
|
||||||
|
if not action_mappings:
|
||||||
|
return False
|
||||||
|
# 尝试直接匹配或 auto- 前缀匹配
|
||||||
|
for key in [action_name, f"auto-{action_name}"]:
|
||||||
|
if key in action_mappings:
|
||||||
|
return action_mappings[key].get("always_free", False)
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
||||||
"""处理query_action_state消息"""
|
"""处理query_action_state消息"""
|
||||||
device_id = data.get("device_id", "")
|
device_id = data.get("device_id", "")
|
||||||
@@ -617,6 +710,9 @@ class MessageProcessor:
|
|||||||
|
|
||||||
device_action_key = f"/devices/{device_id}/{action_name}"
|
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||||
|
|
||||||
|
# 检查action是否为always_free
|
||||||
|
action_always_free = self._check_action_always_free(device_id, action_name)
|
||||||
|
|
||||||
# 创建任务信息
|
# 创建任务信息
|
||||||
job_info = JobInfo(
|
job_info = JobInfo(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
@@ -626,6 +722,7 @@ class MessageProcessor:
|
|||||||
device_action_key=device_action_key,
|
device_action_key=device_action_key,
|
||||||
status=JobStatus.QUEUE,
|
status=JobStatus.QUEUE,
|
||||||
start_time=time.time(),
|
start_time=time.time(),
|
||||||
|
always_free=action_always_free,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到设备管理器
|
# 添加到设备管理器
|
||||||
@@ -637,13 +734,13 @@ class MessageProcessor:
|
|||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||||
)
|
)
|
||||||
logger.info(f"[MessageProcessor] Job {job_log} can start immediately")
|
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||||
else:
|
else:
|
||||||
# 需要排队
|
# 需要排队
|
||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||||
)
|
)
|
||||||
logger.info(f"[MessageProcessor] Job {job_log} queued")
|
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
||||||
|
|
||||||
# 通知QueueProcessor有新的队列更新
|
# 通知QueueProcessor有新的队列更新
|
||||||
if self.queue_processor:
|
if self.queue_processor:
|
||||||
@@ -657,6 +754,32 @@ class MessageProcessor:
|
|||||||
req = JobAddReq(**data)
|
req = JobAddReq(**data)
|
||||||
|
|
||||||
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
||||||
|
|
||||||
|
# 服务端对always_free动作可能跳过query_action_state直接发job_start,
|
||||||
|
# 此时job尚未注册,需要自动补注册
|
||||||
|
existing_job = self.device_manager.get_job_info(req.job_id)
|
||||||
|
if not existing_job:
|
||||||
|
action_name = req.action
|
||||||
|
device_action_key = f"/devices/{req.device_id}/{action_name}"
|
||||||
|
action_always_free = self._check_action_always_free(req.device_id, action_name)
|
||||||
|
|
||||||
|
if action_always_free:
|
||||||
|
job_info = JobInfo(
|
||||||
|
job_id=req.job_id,
|
||||||
|
task_id=req.task_id,
|
||||||
|
device_id=req.device_id,
|
||||||
|
action_name=action_name,
|
||||||
|
device_action_key=device_action_key,
|
||||||
|
status=JobStatus.QUEUE,
|
||||||
|
start_time=time.time(),
|
||||||
|
always_free=True,
|
||||||
|
)
|
||||||
|
self.device_manager.add_queue_request(job_info)
|
||||||
|
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
|
||||||
|
else:
|
||||||
|
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
|
||||||
|
return
|
||||||
|
|
||||||
success = self.device_manager.start_job(req.job_id)
|
success = self.device_manager.start_job(req.job_id)
|
||||||
if not success:
|
if not success:
|
||||||
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
||||||
@@ -850,9 +973,7 @@ class MessageProcessor:
|
|||||||
device_action_groups[key_add] = []
|
device_action_groups[key_add] = []
|
||||||
device_action_groups[key_add].append(item["uuid"])
|
device_action_groups[key_add].append(item["uuid"])
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}")
|
||||||
f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# 正常update
|
# 正常update
|
||||||
key = (device_id, "update")
|
key = (device_id, "update")
|
||||||
@@ -866,7 +987,9 @@ class MessageProcessor:
|
|||||||
device_action_groups[key] = []
|
device_action_groups[key] = []
|
||||||
device_action_groups[key].append(item["uuid"])
|
device_action_groups[key].append(item["uuid"])
|
||||||
|
|
||||||
logger.trace(f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
|
logger.trace(
|
||||||
|
f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}"
|
||||||
|
)
|
||||||
|
|
||||||
# 为每个(device_id, action)创建独立的更新线程
|
# 为每个(device_id, action)创建独立的更新线程
|
||||||
for (device_id, actual_action), items in device_action_groups.items():
|
for (device_id, actual_action), items in device_action_groups.items():
|
||||||
@@ -902,6 +1025,37 @@ class MessageProcessor:
|
|||||||
)
|
)
|
||||||
thread.start()
|
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]):
|
async def _handle_request_restart(self, data: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
处理重启请求
|
处理重启请求
|
||||||
@@ -913,14 +1067,13 @@ class MessageProcessor:
|
|||||||
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
|
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
|
||||||
|
|
||||||
# 发送确认消息
|
# 发送确认消息
|
||||||
if self.websocket_client:
|
self.send_message(
|
||||||
await self.websocket_client.send_message({
|
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
|
||||||
"action": "restart_acknowledged",
|
)
|
||||||
"data": {"reason": reason, "delay": delay}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 设置全局重启标志
|
# 设置全局重启标志
|
||||||
import unilabos.app.main as main_module
|
import unilabos.app.main as main_module
|
||||||
|
|
||||||
main_module._restart_requested = True
|
main_module._restart_requested = True
|
||||||
main_module._restart_reason = reason
|
main_module._restart_reason = reason
|
||||||
|
|
||||||
@@ -930,10 +1083,12 @@ class MessageProcessor:
|
|||||||
# 在新线程中执行清理,避免阻塞当前事件循环
|
# 在新线程中执行清理,避免阻塞当前事件循环
|
||||||
def do_cleanup():
|
def do_cleanup():
|
||||||
import time
|
import time
|
||||||
|
|
||||||
time.sleep(0.5) # 给当前消息处理完成的时间
|
time.sleep(0.5) # 给当前消息处理完成的时间
|
||||||
logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}")
|
logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}")
|
||||||
try:
|
try:
|
||||||
from unilabos.app.utils import cleanup_for_restart
|
from unilabos.app.utils import cleanup_for_restart
|
||||||
|
|
||||||
if cleanup_for_restart():
|
if cleanup_for_restart():
|
||||||
logger.info("[MessageProcessor] Cleanup successful, main() will restart")
|
logger.info("[MessageProcessor] Cleanup successful, main() will restart")
|
||||||
else:
|
else:
|
||||||
@@ -1016,6 +1171,7 @@ class QueueProcessor:
|
|||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止队列处理线程"""
|
"""停止队列处理线程"""
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
|
self.queue_update_event.set() # 立即唤醒等待中的线程
|
||||||
if self.thread and self.thread.is_alive():
|
if self.thread and self.thread.is_alive():
|
||||||
self.thread.join(timeout=2)
|
self.thread.join(timeout=2)
|
||||||
logger.info("[QueueProcessor] Stopped")
|
logger.info("[QueueProcessor] Stopped")
|
||||||
@@ -1116,6 +1272,11 @@ class QueueProcessor:
|
|||||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||||
|
|
||||||
for job_info in queued_jobs:
|
for job_info in queued_jobs:
|
||||||
|
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
||||||
|
# 此时不应再发送 busy/need_more,否则会覆盖已发出的 free=True 通知
|
||||||
|
if job_info.status != JobStatus.QUEUE:
|
||||||
|
continue
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"action": "report_action_state",
|
"action": "report_action_state",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -1131,7 +1292,7 @@ class QueueProcessor:
|
|||||||
success = self.message_processor.send_message(message)
|
success = self.message_processor.send_message(message)
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
if success:
|
if success:
|
||||||
logger.debug(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
logger.trace(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
||||||
|
|
||||||
@@ -1154,7 +1315,7 @@ class QueueProcessor:
|
|||||||
job_info.action_name,
|
job_info.action_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
logger.trace(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
||||||
|
|
||||||
# 结束任务,获取下一个可执行的任务
|
# 结束任务,获取下一个可执行的任务
|
||||||
next_job = self.device_manager.end_job(job_id)
|
next_job = self.device_manager.end_job(job_id)
|
||||||
@@ -1174,8 +1335,8 @@ class QueueProcessor:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
||||||
logger.info(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||||
|
|
||||||
# 立即触发下一轮状态检查
|
# 立即触发下一轮状态检查
|
||||||
self.notify_queue_update()
|
self.notify_queue_update()
|
||||||
@@ -1264,8 +1425,8 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
message = {"action": "normal_exit", "data": {"session_id": session_id}}
|
message = {"action": "normal_exit", "data": {"session_id": session_id}}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
|
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
|
||||||
# 给一点时间让消息发送出去
|
# send_handler 每100ms检查一次队列,等300ms足以让消息发出
|
||||||
time.sleep(1)
|
time.sleep(0.3)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
|
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
|
||||||
|
|
||||||
@@ -1317,7 +1478,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
||||||
|
|
||||||
logger.info(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
# logger.debug(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
||||||
|
|
||||||
# 通知队列处理器job完成(包括timeout的job)
|
# 通知队列处理器job完成(包括timeout的job)
|
||||||
self.queue_processor.handle_job_completed(item.job_id, status)
|
self.queue_processor.handle_job_completed(item.job_id, status)
|
||||||
@@ -1384,7 +1545,9 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
if host_node:
|
if host_node:
|
||||||
# 获取设备信息
|
# 获取设备信息
|
||||||
for device_id, namespace in host_node.devices_names.items():
|
for device_id, namespace in host_node.devices_names.items():
|
||||||
device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
device_key = (
|
||||||
|
f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
||||||
|
)
|
||||||
is_online = device_key in host_node._online_devices
|
is_online = device_key in host_node._online_devices
|
||||||
|
|
||||||
# 获取设备的动作信息
|
# 获取设备的动作信息
|
||||||
@@ -1398,14 +1561,16 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
"action_type": str(type(client).__name__),
|
"action_type": str(type(client).__name__),
|
||||||
}
|
}
|
||||||
|
|
||||||
devices.append({
|
devices.append(
|
||||||
"device_id": device_id,
|
{
|
||||||
"namespace": namespace,
|
"device_id": device_id,
|
||||||
"device_key": device_key,
|
"namespace": namespace,
|
||||||
"is_online": is_online,
|
"device_key": device_key,
|
||||||
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
"is_online": is_online,
|
||||||
"actions": actions,
|
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
||||||
})
|
"actions": actions,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
|
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class BasicConfig:
|
|||||||
startup_json_path = None # 填写绝对路径
|
startup_json_path = None # 填写绝对路径
|
||||||
disable_browser = False # 禁止浏览器自动打开
|
disable_browser = False # 禁止浏览器自动打开
|
||||||
port = 8002 # 本地HTTP服务
|
port = 8002 # 本地HTTP服务
|
||||||
|
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
||||||
|
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
|
||||||
|
extra_resource = False # 是否加载lab_开头的额外资源
|
||||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
||||||
|
|
||||||
@@ -38,7 +41,7 @@ class BasicConfig:
|
|||||||
class WSConfig:
|
class WSConfig:
|
||||||
reconnect_interval = 5 # 重连间隔(秒)
|
reconnect_interval = 5 # 重连间隔(秒)
|
||||||
max_reconnect_attempts = 999 # 最大重连次数
|
max_reconnect_attempts = 999 # 最大重连次数
|
||||||
ping_interval = 30 # ping间隔(秒)
|
ping_interval = 20 # ping间隔(秒)
|
||||||
|
|
||||||
|
|
||||||
# HTTP配置
|
# HTTP配置
|
||||||
@@ -144,5 +147,5 @@ def load_config(config_path=None):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
config_path = os.path.join(os.path.dirname(__file__), "example_config.py")
|
||||||
load_config(config_path)
|
load_config(config_path)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import inspect
|
import inspect
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pylabrobot.resources import (
|
|||||||
ResourceHolder,
|
ResourceHolder,
|
||||||
Lid,
|
Lid,
|
||||||
Trash,
|
Trash,
|
||||||
Tip,
|
Tip, TubeRack,
|
||||||
)
|
)
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
@@ -208,7 +208,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
|
if spread == "":
|
||||||
|
spread = "wide"
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.aspirate(
|
return await self._simulate_handler.aspirate(
|
||||||
resources,
|
resources,
|
||||||
@@ -221,17 +222,33 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
await super().aspirate(
|
try:
|
||||||
resources,
|
await super().aspirate(
|
||||||
vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
spread,
|
blow_out_air_volume,
|
||||||
**backend_kwargs,
|
spread,
|
||||||
)
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
await super().aspirate(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
spread="custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
@@ -243,7 +260,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
channels_to_use = use_channels
|
channels_to_use = use_channels
|
||||||
|
|
||||||
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
||||||
res_samples.append({"name": resource.name, "sample_uuid": resource.unilabos_extra.get("sample_uuid", None)})
|
sample_uuid_value = getattr(resource, "unilabos_extra", {}).get(EXTRA_SAMPLE_UUID, None)
|
||||||
|
res_samples.append({"name": resource.name, "sample_uuid": sample_uuid_value})
|
||||||
res_volumes.append(volume)
|
res_volumes.append(volume)
|
||||||
self.pending_liquids_dict[channel] = {
|
self.pending_liquids_dict[channel] = {
|
||||||
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
||||||
@@ -263,6 +281,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
) -> SimpleReturn:
|
) -> SimpleReturn:
|
||||||
|
if spread == "":
|
||||||
|
spread = "wide"
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.dispense(
|
return await self._simulate_handler.dispense(
|
||||||
resources,
|
resources,
|
||||||
@@ -275,16 +295,33 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
await super().dispense(
|
try:
|
||||||
resources,
|
await super().dispense(
|
||||||
vols,
|
resources,
|
||||||
use_channels,
|
vols,
|
||||||
flow_rates,
|
use_channels,
|
||||||
offsets,
|
flow_rates,
|
||||||
liquid_height,
|
offsets,
|
||||||
blow_out_air_volume,
|
liquid_height,
|
||||||
**backend_kwargs,
|
blow_out_air_volume,
|
||||||
)
|
spread,
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
||||||
|
await super().dispense(
|
||||||
|
resources,
|
||||||
|
vols,
|
||||||
|
use_channels,
|
||||||
|
flow_rates,
|
||||||
|
offsets,
|
||||||
|
liquid_height,
|
||||||
|
blow_out_air_volume,
|
||||||
|
"custom",
|
||||||
|
**backend_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
for resource, volume, channel in zip(resources, vols, use_channels):
|
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||||
@@ -702,10 +739,13 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
|
|
||||||
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
||||||
"""
|
"""
|
||||||
assert issubclass(plate.__class__, Plate), "plate must be a Plate"
|
assert issubclass(plate.__class__, Plate) or issubclass(plate.__class__, TubeRack) , f"plate must be a Plate, now: {type(plate)}"
|
||||||
plate: Plate = cast(Plate, cast(Resource, plate))
|
plate: Union[Plate, TubeRack]
|
||||||
# 根据 well_names 获取对应的 Well 对象
|
# 根据 well_names 获取对应的 Well 对象
|
||||||
wells = [plate.get_well(name) for name in well_names]
|
if issubclass(plate.__class__, Plate):
|
||||||
|
wells = [plate.get_well(name) for name in well_names]
|
||||||
|
elif issubclass(plate.__class__, TubeRack):
|
||||||
|
wells = [plate.get_tube(name) for name in well_names]
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
|
|
||||||
# 如果 liquid_names 和 volumes 都为空,直接返回
|
# 如果 liquid_names 和 volumes 都为空,直接返回
|
||||||
@@ -851,7 +891,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(sources)):
|
for _ in range(len(sources)):
|
||||||
tip = []
|
tip = []
|
||||||
for __ in range(len(use_channels)):
|
for __ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[sources[_]],
|
resources=[sources[_]],
|
||||||
@@ -891,7 +931,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = waste_liquid[i : i + 8]
|
current_targets = waste_liquid[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -985,7 +1025,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for x in range(len(use_channels)):
|
for x in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1037,7 +1077,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = reagent_sources[i : i + 8]
|
current_reagent_sources = reagent_sources[i : i + 8]
|
||||||
@@ -1162,11 +1202,19 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
||||||
mix_stage.
|
mix_stage.
|
||||||
"""
|
"""
|
||||||
|
num_sources = len(sources)
|
||||||
|
num_targets = len(targets)
|
||||||
|
len_asp_vols = len(asp_vols)
|
||||||
|
len_dis_vols = len(dis_vols)
|
||||||
# 确保 use_channels 有默认值
|
# 确保 use_channels 有默认值
|
||||||
if use_channels is None:
|
if use_channels is None:
|
||||||
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
||||||
use_channels = list(range(self.channel_num)) if self.channel_num > 0 else [0]
|
use_channels = list(range(self.channel_num)) if self.channel_num == 8 else [0]
|
||||||
|
elif len(use_channels) == 8:
|
||||||
|
if self.channel_num != 8:
|
||||||
|
raise ValueError(f"if channel_num is 8, use_channels length must be 8, but got {len(use_channels)}")
|
||||||
|
if num_sources%8 != 0 or num_targets%8 != 0 or len_asp_vols%8 != 0 or len_dis_vols%8 != 0:
|
||||||
|
raise ValueError(f"if channel_num is 8, sources, targets, asp_vols, and dis_vols length must be divisible by 8, but got {num_sources}, {num_targets}, {len_asp_vols}, and {len_dis_vols}")
|
||||||
|
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified.
|
pass # This mode is not verified.
|
||||||
@@ -1200,86 +1248,227 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
||||||
num_sources = len(sources)
|
num_sources = len(sources)
|
||||||
num_targets = len(targets)
|
num_targets = len(targets)
|
||||||
|
len_asp_vols = len(asp_vols)
|
||||||
|
len_dis_vols = len(dis_vols)
|
||||||
|
|
||||||
if num_sources == 1 and num_targets > 1:
|
# if num_targets != 1 and num_sources != 1:
|
||||||
# 模式1: 一对多 (1 source -> N targets)
|
# if len_asp_vols != num_sources and len_asp_vols != num_targets:
|
||||||
await self._transfer_one_to_many(
|
# raise ValueError(f"asp_vols length must be equal to sources or targets length, but got {len_asp_vols} and {num_sources} and {num_targets}")
|
||||||
sources[0],
|
# if len_dis_vols != num_sources and len_dis_vols != num_targets:
|
||||||
targets,
|
# raise ValueError(f"dis_vols length must be equal to sources or targets length, but got {len_dis_vols} and {num_sources} and {num_targets}")
|
||||||
tip_racks,
|
|
||||||
use_channels,
|
if len(use_channels) != 8:
|
||||||
asp_vols,
|
max_len = max(num_sources, num_targets)
|
||||||
dis_vols,
|
for i in range(max_len):
|
||||||
asp_flow_rates,
|
|
||||||
dis_flow_rates,
|
# 辅助函数:安全地从列表中获取元素,如果列表为空则返回None
|
||||||
offsets,
|
def safe_get(lst, idx, default=None):
|
||||||
touch_tip,
|
return [lst[idx]] if lst else default
|
||||||
liquid_height,
|
|
||||||
blow_out_air_volume,
|
# 动态构建参数字典,只传递实际提供的参数
|
||||||
spread,
|
kwargs = {
|
||||||
mix_stage,
|
'sources': [sources[i%num_sources]],
|
||||||
mix_times,
|
'targets': [targets[i%num_targets]],
|
||||||
mix_vol,
|
'tip_racks': tip_racks,
|
||||||
mix_rate,
|
'use_channels': use_channels,
|
||||||
mix_liquid_height,
|
'asp_vols': [asp_vols[i%len_asp_vols]],
|
||||||
delays,
|
'dis_vols': [dis_vols[i%len_dis_vols]],
|
||||||
)
|
}
|
||||||
elif num_sources > 1 and num_targets == 1:
|
|
||||||
# 模式2: 多对一 (N sources -> 1 target)
|
# 条件性添加可选参数
|
||||||
await self._transfer_many_to_one(
|
if asp_flow_rates is not None:
|
||||||
sources,
|
kwargs['asp_flow_rates'] = [asp_flow_rates[i%len_asp_vols]]
|
||||||
targets[0],
|
if dis_flow_rates is not None:
|
||||||
tip_racks,
|
kwargs['dis_flow_rates'] = [dis_flow_rates[i%len_dis_vols]]
|
||||||
use_channels,
|
if offsets is not None:
|
||||||
asp_vols,
|
kwargs['offsets'] = safe_get(offsets, i)
|
||||||
dis_vols,
|
if touch_tip is not None:
|
||||||
asp_flow_rates,
|
kwargs['touch_tip'] = touch_tip if touch_tip else False
|
||||||
dis_flow_rates,
|
if liquid_height is not None:
|
||||||
offsets,
|
kwargs['liquid_height'] = safe_get(liquid_height, i)
|
||||||
touch_tip,
|
if blow_out_air_volume is not None:
|
||||||
liquid_height,
|
kwargs['blow_out_air_volume'] = safe_get(blow_out_air_volume, i)
|
||||||
blow_out_air_volume,
|
if spread is not None:
|
||||||
spread,
|
kwargs['spread'] = spread
|
||||||
mix_stage,
|
if mix_stage is not None:
|
||||||
mix_times,
|
kwargs['mix_stage'] = safe_get(mix_stage, i)
|
||||||
mix_vol,
|
if mix_times is not None:
|
||||||
mix_rate,
|
kwargs['mix_times'] = safe_get(mix_times, i)
|
||||||
mix_liquid_height,
|
if mix_vol is not None:
|
||||||
delays,
|
kwargs['mix_vol'] = safe_get(mix_vol, i)
|
||||||
)
|
if mix_rate is not None:
|
||||||
elif num_sources == num_targets:
|
kwargs['mix_rate'] = safe_get(mix_rate, i)
|
||||||
# 模式3: 一对一 (N sources -> N targets)
|
if mix_liquid_height is not None:
|
||||||
await self._transfer_one_to_one(
|
kwargs['mix_liquid_height'] = safe_get(mix_liquid_height, i)
|
||||||
sources,
|
if delays is not None:
|
||||||
targets,
|
kwargs['delays'] = safe_get(delays, i)
|
||||||
tip_racks,
|
|
||||||
use_channels,
|
await self._transfer_base_method(**kwargs)
|
||||||
asp_vols,
|
|
||||||
dis_vols,
|
|
||||||
asp_flow_rates,
|
|
||||||
dis_flow_rates,
|
# if num_sources == 1 and num_targets > 1:
|
||||||
offsets,
|
# # 模式1: 一对多 (1 source -> N targets)
|
||||||
touch_tip,
|
# await self._transfer_one_to_many(
|
||||||
liquid_height,
|
# sources,
|
||||||
blow_out_air_volume,
|
# targets,
|
||||||
spread,
|
# tip_racks,
|
||||||
mix_stage,
|
# use_channels,
|
||||||
mix_times,
|
# asp_vols,
|
||||||
mix_vol,
|
# dis_vols,
|
||||||
mix_rate,
|
# asp_flow_rates,
|
||||||
mix_liquid_height,
|
# dis_flow_rates,
|
||||||
delays,
|
# offsets,
|
||||||
)
|
# touch_tip,
|
||||||
else:
|
# liquid_height,
|
||||||
raise ValueError(
|
# blow_out_air_volume,
|
||||||
f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
# spread,
|
||||||
"Supported modes: 1->N, N->1, or N->N."
|
# mix_stage,
|
||||||
)
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# elif num_sources > 1 and num_targets == 1:
|
||||||
|
# # 模式2: 多对一 (N sources -> 1 target)
|
||||||
|
# await self._transfer_many_to_one(
|
||||||
|
# sources,
|
||||||
|
# targets[0],
|
||||||
|
# tip_racks,
|
||||||
|
# use_channels,
|
||||||
|
# asp_vols,
|
||||||
|
# dis_vols,
|
||||||
|
# asp_flow_rates,
|
||||||
|
# dis_flow_rates,
|
||||||
|
# offsets,
|
||||||
|
# touch_tip,
|
||||||
|
# liquid_height,
|
||||||
|
# blow_out_air_volume,
|
||||||
|
# spread,
|
||||||
|
# mix_stage,
|
||||||
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# elif num_sources == num_targets:
|
||||||
|
# # 模式3: 一对一 (N sources -> N targets)
|
||||||
|
# await self._transfer_one_to_one(
|
||||||
|
# sources,
|
||||||
|
# targets,
|
||||||
|
# tip_racks,
|
||||||
|
# use_channels,
|
||||||
|
# asp_vols,
|
||||||
|
# dis_vols,
|
||||||
|
# asp_flow_rates,
|
||||||
|
# dis_flow_rates,
|
||||||
|
# offsets,
|
||||||
|
# touch_tip,
|
||||||
|
# liquid_height,
|
||||||
|
# blow_out_air_volume,
|
||||||
|
# spread,
|
||||||
|
# mix_stage,
|
||||||
|
# mix_times,
|
||||||
|
# mix_vol,
|
||||||
|
# mix_rate,
|
||||||
|
# mix_liquid_height,
|
||||||
|
# delays,
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# raise ValueError(
|
||||||
|
# f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
||||||
|
# "Supported modes: 1->N, N->1, or N->N."
|
||||||
|
# )
|
||||||
|
|
||||||
return TransferLiquidReturn(
|
return TransferLiquidReturn(
|
||||||
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
||||||
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
||||||
)
|
)
|
||||||
|
async def _transfer_base_method(
|
||||||
|
self,
|
||||||
|
sources: Sequence[Container],
|
||||||
|
targets: Sequence[Container],
|
||||||
|
tip_racks: Sequence[TipRack],
|
||||||
|
use_channels: List[int],
|
||||||
|
asp_vols: List[float],
|
||||||
|
dis_vols: List[float],
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
|
||||||
|
# 从kwargs中提取参数,提供默认值
|
||||||
|
asp_flow_rates = kwargs.get('asp_flow_rates')
|
||||||
|
dis_flow_rates = kwargs.get('dis_flow_rates')
|
||||||
|
offsets = kwargs.get('offsets')
|
||||||
|
touch_tip = kwargs.get('touch_tip', False)
|
||||||
|
liquid_height = kwargs.get('liquid_height')
|
||||||
|
blow_out_air_volume = kwargs.get('blow_out_air_volume')
|
||||||
|
spread = kwargs.get('spread', 'wide')
|
||||||
|
mix_stage = kwargs.get('mix_stage')
|
||||||
|
mix_times = kwargs.get('mix_times')
|
||||||
|
mix_vol = kwargs.get('mix_vol')
|
||||||
|
mix_rate = kwargs.get('mix_rate')
|
||||||
|
mix_liquid_height = kwargs.get('mix_liquid_height')
|
||||||
|
delays = kwargs.get('delays')
|
||||||
|
|
||||||
|
tip = []
|
||||||
|
tip.extend(self._get_next_tip())
|
||||||
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
|
await self.mix(
|
||||||
|
targets=[targets[0]],
|
||||||
|
mix_time=mix_times,
|
||||||
|
mix_vol=mix_vol,
|
||||||
|
offsets=offsets if offsets else None,
|
||||||
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
|
use_channels=use_channels,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[sources[0]],
|
||||||
|
vols=[asp_vols[0]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[asp_flow_rates[0]] if asp_flow_rates and len(asp_flow_rates) > 0 else None,
|
||||||
|
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
||||||
|
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
||||||
|
blow_out_air_volume=(
|
||||||
|
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
||||||
|
),
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
if delays is not None:
|
||||||
|
await self.custom_delay(seconds=delays[0])
|
||||||
|
await self.dispense(
|
||||||
|
resources=[targets[0]],
|
||||||
|
vols=[dis_vols[0]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[dis_flow_rates[0]] if dis_flow_rates and len(dis_flow_rates) > 0 else None,
|
||||||
|
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
||||||
|
blow_out_air_volume=(
|
||||||
|
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
||||||
|
),
|
||||||
|
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
if delays is not None and len(delays) > 1:
|
||||||
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
if mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0:
|
||||||
|
await self.mix(
|
||||||
|
targets=[targets[0]],
|
||||||
|
mix_time=mix_times,
|
||||||
|
mix_vol=mix_vol,
|
||||||
|
offsets=offsets if offsets else None,
|
||||||
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
|
use_channels=use_channels,
|
||||||
|
)
|
||||||
|
if delays is not None and len(delays) > 1:
|
||||||
|
await self.custom_delay(seconds=delays[0])
|
||||||
|
await self.touch_tip(targets[0])
|
||||||
|
await self.discard_tips(use_channels=use_channels)
|
||||||
|
|
||||||
async def _transfer_one_to_one(
|
async def _transfer_one_to_one(
|
||||||
self,
|
self,
|
||||||
@@ -1322,7 +1511,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for ___ in range(len(use_channels)):
|
for ___ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1386,7 +1575,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -1491,7 +1680,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
# 单通道模式:一次吸液,多次分液
|
# 单通道模式:一次吸液,多次分液
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1563,7 +1752,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
@@ -1702,7 +1891,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
@@ -1721,7 +1910,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for idx, source in enumerate(sources):
|
for idx, source in enumerate(sources):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1804,7 +1993,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
@@ -1822,7 +2011,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(next(self.current_tip))
|
tip.extend(self._get_next_tip())
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_sources = sources[i : i + 8]
|
current_sources = sources[i : i + 8]
|
||||||
@@ -2022,7 +2211,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for rack in tip_racks:
|
for rack in tip_racks:
|
||||||
for tip in rack:
|
for tip in rack:
|
||||||
yield tip
|
yield tip
|
||||||
raise RuntimeError("Out of tips!")
|
# raise RuntimeError("Out of tips!")
|
||||||
|
|
||||||
|
def _get_next_tip(self):
|
||||||
|
"""从 current_tip 迭代器获取下一个 tip,耗尽时抛出明确错误而非 StopIteration"""
|
||||||
|
try:
|
||||||
|
return next(self.current_tip)
|
||||||
|
except StopIteration as e:
|
||||||
|
raise RuntimeError("Tip rack exhausted: no more tips available for transfer") from e
|
||||||
|
|
||||||
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
||||||
"""Set the tip racks for the liquid handler."""
|
"""Set the tip racks for the liquid handler."""
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ from pylabrobot.resources import (
|
|||||||
Trash,
|
Trash,
|
||||||
PlateAdapter,
|
PlateAdapter,
|
||||||
TubeRack,
|
TubeRack,
|
||||||
create_homogeneous_resources,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
||||||
@@ -56,8 +55,8 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
|||||||
TransferLiquidReturn,
|
TransferLiquidReturn,
|
||||||
)
|
)
|
||||||
from unilabos.registry.placeholder_type import ResourceSlot
|
from unilabos.registry.placeholder_type import ResourceSlot
|
||||||
from unilabos.resources.itemized_carrier import ItemizedCarrier
|
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class PRCXIError(RuntimeError):
|
class PRCXIError(RuntimeError):
|
||||||
@@ -92,23 +91,103 @@ class PRCXI9300Deck(Deck):
|
|||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
|
# T1-T16 默认位置 (4列×4行)
|
||||||
super().__init__(name, size_x, size_y, size_z)
|
_DEFAULT_SITE_POSITIONS = [
|
||||||
self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位
|
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
|
||||||
self.slot_locations = []
|
(0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8
|
||||||
|
(0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T9-T12
|
||||||
|
(0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T13-T16
|
||||||
|
]
|
||||||
|
_DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86, "depth": 0}
|
||||||
|
_DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack", "adaptor"]
|
||||||
|
|
||||||
for i in range(0, 16):
|
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||||
self.slot_locations.append(Coordinate((i%4)*137.5+5, (3-int(i/4))*96+13, 0))
|
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
||||||
|
super().__init__(size_x, size_y, size_z, name)
|
||||||
|
if sites is not None:
|
||||||
|
self.sites: List[Dict[str, Any]] = [dict(s) for s in sites]
|
||||||
|
else:
|
||||||
|
self.sites = []
|
||||||
|
for i, (x, y, z) in enumerate(self._DEFAULT_SITE_POSITIONS):
|
||||||
|
self.sites.append({
|
||||||
|
"label": f"T{i + 1}",
|
||||||
|
"visible": True,
|
||||||
|
"position": {"x": x, "y": y, "z": z},
|
||||||
|
"size": dict(self._DEFAULT_SITE_SIZE),
|
||||||
|
"content_type": list(self._DEFAULT_CONTENT_TYPE),
|
||||||
|
})
|
||||||
|
# _ordering: label -> None, 用于外部通过 list(keys()).index(site) 将 Tn 转换为 spot index
|
||||||
|
self._ordering = collections.OrderedDict(
|
||||||
|
(site["label"], None) for site in self.sites
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_site_location(self, idx: int) -> Coordinate:
|
||||||
|
pos = self.sites[idx]["position"]
|
||||||
|
return Coordinate(pos["x"], pos["y"], pos["z"])
|
||||||
|
|
||||||
|
def _get_site_resource(self, idx: int) -> Optional[Resource]:
|
||||||
|
site_loc = self._get_site_location(idx)
|
||||||
|
for child in self.children:
|
||||||
|
if child.location == site_loc:
|
||||||
|
return child
|
||||||
|
return None
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: Resource,
|
||||||
|
location: Optional[Coordinate] = None,
|
||||||
|
reassign: bool = True,
|
||||||
|
spot: Optional[int] = None,
|
||||||
|
):
|
||||||
|
idx = spot
|
||||||
|
if spot is not None:
|
||||||
|
idx = spot
|
||||||
|
else:
|
||||||
|
for i, site in enumerate(self.sites):
|
||||||
|
site_loc = self._get_site_location(i)
|
||||||
|
if site.get("label") == resource.name:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
if location is not None and site_loc == location:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if idx is None:
|
||||||
|
for i in range(len(self.sites)):
|
||||||
|
if self._get_site_resource(i) is None:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if idx is None:
|
||||||
|
raise ValueError(f"No available site on deck '{self.name}' for resource '{resource.name}'")
|
||||||
|
|
||||||
|
if not reassign and self._get_site_resource(idx) is not None:
|
||||||
|
raise ValueError(f"Site {idx} ('{self.sites[idx]['label']}') is already occupied")
|
||||||
|
|
||||||
|
loc = self._get_site_location(idx)
|
||||||
|
super().assign_child_resource(resource, location=loc, reassign=reassign)
|
||||||
|
|
||||||
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
|
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
|
||||||
if self.slots[slot - 1] is not None and not reassign:
|
self.assign_child_resource(resource, spot=slot - 1, reassign=reassign)
|
||||||
raise ValueError(f"Spot {slot} is already occupied")
|
|
||||||
|
|
||||||
self.slots[slot - 1] = resource
|
def serialize(self) -> dict:
|
||||||
super().assign_child_resource(resource, location=self.slot_locations[slot - 1])
|
data = super().serialize()
|
||||||
|
sites_out = []
|
||||||
|
for i, site in enumerate(self.sites):
|
||||||
|
occupied = self._get_site_resource(i)
|
||||||
|
sites_out.append({
|
||||||
|
"label": site["label"],
|
||||||
|
"visible": site.get("visible", True),
|
||||||
|
"occupied_by": occupied.name if occupied is not None else None,
|
||||||
|
"position": site["position"],
|
||||||
|
"size": site["size"],
|
||||||
|
"content_type": site["content_type"],
|
||||||
|
})
|
||||||
|
data["sites"] = sites_out
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class PRCXI9300Container(Plate):
|
class PRCXI9300Container(Container):
|
||||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
||||||
|
|
||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
@@ -121,11 +200,10 @@ class PRCXI9300Container(Plate):
|
|||||||
size_y: float,
|
size_y: float,
|
||||||
size_z: float,
|
size_z: float,
|
||||||
category: str,
|
category: str,
|
||||||
ordering: collections.OrderedDict,
|
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
super().__init__(name, size_x, size_y, size_z, category=category, model=model)
|
||||||
self._unilabos_state = {}
|
self._unilabos_state = {}
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
@@ -180,30 +258,9 @@ class PRCXI9300Plate(Plate):
|
|||||||
elif value is None:
|
elif value is None:
|
||||||
ordering_param = ordering
|
ordering_param = ordering
|
||||||
else:
|
else:
|
||||||
# ordering 的值是对象(可能是 Well 对象),检查是否有有效的 location
|
# ordering 的值已经是对象,可以直接使用
|
||||||
# 如果是反序列化过程,Well 对象可能没有正确的 location,需要让 Plate 重新创建
|
items = ordering
|
||||||
sample_value = next(iter(ordering.values()), None)
|
ordering_param = None
|
||||||
if sample_value is not None and hasattr(sample_value, 'location'):
|
|
||||||
# 如果是 Well 对象但 location 为 None,说明是反序列化过程
|
|
||||||
# 让 Plate 自己创建 Well 对象
|
|
||||||
if sample_value.location is None:
|
|
||||||
items = None
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
|
||||||
else:
|
|
||||||
# Well 对象有有效的 location,可以直接使用
|
|
||||||
items = ordering
|
|
||||||
ordering_param = None
|
|
||||||
elif sample_value is None:
|
|
||||||
# ordering 的值都是 None,让 Plate 自己创建 Well 对象
|
|
||||||
items = None
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
|
||||||
else:
|
|
||||||
# 其他情况,直接使用
|
|
||||||
items = ordering
|
|
||||||
ordering_param = None
|
|
||||||
else:
|
|
||||||
items = None
|
|
||||||
ordering_param = collections.OrderedDict() # 提供空的 ordering
|
|
||||||
|
|
||||||
# 根据情况传递不同的参数
|
# 根据情况传递不同的参数
|
||||||
if items is not None:
|
if items is not None:
|
||||||
@@ -274,26 +331,20 @@ class PRCXI9300TipRack(TipRack):
|
|||||||
if ordered_items is not None:
|
if ordered_items is not None:
|
||||||
items = ordered_items
|
items = ordered_items
|
||||||
elif ordering is not None:
|
elif ordering is not None:
|
||||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
# 检查 ordering 中的值类型来决定如何处理:
|
||||||
# 如果是字符串,说明这是位置名称,需要让 TipRack 自己创建 Tip 对象
|
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||||
|
if not ordering or first_val is None or isinstance(first_val, str):
|
||||||
|
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||||
# 传递 ordering 参数而不是 ordered_items,让 TipRack 自己创建 Tip 对象
|
# 传递 ordering 参数而不是 ordered_items,让 TipRack 自己创建 Tip 对象
|
||||||
items = None
|
items = None
|
||||||
# 使用 ordering 参数,只包含位置信息(键)
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||||
else:
|
else:
|
||||||
# ordering 的值已经是对象,需要过滤掉 None 值
|
# ordering 的值已经是对象,可以直接使用
|
||||||
# 只保留有效的对象,用于 ordered_items 参数
|
items = ordering
|
||||||
valid_items = {k: v for k, v in ordering.items() if v is not None}
|
ordering_param = None
|
||||||
if valid_items:
|
|
||||||
items = valid_items
|
|
||||||
ordering_param = None
|
|
||||||
else:
|
|
||||||
# 如果没有有效对象,使用 ordering 参数
|
|
||||||
items = None
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
|
||||||
else:
|
else:
|
||||||
items = None
|
items = None
|
||||||
ordering_param = None
|
ordering_param = None
|
||||||
@@ -352,10 +403,16 @@ class PRCXI9300Trash(Trash):
|
|||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
def __init__(
|
||||||
category: str = "plate",
|
self,
|
||||||
material_info: Optional[Dict[str, Any]] = None,
|
name: str,
|
||||||
**kwargs):
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
category: str = "trash",
|
||||||
|
material_info: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
|
||||||
if name != "trash":
|
if name != "trash":
|
||||||
print(f"Warning: PRCXI9300Trash usually expects name='trash' for backend logic, but got '{name}'.")
|
print(f"Warning: PRCXI9300Trash usually expects name='trash' for backend logic, but got '{name}'.")
|
||||||
@@ -424,25 +481,20 @@ class PRCXI9300TubeRack(TubeRack):
|
|||||||
items_to_pass = ordered_items
|
items_to_pass = ordered_items
|
||||||
ordering_param = None
|
ordering_param = None
|
||||||
elif ordering is not None:
|
elif ordering is not None:
|
||||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
# 检查 ordering 中的值类型来决定如何处理:
|
||||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||||
# ordering 的值是字符串,这种情况下我们让 TubeRack 使用默认行为
|
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||||
# 不在初始化时创建 items,而是在 deserialize 后处理
|
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||||
|
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||||
|
if not ordering or first_val is None or isinstance(first_val, str):
|
||||||
|
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||||
|
# 传递 ordering 参数而不是 ordered_items,让 TubeRack 自己创建 Tube 对象
|
||||||
items_to_pass = None
|
items_to_pass = None
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys()) # 提供空的 ordering 来满足要求
|
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||||
# 保存 ordering 信息以便后续处理
|
|
||||||
self._temp_ordering = ordering
|
|
||||||
else:
|
else:
|
||||||
# ordering 的值已经是对象,需要过滤掉 None 值
|
# ordering 的值已经是对象,可以直接使用
|
||||||
# 只保留有效的对象,用于 ordered_items 参数
|
items_to_pass = ordering
|
||||||
valid_items = {k: v for k, v in ordering.items() if v is not None}
|
ordering_param = None
|
||||||
if valid_items:
|
|
||||||
items_to_pass = valid_items
|
|
||||||
ordering_param = None
|
|
||||||
else:
|
|
||||||
# 如果没有有效对象,创建空的 ordered_items
|
|
||||||
items_to_pass = {}
|
|
||||||
ordering_param = None
|
|
||||||
elif items is not None:
|
elif items is not None:
|
||||||
# 兼容旧的 items 参数
|
# 兼容旧的 items 参数
|
||||||
items_to_pass = items
|
items_to_pass = items
|
||||||
@@ -464,29 +516,6 @@ class PRCXI9300TubeRack(TubeRack):
|
|||||||
if material_info:
|
if material_info:
|
||||||
self._unilabos_state["Material"] = material_info
|
self._unilabos_state["Material"] = material_info
|
||||||
|
|
||||||
# 如果有临时 ordering 信息,在初始化完成后处理
|
|
||||||
if hasattr(self, '_temp_ordering') and self._temp_ordering:
|
|
||||||
self._process_temp_ordering()
|
|
||||||
|
|
||||||
def _process_temp_ordering(self):
|
|
||||||
"""处理临时的 ordering 信息,创建相应的 Tube 对象"""
|
|
||||||
from pylabrobot.resources import Tube, Coordinate
|
|
||||||
|
|
||||||
for location, item_type in self._temp_ordering.items():
|
|
||||||
if item_type == 'Tube' or item_type == 'tube':
|
|
||||||
# 为每个位置创建 Tube 对象
|
|
||||||
tube = Tube(name=f"{self.name}_{location}", size_x=10, size_y=10, size_z=50, max_volume=2000.0)
|
|
||||||
# 使用 assign_child_resource 添加到 rack 中
|
|
||||||
self.assign_child_resource(tube, location=Coordinate(0, 0, 0))
|
|
||||||
|
|
||||||
# 清理临时数据
|
|
||||||
del self._temp_ordering
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""从给定的状态加载工作台信息。"""
|
|
||||||
# super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
data = super().serialize_state()
|
data = super().serialize_state()
|
||||||
@@ -513,97 +542,6 @@ class PRCXI9300TubeRack(TubeRack):
|
|||||||
|
|
||||||
data.update(safe_state)
|
data.update(safe_state)
|
||||||
return data
|
return data
|
||||||
class PRCXI9300PlateAdapterSite(ItemizedCarrier):
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
|
||||||
material_info: Optional[Dict[str, Any]] = None, **kwargs):
|
|
||||||
# 处理 sites 参数的不同格式
|
|
||||||
|
|
||||||
sites = create_homogeneous_resources(
|
|
||||||
klass=ResourceHolder,
|
|
||||||
locations=[Coordinate(0, 0, 0)],
|
|
||||||
resource_size_x=size_x,
|
|
||||||
resource_size_y=size_y,
|
|
||||||
resource_size_z=size_z,
|
|
||||||
name_prefix=name,
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# 确保不传递重复的参数
|
|
||||||
kwargs.pop('layout', None)
|
|
||||||
sites_in = kwargs.pop('sites', None)
|
|
||||||
|
|
||||||
# 创建默认的sites字典
|
|
||||||
sites_dict = {name: sites}
|
|
||||||
# 优先从 sites_in 读取 'content_type',否则使用默认值
|
|
||||||
|
|
||||||
content_type = [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
# 如果提供了sites参数,则用sites_in中的值替换sites_dict中对应的元素
|
|
||||||
if sites_in is not None and isinstance(sites_in, dict):
|
|
||||||
for site_key, site_value in sites_in.items():
|
|
||||||
if site_key in sites_dict:
|
|
||||||
sites_dict[site_key] = site_value
|
|
||||||
|
|
||||||
super().__init__(name, size_x, size_y, size_z,
|
|
||||||
sites=sites_dict,
|
|
||||||
num_items_x=kwargs.pop('num_items_x', 1),
|
|
||||||
num_items_y=kwargs.pop('num_items_y', 1),
|
|
||||||
num_items_z=kwargs.pop('num_items_z', 1),
|
|
||||||
content_type=content_type,
|
|
||||||
**kwargs)
|
|
||||||
self._unilabos_state = {}
|
|
||||||
if material_info:
|
|
||||||
self._unilabos_state["Material"] = material_info
|
|
||||||
|
|
||||||
|
|
||||||
def assign_child_resource(self, resource, location=Coordinate(0, 0, 0), reassign=True, spot=None):
|
|
||||||
"""重写 assign_child_resource 方法,对于适配器位置,不使用索引分配"""
|
|
||||||
# 直接调用 Resource 的 assign_child_resource,避免 ItemizedCarrier 的索引逻辑
|
|
||||||
from pylabrobot.resources.resource import Resource
|
|
||||||
Resource.assign_child_resource(self, resource, location=location, reassign=reassign)
|
|
||||||
|
|
||||||
def unassign_child_resource(self, resource):
|
|
||||||
"""重写 unassign_child_resource 方法,对于适配器位置,不使用 sites 列表"""
|
|
||||||
# 直接调用 Resource 的 unassign_child_resource,避免 ItemizedCarrier 的 sites 逻辑
|
|
||||||
from pylabrobot.resources.resource import Resource
|
|
||||||
Resource.unassign_child_resource(self, resource)
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
try:
|
|
||||||
data = super().serialize_state()
|
|
||||||
except AttributeError:
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# 包含 sites 配置信息,但避免序列化 ResourceHolder 对象
|
|
||||||
if hasattr(self, 'sites') and self.sites:
|
|
||||||
# 只保存 sites 的基本信息,不保存 ResourceHolder 对象本身
|
|
||||||
sites_info = []
|
|
||||||
for site in self.sites:
|
|
||||||
if hasattr(site, '__class__') and 'pylabrobot' in str(site.__class__.__module__):
|
|
||||||
# 对于 pylabrobot 对象,只保存基本信息
|
|
||||||
sites_info.append({
|
|
||||||
"__pylabrobot_object__": True,
|
|
||||||
"class": site.__class__.__name__,
|
|
||||||
"module": site.__class__.__module__,
|
|
||||||
"name": getattr(site, 'name', str(site))
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
sites_info.append(site)
|
|
||||||
data['sites'] = sites_info
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""加载状态,包括 sites 配置信息"""
|
|
||||||
super().load_state(state)
|
|
||||||
|
|
||||||
# 从状态中恢复 sites 配置信息
|
|
||||||
if 'sites' in state:
|
|
||||||
self.sites = [state['sites']]
|
|
||||||
|
|
||||||
|
|
||||||
class PRCXI9300PlateAdapter(PlateAdapter):
|
class PRCXI9300PlateAdapter(PlateAdapter):
|
||||||
@@ -696,7 +634,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
deck: Deck,
|
deck: PRCXI9300Deck,
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
@@ -708,51 +646,18 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
step_mode=False,
|
step_mode=False,
|
||||||
matrix_id="",
|
matrix_id="",
|
||||||
is_9320=False,
|
is_9320=False,
|
||||||
start_rail=2,
|
|
||||||
rail_nums=4,
|
|
||||||
rail_interval=0,
|
|
||||||
x_increase = -0.003636,
|
|
||||||
y_increase = -0.003636,
|
|
||||||
x_offset = -0.8,
|
|
||||||
y_offset = -37.98,
|
|
||||||
deck_z = 300,
|
|
||||||
deck_y = 400,
|
|
||||||
rail_width=27.5,
|
|
||||||
xy_coupling = -0.0045,
|
|
||||||
):
|
):
|
||||||
|
tablets_info = []
|
||||||
self.deck_x = (start_rail + rail_nums*5 + (rail_nums-1)*rail_interval) * rail_width
|
for site_id in range(len(deck.sites)):
|
||||||
self.deck_y = deck_y
|
child = deck._get_site_resource(site_id)
|
||||||
self.deck_z = deck_z
|
# 如果放其他类型的物料,是不可以的
|
||||||
self.x_increase = x_increase
|
if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state:
|
||||||
self.y_increase = y_increase
|
number = site_id + 1
|
||||||
self.x_offset = x_offset
|
tablets_info.append(
|
||||||
self.y_offset = y_offset
|
WorkTablets(
|
||||||
self.xy_coupling = xy_coupling
|
Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"]
|
||||||
|
)
|
||||||
tablets_info = {}
|
)
|
||||||
plate_positions = []
|
|
||||||
for child in deck.children:
|
|
||||||
number = int(child.name.replace("T", ""))
|
|
||||||
|
|
||||||
if child.children:
|
|
||||||
if "Material" in child.children[0]._unilabos_state:
|
|
||||||
tablets_info[number] = child.children[0]._unilabos_state["Material"].get("uuid", "730067cf07ae43849ddf4034299030e9")
|
|
||||||
else:
|
|
||||||
tablets_info[number] = "730067cf07ae43849ddf4034299030e9"
|
|
||||||
else:
|
|
||||||
tablets_info[number] = "730067cf07ae43849ddf4034299030e9"
|
|
||||||
pos = self.plr_pos_to_prcxi(child)
|
|
||||||
plate_positions.append(
|
|
||||||
{
|
|
||||||
"Number": number,
|
|
||||||
"XPos": pos.x,
|
|
||||||
"YPos": pos.y,
|
|
||||||
"ZPos": pos.z
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if is_9320:
|
if is_9320:
|
||||||
print("当前设备是9320")
|
print("当前设备是9320")
|
||||||
# 始终初始化 step_mode 属性
|
# 始终初始化 step_mode 属性
|
||||||
@@ -762,39 +667,11 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
self.step_mode = step_mode
|
self.step_mode = step_mode
|
||||||
else:
|
else:
|
||||||
print("9300设备不支持 单点动作模式")
|
print("9300设备不支持 单点动作模式")
|
||||||
|
|
||||||
self._unilabos_backend = PRCXI9300Backend(
|
self._unilabos_backend = PRCXI9300Backend(
|
||||||
tablets_info, plate_positions, host, port, timeout, channel_num, axis, setup, debug, matrix_id, is_9320,
|
tablets_info, host, port, timeout, channel_num, axis, setup, debug, matrix_id, is_9320
|
||||||
x_increase, y_increase, x_offset, y_offset,
|
|
||||||
deck_z, deck_x=self.deck_x, deck_y=self.deck_y, xy_coupling=xy_coupling
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
||||||
|
|
||||||
def plr_pos_to_prcxi(self, resource: Resource):
|
|
||||||
resource_pos = resource.get_absolute_location(x="c",y="c",z="t")
|
|
||||||
x = resource_pos.x
|
|
||||||
y = resource_pos.y
|
|
||||||
z = resource_pos.z
|
|
||||||
# 如果z等于0,则递归resource.parent的高度并向z加,使用get_size_z方法
|
|
||||||
|
|
||||||
parent = resource.parent
|
|
||||||
res_z = resource.location.z
|
|
||||||
while not isinstance(parent, LiquidHandlerAbstract) and (res_z == 0) and parent is not None:
|
|
||||||
z += parent.get_size_z()
|
|
||||||
res_z = parent.location.z
|
|
||||||
parent = getattr(parent, "parent", None)
|
|
||||||
|
|
||||||
prcxi_x = (self.deck_x - x)*(1+self.x_increase) + self.x_offset + self.xy_coupling * (self.deck_y - y)
|
|
||||||
prcxi_y = (self.deck_y - y)*(1+self.y_increase) + self.y_offset
|
|
||||||
prcxi_z = self.deck_z - z
|
|
||||||
|
|
||||||
prcxi_x = min(max(0, prcxi_x),self.deck_x)
|
|
||||||
prcxi_y = min(max(0, prcxi_y),self.deck_y)
|
|
||||||
prcxi_z = min(max(0, prcxi_z),self.deck_z)
|
|
||||||
|
|
||||||
return Coordinate(prcxi_x, prcxi_y, prcxi_z)
|
|
||||||
|
|
||||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
super().post_init(ros_node)
|
super().post_init(ros_node)
|
||||||
self._unilabos_backend.post_init(ros_node)
|
self._unilabos_backend.post_init(ros_node)
|
||||||
@@ -927,10 +804,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
delays: Optional[List[int]] = None,
|
delays: Optional[List[int]] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
) -> TransferLiquidReturn:
|
) -> TransferLiquidReturn:
|
||||||
if self.step_mode:
|
return await super().transfer_liquid(
|
||||||
self._unilabos_backend.create_protocol(f"step_mode_protocol_{time.time()}")
|
|
||||||
|
|
||||||
res =await super().transfer_liquid(
|
|
||||||
sources,
|
sources,
|
||||||
targets,
|
targets,
|
||||||
tip_racks,
|
tip_racks,
|
||||||
@@ -952,33 +826,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
mix_liquid_height=mix_liquid_height,
|
mix_liquid_height=mix_liquid_height,
|
||||||
delays=delays,
|
delays=delays,
|
||||||
none_keys=none_keys,
|
none_keys=none_keys,
|
||||||
)
|
)
|
||||||
self._unilabos_backend.run_protocol()
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return await super().transfer_liquid(
|
|
||||||
sources,
|
|
||||||
targets,
|
|
||||||
tip_racks,
|
|
||||||
use_channels=use_channels,
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
asp_flow_rates=asp_flow_rates,
|
|
||||||
dis_flow_rates=dis_flow_rates,
|
|
||||||
offsets=offsets,
|
|
||||||
touch_tip=touch_tip,
|
|
||||||
liquid_height=liquid_height,
|
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
|
||||||
spread=spread,
|
|
||||||
is_96_well=is_96_well,
|
|
||||||
mix_stage=mix_stage,
|
|
||||||
mix_times=mix_times,
|
|
||||||
mix_vol=mix_vol,
|
|
||||||
mix_rate=mix_rate,
|
|
||||||
mix_liquid_height=mix_liquid_height,
|
|
||||||
delays=delays,
|
|
||||||
none_keys=none_keys,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
return await super().custom_delay(seconds, msg)
|
return await super().custom_delay(seconds, msg)
|
||||||
@@ -1093,11 +941,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
async def shaker_action(self, time: int, module_no: int, amplitude: int, is_wait: bool):
|
async def shaker_action(self, time: int, module_no: int, amplitude: int, is_wait: bool):
|
||||||
return await self._unilabos_backend.shaker_action(time, module_no, amplitude, is_wait)
|
return await self._unilabos_backend.shaker_action(time, module_no, amplitude, is_wait)
|
||||||
|
|
||||||
async def magnetic_action(self, time: int, module_no: int, height: int, is_wait: bool):
|
|
||||||
return await self._unilabos_backend.magnetic_action(time, module_no, height, is_wait)
|
|
||||||
|
|
||||||
async def shaking_incubation_action(self, time: int, module_no: int, amplitude: int, is_wait: bool, temperature: int):
|
|
||||||
return await self._unilabos_backend.shaking_incubation_action(time, module_no, amplitude, is_wait, temperature)
|
|
||||||
async def heater_action(self, temperature: float, time: int):
|
async def heater_action(self, temperature: float, time: int):
|
||||||
return await self._unilabos_backend.heater_action(temperature, time)
|
return await self._unilabos_backend.heater_action(temperature, time)
|
||||||
|
|
||||||
@@ -1114,7 +957,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
|
|
||||||
res = await super().move_plate(
|
return await super().move_plate(
|
||||||
plate,
|
plate,
|
||||||
to,
|
to,
|
||||||
intermediate_locations,
|
intermediate_locations,
|
||||||
@@ -1126,12 +969,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
target_plate_number=to,
|
target_plate_number=to,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
plate.unassign()
|
|
||||||
to.assign_child_resource(plate, location=Coordinate(0, 0, 0))
|
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
|
||||||
"resources": [self.deck]
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class PRCXI9300Backend(LiquidHandlerBackend):
|
class PRCXI9300Backend(LiquidHandlerBackend):
|
||||||
@@ -1156,7 +993,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tablets_info: list[WorkTablets],
|
tablets_info: list[WorkTablets],
|
||||||
plate_positions: dict[int, Coordinate],
|
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 9999,
|
port: int = 9999,
|
||||||
timeout: float = 10.0,
|
timeout: float = 10.0,
|
||||||
@@ -1166,18 +1002,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
debug=False,
|
debug=False,
|
||||||
matrix_id="",
|
matrix_id="",
|
||||||
is_9320=False,
|
is_9320=False,
|
||||||
x_increase = 0,
|
|
||||||
y_increase = 0,
|
|
||||||
x_offset = 0,
|
|
||||||
y_offset = 0,
|
|
||||||
deck_z = 300,
|
|
||||||
deck_x = 0,
|
|
||||||
deck_y = 0,
|
|
||||||
xy_coupling = 0.0,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.tablets_info = tablets_info
|
self.tablets_info = tablets_info
|
||||||
self.plate_positions = plate_positions
|
|
||||||
self.matrix_id = matrix_id
|
self.matrix_id = matrix_id
|
||||||
self.api_client = PRCXI9300Api(host, port, timeout, axis, debug, is_9320)
|
self.api_client = PRCXI9300Api(host, port, timeout, axis, debug, is_9320)
|
||||||
self.host, self.port, self.timeout = host, port, timeout
|
self.host, self.port, self.timeout = host, port, timeout
|
||||||
@@ -1185,15 +1012,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self._execute_setup = setup
|
self._execute_setup = setup
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.axis = "Left"
|
self.axis = "Left"
|
||||||
self.x_increase = x_increase
|
|
||||||
self.y_increase = y_increase
|
|
||||||
self.xy_coupling = xy_coupling
|
|
||||||
self.x_offset = x_offset
|
|
||||||
self.y_offset = y_offset
|
|
||||||
self.deck_x = deck_x
|
|
||||||
self.deck_y = deck_y
|
|
||||||
self.deck_z = deck_z
|
|
||||||
self.tip_length = 0
|
|
||||||
|
|
||||||
async def shaker_action(self, time: int, module_no: int, amplitude: int, is_wait: bool):
|
async def shaker_action(self, time: int, module_no: int, amplitude: int, is_wait: bool):
|
||||||
step = self.api_client.shaker_action(
|
step = self.api_client.shaker_action(
|
||||||
@@ -1205,27 +1023,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self.steps_todo_list.append(step)
|
self.steps_todo_list.append(step)
|
||||||
return step
|
return step
|
||||||
|
|
||||||
async def shaking_incubation_action(self, time: int, module_no: int, amplitude: int, is_wait: bool, temperature: int):
|
|
||||||
step = self.api_client.shaking_incubation_action(
|
|
||||||
time=time,
|
|
||||||
module_no=module_no,
|
|
||||||
amplitude=amplitude,
|
|
||||||
is_wait=is_wait,
|
|
||||||
temperature=temperature,
|
|
||||||
)
|
|
||||||
self.steps_todo_list.append(step)
|
|
||||||
return step
|
|
||||||
|
|
||||||
async def magnetic_action(self, time: int, module_no: int, height: int, is_wait: bool):
|
|
||||||
step = self.api_client.magnetic_action(
|
|
||||||
time=time,
|
|
||||||
module_no=module_no,
|
|
||||||
height=height,
|
|
||||||
is_wait=is_wait,
|
|
||||||
)
|
|
||||||
self.steps_todo_list.append(step)
|
|
||||||
return step
|
|
||||||
|
|
||||||
async def pick_up_resource(self, pickup: ResourcePickup, **backend_kwargs):
|
async def pick_up_resource(self, pickup: ResourcePickup, **backend_kwargs):
|
||||||
|
|
||||||
resource = pickup.resource
|
resource = pickup.resource
|
||||||
@@ -1264,42 +1061,29 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def create_protocol(self, protocol_name):
|
def create_protocol(self, protocol_name):
|
||||||
if protocol_name == "":
|
|
||||||
protocol_name = f"protocol_{time.time()}"
|
|
||||||
self.protocol_name = protocol_name
|
self.protocol_name = protocol_name
|
||||||
self.steps_todo_list = []
|
self.steps_todo_list = []
|
||||||
|
|
||||||
if not len(self.matrix_id):
|
|
||||||
self.matrix_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
material_list = self.api_client.get_all_materials()
|
|
||||||
material_dict = {material["uuid"]: material for material in material_list}
|
|
||||||
|
|
||||||
work_tablets = []
|
|
||||||
for num, material_id in self.tablets_info.items():
|
|
||||||
work_tablets.append({
|
|
||||||
"Number": num,
|
|
||||||
"Material": material_dict[material_id]
|
|
||||||
})
|
|
||||||
|
|
||||||
self.matrix_info = {
|
|
||||||
"MatrixId": self.matrix_id,
|
|
||||||
"MatrixName": self.matrix_id,
|
|
||||||
"WorkTablets": work_tablets,
|
|
||||||
}
|
|
||||||
# print(json.dumps(self.matrix_info, indent=2))
|
|
||||||
res = self.api_client.add_WorkTablet_Matrix(self.matrix_info)
|
|
||||||
if not res["Success"]:
|
|
||||||
self.matrix_id = ""
|
|
||||||
raise AssertionError(f"Failed to create matrix: {res.get('Message', 'Unknown error')}")
|
|
||||||
print(f"PRCXI9300Backend created matrix with ID: {self.matrix_info['MatrixId']}, result: {res}")
|
|
||||||
|
|
||||||
def run_protocol(self):
|
def run_protocol(self):
|
||||||
assert self.is_reset_ok, "PRCXI9300Backend is not reset successfully. Please call setup() first."
|
assert self.is_reset_ok, "PRCXI9300Backend is not reset successfully. Please call setup() first."
|
||||||
run_time = time.time()
|
run_time = time.time()
|
||||||
solution_id = self.api_client.add_solution(
|
self.matrix_info = MatrixInfo(
|
||||||
f"protocol_{run_time}", self.matrix_id, self.steps_todo_list
|
MatrixId=f"{int(run_time)}",
|
||||||
|
MatrixName=f"protocol_{run_time}",
|
||||||
|
MatrixCount=len(self.tablets_info),
|
||||||
|
WorkTablets=self.tablets_info,
|
||||||
)
|
)
|
||||||
|
# print(json.dumps(self.matrix_info, indent=2))
|
||||||
|
if not len(self.matrix_id):
|
||||||
|
res = self.api_client.add_WorkTablet_Matrix(self.matrix_info)
|
||||||
|
assert res["Success"], f"Failed to create matrix: {res.get('Message', 'Unknown error')}"
|
||||||
|
print(f"PRCXI9300Backend created matrix with ID: {self.matrix_info['MatrixId']}, result: {res}")
|
||||||
|
solution_id = self.api_client.add_solution(
|
||||||
|
f"protocol_{run_time}", self.matrix_info["MatrixId"], self.steps_todo_list
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"PRCXI9300Backend using predefined worktable {self.matrix_id}, skipping matrix creation.")
|
||||||
|
solution_id = self.api_client.add_solution(f"protocol_{run_time}", self.matrix_id, self.steps_todo_list)
|
||||||
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
|
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
|
||||||
self.api_client.load_solution(solution_id)
|
self.api_client.load_solution(solution_id)
|
||||||
print(json.dumps(self.steps_todo_list, indent=2))
|
print(json.dumps(self.steps_todo_list, indent=2))
|
||||||
@@ -1342,9 +1126,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
else:
|
else:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
print("PRCXI9300 reset successfully.")
|
print("PRCXI9300 reset successfully.")
|
||||||
|
|
||||||
self.api_client.update_clamp_jaw_position(self.matrix_id, self.plate_positions)
|
|
||||||
|
|
||||||
except ConnectionRefusedError as e:
|
except ConnectionRefusedError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. "
|
f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. "
|
||||||
@@ -1393,7 +1174,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
if self.num_channels != 8:
|
if self._num_channels == 1:
|
||||||
hole_row = tipspot_index % 8 + 1
|
hole_row = tipspot_index % 8 + 1
|
||||||
|
|
||||||
step = self.api_client.Load(
|
step = self.api_client.Load(
|
||||||
@@ -1406,7 +1187,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
blending_times=0,
|
blending_times=0,
|
||||||
balance_height=0,
|
balance_height=0,
|
||||||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||||||
hole_numbers=f"{(hole_col - 1) * 8 + hole_row}" if self._num_channels != 8 else "1,2,3,4,5",
|
hole_numbers=f"{(hole_col - 1) * 8 + hole_row}" if self._num_channels == 1 else "1,2,3,4,5",
|
||||||
)
|
)
|
||||||
self.steps_todo_list.append(step)
|
self.steps_todo_list.append(step)
|
||||||
|
|
||||||
@@ -1467,7 +1248,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
|
|
||||||
if self.num_channels != 8:
|
if self.channel_num == 1:
|
||||||
hole_row = tipspot_index % 8 + 1
|
hole_row = tipspot_index % 8 + 1
|
||||||
|
|
||||||
step = self.api_client.UnLoad(
|
step = self.api_client.UnLoad(
|
||||||
@@ -1519,7 +1300,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
if self.num_channels != 8:
|
if self.num_channels == 1:
|
||||||
hole_row = tipspot_index % 8 + 1
|
hole_row = tipspot_index % 8 + 1
|
||||||
|
|
||||||
assert mix_time > 0
|
assert mix_time > 0
|
||||||
@@ -1576,7 +1357,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
if self.num_channels != 8:
|
if self.num_channels == 1:
|
||||||
hole_row = tipspot_index % 8 + 1
|
hole_row = tipspot_index % 8 + 1
|
||||||
|
|
||||||
step = self.api_client.Imbibing(
|
step = self.api_client.Imbibing(
|
||||||
@@ -1634,7 +1415,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
|
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
if self.num_channels != 8:
|
if self.num_channels == 1:
|
||||||
hole_row = tipspot_index % 8 + 1
|
hole_row = tipspot_index % 8 + 1
|
||||||
|
|
||||||
step = self.api_client.Tapping(
|
step = self.api_client.Tapping(
|
||||||
@@ -1760,10 +1541,10 @@ class PRCXI9300Api:
|
|||||||
start = False
|
start = False
|
||||||
while not success:
|
while not success:
|
||||||
status = self.step_state_list()
|
status = self.step_state_list()
|
||||||
if status is None:
|
|
||||||
break
|
|
||||||
if len(status) == 1:
|
if len(status) == 1:
|
||||||
start = True
|
start = True
|
||||||
|
if status is None:
|
||||||
|
break
|
||||||
if len(status) == 0:
|
if len(status) == 0:
|
||||||
break
|
break
|
||||||
if status[-1]["State"] == 2 and start:
|
if status[-1]["State"] == 2 and start:
|
||||||
@@ -1842,13 +1623,6 @@ class PRCXI9300Api:
|
|||||||
"""GetWorkTabletMatrixById"""
|
"""GetWorkTabletMatrixById"""
|
||||||
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
|
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
|
||||||
|
|
||||||
def update_clamp_jaw_position(self, target_matrix_id: str, plate_positions: List[Dict[str, Any]]):
|
|
||||||
position_params = {
|
|
||||||
"MatrixId": target_matrix_id,
|
|
||||||
"WorkTablets": plate_positions
|
|
||||||
}
|
|
||||||
return self.call("IMatrix", "UpdateClampJawPosition", [position_params])
|
|
||||||
|
|
||||||
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
|
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
|
||||||
return self.call("IMatrix", "AddWorkTabletMatrix2" if self.is_9320 else "AddWorkTabletMatrix", [matrix])
|
return self.call("IMatrix", "AddWorkTabletMatrix2" if self.is_9320 else "AddWorkTabletMatrix", [matrix])
|
||||||
|
|
||||||
@@ -2091,26 +1865,6 @@ class PRCXI9300Api:
|
|||||||
"AssistFun4": is_wait,
|
"AssistFun4": is_wait,
|
||||||
}
|
}
|
||||||
|
|
||||||
def shaking_incubation_action(self, time: int, module_no: int, amplitude: int, is_wait: bool, temperature: int):
|
|
||||||
return {
|
|
||||||
"StepAxis": "Left",
|
|
||||||
"Function": "Shaking_Incubation",
|
|
||||||
"AssistFun1": time,
|
|
||||||
"AssistFun2": module_no,
|
|
||||||
"AssistFun3": amplitude,
|
|
||||||
"AssistFun4": is_wait,
|
|
||||||
"AssistFun5": temperature,
|
|
||||||
}
|
|
||||||
|
|
||||||
def magnetic_action(self, time: int, module_no: int, height: int, is_wait: bool):
|
|
||||||
return {
|
|
||||||
"StepAxis": "Left",
|
|
||||||
"Function": "Magnetic",
|
|
||||||
"AssistFun1": time,
|
|
||||||
"AssistFun2": module_no,
|
|
||||||
"AssistFun3": height,
|
|
||||||
"AssistFun4": is_wait,
|
|
||||||
}
|
|
||||||
|
|
||||||
class DefaultLayout:
|
class DefaultLayout:
|
||||||
|
|
||||||
@@ -2142,82 +1896,82 @@ class DefaultLayout:
|
|||||||
{
|
{
|
||||||
"Number": 1,
|
"Number": 1,
|
||||||
"Code": "T1",
|
"Code": "T1",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 2,
|
"Number": 2,
|
||||||
"Code": "T2",
|
"Code": "T2",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 3,
|
"Number": 3,
|
||||||
"Code": "T3",
|
"Code": "T3",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 4,
|
"Number": 4,
|
||||||
"Code": "T4",
|
"Code": "T4",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 5,
|
"Number": 5,
|
||||||
"Code": "T5",
|
"Code": "T5",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 6,
|
"Number": 6,
|
||||||
"Code": "T6",
|
"Code": "T6",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 7,
|
"Number": 7,
|
||||||
"Code": "T7",
|
"Code": "T7",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 8,
|
"Number": 8,
|
||||||
"Code": "T8",
|
"Code": "T8",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 9,
|
"Number": 9,
|
||||||
"Code": "T9",
|
"Code": "T9",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 10,
|
"Number": 10,
|
||||||
"Code": "T10",
|
"Code": "T10",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 11,
|
"Number": 11,
|
||||||
"Code": "T11",
|
"Code": "T11",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 12,
|
"Number": 12,
|
||||||
"Code": "T12",
|
"Code": "T12",
|
||||||
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9"},
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
}, # 这个设置成废液槽,用储液槽表示
|
}, # 这个设置成废液槽,用储液槽表示
|
||||||
{
|
{
|
||||||
"Number": 13,
|
"Number": 13,
|
||||||
"Code": "T13",
|
"Code": "T13",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 14,
|
"Number": 14,
|
||||||
"Code": "T14",
|
"Code": "T14",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 15,
|
"Number": 15,
|
||||||
"Code": "T15",
|
"Code": "T15",
|
||||||
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f"},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Number": 16,
|
"Number": 16,
|
||||||
"Code": "T16",
|
"Code": "T16",
|
||||||
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9"},
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
}, # 这个设置成垃圾桶,用储液槽表示
|
}, # 这个设置成垃圾桶,用储液槽表示
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ from rclpy.node import Node
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", **kwargs):
|
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", registry_name: str = "lh_joint_publisher", **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
88
unilabos/devices/virtual/virtual_sample_demo.py
Normal file
88
unilabos/devices/virtual/virtual_sample_demo.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""虚拟样品演示设备 — 用于前端 sample tracking 功能的极简 demo"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualSampleDemo:
|
||||||
|
"""虚拟样品追踪演示设备,提供两种典型返回模式:
|
||||||
|
- measure_samples: 等长输入输出 (前端按 index 自动对齐)
|
||||||
|
- split_and_measure: 输出比输入长,附带 samples 列标注归属
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
|
if device_id is None and "id" in kwargs:
|
||||||
|
device_id = kwargs.pop("id")
|
||||||
|
if config is None and "config" in kwargs:
|
||||||
|
config = kwargs.pop("config")
|
||||||
|
|
||||||
|
self.device_id = device_id or "unknown_sample_demo"
|
||||||
|
self.config = config or {}
|
||||||
|
self.logger = logging.getLogger(f"VirtualSampleDemo.{self.device_id}")
|
||||||
|
self.data: Dict[str, Any] = {"status": "Idle"}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Action 1: 等长输入输出,无 samples 列
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def measure_samples(self, concentrations: List[float]) -> Dict[str, Any]:
|
||||||
|
"""模拟光度测量。absorbance = concentration * 0.05 + noise
|
||||||
|
|
||||||
|
入参和出参 list 长度相等,前端按 index 自动对齐。
|
||||||
|
"""
|
||||||
|
self.logger.info(f"measure_samples: concentrations={concentrations}")
|
||||||
|
absorbance = [round(c * 0.05 + random.gauss(0, 0.005), 4) for c in concentrations]
|
||||||
|
return {"concentrations": concentrations, "absorbance": absorbance}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Action 2: 输出比输入长,带 samples 列
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def split_and_measure(self, volumes: List[float], split_count: int = 3) -> Dict[str, Any]:
|
||||||
|
"""将每个样品均分为 split_count 份后逐份测量。
|
||||||
|
|
||||||
|
返回的 list 长度 = len(volumes) * split_count,
|
||||||
|
附带 samples 列标注每行属于第几个输入样品 (0-based index)。
|
||||||
|
"""
|
||||||
|
self.logger.info(f"split_and_measure: volumes={volumes}, split_count={split_count}")
|
||||||
|
out_volumes: List[float] = []
|
||||||
|
readings: List[float] = []
|
||||||
|
samples: List[int] = []
|
||||||
|
|
||||||
|
for idx, vol in enumerate(volumes):
|
||||||
|
split_vol = round(vol / split_count, 2)
|
||||||
|
for _ in range(split_count):
|
||||||
|
out_volumes.append(split_vol)
|
||||||
|
readings.append(round(random.uniform(0.1, 1.0), 4))
|
||||||
|
samples.append(idx)
|
||||||
|
|
||||||
|
return {"volumes": out_volumes, "readings": readings, "unilabos_samples": samples}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Action 3: 入参和出参都带 samples 列(不等长)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
async def analyze_readings(self, readings: List[float], samples: List[int]) -> Dict[str, Any]:
|
||||||
|
"""对 split_and_measure 的输出做二次分析。
|
||||||
|
|
||||||
|
入参 readings/samples 长度相同但 > 原始样品数,
|
||||||
|
出参同样带 samples 列,长度与入参一致。
|
||||||
|
"""
|
||||||
|
self.logger.info(f"analyze_readings: readings={readings}, samples={samples}")
|
||||||
|
scores: List[float] = []
|
||||||
|
passed: List[bool] = []
|
||||||
|
threshold = 0.4
|
||||||
|
|
||||||
|
for r in readings:
|
||||||
|
score = round(r * 100 + random.gauss(0, 2), 2)
|
||||||
|
scores.append(score)
|
||||||
|
passed.append(r >= threshold)
|
||||||
|
|
||||||
|
return {"scores": scores, "passed": passed, "unilabos_samples": samples}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 状态属性
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@property
|
||||||
|
def status(self) -> str:
|
||||||
|
return self.data.get("status", "Idle")
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Virtual Workbench Device - 模拟工作台设备
|
Virtual Workbench Device - 模拟工作台设备
|
||||||
包含:
|
包含:
|
||||||
- 1个机械臂 (每次操作3s, 独占锁)
|
- 1个机械臂 (每次操作3s, 独占锁)
|
||||||
- 3个加热台 (每次加热10s, 可并行)
|
- 3个加热台 (每次加热10s, 可并行)
|
||||||
|
|
||||||
工作流程:
|
工作流程:
|
||||||
1. A1-A5 物料同时启动,竞争机械臂
|
1. A1-A5 物料同时启动, 竞争机械臂
|
||||||
2. 机械臂将物料移动到空闲加热台
|
2. 机械臂将物料移动到空闲加热台
|
||||||
3. 加热完成后,机械臂将物料移动到C1-C5
|
3. 加热完成后, 机械臂将物料移动到C1-C5
|
||||||
|
|
||||||
注意:调用来自线程池,使用 threading.Lock 进行同步
|
注意: 调用来自线程池, 使用 threading.Lock 进行同步
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -21,9 +21,11 @@ from threading import Lock, RLock
|
|||||||
|
|
||||||
from typing_extensions import TypedDict
|
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.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
from unilabos.utils.decorator import not_action
|
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
|
||||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
|
||||||
|
|
||||||
|
|
||||||
# ============ TypedDict 返回类型定义 ============
|
# ============ TypedDict 返回类型定义 ============
|
||||||
@@ -57,6 +59,8 @@ class MoveToOutputResult(TypedDict):
|
|||||||
success: bool
|
success: bool
|
||||||
station_id: int
|
station_id: int
|
||||||
material_id: str
|
material_id: str
|
||||||
|
output_position: str
|
||||||
|
message: str
|
||||||
unilabos_samples: List[LabSample]
|
unilabos_samples: List[LabSample]
|
||||||
|
|
||||||
|
|
||||||
@@ -81,9 +85,9 @@ class HeatingStationState(Enum):
|
|||||||
"""加热台状态枚举"""
|
"""加热台状态枚举"""
|
||||||
|
|
||||||
IDLE = "idle" # 空闲
|
IDLE = "idle" # 空闲
|
||||||
OCCUPIED = "occupied" # 已放置物料,等待加热
|
OCCUPIED = "occupied" # 已放置物料, 等待加热
|
||||||
HEATING = "heating" # 加热中
|
HEATING = "heating" # 加热中
|
||||||
COMPLETED = "completed" # 加热完成,等待取走
|
COMPLETED = "completed" # 加热完成, 等待取走
|
||||||
|
|
||||||
|
|
||||||
class ArmState(Enum):
|
class ArmState(Enum):
|
||||||
@@ -105,26 +109,31 @@ class HeatingStation:
|
|||||||
heating_progress: float = 0.0
|
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:
|
class VirtualWorkbench:
|
||||||
"""
|
"""
|
||||||
Virtual Workbench Device - 虚拟工作台设备
|
Virtual Workbench Device - 虚拟工作台设备
|
||||||
|
|
||||||
模拟一个包含1个机械臂和3个加热台的工作站
|
模拟一个包含1个机械臂和3个加热台的工作站
|
||||||
- 机械臂操作耗时3秒,同一时间只能执行一个操作
|
- 机械臂操作耗时3秒, 同一时间只能执行一个操作
|
||||||
- 加热台加热耗时10秒,3个加热台可并行工作
|
- 加热台加热耗时10秒, 3个加热台可并行工作
|
||||||
|
|
||||||
工作流:
|
工作流:
|
||||||
1. 物料A1-A5并发启动(线程池),竞争机械臂使用权
|
1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
|
||||||
2. 获取机械臂后,查找空闲加热台
|
2. 获取机械臂后, 查找空闲加热台
|
||||||
3. 机械臂将物料放入加热台,开始加热
|
3. 机械臂将物料放入加热台, 开始加热
|
||||||
4. 加热完成后,机械臂将物料移动到目标位置Cn
|
4. 加热完成后, 机械臂将物料移动到目标位置Cn
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_ros_node: BaseROS2DeviceNode
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
# 配置常量
|
# 配置常量
|
||||||
ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒)
|
ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
|
||||||
HEATING_TIME: float = 10.0 # 加热时间(秒)
|
HEATING_TIME: float = 60.0 # 加热时间(秒)
|
||||||
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
@@ -141,23 +150,23 @@ class VirtualWorkbench:
|
|||||||
self.data: Dict[str, Any] = {}
|
self.data: Dict[str, Any] = {}
|
||||||
|
|
||||||
# 从config中获取可配置参数
|
# 从config中获取可配置参数
|
||||||
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0))
|
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
|
||||||
self.HEATING_TIME = float(self.config.get("heating_time", 10.0))
|
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
|
||||||
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3))
|
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
|
||||||
|
|
||||||
# 机械臂状态和锁 (使用threading.Lock)
|
# 机械臂状态和锁
|
||||||
self._arm_lock = Lock()
|
self._arm_lock = Lock()
|
||||||
self._arm_state = ArmState.IDLE
|
self._arm_state = ArmState.IDLE
|
||||||
self._arm_current_task: Optional[str] = None
|
self._arm_current_task: Optional[str] = None
|
||||||
|
|
||||||
# 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize()
|
# 加热台状态
|
||||||
self._heating_stations: Dict[int, HeatingStation] = {
|
self._heating_stations: Dict[int, HeatingStation] = {
|
||||||
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
||||||
}
|
}
|
||||||
self._stations_lock = RLock() # 可重入锁,保护加热台状态
|
self._stations_lock = RLock()
|
||||||
|
|
||||||
# 任务追踪
|
# 任务追踪
|
||||||
self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info
|
self._active_tasks: Dict[str, Dict[str, Any]] = {}
|
||||||
self._tasks_lock = Lock()
|
self._tasks_lock = Lock()
|
||||||
|
|
||||||
# 处理其他kwargs参数
|
# 处理其他kwargs参数
|
||||||
@@ -183,7 +192,6 @@ class VirtualWorkbench:
|
|||||||
"""初始化虚拟工作台"""
|
"""初始化虚拟工作台"""
|
||||||
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
||||||
|
|
||||||
# 重置加热台状态 (已在__init__中创建,这里重置为初始状态)
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
for station in self._heating_stations.values():
|
for station in self._heating_stations.values():
|
||||||
station.state = HeatingStationState.IDLE
|
station.state = HeatingStationState.IDLE
|
||||||
@@ -191,7 +199,6 @@ class VirtualWorkbench:
|
|||||||
station.material_number = None
|
station.material_number = None
|
||||||
station.heating_progress = 0.0
|
station.heating_progress = 0.0
|
||||||
|
|
||||||
# 初始化状态
|
|
||||||
self.data.update(
|
self.data.update(
|
||||||
{
|
{
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
@@ -257,11 +264,7 @@ class VirtualWorkbench:
|
|||||||
self.data["message"] = message
|
self.data["message"] = message
|
||||||
|
|
||||||
def _find_available_heating_station(self) -> Optional[int]:
|
def _find_available_heating_station(self) -> Optional[int]:
|
||||||
"""查找空闲的加热台
|
"""查找空闲的加热台"""
|
||||||
|
|
||||||
Returns:
|
|
||||||
空闲加热台ID,如果没有则返回None
|
|
||||||
"""
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
for station_id, station in self._heating_stations.items():
|
for station_id, station in self._heating_stations.items():
|
||||||
if station.state == HeatingStationState.IDLE:
|
if station.state == HeatingStationState.IDLE:
|
||||||
@@ -269,23 +272,12 @@ class VirtualWorkbench:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _acquire_arm(self, task_description: str) -> bool:
|
def _acquire_arm(self, task_description: str) -> bool:
|
||||||
"""获取机械臂使用权(阻塞直到获取)
|
"""获取机械臂使用权(阻塞直到获取)"""
|
||||||
|
|
||||||
Args:
|
|
||||||
task_description: 任务描述,用于日志
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否成功获取
|
|
||||||
"""
|
|
||||||
self.logger.info(f"[{task_description}] 等待获取机械臂...")
|
self.logger.info(f"[{task_description}] 等待获取机械臂...")
|
||||||
|
|
||||||
# 阻塞等待获取锁
|
|
||||||
self._arm_lock.acquire()
|
self._arm_lock.acquire()
|
||||||
|
|
||||||
self._arm_state = ArmState.BUSY
|
self._arm_state = ArmState.BUSY
|
||||||
self._arm_current_task = task_description
|
self._arm_current_task = task_description
|
||||||
self._update_data_status(f"机械臂执行: {task_description}")
|
self._update_data_status(f"机械臂执行: {task_description}")
|
||||||
|
|
||||||
self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
|
self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -298,6 +290,22 @@ class VirtualWorkbench:
|
|||||||
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
||||||
self.logger.info(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(
|
def prepare_materials(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -306,19 +314,14 @@ class VirtualWorkbench:
|
|||||||
"""
|
"""
|
||||||
批量准备物料 - 虚拟起始节点
|
批量准备物料 - 虚拟起始节点
|
||||||
|
|
||||||
作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。
|
作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
|
||||||
输出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)]
|
materials = [i for i in range(1, count + 1)]
|
||||||
|
|
||||||
self.logger.info(f"[准备物料] 生成 {count} 个物料: " f"A1-A{count} -> material_1~material_{count}")
|
self.logger.info(
|
||||||
|
f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -329,9 +332,28 @@ class VirtualWorkbench:
|
|||||||
"material_4": materials[3] if len(materials) > 3 else 0,
|
"material_4": materials[3] if len(materials) > 3 else 0,
|
||||||
"material_5": materials[4] if len(materials) > 4 else 0,
|
"material_5": materials[4] if len(materials) > 4 else 0,
|
||||||
"message": f"已准备 {count} 个物料: A1-A{count}",
|
"message": f"已准备 {count} 个物料: A1-A{count}",
|
||||||
"unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()]
|
"unilabos_samples": [
|
||||||
|
LabSample(
|
||||||
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action(
|
||||||
|
auto_prefix=True,
|
||||||
|
description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID",
|
||||||
|
handles=[
|
||||||
|
ActionInputHandle(key="material_input", data_type="workbench_material",
|
||||||
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
||||||
|
ActionOutputHandle(key="heating_station_output", data_type="workbench_station",
|
||||||
|
label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
||||||
|
ActionOutputHandle(key="material_number_output", data_type="workbench_material",
|
||||||
|
label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
||||||
|
],
|
||||||
|
)
|
||||||
def move_to_heating_station(
|
def move_to_heating_station(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -340,20 +362,12 @@ class VirtualWorkbench:
|
|||||||
"""
|
"""
|
||||||
将物料从An位置移动到加热台
|
将物料从An位置移动到加热台
|
||||||
|
|
||||||
多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台
|
多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
|
||||||
|
|
||||||
Args:
|
|
||||||
material_number: 物料编号 (1-5)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点
|
|
||||||
"""
|
"""
|
||||||
# 根据物料编号生成物料ID
|
|
||||||
material_id = f"A{material_number}"
|
material_id = f"A{material_number}"
|
||||||
task_desc = f"移动{material_id}到加热台"
|
task_desc = f"移动{material_id}到加热台"
|
||||||
self.logger.info(f"[任务] {task_desc} - 开始执行")
|
self.logger.info(f"[任务] {task_desc} - 开始执行")
|
||||||
|
|
||||||
# 记录任务
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
self._active_tasks[material_id] = {
|
self._active_tasks[material_id] = {
|
||||||
"status": "waiting_for_arm",
|
"status": "waiting_for_arm",
|
||||||
@@ -361,33 +375,27 @@ class VirtualWorkbench:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 步骤1: 等待获取机械臂使用权(竞争)
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
||||||
self._acquire_arm(task_desc)
|
self._acquire_arm(task_desc)
|
||||||
|
|
||||||
# 步骤2: 查找空闲加热台
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
self._active_tasks[material_id]["status"] = "finding_station"
|
self._active_tasks[material_id]["status"] = "finding_station"
|
||||||
station_id = None
|
station_id = None
|
||||||
|
|
||||||
# 循环等待直到找到空闲加热台
|
|
||||||
while station_id is None:
|
while station_id is None:
|
||||||
station_id = self._find_available_heating_station()
|
station_id = self._find_available_heating_station()
|
||||||
if station_id is None:
|
if station_id is None:
|
||||||
self.logger.info(f"[{material_id}] 没有空闲加热台,等待中...")
|
self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
|
||||||
# 释放机械臂,等待后重试
|
|
||||||
self._release_arm()
|
self._release_arm()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self._acquire_arm(task_desc)
|
self._acquire_arm(task_desc)
|
||||||
|
|
||||||
# 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
|
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
|
||||||
self._heating_stations[station_id].current_material = material_id
|
self._heating_stations[station_id].current_material = material_id
|
||||||
self._heating_stations[station_id].material_number = material_number
|
self._heating_stations[station_id].material_number = material_number
|
||||||
|
|
||||||
# 步骤4: 模拟机械臂移动操作 (3秒)
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving"
|
self._active_tasks[material_id]["status"] = "arm_moving"
|
||||||
self._active_tasks[material_id]["assigned_station"] = station_id
|
self._active_tasks[material_id]["assigned_station"] = station_id
|
||||||
@@ -395,11 +403,11 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
time.sleep(self.ARM_OPERATION_TIME)
|
||||||
|
|
||||||
# 步骤5: 放入加热台完成
|
|
||||||
self._update_data_status(f"{material_id}已放入加热台{station_id}")
|
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()
|
self._release_arm()
|
||||||
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
@@ -412,8 +420,16 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"{material_id}已成功移动到加热台{station_id}",
|
"message": f"{material_id}已成功移动到加热台{station_id}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -427,10 +443,33 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"移动失败: {str(e)}",
|
"message": f"移动失败: {str(e)}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action(
|
||||||
|
auto_prefix=True,
|
||||||
|
always_free=True,
|
||||||
|
description="启动指定加热台的加热程序",
|
||||||
|
handles=[
|
||||||
|
ActionInputHandle(key="station_id_input", data_type="workbench_station",
|
||||||
|
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
||||||
|
ActionInputHandle(key="material_number_input", data_type="workbench_material",
|
||||||
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
||||||
|
ActionOutputHandle(key="heating_done_station", data_type="workbench_station",
|
||||||
|
label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
||||||
|
ActionOutputHandle(key="heating_done_material", data_type="workbench_material",
|
||||||
|
label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
||||||
|
],
|
||||||
|
)
|
||||||
def start_heating(
|
def start_heating(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -439,13 +478,6 @@ class VirtualWorkbench:
|
|||||||
) -> StartHeatingResult:
|
) -> 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}] 开始加热")
|
self.logger.info(f"[加热台{station_id}] 开始加热")
|
||||||
|
|
||||||
@@ -457,8 +489,16 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
"message": f"无效的加热台ID: {station_id}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
@@ -472,8 +512,16 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"加热台{station_id}上没有物料",
|
"message": f"加热台{station_id}上没有物料",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if station.state == HeatingStationState.HEATING:
|
if station.state == HeatingStationState.HEATING:
|
||||||
@@ -484,13 +532,20 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"加热台{station_id}已经在加热中",
|
"message": f"加热台{station_id}已经在加热中",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
material_id = station.current_material
|
material_id = station.current_material
|
||||||
|
|
||||||
# 开始加热
|
|
||||||
station.state = HeatingStationState.HEATING
|
station.state = HeatingStationState.HEATING
|
||||||
station.heating_start_time = time.time()
|
station.heating_start_time = time.time()
|
||||||
station.heating_progress = 0.0
|
station.heating_progress = 0.0
|
||||||
@@ -501,10 +556,19 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
||||||
|
|
||||||
# 模拟加热过程 (10秒)
|
with self._stations_lock:
|
||||||
|
heating_list = [
|
||||||
|
f"加热台{sid}:{s.current_material}"
|
||||||
|
for sid, s in self._heating_stations.items()
|
||||||
|
if s.state == HeatingStationState.HEATING and s.current_material
|
||||||
|
]
|
||||||
|
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
last_countdown_log = start_time
|
||||||
while True:
|
while True:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
|
remaining = max(0.0, self.HEATING_TIME - elapsed)
|
||||||
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
||||||
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
@@ -512,12 +576,15 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
||||||
|
|
||||||
|
if time.time() - last_countdown_log >= 5.0:
|
||||||
|
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
||||||
|
last_countdown_log = time.time()
|
||||||
|
|
||||||
if elapsed >= self.HEATING_TIME:
|
if elapsed >= self.HEATING_TIME:
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
|
|
||||||
# 加热完成
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
self._heating_stations[station_id].state = HeatingStationState.COMPLETED
|
self._heating_stations[station_id].state = HeatingStationState.COMPLETED
|
||||||
self._heating_stations[station_id].heating_progress = 100.0
|
self._heating_stations[station_id].heating_progress = 100.0
|
||||||
@@ -536,10 +603,28 @@ class VirtualWorkbench:
|
|||||||
"material_number": material_number,
|
"material_number": material_number,
|
||||||
"message": f"加热台{station_id}加热完成",
|
"message": f"加热台{station_id}加热完成",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action(
|
||||||
|
auto_prefix=True,
|
||||||
|
description="将物料从加热台移动到输出位置Cn",
|
||||||
|
handles=[
|
||||||
|
ActionInputHandle(key="output_station_input", data_type="workbench_station",
|
||||||
|
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
||||||
|
ActionInputHandle(key="output_material_input", data_type="workbench_material",
|
||||||
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
||||||
|
],
|
||||||
|
)
|
||||||
def move_to_output(
|
def move_to_output(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -548,15 +633,8 @@ class VirtualWorkbench:
|
|||||||
) -> MoveToOutputResult:
|
) -> MoveToOutputResult:
|
||||||
"""
|
"""
|
||||||
将物料从加热台移动到输出位置Cn
|
将物料从加热台移动到输出位置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:
|
if station_id not in self._heating_stations:
|
||||||
return {
|
return {
|
||||||
@@ -566,8 +644,16 @@ class VirtualWorkbench:
|
|||||||
"output_position": f"C{output_number}",
|
"output_position": f"C{output_number}",
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
"message": f"无效的加热台ID: {station_id}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
@@ -582,8 +668,16 @@ class VirtualWorkbench:
|
|||||||
"output_position": f"C{output_number}",
|
"output_position": f"C{output_number}",
|
||||||
"message": f"加热台{station_id}上没有物料",
|
"message": f"加热台{station_id}上没有物料",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if station.state != HeatingStationState.COMPLETED:
|
if station.state != HeatingStationState.COMPLETED:
|
||||||
@@ -594,8 +688,16 @@ class VirtualWorkbench:
|
|||||||
"output_position": f"C{output_number}",
|
"output_position": f"C{output_number}",
|
||||||
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
|
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
output_position = f"C{output_number}"
|
output_position = f"C{output_number}"
|
||||||
@@ -607,18 +709,17 @@ class VirtualWorkbench:
|
|||||||
if material_id in self._active_tasks:
|
if material_id in self._active_tasks:
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
|
self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
|
||||||
|
|
||||||
# 获取机械臂
|
|
||||||
self._acquire_arm(task_desc)
|
self._acquire_arm(task_desc)
|
||||||
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
if material_id in self._active_tasks:
|
if material_id in self._active_tasks:
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving_to_output"
|
self._active_tasks[material_id]["status"] = "arm_moving_to_output"
|
||||||
|
|
||||||
# 模拟机械臂操作 (3秒)
|
self.logger.info(
|
||||||
self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...")
|
f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..."
|
||||||
|
)
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
time.sleep(self.ARM_OPERATION_TIME)
|
||||||
|
|
||||||
# 清空加热台
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
self._heating_stations[station_id].state = HeatingStationState.IDLE
|
self._heating_stations[station_id].state = HeatingStationState.IDLE
|
||||||
self._heating_stations[station_id].current_material = None
|
self._heating_stations[station_id].current_material = None
|
||||||
@@ -626,17 +727,17 @@ class VirtualWorkbench:
|
|||||||
self._heating_stations[station_id].heating_progress = 0.0
|
self._heating_stations[station_id].heating_progress = 0.0
|
||||||
self._heating_stations[station_id].heating_start_time = None
|
self._heating_stations[station_id].heating_start_time = None
|
||||||
|
|
||||||
# 释放机械臂
|
|
||||||
self._release_arm()
|
self._release_arm()
|
||||||
|
|
||||||
# 任务完成
|
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
if material_id in self._active_tasks:
|
if material_id in self._active_tasks:
|
||||||
self._active_tasks[material_id]["status"] = "completed"
|
self._active_tasks[material_id]["status"] = "completed"
|
||||||
self._active_tasks[material_id]["end_time"] = time.time()
|
self._active_tasks[material_id]["end_time"] = time.time()
|
||||||
|
|
||||||
self._update_data_status(f"{material_id}已移动到{output_position}")
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -645,8 +746,17 @@ class VirtualWorkbench:
|
|||||||
"output_position": output_position,
|
"output_position": output_position,
|
||||||
"message": f"{material_id}已成功移动到{output_position}",
|
"message": f"{material_id}已成功移动到{output_position}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str)
|
||||||
|
else (content.serialize() if content is not None else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -660,83 +770,105 @@ class VirtualWorkbench:
|
|||||||
"output_position": output_position,
|
"output_position": output_position,
|
||||||
"message": f"移动失败: {str(e)}",
|
"message": f"移动失败: {str(e)}",
|
||||||
"unilabos_samples": [
|
"unilabos_samples": [
|
||||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
LabSample(
|
||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid=sample_uuid,
|
||||||
|
oss_path="",
|
||||||
|
extra=(
|
||||||
|
{"material_uuid": content}
|
||||||
|
if isinstance(content, str) else (content.serialize() if content else {})
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for sample_uuid, content in sample_uuids.items()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============ 状态属性 ============
|
# ============ 状态属性 ============
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self.data.get("status", "Unknown")
|
return self.data.get("status", "Unknown")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def arm_state(self) -> str:
|
def arm_state(self) -> str:
|
||||||
return self._arm_state.value
|
return self._arm_state.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def arm_current_task(self) -> str:
|
def arm_current_task(self) -> str:
|
||||||
return self._arm_current_task or ""
|
return self._arm_current_task or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_1_state(self) -> str:
|
def heating_station_1_state(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(1)
|
station = self._heating_stations.get(1)
|
||||||
return station.state.value if station else "unknown"
|
return station.state.value if station else "unknown"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_1_material(self) -> str:
|
def heating_station_1_material(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(1)
|
station = self._heating_stations.get(1)
|
||||||
return station.current_material or "" if station else ""
|
return station.current_material or "" if station else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_1_progress(self) -> float:
|
def heating_station_1_progress(self) -> float:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(1)
|
station = self._heating_stations.get(1)
|
||||||
return station.heating_progress if station else 0.0
|
return station.heating_progress if station else 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_2_state(self) -> str:
|
def heating_station_2_state(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(2)
|
station = self._heating_stations.get(2)
|
||||||
return station.state.value if station else "unknown"
|
return station.state.value if station else "unknown"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_2_material(self) -> str:
|
def heating_station_2_material(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(2)
|
station = self._heating_stations.get(2)
|
||||||
return station.current_material or "" if station else ""
|
return station.current_material or "" if station else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_2_progress(self) -> float:
|
def heating_station_2_progress(self) -> float:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(2)
|
station = self._heating_stations.get(2)
|
||||||
return station.heating_progress if station else 0.0
|
return station.heating_progress if station else 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_3_state(self) -> str:
|
def heating_station_3_state(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(3)
|
station = self._heating_stations.get(3)
|
||||||
return station.state.value if station else "unknown"
|
return station.state.value if station else "unknown"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_3_material(self) -> str:
|
def heating_station_3_material(self) -> str:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(3)
|
station = self._heating_stations.get(3)
|
||||||
return station.current_material or "" if station else ""
|
return station.current_material or "" if station else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def heating_station_3_progress(self) -> float:
|
def heating_station_3_progress(self) -> float:
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
station = self._heating_stations.get(3)
|
station = self._heating_stations.get(3)
|
||||||
return station.heating_progress if station else 0.0
|
return station.heating_progress if station else 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def active_tasks_count(self) -> int:
|
def active_tasks_count(self) -> int:
|
||||||
with self._tasks_lock:
|
with self._tasks_lock:
|
||||||
return len(self._active_tasks)
|
return len(self._active_tasks)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@topic_config()
|
||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
return self.data.get("message", "")
|
return self.data.get("message", "")
|
||||||
|
|||||||
1044
unilabos/registry/ast_registry_scanner.py
Normal file
1044
unilabos/registry/ast_registry_scanner.py
Normal file
File diff suppressed because it is too large
Load Diff
658
unilabos/registry/decorators.py
Normal file
658
unilabos/registry/decorators.py
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
"""
|
||||||
|
装饰器注册表系统
|
||||||
|
|
||||||
|
通过 @device, @action, @resource 装饰器替代 YAML 配置文件来定义设备/动作/资源注册表信息。
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from unilabos.registry.decorators import (
|
||||||
|
device, action, resource,
|
||||||
|
InputHandle, OutputHandle,
|
||||||
|
ActionInputHandle, ActionOutputHandle,
|
||||||
|
HardwareInterface, Side, DataSource, NodeType,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeType(str, Enum):
|
||||||
|
"""动作的节点类型(用于区分 ILab 节点和人工确认节点等)"""
|
||||||
|
|
||||||
|
ILAB = "ILab"
|
||||||
|
MANUAL_CONFIRM = "manual_confirm"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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,
|
||||||
|
node_type: Optional["NodeType"] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
动作方法装饰器
|
||||||
|
|
||||||
|
标记方法为注册表动作。有三种用法:
|
||||||
|
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 从父类获取真实方法参数
|
||||||
|
node_type: 动作的节点类型 (NodeType.ILAB / NodeType.MANUAL_CONFIRM)。
|
||||||
|
不填写时不写入注册表。
|
||||||
|
"""
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
if node_type is not None:
|
||||||
|
meta["node_type"] = node_type.value if isinstance(node_type, NodeType) else str(node_type)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 枚举值归一化
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_enum_value(raw: Any, enum_cls) -> Optional[str]:
|
||||||
|
"""将 AST 提取的枚举成员名 / YAML 值字符串 / 旧格式长路径统一归一化为枚举值。
|
||||||
|
|
||||||
|
适用于 Side、DataSource、NodeType 等继承自 ``str, Enum`` 的装饰器枚举。
|
||||||
|
|
||||||
|
处理以下格式:
|
||||||
|
- "MANUAL_CONFIRM" → NodeType["MANUAL_CONFIRM"].value = "manual_confirm"
|
||||||
|
- "manual_confirm" → NodeType("manual_confirm").value = "manual_confirm"
|
||||||
|
- "HANDLE" → DataSource["HANDLE"].value = "handle"
|
||||||
|
- "NORTH" → Side["NORTH"].value = "NORTH"
|
||||||
|
- 旧缓存长路径 "unilabos...NodeType.MANUAL_CONFIRM" → 先 rsplit 再查找
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
raw_str = str(raw)
|
||||||
|
if "." in raw_str:
|
||||||
|
raw_str = raw_str.rsplit(".", 1)[-1]
|
||||||
|
try:
|
||||||
|
return enum_cls[raw_str].value
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return enum_cls(raw_str).value
|
||||||
|
except ValueError:
|
||||||
|
return raw_str
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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)
|
||||||
@@ -96,10 +96,13 @@ serial:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: string
|
type: string
|
||||||
|
registry_name:
|
||||||
|
type: string
|
||||||
resource_tracker:
|
resource_tracker:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- device_id
|
- device_id
|
||||||
|
- registry_name
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
|
|||||||
@@ -13,21 +13,18 @@ Qone_nmr:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -71,31 +68,6 @@ Qone_nmr:
|
|||||||
title: monitor_folder_for_new_content参数
|
title: monitor_folder_for_new_content参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-strings_to_txt:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -138,21 +110,18 @@ Qone_nmr:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -167,32 +136,31 @@ Qone_nmr:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
2171
unilabos/registry/devices/bioyond_cell.yaml
Normal file
2171
unilabos/registry/devices/bioyond_cell.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,39 +24,321 @@ bioyond_dispensing_station:
|
|||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: brief_step_parameters参数
|
title: brief_step_parameters参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-compute_experiment_design:
|
auto-process_order_finish_report:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
m_tot: '70'
|
report_request: null
|
||||||
ratio: null
|
used_materials: null
|
||||||
titration_percent: '0.03'
|
|
||||||
wt_percent: '0.25'
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
report_request:
|
||||||
|
type: string
|
||||||
|
used_materials:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- report_request
|
||||||
|
- used_materials
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: process_order_finish_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-project_order_report:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
order_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: project_order_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-query_resource_by_name:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
material_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
material_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- material_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: query_resource_by_name参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-workflow_sample_locations:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
workflow_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- workflow_id
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: workflow_sample_locations参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
batch_create_90_10_vial_feeding_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
hold_m_name: hold_m_name
|
||||||
|
liquid_material_name: liquid_material_name
|
||||||
|
speed: speed
|
||||||
|
temperature: temperature
|
||||||
|
titration: titration
|
||||||
|
goal_default:
|
||||||
|
delay_time: null
|
||||||
|
hold_m_name: null
|
||||||
|
liquid_material_name: NMP
|
||||||
|
speed: null
|
||||||
|
temperature: null
|
||||||
|
titration: null
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: titration
|
||||||
|
data_source: handle
|
||||||
|
data_type: object
|
||||||
|
handler_key: titration
|
||||||
|
io_type: source
|
||||||
|
label: Titration Data From Calculation Node
|
||||||
|
output:
|
||||||
|
- data_key: return_info
|
||||||
|
data_source: executor
|
||||||
|
data_type: string
|
||||||
|
handler_key: BATCH_CREATE_RESULT
|
||||||
|
io_type: sink
|
||||||
|
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Feedback
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
description: 延迟时间(秒),默认600
|
||||||
|
type: string
|
||||||
|
hold_m_name:
|
||||||
|
description: 库位名称,如"C01",必填参数
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
description: 10%物料的液体物料名称,默认为"NMP"
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
description: 搅拌速度,默认400
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
description: 温度(℃),默认40
|
||||||
|
type: string
|
||||||
|
titration:
|
||||||
|
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
|
||||||
|
titration_solvent(滴定溶液体积mL)'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- titration
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Result
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: batch_create_90_10_vial_feeding_tasks参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
batch_create_diamine_solution_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
liquid_material_name: liquid_material_name
|
||||||
|
solutions: solutions
|
||||||
|
speed: speed
|
||||||
|
temperature: temperature
|
||||||
|
goal_default:
|
||||||
|
delay_time: null
|
||||||
|
liquid_material_name: NMP
|
||||||
|
solutions: null
|
||||||
|
speed: null
|
||||||
|
temperature: null
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: solutions
|
||||||
|
data_source: handle
|
||||||
|
data_type: array
|
||||||
|
handler_key: solutions
|
||||||
|
io_type: source
|
||||||
|
label: Solution Data From Python
|
||||||
|
output:
|
||||||
|
- data_key: return_info
|
||||||
|
data_source: executor
|
||||||
|
data_type: string
|
||||||
|
handler_key: BATCH_CREATE_RESULT
|
||||||
|
io_type: sink
|
||||||
|
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Feedback
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
description: 液体溶剂名称,用于溶解固体物料,默认为NMP(N-甲基吡咯烷酮)
|
||||||
|
type: string
|
||||||
|
solutions:
|
||||||
|
description: '溶液列表,JSON数组格式,每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
|
||||||
|
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
|
||||||
|
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
|
||||||
|
4.5, "solvent_volume": 18}]'
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- solutions
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Result
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: batch_create_diamine_solution_tasks参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
compute_experiment_design:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
m_tot: m_tot
|
||||||
|
ratio: ratio
|
||||||
|
titration_percent: titration_percent
|
||||||
|
wt_percent: wt_percent
|
||||||
|
goal_default:
|
||||||
|
m_tot: '70'
|
||||||
|
ratio: null
|
||||||
|
titration_percent: '0.03'
|
||||||
|
wt_percent: '0.25'
|
||||||
|
handles:
|
||||||
|
output:
|
||||||
|
- data_key: solutions
|
||||||
|
data_source: executor
|
||||||
|
data_type: array
|
||||||
|
handler_key: solutions
|
||||||
|
io_type: sink
|
||||||
|
label: Solution Data From Python
|
||||||
|
- data_key: titration
|
||||||
|
data_source: executor
|
||||||
|
data_type: object
|
||||||
|
handler_key: titration
|
||||||
|
io_type: sink
|
||||||
|
label: Titration Data From Calculation Node
|
||||||
|
- data_key: solvents
|
||||||
|
data_source: executor
|
||||||
|
data_type: object
|
||||||
|
handler_key: solvents
|
||||||
|
io_type: sink
|
||||||
|
label: Solvents Data From Calculation Node
|
||||||
|
- data_key: feeding_order
|
||||||
|
data_source: executor
|
||||||
|
data_type: array
|
||||||
|
handler_key: feeding_order
|
||||||
|
io_type: sink
|
||||||
|
label: Feeding Order Data From Calculation Node
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
m_tot:
|
m_tot:
|
||||||
default: '70'
|
default: '70'
|
||||||
|
description: 总质量(g)
|
||||||
type: string
|
type: string
|
||||||
ratio:
|
ratio:
|
||||||
|
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
|
||||||
type: object
|
type: object
|
||||||
titration_percent:
|
titration_percent:
|
||||||
default: '0.03'
|
default: '0.03'
|
||||||
|
description: 滴定比例(10%部分)
|
||||||
type: string
|
type: string
|
||||||
wt_percent:
|
wt_percent:
|
||||||
default: '0.25'
|
default: '0.25'
|
||||||
|
description: 目标固含质量分数
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- ratio
|
- ratio
|
||||||
@@ -95,305 +377,6 @@ bioyond_dispensing_station:
|
|||||||
title: compute_experiment_design参数
|
title: compute_experiment_design参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-process_order_finish_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
report_request: null
|
|
||||||
used_materials: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
report_request:
|
|
||||||
type: string
|
|
||||||
used_materials:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- report_request
|
|
||||||
- used_materials
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_order_finish_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-project_order_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
order_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: project_order_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-query_resource_by_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- material_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: query_resource_by_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-transfer_materials_to_reaction_station:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
target_device_id: null
|
|
||||||
transfer_groups: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
target_device_id:
|
|
||||||
type: string
|
|
||||||
transfer_groups:
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- target_device_id
|
|
||||||
- transfer_groups
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: transfer_materials_to_reaction_station参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sample_locations:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sample_locations参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_90_10_vial_feeding_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
titration: titration
|
|
||||||
goal_default:
|
|
||||||
delay_time: '600'
|
|
||||||
hold_m_name: ''
|
|
||||||
liquid_material_name: NMP
|
|
||||||
speed: '400'
|
|
||||||
temperature: '40'
|
|
||||||
titration: ''
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: titration
|
|
||||||
data_source: handle
|
|
||||||
data_type: object
|
|
||||||
handler_key: titration
|
|
||||||
io_type: source
|
|
||||||
label: Titration Data From Calculation Node
|
|
||||||
output:
|
|
||||||
- data_key: return_info
|
|
||||||
data_source: executor
|
|
||||||
data_type: string
|
|
||||||
handler_key: BATCH_CREATE_RESULT
|
|
||||||
io_type: sink
|
|
||||||
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
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:
|
|
||||||
description: 库位名称,如"C01",必填参数
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
default: NMP
|
|
||||||
description: 10%物料的液体物料名称,默认为"NMP"
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
default: '400'
|
|
||||||
description: 搅拌速度,默认400
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
default: '40'
|
|
||||||
description: 温度(℃),默认40
|
|
||||||
type: string
|
|
||||||
titration:
|
|
||||||
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
|
|
||||||
titration_solvent(滴定溶液体积mL)'
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- titration
|
|
||||||
- hold_m_name
|
|
||||||
title: BatchCreate9010VialFeedingTasks_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: BatchCreate9010VialFeedingTasks_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: BatchCreate9010VialFeedingTasks
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_diamine_solution_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
solutions: solutions
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
goal_default:
|
|
||||||
delay_time: '600'
|
|
||||||
liquid_material_name: NMP
|
|
||||||
solutions: ''
|
|
||||||
speed: '400'
|
|
||||||
temperature: '20'
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: solutions
|
|
||||||
data_source: handle
|
|
||||||
data_type: array
|
|
||||||
handler_key: solutions
|
|
||||||
io_type: source
|
|
||||||
label: Solution Data From Python
|
|
||||||
output:
|
|
||||||
- data_key: return_info
|
|
||||||
data_source: executor
|
|
||||||
data_type: string
|
|
||||||
handler_key: BATCH_CREATE_RESULT
|
|
||||||
io_type: sink
|
|
||||||
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
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:
|
|
||||||
default: NMP
|
|
||||||
description: 液体溶剂名称,用于溶解固体物料,默认为NMP(N-甲基吡咯烷酮)
|
|
||||||
type: string
|
|
||||||
solutions:
|
|
||||||
description: '溶液列表,JSON数组格式,每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
|
|
||||||
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
|
|
||||||
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
|
|
||||||
4.5, "solvent_volume": 18}]'
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
default: '400'
|
|
||||||
description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
default: '20'
|
|
||||||
description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- solutions
|
|
||||||
title: BatchCreateDiamineSolutionTasks_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: BatchCreateDiamineSolutionTasks_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: BatchCreateDiamineSolutionTasks
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
create_90_10_vial_feeding_task:
|
create_90_10_vial_feeding_task:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -445,17 +428,18 @@ bioyond_dispensing_station:
|
|||||||
speed: ''
|
speed: ''
|
||||||
temperature: ''
|
temperature: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: DispenStationVialFeed_Feedback
|
title: DispenStationVialFeed_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
delay_time:
|
delay_time:
|
||||||
type: string
|
type: string
|
||||||
@@ -503,38 +487,13 @@ bioyond_dispensing_station:
|
|||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
type: string
|
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
|
title: DispenStationVialFeed_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationVialFeed_Result
|
title: DispenStationVialFeed_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -565,17 +524,18 @@ bioyond_dispensing_station:
|
|||||||
temperature: ''
|
temperature: ''
|
||||||
volume: ''
|
volume: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: DispenStationSolnPrep_Feedback
|
title: DispenStationSolnPrep_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
delay_time:
|
delay_time:
|
||||||
type: string
|
type: string
|
||||||
@@ -595,24 +555,13 @@ bioyond_dispensing_station:
|
|||||||
type: string
|
type: string
|
||||||
volume:
|
volume:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- order_name
|
|
||||||
- material_name
|
|
||||||
- target_weigh
|
|
||||||
- volume
|
|
||||||
- liquid_material_name
|
|
||||||
- speed
|
|
||||||
- temperature
|
|
||||||
- delay_time
|
|
||||||
- hold_m_name
|
|
||||||
title: DispenStationSolnPrep_Goal
|
title: DispenStationSolnPrep_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationSolnPrep_Result
|
title: DispenStationSolnPrep_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -620,6 +569,64 @@ bioyond_dispensing_station:
|
|||||||
title: DispenStationSolnPrep
|
title: DispenStationSolnPrep
|
||||||
type: object
|
type: object
|
||||||
type: DispenStationSolnPrep
|
type: DispenStationSolnPrep
|
||||||
|
scheduler_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
title: scheduler_start结果
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: scheduler_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
transfer_materials_to_reaction_station:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
target_device_id: target_device_id
|
||||||
|
transfer_groups: transfer_groups
|
||||||
|
goal_default:
|
||||||
|
target_device_id: null
|
||||||
|
transfer_groups: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys:
|
||||||
|
target_device_id: unilabos_devices
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 将配液站完成的物料(溶液、样品等)转移到指定反应站的堆栈库位。支持配置多组转移任务,每组包含物料名称、目标堆栈和目标库位。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
target_device_id:
|
||||||
|
description: 目标反应站设备ID(从设备列表中选择,所有转移组都使用同一个目标设备)
|
||||||
|
type: string
|
||||||
|
transfer_groups:
|
||||||
|
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- target_device_id
|
||||||
|
- transfer_groups
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: transfer_materials_to_reaction_station参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
wait_for_multiple_orders_and_get_reports:
|
wait_for_multiple_orders_and_get_reports:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -627,9 +634,9 @@ bioyond_dispensing_station:
|
|||||||
check_interval: check_interval
|
check_interval: check_interval
|
||||||
timeout: timeout
|
timeout: timeout
|
||||||
goal_default:
|
goal_default:
|
||||||
batch_create_result: ''
|
batch_create_result: null
|
||||||
check_interval: '10'
|
check_interval: 10
|
||||||
timeout: '7200'
|
timeout: 7200
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: batch_create_result
|
- data_key: batch_create_result
|
||||||
@@ -645,50 +652,38 @@ bioyond_dispensing_station:
|
|||||||
handler_key: batch_reports_result
|
handler_key: batch_reports_result
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Batch Order Completion Reports
|
label: Batch Order Completion Reports
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。
|
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: WaitForMultipleOrdersAndGetReports_Feedback
|
title: WaitForMultipleOrdersAndGetReports_Feedback
|
||||||
type: object
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
batch_create_result:
|
batch_create_result:
|
||||||
description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
|
description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
|
||||||
type: string
|
type: string
|
||||||
check_interval:
|
check_interval:
|
||||||
default: '10'
|
default: 10
|
||||||
description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务
|
description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务
|
||||||
type: string
|
type: integer
|
||||||
timeout:
|
timeout:
|
||||||
default: '7200'
|
default: 7200
|
||||||
description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout
|
description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout
|
||||||
type: string
|
type: integer
|
||||||
required:
|
required: []
|
||||||
- batch_create_result
|
|
||||||
title: WaitForMultipleOrdersAndGetReports_Goal
|
title: WaitForMultipleOrdersAndGetReports_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
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
|
title: WaitForMultipleOrdersAndGetReports_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: WaitForMultipleOrdersAndGetReports
|
title: wait_for_multiple_orders_and_get_reports参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation
|
||||||
status_types: {}
|
status_types: {}
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
@@ -699,15 +694,16 @@ bioyond_dispensing_station:
|
|||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
type: string
|
type: object
|
||||||
deck:
|
deck:
|
||||||
type: string
|
type: string
|
||||||
required:
|
protocol_type:
|
||||||
- config
|
type: string
|
||||||
- deck
|
required: []
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
|
model: {}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
camera:
|
|
||||||
category:
|
|
||||||
- camera
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
auto-destroy_node:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 用于安全地关闭摄像头设备,释放摄像头资源,停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: destroy_node参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-timer_callback:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧,将OpenCV格式的图像转换为ROS Image消息格式,并发布到指定的视频话题。默认以10Hz频率执行,确保视频流的连续性和实时性。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: timer_callback参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.ros.nodes.presets.camera:VideoPublisher
|
|
||||||
status_types: {}
|
|
||||||
type: ros2
|
|
||||||
config_info: []
|
|
||||||
description: VideoPublisher摄像头设备节点,用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头(如USB摄像头、内置摄像头等),定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
camera_index:
|
|
||||||
default: 0
|
|
||||||
type: string
|
|
||||||
device_id:
|
|
||||||
default: video_publisher
|
|
||||||
type: string
|
|
||||||
device_uuid:
|
|
||||||
default: ''
|
|
||||||
type: string
|
|
||||||
period:
|
|
||||||
default: 0.1
|
|
||||||
type: number
|
|
||||||
resource_tracker:
|
|
||||||
type: object
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -18,7 +18,7 @@ cameracontroller_device:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
type: string
|
type: object
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
@@ -42,7 +42,8 @@ cameracontroller_device:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: stop参数
|
title: stop参数
|
||||||
@@ -50,7 +51,7 @@ cameracontroller_device:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.cameraSII.cameraUSB:CameraController
|
module: unilabos.devices.cameraSII.cameraUSB:CameraController
|
||||||
status_types:
|
status_types:
|
||||||
status: dict
|
status: Dict[str, Any]
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
||||||
@@ -103,5 +104,4 @@ cameracontroller_device:
|
|||||||
required:
|
required:
|
||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
registry_type: device
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -141,30 +141,26 @@ hplc.agilent:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -175,7 +171,6 @@ hplc.agilent:
|
|||||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||||
status_types:
|
status_types:
|
||||||
could_run: bool
|
could_run: bool
|
||||||
data_file: String
|
|
||||||
device_status: str
|
device_status: str
|
||||||
driver_init_ok: bool
|
driver_init_ok: bool
|
||||||
finish_status: str
|
finish_status: str
|
||||||
@@ -199,10 +194,6 @@ hplc.agilent:
|
|||||||
properties:
|
properties:
|
||||||
could_run:
|
could_run:
|
||||||
type: boolean
|
type: boolean
|
||||||
data_file:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
device_status:
|
device_status:
|
||||||
type: string
|
type: string
|
||||||
driver_init_ok:
|
driver_init_ok:
|
||||||
@@ -216,14 +207,13 @@ hplc.agilent:
|
|||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- status_text
|
|
||||||
- device_status
|
|
||||||
- could_run
|
- could_run
|
||||||
|
- device_status
|
||||||
- driver_init_ok
|
- driver_init_ok
|
||||||
- is_running
|
|
||||||
- success
|
|
||||||
- finish_status
|
- finish_status
|
||||||
- data_file
|
- is_running
|
||||||
|
- status_text
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
hplc.agilent-zhida:
|
hplc.agilent-zhida:
|
||||||
@@ -236,26 +226,25 @@ hplc.agilent-zhida:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -315,21 +304,18 @@ hplc.agilent-zhida:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -341,35 +327,35 @@ hplc.agilent-zhida:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: string
|
string: string
|
||||||
|
text: text
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -407,7 +393,7 @@ hplc.agilent-zhida:
|
|||||||
status:
|
status:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- methods
|
- methods
|
||||||
|
- status
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -120,42 +120,41 @@ raman.home_made:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
raman_cmd:
|
raman_cmd:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ separator.chinwe:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: connect参数
|
title: connect参数
|
||||||
@@ -65,135 +66,145 @@ separator.chinwe:
|
|||||||
required:
|
required:
|
||||||
- command_dict
|
- command_dict
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: execute_command_from_outer参数
|
title: execute_command_from_outer参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_rotate_quarter:
|
motor_rotate_quarter:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
direction: 顺时针
|
direction: 顺时针
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
speed: 60
|
speed: 60
|
||||||
|
goal_default:
|
||||||
|
direction: 顺时针
|
||||||
|
motor_id: null
|
||||||
|
speed: 60
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 电机旋转 1/4 圈
|
description: 电机旋转 1/4 圈
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
direction:
|
direction:
|
||||||
default: 顺时针
|
default: 顺时针
|
||||||
description: 旋转方向
|
description: 旋转方向
|
||||||
enum:
|
|
||||||
- 顺时针
|
|
||||||
- 逆时针
|
|
||||||
type: string
|
type: string
|
||||||
motor_id:
|
motor_id:
|
||||||
default: '4'
|
|
||||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
enum:
|
type: integer
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
type: string
|
|
||||||
speed:
|
speed:
|
||||||
default: 60
|
default: 60
|
||||||
description: 速度 (RPM)
|
description: 速度 (RPM)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
- speed
|
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: motor_rotate_quarter参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_run_continuous:
|
motor_run_continuous:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
direction: 顺时针
|
direction: 顺时针
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
speed: 60
|
speed: 60
|
||||||
|
goal_default:
|
||||||
|
direction: 顺时针
|
||||||
|
motor_id: null
|
||||||
|
speed: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 电机一直旋转 (速度模式)
|
description: 电机一直旋转 (速度模式)
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
direction:
|
direction:
|
||||||
default: 顺时针
|
default: 顺时针
|
||||||
description: 旋转方向
|
description: 旋转方向
|
||||||
enum:
|
|
||||||
- 顺时针
|
|
||||||
- 逆时针
|
|
||||||
type: string
|
type: string
|
||||||
motor_id:
|
motor_id:
|
||||||
default: '4'
|
|
||||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
enum:
|
type: integer
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
type: string
|
|
||||||
speed:
|
speed:
|
||||||
default: 60
|
|
||||||
description: 速度 (RPM)
|
description: 速度 (RPM)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
- speed
|
- speed
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: motor_run_continuous参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_stop:
|
motor_stop:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
|
goal_default:
|
||||||
|
motor_id: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 停止指定步进电机
|
description: 停止指定步进电机
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
motor_id:
|
motor_id:
|
||||||
default: '4'
|
|
||||||
description: 选择电机
|
description: 选择电机
|
||||||
enum:
|
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
title: '注: 4=搅拌, 5=旋钮'
|
title: '注: 4=搅拌, 5=旋钮'
|
||||||
type: string
|
type: integer
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: motor_stop参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_aspirate:
|
pump_aspirate:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
valve_port: 1
|
valve_port: 1
|
||||||
volume: 1000
|
volume: 1000
|
||||||
|
goal_default:
|
||||||
|
pump_id: null
|
||||||
|
valve_port: null
|
||||||
|
volume: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 注射泵吸液
|
description: 注射泵吸液
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
pump_id:
|
pump_id:
|
||||||
default: '1'
|
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
type: string
|
|
||||||
valve_port:
|
valve_port:
|
||||||
default: '1'
|
|
||||||
description: 阀门端口
|
description: 阀门端口
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
- '6'
|
|
||||||
- '7'
|
|
||||||
- '8'
|
|
||||||
type: string
|
|
||||||
volume:
|
volume:
|
||||||
default: 1000
|
|
||||||
description: 吸液步数
|
description: 吸液步数
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
@@ -201,41 +212,38 @@ separator.chinwe:
|
|||||||
- volume
|
- volume
|
||||||
- valve_port
|
- valve_port
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: pump_aspirate参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_dispense:
|
pump_dispense:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
valve_port: 1
|
valve_port: 1
|
||||||
volume: 1000
|
volume: 1000
|
||||||
|
goal_default:
|
||||||
|
pump_id: null
|
||||||
|
valve_port: null
|
||||||
|
volume: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 注射泵排液
|
description: 注射泵排液
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
pump_id:
|
pump_id:
|
||||||
default: '1'
|
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
type: string
|
|
||||||
valve_port:
|
valve_port:
|
||||||
default: '1'
|
|
||||||
description: 阀门端口
|
description: 阀门端口
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
- '6'
|
|
||||||
- '7'
|
|
||||||
- '8'
|
|
||||||
type: string
|
|
||||||
volume:
|
volume:
|
||||||
default: 1000
|
|
||||||
description: 排液步数
|
description: 排液步数
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
@@ -243,121 +251,152 @@ separator.chinwe:
|
|||||||
- volume
|
- volume
|
||||||
- valve_port
|
- valve_port
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: pump_dispense参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_initialize:
|
pump_initialize:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
drain_port: 0
|
drain_port: 0
|
||||||
output_port: 0
|
output_port: 0
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
speed: 10
|
speed: 10
|
||||||
|
goal_default:
|
||||||
|
drain_port: 0
|
||||||
|
output_port: 0
|
||||||
|
pump_id: null
|
||||||
|
speed: 10
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 初始化指定注射泵
|
description: 初始化指定注射泵
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
drain_port:
|
drain_port:
|
||||||
default: 0
|
default: 0
|
||||||
description: 排液口索引
|
description: 排液口索引
|
||||||
type: integer
|
type: string
|
||||||
output_port:
|
output_port:
|
||||||
default: 0
|
default: 0
|
||||||
description: 输出口索引
|
description: 输出口索引
|
||||||
type: integer
|
|
||||||
pump_id:
|
|
||||||
default: '1'
|
|
||||||
description: 选择泵
|
|
||||||
enum:
|
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
title: '注: 1号泵, 2号泵, 3号泵'
|
|
||||||
type: string
|
type: string
|
||||||
|
pump_id:
|
||||||
|
description: 选择泵
|
||||||
|
title: '注: 1号泵, 2号泵, 3号泵'
|
||||||
|
type: integer
|
||||||
speed:
|
speed:
|
||||||
default: 10
|
default: 10
|
||||||
description: 运动速度
|
description: 运动速度
|
||||||
type: integer
|
type: string
|
||||||
required:
|
required:
|
||||||
- pump_id
|
- pump_id
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: pump_initialize参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_valve:
|
pump_valve:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
port: 1
|
port: 1
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
|
goal_default:
|
||||||
|
port: null
|
||||||
|
pump_id: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 切换指定泵的阀门端口
|
description: 切换指定泵的阀门端口
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
port:
|
port:
|
||||||
default: '1'
|
|
||||||
description: 阀门端口号 (1-8)
|
description: 阀门端口号 (1-8)
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
- '4'
|
|
||||||
- '5'
|
|
||||||
- '6'
|
|
||||||
- '7'
|
|
||||||
- '8'
|
|
||||||
type: string
|
|
||||||
pump_id:
|
pump_id:
|
||||||
default: '1'
|
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
enum:
|
type: integer
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- pump_id
|
- pump_id
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: pump_valve参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
wait_sensor_level:
|
wait_sensor_level:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
target_state: 有液
|
target_state: 有液
|
||||||
timeout: 30
|
timeout: 30
|
||||||
|
goal_default:
|
||||||
|
target_state: 有液
|
||||||
|
timeout: 30
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 等待传感器液位条件
|
description: 等待传感器液位条件
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
target_state:
|
target_state:
|
||||||
default: 有液
|
default: 有液
|
||||||
description: 目标液位状态
|
description: 目标液位状态
|
||||||
enum:
|
|
||||||
- 有液
|
|
||||||
- 无液
|
|
||||||
type: string
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
default: 30
|
default: 30
|
||||||
description: 超时时间 (秒)
|
description: 超时时间 (秒)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required: []
|
||||||
- target_state
|
|
||||||
type: object
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: wait_sensor_level参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
wait_time:
|
wait_time:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
duration: 10
|
duration: 10
|
||||||
|
goal_default:
|
||||||
|
duration: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 等待指定时间
|
description: 等待指定时间
|
||||||
properties:
|
properties:
|
||||||
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
duration:
|
duration:
|
||||||
default: 10
|
|
||||||
description: 等待时间 (秒)
|
description: 等待时间 (秒)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- duration
|
- duration
|
||||||
type: object
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: wait_time参数
|
||||||
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.separator.chinwe:ChinweDevice
|
module: unilabos.devices.separator.chinwe:ChinweDevice
|
||||||
status_types:
|
status_types:
|
||||||
@@ -406,8 +445,8 @@ separator.chinwe:
|
|||||||
sensor_rssi:
|
sensor_rssi:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
|
- is_connected
|
||||||
- sensor_level
|
- sensor_level
|
||||||
- sensor_rssi
|
- sensor_rssi
|
||||||
- is_connected
|
|
||||||
type: object
|
type: object
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
|||||||
834
unilabos/registry/devices/coin_cell_workstation.yaml
Normal file
834
unilabos/registry/devices/coin_cell_workstation.yaml
Normal file
@@ -0,0 +1,834 @@
|
|||||||
|
coincellassemblyworkstation_device:
|
||||||
|
category:
|
||||||
|
- coin_cell_workstation
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-change_hole_sheet_to_2:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
hole: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
hole:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- hole
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: change_hole_sheet_to_2参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-fill_plate:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: fill_plate参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommandAsync
|
||||||
|
auto-fun_wuliao_test:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: fun_wuliao_test参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_allpack_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: 4200
|
||||||
|
assembly_type: 7
|
||||||
|
elec_num: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: 50
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
default: 4200
|
||||||
|
type: integer
|
||||||
|
assembly_type:
|
||||||
|
default: 7
|
||||||
|
type: integer
|
||||||
|
elec_num:
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
default: 50
|
||||||
|
type: integer
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- elec_num
|
||||||
|
- elec_use_num
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_allpack_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_allpack_cmd_simp:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: 4200
|
||||||
|
assembly_type: 7
|
||||||
|
battery_clean_ignore: false
|
||||||
|
battery_pressure_mode: true
|
||||||
|
dual_drop_first_volume: 25
|
||||||
|
dual_drop_mode: false
|
||||||
|
dual_drop_start_timing: false
|
||||||
|
dual_drop_suction_timing: false
|
||||||
|
elec_num: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: 50
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
fujipian_juzhendianwei: 0
|
||||||
|
fujipian_panshu: 0
|
||||||
|
gemo_juzhendianwei: 0
|
||||||
|
gemopanshu: 0
|
||||||
|
lvbodian: true
|
||||||
|
qiangtou_juzhendianwei: 0
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 简化版电池组装函数,整合了参数设置和双滴模式
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
default: 4200
|
||||||
|
description: 电池压制力(N)
|
||||||
|
type: integer
|
||||||
|
assembly_type:
|
||||||
|
default: 7
|
||||||
|
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
|
||||||
|
type: integer
|
||||||
|
battery_clean_ignore:
|
||||||
|
default: false
|
||||||
|
description: 是否忽略电池清洁步骤
|
||||||
|
type: boolean
|
||||||
|
battery_pressure_mode:
|
||||||
|
default: true
|
||||||
|
description: 是否启用压力模式
|
||||||
|
type: boolean
|
||||||
|
dual_drop_first_volume:
|
||||||
|
default: 25
|
||||||
|
description: 二次滴液第一次排液体积(μL)
|
||||||
|
type: integer
|
||||||
|
dual_drop_mode:
|
||||||
|
default: false
|
||||||
|
description: 电解液添加模式(false=单次滴液, true=二次滴液)
|
||||||
|
type: boolean
|
||||||
|
dual_drop_start_timing:
|
||||||
|
default: false
|
||||||
|
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
|
||||||
|
type: boolean
|
||||||
|
dual_drop_suction_timing:
|
||||||
|
default: false
|
||||||
|
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
|
||||||
|
type: boolean
|
||||||
|
elec_num:
|
||||||
|
description: 电解液瓶数
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
description: 每瓶电解液组装电池数
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
default: 50
|
||||||
|
description: 电解液吸液量(μL)
|
||||||
|
type: integer
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
description: 实验记录保存路径
|
||||||
|
type: string
|
||||||
|
fujipian_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
fujipian_panshu:
|
||||||
|
default: 0
|
||||||
|
description: 负极片盘数
|
||||||
|
type: integer
|
||||||
|
gemo_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
gemopanshu:
|
||||||
|
default: 0
|
||||||
|
description: 隔膜盘数
|
||||||
|
type: integer
|
||||||
|
lvbodian:
|
||||||
|
default: true
|
||||||
|
description: 是否使用铝箔垫片
|
||||||
|
type: boolean
|
||||||
|
qiangtou_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- elec_num
|
||||||
|
- elec_use_num
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_allpack_cmd_simp参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_get_csv_export_status:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_get_csv_export_status参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_auto:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_auto参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_init_auto_start_combined:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
material_search_enable: false
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
material_search_enable:
|
||||||
|
default: false
|
||||||
|
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
|
||||||
|
type: boolean
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_init_auto_start_combined参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_device_stop:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_device_stop参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_get_msg_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: D:\coin_cell_data
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
default: D:\coin_cell_data
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_get_msg_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_bottle_num:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
bottle_num: null
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: bottle_num
|
||||||
|
data_source: workflow
|
||||||
|
data_type: integer
|
||||||
|
handler_key: bottle_count
|
||||||
|
io_type: source
|
||||||
|
label: 配液瓶数
|
||||||
|
required: true
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
bottle_num:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- bottle_num
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_bottle_num参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_finished_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_finished_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_pack_send_msg_cmd:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: null
|
||||||
|
assembly_type: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
type: string
|
||||||
|
assembly_type:
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- elec_use_num
|
||||||
|
- elec_vol
|
||||||
|
- assembly_type
|
||||||
|
- assembly_pressure
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_pack_send_msg_cmd参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_read_data_and_output:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_read_data_and_output参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_sendbottle_allpack_multi:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
assembly_pressure: 4200
|
||||||
|
assembly_type: 7
|
||||||
|
battery_clean_ignore: false
|
||||||
|
battery_pressure_mode: true
|
||||||
|
dual_drop_first_volume: 25
|
||||||
|
dual_drop_mode: false
|
||||||
|
dual_drop_start_timing: false
|
||||||
|
dual_drop_suction_timing: false
|
||||||
|
elec_num: null
|
||||||
|
elec_use_num: null
|
||||||
|
elec_vol: 50
|
||||||
|
file_path: /Users/sml/work
|
||||||
|
fujipian_juzhendianwei: 0
|
||||||
|
fujipian_panshu: 0
|
||||||
|
gemo_juzhendianwei: 0
|
||||||
|
gemopanshu: 0
|
||||||
|
lvbodian: true
|
||||||
|
qiangtou_juzhendianwei: 0
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: elec_num
|
||||||
|
data_source: workflow
|
||||||
|
data_type: integer
|
||||||
|
handler_key: bottle_count
|
||||||
|
io_type: source
|
||||||
|
label: 配液瓶数
|
||||||
|
required: true
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 发送瓶数+简化组装函数(适用于第二批次及后续批次),合并了发送瓶数和简化组装流程
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assembly_pressure:
|
||||||
|
default: 4200
|
||||||
|
description: 电池压制力(N)
|
||||||
|
type: integer
|
||||||
|
assembly_type:
|
||||||
|
default: 7
|
||||||
|
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
|
||||||
|
type: integer
|
||||||
|
battery_clean_ignore:
|
||||||
|
default: false
|
||||||
|
description: 是否忽略电池清洁步骤
|
||||||
|
type: boolean
|
||||||
|
battery_pressure_mode:
|
||||||
|
default: true
|
||||||
|
description: 是否启用压力模式
|
||||||
|
type: boolean
|
||||||
|
dual_drop_first_volume:
|
||||||
|
default: 25
|
||||||
|
description: 二次滴液第一次排液体积(μL)
|
||||||
|
type: integer
|
||||||
|
dual_drop_mode:
|
||||||
|
default: false
|
||||||
|
description: 电解液添加模式(false=单次滴液, true=二次滴液)
|
||||||
|
type: boolean
|
||||||
|
dual_drop_start_timing:
|
||||||
|
default: false
|
||||||
|
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
|
||||||
|
type: boolean
|
||||||
|
dual_drop_suction_timing:
|
||||||
|
default: false
|
||||||
|
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
|
||||||
|
type: boolean
|
||||||
|
elec_num:
|
||||||
|
description: 电解液瓶数,如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数
|
||||||
|
type: string
|
||||||
|
elec_use_num:
|
||||||
|
description: 每瓶电解液组装电池数
|
||||||
|
type: string
|
||||||
|
elec_vol:
|
||||||
|
default: 50
|
||||||
|
description: 电解液吸液量(μL)
|
||||||
|
type: integer
|
||||||
|
file_path:
|
||||||
|
default: /Users/sml/work
|
||||||
|
description: 实验记录保存路径
|
||||||
|
type: string
|
||||||
|
fujipian_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
fujipian_panshu:
|
||||||
|
default: 0
|
||||||
|
description: 负极片盘数
|
||||||
|
type: integer
|
||||||
|
gemo_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
gemopanshu:
|
||||||
|
default: 0
|
||||||
|
description: 隔膜盘数
|
||||||
|
type: integer
|
||||||
|
lvbodian:
|
||||||
|
default: true
|
||||||
|
description: 是否使用铝箔垫片
|
||||||
|
type: boolean
|
||||||
|
qiangtou_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- elec_num
|
||||||
|
- elec_use_num
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_sendbottle_allpack_multi参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-func_stop_read_data:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: func_stop_read_data参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-modify_deck_name:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
resource_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
resource_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- resource_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: modify_deck_name参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-qiming_coin_cell_code:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
battery_clean_ignore: false
|
||||||
|
battery_pressure: 4000
|
||||||
|
battery_pressure_mode: true
|
||||||
|
fujipian_juzhendianwei: 0
|
||||||
|
fujipian_panshu: null
|
||||||
|
gemo_juzhendianwei: 0
|
||||||
|
gemopanshu: 0
|
||||||
|
lvbodian: true
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
battery_clean_ignore:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
battery_pressure:
|
||||||
|
default: 4000
|
||||||
|
type: integer
|
||||||
|
battery_pressure_mode:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
fujipian_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
fujipian_panshu:
|
||||||
|
type: integer
|
||||||
|
gemo_juzhendianwei:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
gemopanshu:
|
||||||
|
default: 0
|
||||||
|
type: integer
|
||||||
|
lvbodian:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- fujipian_panshu
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: qiming_coin_cell_code参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||||
|
status_types:
|
||||||
|
data_assembly_coin_cell_num: int
|
||||||
|
data_assembly_pressure: int
|
||||||
|
data_assembly_time: float
|
||||||
|
data_axis_x_pos: float
|
||||||
|
data_axis_y_pos: float
|
||||||
|
data_axis_z_pos: float
|
||||||
|
data_coin_cell_code: str
|
||||||
|
data_coin_num: int
|
||||||
|
data_electrolyte_code: str
|
||||||
|
data_electrolyte_volume: int
|
||||||
|
data_glove_box_o2_content: float
|
||||||
|
data_glove_box_pressure: float
|
||||||
|
data_glove_box_water_content: float
|
||||||
|
data_open_circuit_voltage: float
|
||||||
|
data_pole_weight: float
|
||||||
|
request_rec_msg_status: bool
|
||||||
|
request_send_msg_status: bool
|
||||||
|
sys_mode: str
|
||||||
|
sys_status: str
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: ''
|
||||||
|
handles: []
|
||||||
|
icon: koudian.webp
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
address:
|
||||||
|
default: 172.16.28.102
|
||||||
|
type: string
|
||||||
|
config:
|
||||||
|
type: object
|
||||||
|
debug_mode:
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
default: '502'
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
data_assembly_coin_cell_num:
|
||||||
|
type: integer
|
||||||
|
data_assembly_pressure:
|
||||||
|
type: integer
|
||||||
|
data_assembly_time:
|
||||||
|
type: number
|
||||||
|
data_axis_x_pos:
|
||||||
|
type: number
|
||||||
|
data_axis_y_pos:
|
||||||
|
type: number
|
||||||
|
data_axis_z_pos:
|
||||||
|
type: number
|
||||||
|
data_coin_cell_code:
|
||||||
|
type: string
|
||||||
|
data_coin_num:
|
||||||
|
type: integer
|
||||||
|
data_electrolyte_code:
|
||||||
|
type: string
|
||||||
|
data_electrolyte_volume:
|
||||||
|
type: integer
|
||||||
|
data_glove_box_o2_content:
|
||||||
|
type: number
|
||||||
|
data_glove_box_pressure:
|
||||||
|
type: number
|
||||||
|
data_glove_box_water_content:
|
||||||
|
type: number
|
||||||
|
data_open_circuit_voltage:
|
||||||
|
type: number
|
||||||
|
data_pole_weight:
|
||||||
|
type: number
|
||||||
|
request_rec_msg_status:
|
||||||
|
type: boolean
|
||||||
|
request_send_msg_status:
|
||||||
|
type: boolean
|
||||||
|
sys_mode:
|
||||||
|
type: string
|
||||||
|
sys_status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- data_assembly_coin_cell_num
|
||||||
|
- data_assembly_pressure
|
||||||
|
- data_assembly_time
|
||||||
|
- data_axis_x_pos
|
||||||
|
- data_axis_y_pos
|
||||||
|
- data_axis_z_pos
|
||||||
|
- data_coin_cell_code
|
||||||
|
- data_coin_num
|
||||||
|
- data_electrolyte_code
|
||||||
|
- data_electrolyte_volume
|
||||||
|
- data_glove_box_o2_content
|
||||||
|
- data_glove_box_pressure
|
||||||
|
- data_glove_box_water_content
|
||||||
|
- data_open_circuit_voltage
|
||||||
|
- data_pole_weight
|
||||||
|
- request_rec_msg_status
|
||||||
|
- request_send_msg_status
|
||||||
|
- sys_mode
|
||||||
|
- sys_status
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
@@ -50,26 +50,25 @@ gas_source.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -82,26 +81,25 @@ gas_source.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -116,32 +114,31 @@ gas_source.mock:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -232,26 +229,25 @@ vacuum_pump.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -264,26 +260,25 @@ vacuum_pump.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -298,32 +293,31 @@ vacuum_pump.mock:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel:
|
|||||||
action_value_mappings: {}
|
action_value_mappings: {}
|
||||||
module: unilabos.devices.resource_container.container:HotelContainer
|
module: unilabos.devices.resource_container.container:HotelContainer
|
||||||
status_types:
|
status_types:
|
||||||
rotation: String
|
rotation: ''
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
|
description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- degrees
|
- degrees
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: degrees_to_steps参数
|
title: degrees_to_steps参数
|
||||||
@@ -47,7 +48,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: emergency_stop参数
|
title: emergency_stop参数
|
||||||
@@ -72,7 +74,10 @@ xyz_stepper_controller:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: enable_all_axes参数
|
title: enable_all_axes参数
|
||||||
@@ -101,7 +106,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: enable_motor参数
|
title: enable_motor参数
|
||||||
@@ -122,7 +128,10 @@ xyz_stepper_controller:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: home_all_axes参数
|
title: home_all_axes参数
|
||||||
@@ -147,7 +156,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: home_axis参数
|
title: home_axis参数
|
||||||
@@ -188,7 +198,8 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position参数
|
title: move_to_position参数
|
||||||
@@ -229,7 +240,8 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- degrees
|
- degrees
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position_degrees参数
|
title: move_to_position_degrees参数
|
||||||
@@ -270,7 +282,8 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- revolutions
|
- revolutions
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position_revolutions参数
|
title: move_to_position_revolutions参数
|
||||||
@@ -301,14 +314,17 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x:
|
x:
|
||||||
type: string
|
type: integer
|
||||||
y:
|
y:
|
||||||
type: string
|
type: integer
|
||||||
z:
|
z:
|
||||||
type: string
|
type: integer
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz参数
|
title: move_xyz参数
|
||||||
@@ -339,14 +355,17 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x_deg:
|
x_deg:
|
||||||
type: string
|
type: number
|
||||||
y_deg:
|
y_deg:
|
||||||
type: string
|
type: number
|
||||||
z_deg:
|
z_deg:
|
||||||
type: string
|
type: number
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz_degrees参数
|
title: move_xyz_degrees参数
|
||||||
@@ -377,14 +396,17 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x_rev:
|
x_rev:
|
||||||
type: string
|
type: number
|
||||||
y_rev:
|
y_rev:
|
||||||
type: string
|
type: number
|
||||||
z_rev:
|
z_rev:
|
||||||
type: string
|
type: number
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz_revolutions参数
|
title: move_xyz_revolutions参数
|
||||||
@@ -409,7 +431,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- revolutions
|
- revolutions
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: revolutions_to_steps参数
|
title: revolutions_to_steps参数
|
||||||
@@ -442,7 +465,8 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- speed
|
- speed
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: set_speed_mode参数
|
title: set_speed_mode参数
|
||||||
@@ -467,7 +491,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- steps
|
- steps
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: number
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: steps_to_degrees参数
|
title: steps_to_degrees参数
|
||||||
@@ -492,7 +517,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- steps
|
- steps
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: number
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: steps_to_revolutions参数
|
title: steps_to_revolutions参数
|
||||||
@@ -513,7 +539,10 @@ xyz_stepper_controller:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
additionalProperties:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: stop_all_axes参数
|
title: stop_all_axes参数
|
||||||
@@ -542,7 +571,8 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: wait_for_completion参数
|
title: wait_for_completion参数
|
||||||
@@ -550,8 +580,7 @@ xyz_stepper_controller:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
|
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
|
||||||
status_types:
|
status_types:
|
||||||
all_positions: dict
|
all_positions: Dict[MotorAxis, MotorPosition]
|
||||||
motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition
|
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 新XYZ控制器
|
description: 新XYZ控制器
|
||||||
@@ -574,12 +603,10 @@ xyz_stepper_controller:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
all_positions:
|
all_positions:
|
||||||
type: object
|
additionalProperties:
|
||||||
motor_status:
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- motor_status
|
|
||||||
- all_positions
|
- all_positions
|
||||||
type: object
|
type: object
|
||||||
registry_type: device
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,31 +5,6 @@ neware_battery_test_system:
|
|||||||
- battery_test
|
- battery_test
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
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:
|
auto-print_status_summary:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -66,7 +41,8 @@ neware_battery_test_system:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: test_connection参数
|
title: test_connection参数
|
||||||
@@ -77,9 +53,8 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 调试方法:显示所有资源的实际名称
|
description: 调试方法:显示所有资源的实际名称
|
||||||
properties:
|
properties:
|
||||||
@@ -89,19 +64,10 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 资源调试信息
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: 是否成功
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: debug_resource_names参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
export_status_json:
|
export_status_json:
|
||||||
@@ -111,9 +77,8 @@ neware_battery_test_system:
|
|||||||
goal_default:
|
goal_default:
|
||||||
filepath: bts_status.json
|
filepath: bts_status.json
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 导出当前状态数据到JSON文件
|
description: 导出当前状态数据到JSON文件
|
||||||
properties:
|
properties:
|
||||||
@@ -127,19 +92,10 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 导出操作结果信息
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: 导出是否成功
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: export_status_json参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
get_device_summary:
|
get_device_summary:
|
||||||
@@ -181,10 +137,8 @@ neware_battery_test_system:
|
|||||||
goal_default:
|
goal_default:
|
||||||
plate_num: null
|
plate_num: null
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
plate_data: plate_data
|
result: {}
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 获取指定盘或所有盘的状态信息
|
description: 获取指定盘或所有盘的状态信息
|
||||||
properties:
|
properties:
|
||||||
@@ -193,29 +147,14 @@ neware_battery_test_system:
|
|||||||
properties:
|
properties:
|
||||||
plate_num:
|
plate_num:
|
||||||
description: 盘号 (1 或 2),如果为null则返回所有盘的状态
|
description: 盘号 (1 或 2),如果为null则返回所有盘的状态
|
||||||
maximum: 2
|
|
||||||
minimum: 1
|
|
||||||
type: integer
|
type: integer
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
plate_data:
|
|
||||||
description: 盘状态数据(单盘或所有盘)
|
|
||||||
type: object
|
|
||||||
return_info:
|
|
||||||
description: 操作结果信息
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: 查询是否成功
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
- plate_data
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: get_plate_status参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
print_status_summary_action:
|
print_status_summary_action:
|
||||||
@@ -223,9 +162,8 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 打印通道状态摘要信息到控制台
|
description: 打印通道状态摘要信息到控制台
|
||||||
properties:
|
properties:
|
||||||
@@ -235,28 +173,21 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 打印操作结果信息
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: 打印是否成功
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: print_status_summary_action参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
query_plate_action:
|
query_plate_action:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: plate_id
|
plate_id: plate_id
|
||||||
|
string: string
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
@@ -264,27 +195,23 @@ neware_battery_test_system:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -298,13 +225,11 @@ neware_battery_test_system:
|
|||||||
csv_path: string
|
csv_path: string
|
||||||
output_dir: string
|
output_dir: string
|
||||||
goal_default:
|
goal_default:
|
||||||
csv_path: ''
|
csv_path: null
|
||||||
output_dir: .
|
output_dir: .
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
submitted_count: submitted_count
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 从CSV文件批量提交Neware测试任务
|
description: 从CSV文件批量提交Neware测试任务
|
||||||
properties:
|
properties:
|
||||||
@@ -315,31 +240,17 @@ neware_battery_test_system:
|
|||||||
description: 输入CSV文件的绝对路径
|
description: 输入CSV文件的绝对路径
|
||||||
type: string
|
type: string
|
||||||
output_dir:
|
output_dir:
|
||||||
|
default: .
|
||||||
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- csv_path
|
- csv_path
|
||||||
type: object
|
type: object
|
||||||
result:
|
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
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: submit_from_csv参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
test_connection_action:
|
test_connection_action:
|
||||||
@@ -347,9 +258,8 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: 测试与电池测试系统的TCP连接
|
description: 测试与电池测试系统的TCP连接
|
||||||
properties:
|
properties:
|
||||||
@@ -359,19 +269,10 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: 连接测试结果信息
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
description: 连接测试是否成功
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: test_connection_action参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
upload_backup_to_oss:
|
upload_backup_to_oss:
|
||||||
@@ -392,12 +293,8 @@ neware_battery_test_system:
|
|||||||
handler_key: uploaded_files
|
handler_key: uploaded_files
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Uploaded Files (with standard flow info)
|
label: Uploaded Files (with standard flow info)
|
||||||
result:
|
placeholder_keys: {}
|
||||||
failed_files: failed_files
|
result: {}
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
total_count: total_count
|
|
||||||
uploaded_count: uploaded_count
|
|
||||||
schema:
|
schema:
|
||||||
description: 上传备份文件到阿里云OSS
|
description: 上传备份文件到阿里云OSS
|
||||||
properties:
|
properties:
|
||||||
@@ -417,65 +314,17 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
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
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
|
title: upload_backup_to_oss参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
||||||
status_types:
|
status_types:
|
||||||
channel_status: dict
|
channel_status: Dict[int, Dict]
|
||||||
connection_info: dict
|
connection_info: Dict[str, str]
|
||||||
device_summary: dict
|
device_summary: dict
|
||||||
plate_status: dict
|
|
||||||
status: str
|
status: str
|
||||||
total_channels: int
|
total_channels: int
|
||||||
type: python
|
type: python
|
||||||
@@ -517,23 +366,24 @@ neware_battery_test_system:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
channel_status:
|
channel_status:
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
connection_info:
|
connection_info:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
device_summary:
|
device_summary:
|
||||||
type: object
|
type: object
|
||||||
plate_status:
|
|
||||||
type: object
|
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
total_channels:
|
total_channels:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- channel_status
|
- channel_status
|
||||||
- connection_info
|
- connection_info
|
||||||
- total_channels
|
|
||||||
- plate_status
|
|
||||||
- device_summary
|
- device_summary
|
||||||
|
- status
|
||||||
|
- total_channels
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -49,32 +49,7 @@ opcua_example:
|
|||||||
title: load_config参数
|
title: load_config参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-post_init:
|
auto-refresh_node_values:
|
||||||
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: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
@@ -92,32 +67,7 @@ opcua_example:
|
|||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: print_cache_stats参数
|
title: refresh_node_values参数
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-read_node:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
node_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
node_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- node_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: read_node参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-set_node_value:
|
auto-set_node_value:
|
||||||
@@ -149,10 +99,50 @@ opcua_example:
|
|||||||
title: set_node_value参数
|
title: set_node_value参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-start_node_refresh:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: start_node_refresh参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-stop_node_refresh:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: stop_node_refresh参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
||||||
status_types:
|
status_types: {}
|
||||||
cache_stats: dict
|
|
||||||
node_value: String
|
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: null
|
description: null
|
||||||
@@ -161,36 +151,22 @@ opcua_example:
|
|||||||
init_param_schema:
|
init_param_schema:
|
||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
cache_timeout:
|
|
||||||
default: 5.0
|
|
||||||
type: number
|
|
||||||
config_path:
|
config_path:
|
||||||
type: string
|
type: string
|
||||||
deck:
|
|
||||||
type: string
|
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
subscription_interval:
|
refresh_interval:
|
||||||
default: 500
|
default: 1.0
|
||||||
type: integer
|
type: number
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
use_subscription:
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- url
|
- url
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties:
|
properties: {}
|
||||||
cache_stats:
|
required: []
|
||||||
type: object
|
|
||||||
node_value:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- node_value
|
|
||||||
- cache_stats
|
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ opsky_ATR30007:
|
|||||||
type: string
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: run_once参数
|
title: run_once参数
|
||||||
|
|||||||
@@ -100,42 +100,41 @@ rotavap.one:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_timer:
|
set_timer:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -250,9 +249,13 @@ separator.homemade:
|
|||||||
feedback:
|
feedback:
|
||||||
status: status
|
status: status
|
||||||
goal:
|
goal:
|
||||||
|
event: event
|
||||||
settling_time: settling_time
|
settling_time: settling_time
|
||||||
stir_speed: stir_speed
|
stir_speed: stir_speed
|
||||||
stir_time: stir_time,
|
stir_time: stir_time
|
||||||
|
time: time
|
||||||
|
time_spec: time_spec
|
||||||
|
vessel: vessel
|
||||||
goal_default:
|
goal_default:
|
||||||
event: ''
|
event: ''
|
||||||
settling_time: ''
|
settling_time: ''
|
||||||
@@ -281,34 +284,42 @@ separator.homemade:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
type: ''
|
type: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
message: message
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: Stir_Feedback
|
title: Stir_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
event:
|
event:
|
||||||
type: string
|
type: string
|
||||||
settling_time:
|
settling_time:
|
||||||
type: string
|
type: string
|
||||||
stir_speed:
|
stir_speed:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
stir_time:
|
stir_time:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
time:
|
time:
|
||||||
type: string
|
type: string
|
||||||
time_spec:
|
time_spec:
|
||||||
type: string
|
type: string
|
||||||
vessel:
|
vessel:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
@@ -327,16 +338,26 @@ separator.homemade:
|
|||||||
parent:
|
parent:
|
||||||
type: string
|
type: string
|
||||||
pose:
|
pose:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -346,12 +367,19 @@ separator.homemade:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -381,17 +409,10 @@ separator.homemade:
|
|||||||
- data
|
- data
|
||||||
title: vessel
|
title: vessel
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- vessel
|
|
||||||
- time
|
|
||||||
- event
|
|
||||||
- time_spec
|
|
||||||
- stir_time
|
|
||||||
- stir_speed
|
|
||||||
- settling_time
|
|
||||||
title: Stir_Goal
|
title: Stir_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
@@ -399,10 +420,6 @@ separator.homemade:
|
|||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- success
|
|
||||||
- message
|
|
||||||
- return_info
|
|
||||||
title: Stir_Result
|
title: Stir_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -418,36 +435,34 @@ separator.homemade:
|
|||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -28,31 +28,6 @@ post_process_station:
|
|||||||
title: load_config参数
|
title: load_config参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-print_cache_stats:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -104,42 +79,41 @@ post_process_station:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
disconnect:
|
disconnect:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: {}
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -149,42 +123,41 @@ post_process_station:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
read_node:
|
read_node:
|
||||||
feedback:
|
feedback:
|
||||||
result: result
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: node_name
|
command: command
|
||||||
|
node_name: node_name
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -283,17 +256,19 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: PostProcessTriggerClean_Feedback
|
title: PostProcessTriggerClean_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
acetone_inner_wall_cleaning_count:
|
acetone_inner_wall_cleaning_count:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_inner_wall_cleaning_injection:
|
acetone_inner_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
acetone_inner_wall_cleaning_waste_time:
|
acetone_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -304,6 +279,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_outer_wall_cleaning_injection:
|
acetone_outer_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
acetone_outer_wall_cleaning_wait_time:
|
acetone_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -322,6 +299,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_stirrer_cleaning_injection:
|
acetone_stirrer_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
acetone_stirrer_cleaning_wait_time:
|
acetone_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -348,6 +327,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_inner_wall_cleaning_injection:
|
nmp_inner_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
nmp_inner_wall_cleaning_waste_time:
|
nmp_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -358,6 +339,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_outer_wall_cleaning_injection:
|
nmp_outer_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
nmp_outer_wall_cleaning_wait_time:
|
nmp_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -376,6 +359,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_stirrer_cleaning_injection:
|
nmp_stirrer_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
nmp_stirrer_cleaning_wait_time:
|
nmp_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -394,6 +379,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_inner_wall_cleaning_injection:
|
water_inner_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
water_inner_wall_cleaning_waste_time:
|
water_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -404,6 +391,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_outer_wall_cleaning_injection:
|
water_outer_wall_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
water_outer_wall_cleaning_wait_time:
|
water_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -422,6 +411,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_stirrer_cleaning_injection:
|
water_stirrer_cleaning_injection:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
water_stirrer_cleaning_wait_time:
|
water_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -431,55 +422,13 @@ post_process_station:
|
|||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
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
|
title: PostProcessTriggerClean_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: PostProcessTriggerClean_Result
|
title: PostProcessTriggerClean_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -502,11 +451,11 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: PostProcessGrab_Feedback
|
title: PostProcessGrab_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
raw_tank_number:
|
raw_tank_number:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -516,17 +465,13 @@ post_process_station:
|
|||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
|
||||||
- reaction_tank_number
|
|
||||||
- raw_tank_number
|
|
||||||
title: PostProcessGrab_Goal
|
title: PostProcessGrab_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: PostProcessGrab_Result
|
title: PostProcessGrab_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -573,13 +518,15 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: PostProcessTriggerPostPro_Feedback
|
title: PostProcessTriggerPostPro_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
atomization_fast_speed:
|
atomization_fast_speed:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
atomization_pressure_kpa:
|
atomization_pressure_kpa:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -594,8 +541,12 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
first_wash_water_amount:
|
first_wash_water_amount:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
initial_water_amount:
|
initial_water_amount:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
injection_pump_push_speed:
|
injection_pump_push_speed:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -622,32 +573,20 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
second_wash_water_amount:
|
second_wash_water_amount:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
wash_slow_speed:
|
wash_slow_speed:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
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
|
title: PostProcessTriggerPostPro_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: PostProcessTriggerPostPro_Result
|
title: PostProcessTriggerPostPro_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -669,30 +608,26 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -702,8 +637,7 @@ post_process_station:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
|
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
|
||||||
status_types:
|
status_types:
|
||||||
cache_stats: dict
|
cache_stats: Dict[str, Any]
|
||||||
node_value: String
|
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 后处理站
|
description: 后处理站
|
||||||
@@ -718,7 +652,9 @@ post_process_station:
|
|||||||
config_path:
|
config_path:
|
||||||
type: string
|
type: string
|
||||||
deck:
|
deck:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: object
|
||||||
|
- type: object
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
subscription_interval:
|
subscription_interval:
|
||||||
@@ -738,10 +674,7 @@ post_process_station:
|
|||||||
properties:
|
properties:
|
||||||
cache_stats:
|
cache_stats:
|
||||||
type: object
|
type: object
|
||||||
node_value:
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- node_value
|
|
||||||
- cache_stats
|
- cache_stats
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -136,36 +136,36 @@ solenoid_valve:
|
|||||||
set_valve_position:
|
set_valve_position:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: position
|
position: position
|
||||||
|
string: string
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -278,26 +278,25 @@ solenoid_valve.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -310,26 +309,25 @@ solenoid_valve.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -422,6 +420,27 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-pull_plunger:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -695,7 +714,10 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
position:
|
position:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
@@ -720,7 +742,9 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
velocity:
|
velocity:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
required:
|
required:
|
||||||
- velocity
|
- velocity
|
||||||
type: object
|
type: object
|
||||||
@@ -780,13 +804,13 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
status_types:
|
status_types:
|
||||||
max_velocity: float
|
max_velocity: float
|
||||||
mode: int
|
mode: int
|
||||||
plunger_position: String
|
plunger_position: ''
|
||||||
position: float
|
position: float
|
||||||
status: str
|
status: str
|
||||||
valve_position: str
|
valve_position: str
|
||||||
velocity_end: String
|
velocity_end: ''
|
||||||
velocity_grade: String
|
velocity_grade: ''
|
||||||
velocity_init: String
|
velocity_init: ''
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||||
@@ -885,15 +909,15 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
velocity_init:
|
velocity_init:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- mode
|
|
||||||
- max_velocity
|
- max_velocity
|
||||||
|
- mode
|
||||||
|
- plunger_position
|
||||||
|
- position
|
||||||
|
- status
|
||||||
|
- valve_position
|
||||||
|
- velocity_end
|
||||||
- velocity_grade
|
- velocity_grade
|
||||||
- velocity_init
|
- velocity_init
|
||||||
- velocity_end
|
|
||||||
- valve_position
|
|
||||||
- position
|
|
||||||
- plunger_position
|
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
syringe_pump_with_valve.runze.SY03B-T08:
|
syringe_pump_with_valve.runze.SY03B-T08:
|
||||||
@@ -943,6 +967,27 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-pull_plunger:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -1216,7 +1261,10 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
position:
|
position:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
@@ -1241,7 +1289,9 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
velocity:
|
velocity:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
required:
|
required:
|
||||||
- velocity
|
- velocity
|
||||||
type: object
|
type: object
|
||||||
@@ -1301,13 +1351,13 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
status_types:
|
status_types:
|
||||||
max_velocity: float
|
max_velocity: float
|
||||||
mode: int
|
mode: int
|
||||||
plunger_position: String
|
plunger_position: ''
|
||||||
position: float
|
position: float
|
||||||
status: str
|
status: str
|
||||||
valve_position: str
|
valve_position: str
|
||||||
velocity_end: String
|
velocity_end: ''
|
||||||
velocity_grade: String
|
velocity_grade: ''
|
||||||
velocity_init: String
|
velocity_init: ''
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||||
@@ -1422,14 +1472,14 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
velocity_init:
|
velocity_init:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- mode
|
|
||||||
- max_velocity
|
- max_velocity
|
||||||
|
- mode
|
||||||
|
- plunger_position
|
||||||
|
- position
|
||||||
|
- status
|
||||||
|
- valve_position
|
||||||
|
- velocity_end
|
||||||
- velocity_grade
|
- velocity_grade
|
||||||
- velocity_init
|
- velocity_init
|
||||||
- velocity_end
|
|
||||||
- valve_position
|
|
||||||
- position
|
|
||||||
- plunger_position
|
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -4,6 +4,78 @@ reaction_station.bioyond:
|
|||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
|
add_time_constraint:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
duration: duration
|
||||||
|
end_point: end_point
|
||||||
|
end_step_key: end_step_key
|
||||||
|
start_point: start_point
|
||||||
|
start_step_key: start_step_key
|
||||||
|
goal_default:
|
||||||
|
duration: null
|
||||||
|
end_point: 0
|
||||||
|
end_step_key: ''
|
||||||
|
start_point: 0
|
||||||
|
start_step_key: ''
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 添加时间约束 - 在两个工作流之间添加时间约束
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
duration:
|
||||||
|
description: 时间(秒)
|
||||||
|
type: integer
|
||||||
|
end_point:
|
||||||
|
default: 0
|
||||||
|
description: 终点计时点 (Start=开始前, End=结束后)
|
||||||
|
type: integer
|
||||||
|
end_step_key:
|
||||||
|
default: ''
|
||||||
|
description: 终点步骤Key (可选, 默认为空则自动选择)
|
||||||
|
type: string
|
||||||
|
start_point:
|
||||||
|
default: 0
|
||||||
|
description: 起点计时点 (Start=开始前, End=结束后)
|
||||||
|
type: integer
|
||||||
|
start_step_key:
|
||||||
|
default: ''
|
||||||
|
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- duration
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: add_time_constraint参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-clear_workflows:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: clear_workflows参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-create_order:
|
auto-create_order:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -23,7 +95,8 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- json_str
|
- json_str
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: create_order参数
|
title: create_order参数
|
||||||
@@ -50,7 +123,8 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- workflow_ids
|
- workflow_ids
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: hard_delete_merged_workflows参数
|
title: hard_delete_merged_workflows参数
|
||||||
@@ -75,7 +149,8 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- json_str
|
- json_str
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: merge_workflow_with_parameters参数
|
title: merge_workflow_with_parameters参数
|
||||||
@@ -100,7 +175,8 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- report_request
|
- report_request
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_temperature_cutoff_report参数
|
title: process_temperature_cutoff_report参数
|
||||||
@@ -125,12 +201,47 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- web_workflow_json
|
- web_workflow_json
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
items:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_web_workflows参数
|
title: process_web_workflows参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-set_reactor_temperature:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
reactor_id: null
|
||||||
|
temperature: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
reactor_id:
|
||||||
|
type: integer
|
||||||
|
temperature:
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- reactor_id
|
||||||
|
- temperature
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: set_reactor_temperature参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-skip_titration_steps:
|
auto-skip_titration_steps:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -150,12 +261,35 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- preintake_id
|
- preintake_id
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: skip_titration_steps参数
|
title: skip_titration_steps参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-sync_workflow_sequence_from_bioyond:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: sync_workflow_sequence_from_bioyond参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-wait_for_multiple_orders_and_get_reports:
|
auto-wait_for_multiple_orders_and_get_reports:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -182,12 +316,40 @@ reaction_station.bioyond:
|
|||||||
type: integer
|
type: integer
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
title: wait_for_multiple_orders_and_get_reports参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-workflow_sequence:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
value: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- value
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: workflow_sequence参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-workflow_step_query:
|
auto-workflow_step_query:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -207,12 +369,35 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- workflow_id
|
- workflow_id
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: workflow_step_query参数
|
title: workflow_step_query参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
clean_all_server_workflows:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 清空服务端所有非核心工作流 (保留核心流程)
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: clean_all_server_workflows参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
drip_back:
|
drip_back:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -223,13 +408,14 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
temperature: ''
|
temperature: 25.0
|
||||||
time: ''
|
time: '90'
|
||||||
titration_type: ''
|
titration_type: '1'
|
||||||
torque_variation: ''
|
torque_variation: 2
|
||||||
volume: ''
|
volume: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 滴回去
|
description: 滴回去
|
||||||
@@ -241,27 +427,27 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称(不能为空)
|
description: 物料名称(不能为空)
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
|
default: 25.0
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
|
default: '90'
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
default: '1'
|
||||||
|
description: 是否滴定(NO=否, YES=是)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
default: 2
|
||||||
type: string
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
type: integer
|
||||||
volume:
|
volume:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume
|
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
- time
|
- volume
|
||||||
- torque_variation
|
|
||||||
- titration_type
|
|
||||||
- temperature
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -274,7 +460,7 @@ reaction_station.bioyond:
|
|||||||
goal:
|
goal:
|
||||||
batch_reports_result: batch_reports_result
|
batch_reports_result: batch_reports_result
|
||||||
goal_default:
|
goal_default:
|
||||||
batch_reports_result: ''
|
batch_reports_result: null
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: batch_reports_result
|
- data_key: batch_reports_result
|
||||||
@@ -290,8 +476,8 @@ reaction_station.bioyond:
|
|||||||
handler_key: ACTUALS_EXTRACTED
|
handler_key: ACTUALS_EXTRACTED
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Extracted Actuals
|
label: Extracted Actuals
|
||||||
result:
|
placeholder_keys: {}
|
||||||
return_info: return_info
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。
|
description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。
|
||||||
properties:
|
properties:
|
||||||
@@ -305,13 +491,6 @@ reaction_station.bioyond:
|
|||||||
- batch_reports_result
|
- batch_reports_result
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
description: JSON字符串,包含actuals数组,每项含order_code, order_id, actualTargetWeigh,
|
|
||||||
actualVolume
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: extract_actuals_from_batch_reports结果
|
title: extract_actuals_from_batch_reports结果
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -329,13 +508,14 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: BAPP
|
||||||
temperature: ''
|
temperature: 25.0
|
||||||
time: ''
|
time: '0'
|
||||||
titration_type: ''
|
titration_type: '1'
|
||||||
torque_variation: ''
|
torque_variation: 1
|
||||||
volume: ''
|
volume: '350'
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料烧杯
|
description: 液体进料烧杯
|
||||||
@@ -344,30 +524,30 @@ reaction_station.bioyond:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
assign_material_name:
|
assign_material_name:
|
||||||
|
default: BAPP
|
||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
|
default: 25.0
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
|
default: '0'
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
default: '1'
|
||||||
|
description: 是否滴定(NO=否, YES=是)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
default: 1
|
||||||
type: string
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
type: integer
|
||||||
volume:
|
volume:
|
||||||
description: 分液公式(μL)
|
default: '350'
|
||||||
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required: []
|
||||||
- volume
|
|
||||||
- assign_material_name
|
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
- titration_type
|
|
||||||
- temperature
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -386,13 +566,13 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
solvents: ''
|
solvents: null
|
||||||
temperature: '25.00'
|
temperature: 25.0
|
||||||
time: '360'
|
time: '360'
|
||||||
titration_type: '1'
|
titration_type: '1'
|
||||||
torque_variation: '2'
|
torque_variation: 2
|
||||||
volume: ''
|
volume: null
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: solvents
|
- data_key: solvents
|
||||||
@@ -401,9 +581,10 @@ reaction_station.bioyond:
|
|||||||
handler_key: solvents
|
handler_key: solvents
|
||||||
io_type: source
|
io_type: source
|
||||||
label: Solvents Data From Calculation Node
|
label: Solvents Data From Calculation Node
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -415,23 +596,23 @@ reaction_station.bioyond:
|
|||||||
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: '25.00'
|
default: 25.0
|
||||||
description: 温度设定(°C),默认25.00
|
description: 温度设定(°C),默认25.00
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
default: '360'
|
default: '360'
|
||||||
description: 观察时间(分钟),默认360
|
description: 观察时间(分钟),默认360
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
default: '1'
|
||||||
description: 是否滴定(1=否, 2=是),默认1
|
description: 是否滴定(NO=否, YES=是),默认NO
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: '2'
|
default: 2
|
||||||
description: 是否观察 (1=否, 2=是),默认2
|
description: 是否观察 (NO=否, YES=是),默认YES
|
||||||
type: string
|
type: integer
|
||||||
volume:
|
volume:
|
||||||
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
|
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
@@ -455,15 +636,15 @@ reaction_station.bioyond:
|
|||||||
volume_formula: volume_formula
|
volume_formula: volume_formula
|
||||||
x_value: x_value
|
x_value: x_value
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
extracted_actuals: ''
|
extracted_actuals: null
|
||||||
feeding_order_data: ''
|
feeding_order_data: null
|
||||||
temperature: '25.00'
|
temperature: 25.0
|
||||||
time: '90'
|
time: '90'
|
||||||
titration_type: '2'
|
titration_type: '2'
|
||||||
torque_variation: '2'
|
torque_variation: 2
|
||||||
volume_formula: ''
|
volume_formula: null
|
||||||
x_value: ''
|
x_value: null
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: extracted_actuals
|
- data_key: extracted_actuals
|
||||||
@@ -478,6 +659,7 @@ reaction_station.bioyond:
|
|||||||
handler_key: feeding_order
|
handler_key: feeding_order
|
||||||
io_type: source
|
io_type: source
|
||||||
label: Feeding Order Data From Calculation Node
|
label: Feeding Order Data From Calculation Node
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
|
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
|
||||||
@@ -496,23 +678,23 @@ reaction_station.bioyond:
|
|||||||
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
|
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: '25.00'
|
default: 25.0
|
||||||
description: 温度设定(°C),默认25.00
|
description: 温度设定(°C),默认25.00
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
default: '90'
|
default: '90'
|
||||||
description: 观察时间(分钟),默认90
|
description: 观察时间(分钟),默认90
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '2'
|
default: '2'
|
||||||
description: 是否滴定(1=否, 2=是),默认2
|
description: 是否滴定(NO=否, YES=是),默认YES
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: '2'
|
default: 2
|
||||||
description: 是否观察 (1=否, 2=是),默认2
|
description: 是否观察 (NO=否, YES=是),默认YES
|
||||||
type: string
|
type: integer
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
||||||
type: string
|
type: string
|
||||||
x_value:
|
x_value:
|
||||||
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
||||||
@@ -536,13 +718,14 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume_formula: volume_formula
|
volume_formula: volume_formula
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
temperature: ''
|
temperature: 25.0
|
||||||
time: ''
|
time: '0'
|
||||||
titration_type: ''
|
titration_type: '1'
|
||||||
torque_variation: ''
|
torque_variation: 1
|
||||||
volume_formula: ''
|
volume_formula: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料小瓶(非滴定)
|
description: 液体进料小瓶(非滴定)
|
||||||
@@ -554,27 +737,27 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
|
default: 25.0
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
|
default: '0'
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
default: '1'
|
||||||
|
description: 是否滴定(NO=否, YES=是)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
default: 1
|
||||||
type: string
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
type: integer
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume_formula
|
- volume_formula
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
- titration_type
|
|
||||||
- temperature
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -588,9 +771,10 @@ reaction_station.bioyond:
|
|||||||
task_name: task_name
|
task_name: task_name
|
||||||
workflow_name: workflow_name
|
workflow_name: workflow_name
|
||||||
goal_default:
|
goal_default:
|
||||||
task_name: ''
|
task_name: null
|
||||||
workflow_name: ''
|
workflow_name: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 处理并执行工作流
|
description: 处理并执行工作流
|
||||||
@@ -608,7 +792,8 @@ reaction_station.bioyond:
|
|||||||
- workflow_name
|
- workflow_name
|
||||||
- task_name
|
- task_name
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_and_execute_workflow参数
|
title: process_and_execute_workflow参数
|
||||||
@@ -621,10 +806,11 @@ reaction_station.bioyond:
|
|||||||
cutoff: cutoff
|
cutoff: cutoff
|
||||||
temperature: temperature
|
temperature: temperature
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
cutoff: ''
|
cutoff: '900000'
|
||||||
temperature: ''
|
temperature: -10.0
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
|
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
|
||||||
@@ -636,14 +822,14 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
cutoff:
|
cutoff:
|
||||||
|
default: '900000'
|
||||||
description: 粘度上限
|
description: 粘度上限
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
|
default: -10.0
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: string
|
type: number
|
||||||
required:
|
required:
|
||||||
- cutoff
|
|
||||||
- temperature
|
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
@@ -657,6 +843,7 @@ reaction_station.bioyond:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
|
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
|
||||||
@@ -666,20 +853,35 @@ reaction_station.bioyond:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
description: 操作结果代码(1表示成功,0表示失败)
|
|
||||||
type: integer
|
|
||||||
return_info:
|
|
||||||
description: 操作结果详细信息
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reactor_taken_out参数
|
title: reactor_taken_out参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
scheduler_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
title: scheduler_start结果
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: scheduler_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
solid_feeding_vials:
|
solid_feeding_vials:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -689,12 +891,13 @@ reaction_station.bioyond:
|
|||||||
time: time
|
time: time
|
||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: null
|
||||||
material_id: ''
|
material_id: null
|
||||||
temperature: ''
|
temperature: 25.0
|
||||||
time: ''
|
time: '0'
|
||||||
torque_variation: ''
|
torque_variation: 1
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
|
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
|
||||||
@@ -706,23 +909,22 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称(用于获取试剂瓶位ID)
|
description: 物料名称(用于获取试剂瓶位ID)
|
||||||
type: string
|
type: string
|
||||||
material_id:
|
material_id:
|
||||||
description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟)
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
|
default: 25.0
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: string
|
type: number
|
||||||
time:
|
time:
|
||||||
|
default: '0'
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
default: 1
|
||||||
type: string
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
|
||||||
- material_id
|
- material_id
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
- temperature
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -730,10 +932,10 @@ reaction_station.bioyond:
|
|||||||
title: solid_feeding_vials参数
|
title: solid_feeding_vials参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
|
||||||
protocol_type: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
workflow_sequence: String
|
workflow_sequence: str
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Bioyond反应站
|
description: Bioyond反应站
|
||||||
@@ -753,9 +955,7 @@ reaction_station.bioyond:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
workflow_sequence:
|
workflow_sequence:
|
||||||
items:
|
type: string
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
required:
|
required:
|
||||||
- workflow_sequence
|
- workflow_sequence
|
||||||
type: object
|
type: object
|
||||||
@@ -791,7 +991,7 @@ reaction_station.reactor:
|
|||||||
title: update_metrics参数
|
title: update_metrics参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
|
||||||
status_types: {}
|
status_types: {}
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
|
|||||||
@@ -37,42 +37,41 @@ agv.SEER:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
send_nav_task:
|
send_nav_task:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -122,31 +122,6 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
title: moveit_task参数
|
title: moveit_task参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-resource_manager:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -198,41 +173,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pick_and_place:
|
pick_and_place:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -241,41 +216,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_position:
|
set_position:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -284,41 +259,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_status:
|
set_status:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -455,42 +430,41 @@ robotic_arm.UR:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
move_pos_task:
|
move_pos_task:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -532,8 +506,8 @@ robotic_arm.UR:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- arm_pose
|
- arm_pose
|
||||||
- gripper_pose
|
|
||||||
- arm_status
|
- arm_status
|
||||||
|
- gripper_pose
|
||||||
- gripper_status
|
- gripper_status
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -726,41 +700,41 @@ robotic_arm.elite:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
modbus_task_cmd:
|
modbus_task_cmd:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -770,8 +744,8 @@ robotic_arm.elite:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
module: unilabos.devices.arm.elite_robot:EliteRobot
|
module: unilabos.devices.arm.elite_robot:EliteRobot
|
||||||
status_types:
|
status_types:
|
||||||
actual_joint_positions: String
|
actual_joint_positions: ''
|
||||||
arm_pose: String
|
arm_pose: list[float]
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Elite robot arm
|
description: Elite robot arm
|
||||||
@@ -797,8 +771,8 @@ robotic_arm.elite:
|
|||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- arm_pose
|
|
||||||
- actual_joint_positions
|
- actual_joint_positions
|
||||||
|
- arm_pose
|
||||||
type: object
|
type: object
|
||||||
model:
|
model:
|
||||||
mesh: elite_robot
|
mesh: elite_robot
|
||||||
|
|||||||
@@ -114,11 +114,12 @@ gripper.misumi_rz:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: string
|
type: object
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: modbus_crc参数
|
title: modbus_crc参数
|
||||||
@@ -398,30 +399,26 @@ gripper.misumi_rz:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -504,71 +501,82 @@ gripper.mock:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
push_to:
|
push_to:
|
||||||
feedback:
|
feedback:
|
||||||
effort: torque
|
effort: effort
|
||||||
position: position
|
position: position
|
||||||
|
reached_goal: reached_goal
|
||||||
|
stalled: stalled
|
||||||
goal:
|
goal:
|
||||||
command.max_effort: torque
|
command: command
|
||||||
command.position: position
|
position: position
|
||||||
|
torque: torque
|
||||||
|
velocity: velocity
|
||||||
goal_default:
|
goal_default:
|
||||||
command:
|
command:
|
||||||
max_effort: 0.0
|
max_effort: 0.0
|
||||||
position: 0.0
|
position: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
effort: torque
|
effort: effort
|
||||||
position: position
|
position: position
|
||||||
|
reached_goal: reached_goal
|
||||||
|
stalled: stalled
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
effort:
|
effort:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
reached_goal:
|
reached_goal:
|
||||||
type: boolean
|
type: boolean
|
||||||
stalled:
|
stalled:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- position
|
|
||||||
- effort
|
|
||||||
- stalled
|
|
||||||
- reached_goal
|
|
||||||
title: GripperCommand_Feedback
|
title: GripperCommand_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
max_effort:
|
max_effort:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
- max_effort
|
- max_effort
|
||||||
title: command
|
title: command
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: GripperCommand_Goal
|
title: GripperCommand_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
effort:
|
effort:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
reached_goal:
|
reached_goal:
|
||||||
type: boolean
|
type: boolean
|
||||||
stalled:
|
stalled:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- position
|
|
||||||
- effort
|
|
||||||
- stalled
|
|
||||||
- reached_goal
|
|
||||||
title: GripperCommand_Result
|
title: GripperCommand_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -604,8 +612,8 @@ gripper.mock:
|
|||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
- velocity
|
|
||||||
- torque
|
|
||||||
- status
|
- status
|
||||||
|
- torque
|
||||||
|
- velocity
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -24,6 +24,27 @@ linear_motion.grbl:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-set_position:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -93,44 +114,39 @@ linear_motion.grbl:
|
|||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
move_through_points:
|
move_through_points:
|
||||||
feedback:
|
feedback:
|
||||||
current_pose.pose.position: position
|
current_pose: current_pose
|
||||||
estimated_time_remaining.sec: time_remaining
|
distance_remaining: distance_remaining
|
||||||
navigation_time.sec: time_spent
|
estimated_time_remaining: estimated_time_remaining
|
||||||
number_of_poses_remaining: pose_number_remaining
|
navigation_time: navigation_time
|
||||||
|
number_of_poses_remaining: number_of_poses_remaining
|
||||||
|
number_of_recoveries: number_of_recoveries
|
||||||
goal:
|
goal:
|
||||||
poses[].pose.position: positions[]
|
behavior_tree: behavior_tree
|
||||||
|
poses: poses
|
||||||
|
positions: positions
|
||||||
goal_default:
|
goal_default:
|
||||||
behavior_tree: ''
|
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: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
result: result
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
current_pose:
|
current_pose:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
header:
|
header:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
frame_id:
|
frame_id:
|
||||||
type: string
|
type: string
|
||||||
stamp:
|
stamp:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -151,16 +167,26 @@ linear_motion.grbl:
|
|||||||
title: header
|
title: header
|
||||||
type: object
|
type: object
|
||||||
pose:
|
pose:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -170,12 +196,19 @@ linear_motion.grbl:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -194,8 +227,11 @@ linear_motion.grbl:
|
|||||||
title: current_pose
|
title: current_pose
|
||||||
type: object
|
type: object
|
||||||
distance_remaining:
|
distance_remaining:
|
||||||
|
maximum: 3.4028235e+38
|
||||||
|
minimum: -3.4028235e+38
|
||||||
type: number
|
type: number
|
||||||
estimated_time_remaining:
|
estimated_time_remaining:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -211,6 +247,7 @@ linear_motion.grbl:
|
|||||||
title: estimated_time_remaining
|
title: estimated_time_remaining
|
||||||
type: object
|
type: object
|
||||||
navigation_time:
|
navigation_time:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -233,16 +270,10 @@ linear_motion.grbl:
|
|||||||
maximum: 32767
|
maximum: 32767
|
||||||
minimum: -32768
|
minimum: -32768
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
|
||||||
- current_pose
|
|
||||||
- navigation_time
|
|
||||||
- estimated_time_remaining
|
|
||||||
- number_of_recoveries
|
|
||||||
- distance_remaining
|
|
||||||
- number_of_poses_remaining
|
|
||||||
title: NavigateThroughPoses_Feedback
|
title: NavigateThroughPoses_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
behavior_tree:
|
behavior_tree:
|
||||||
type: string
|
type: string
|
||||||
@@ -256,12 +287,8 @@ linear_motion.grbl:
|
|||||||
stamp:
|
stamp:
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
|
||||||
minimum: 0
|
|
||||||
type: integer
|
type: integer
|
||||||
sec:
|
sec:
|
||||||
maximum: 2147483647
|
|
||||||
minimum: -2147483648
|
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- sec
|
- sec
|
||||||
@@ -314,23 +341,17 @@ linear_motion.grbl:
|
|||||||
required:
|
required:
|
||||||
- header
|
- header
|
||||||
- pose
|
- pose
|
||||||
title: poses
|
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
required:
|
|
||||||
- poses
|
|
||||||
- behavior_tree
|
|
||||||
title: NavigateThroughPoses_Goal
|
title: NavigateThroughPoses_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
result:
|
result:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: result
|
title: result
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- result
|
|
||||||
title: NavigateThroughPoses_Result
|
title: NavigateThroughPoses_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -340,9 +361,15 @@ linear_motion.grbl:
|
|||||||
type: NavigateThroughPoses
|
type: NavigateThroughPoses
|
||||||
set_spindle_speed:
|
set_spindle_speed:
|
||||||
feedback:
|
feedback:
|
||||||
position: spindle_speed
|
error: error
|
||||||
|
header: header
|
||||||
|
position: position
|
||||||
|
velocity: velocity
|
||||||
goal:
|
goal:
|
||||||
position: spindle_speed
|
max_velocity: max_velocity
|
||||||
|
min_duration: min_duration
|
||||||
|
position: position
|
||||||
|
spindle_speed: spindle_speed
|
||||||
goal_default:
|
goal_default:
|
||||||
max_velocity: 0.0
|
max_velocity: 0.0
|
||||||
min_duration:
|
min_duration:
|
||||||
@@ -350,19 +377,25 @@ linear_motion.grbl:
|
|||||||
sec: 0
|
sec: 0
|
||||||
position: 0.0
|
position: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
error:
|
error:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
header:
|
header:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
frame_id:
|
frame_id:
|
||||||
type: string
|
type: string
|
||||||
stamp:
|
stamp:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -383,21 +416,24 @@ linear_motion.grbl:
|
|||||||
title: header
|
title: header
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
velocity:
|
velocity:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
|
||||||
- header
|
|
||||||
- position
|
|
||||||
- velocity
|
|
||||||
- error
|
|
||||||
title: SingleJointPosition_Feedback
|
title: SingleJointPosition_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
max_velocity:
|
max_velocity:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
min_duration:
|
min_duration:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -413,16 +449,13 @@ linear_motion.grbl:
|
|||||||
title: min_duration
|
title: min_duration
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
|
||||||
- position
|
|
||||||
- min_duration
|
|
||||||
- max_velocity
|
|
||||||
title: SingleJointPosition_Goal
|
title: SingleJointPosition_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: SingleJointPosition_Result
|
title: SingleJointPosition_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -432,7 +465,7 @@ linear_motion.grbl:
|
|||||||
type: SingleJointPosition
|
type: SingleJointPosition
|
||||||
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
||||||
status_types:
|
status_types:
|
||||||
position: unilabos.messages:Point3D
|
position: Point3D
|
||||||
spindle_speed: float
|
spindle_speed: float
|
||||||
status: str
|
status: str
|
||||||
type: python
|
type: python
|
||||||
@@ -471,9 +504,9 @@ linear_motion.grbl:
|
|||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- position
|
- position
|
||||||
- spindle_speed
|
- spindle_speed
|
||||||
|
- status
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
linear_motion.toyo_xyz.sim:
|
linear_motion.toyo_xyz.sim:
|
||||||
@@ -600,31 +633,6 @@ linear_motion.toyo_xyz.sim:
|
|||||||
title: moveit_task参数
|
title: moveit_task参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-resource_manager:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -676,41 +684,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pick_and_place:
|
pick_and_place:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -719,41 +727,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_position:
|
set_position:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -762,41 +770,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_status:
|
set_status:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -939,30 +947,26 @@ motor.iCL42:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1000,8 +1004,8 @@ motor.iCL42:
|
|||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- motor_position
|
|
||||||
- is_executing_run
|
- is_executing_run
|
||||||
|
- motor_position
|
||||||
- success
|
- success
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -14,19 +14,24 @@ solid_dispenser.laiyu:
|
|||||||
powder_tube_number: 0
|
powder_tube_number: 0
|
||||||
target_tube_position: ''
|
target_tube_position: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
actual_mass_mg: actual_mass_mg
|
actual_mass_mg: actual_mass_mg
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: SolidDispenseAddPowderTube_Feedback
|
title: SolidDispenseAddPowderTube_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
compound_mass:
|
compound_mass:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
powder_tube_number:
|
powder_tube_number:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -34,24 +39,19 @@ solid_dispenser.laiyu:
|
|||||||
type: integer
|
type: integer
|
||||||
target_tube_position:
|
target_tube_position:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- powder_tube_number
|
|
||||||
- target_tube_position
|
|
||||||
- compound_mass
|
|
||||||
title: SolidDispenseAddPowderTube_Goal
|
title: SolidDispenseAddPowderTube_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
actual_mass_mg:
|
actual_mass_mg:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- actual_mass_mg
|
|
||||||
- success
|
|
||||||
title: SolidDispenseAddPowderTube_Result
|
title: SolidDispenseAddPowderTube_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -74,11 +74,12 @@ solid_dispenser.laiyu:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: string
|
type: object
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: calculate_crc参数
|
title: calculate_crc参数
|
||||||
@@ -99,11 +100,12 @@ solid_dispenser.laiyu:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: object
|
||||||
required:
|
required:
|
||||||
- command
|
- command
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: send_command参数
|
title: send_command参数
|
||||||
@@ -112,36 +114,37 @@ solid_dispenser.laiyu:
|
|||||||
discharge:
|
discharge:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
float_input: float_input
|
float_in: float_in
|
||||||
goal_default:
|
goal_default:
|
||||||
float_in: 0.0
|
float_in: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: FloatSingleInput_Feedback
|
title: FloatSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
float_in:
|
float_in:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
|
||||||
- float_in
|
|
||||||
title: FloatSingleInput_Goal
|
title: FloatSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: FloatSingleInput_Result
|
title: FloatSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -156,32 +159,31 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -200,38 +202,41 @@ solid_dispenser.laiyu:
|
|||||||
y: 0.0
|
y: 0.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: Point3DSeparateInput_Feedback
|
title: Point3DSeparateInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
|
||||||
- x
|
|
||||||
- y
|
|
||||||
- z
|
|
||||||
title: Point3DSeparateInput_Goal
|
title: Point3DSeparateInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: Point3DSeparateInput_Result
|
title: Point3DSeparateInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -246,34 +251,33 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
|
||||||
- int_input
|
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -288,34 +292,33 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
|
||||||
- int_input
|
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -328,26 +331,25 @@ solid_dispenser.laiyu:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ chiller:
|
|||||||
- register_address
|
- register_address
|
||||||
- value
|
- value
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: build_modbus_frame参数
|
title: build_modbus_frame参数
|
||||||
@@ -63,7 +64,8 @@ chiller:
|
|||||||
required:
|
required:
|
||||||
- temperature
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: convert_temperature_to_modbus_value参数
|
title: convert_temperature_to_modbus_value参数
|
||||||
@@ -84,11 +86,12 @@ chiller:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: string
|
type: object
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: modbus_crc参数
|
title: modbus_crc参数
|
||||||
@@ -116,42 +119,41 @@ chiller:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_temperature:
|
set_temperature:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -266,9 +268,15 @@ heaterstirrer.dalong:
|
|||||||
feedback:
|
feedback:
|
||||||
status: status
|
status: status
|
||||||
goal:
|
goal:
|
||||||
|
pressure: pressure
|
||||||
purpose: purpose
|
purpose: purpose
|
||||||
|
reflux_solvent: reflux_solvent
|
||||||
|
stir: stir
|
||||||
|
stir_speed: stir_speed
|
||||||
temp: temp
|
temp: temp
|
||||||
|
temp_spec: temp_spec
|
||||||
time: time
|
time: time
|
||||||
|
time_spec: time_spec
|
||||||
vessel: vessel
|
vessel: vessel
|
||||||
goal_default:
|
goal_default:
|
||||||
pressure: ''
|
pressure: ''
|
||||||
@@ -301,20 +309,23 @@ heaterstirrer.dalong:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
type: ''
|
type: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
message: message
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: HeatChill_Feedback
|
title: HeatChill_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
pressure:
|
pressure:
|
||||||
type: string
|
type: string
|
||||||
@@ -325,8 +336,12 @@ heaterstirrer.dalong:
|
|||||||
stir:
|
stir:
|
||||||
type: boolean
|
type: boolean
|
||||||
stir_speed:
|
stir_speed:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
temp:
|
temp:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
temp_spec:
|
temp_spec:
|
||||||
type: string
|
type: string
|
||||||
@@ -335,6 +350,7 @@ heaterstirrer.dalong:
|
|||||||
time_spec:
|
time_spec:
|
||||||
type: string
|
type: string
|
||||||
vessel:
|
vessel:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
@@ -353,16 +369,26 @@ heaterstirrer.dalong:
|
|||||||
parent:
|
parent:
|
||||||
type: string
|
type: string
|
||||||
pose:
|
pose:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -372,12 +398,19 @@ heaterstirrer.dalong:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
|
maximum: 1.7976931348623157e+308
|
||||||
|
minimum: -1.7976931348623157e+308
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -407,20 +440,10 @@ heaterstirrer.dalong:
|
|||||||
- data
|
- data
|
||||||
title: vessel
|
title: vessel
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- vessel
|
|
||||||
- temp
|
|
||||||
- time
|
|
||||||
- temp_spec
|
|
||||||
- time_spec
|
|
||||||
- pressure
|
|
||||||
- reflux_solvent
|
|
||||||
- stir
|
|
||||||
- stir_speed
|
|
||||||
- purpose
|
|
||||||
title: HeatChill_Goal
|
title: HeatChill_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
@@ -428,10 +451,6 @@ heaterstirrer.dalong:
|
|||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- success
|
|
||||||
- message
|
|
||||||
- return_info
|
|
||||||
title: HeatChill_Result
|
title: HeatChill_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -440,42 +459,42 @@ heaterstirrer.dalong:
|
|||||||
type: object
|
type: object
|
||||||
type: HeatChill
|
type: HeatChill
|
||||||
set_temp_target:
|
set_temp_target:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: temp
|
command: command
|
||||||
|
temp: temp
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -484,42 +503,42 @@ heaterstirrer.dalong:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_temp_warning:
|
set_temp_warning:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: temp
|
command: command
|
||||||
|
temp: temp
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -569,8 +588,8 @@ heaterstirrer.dalong:
|
|||||||
- status
|
- status
|
||||||
- stir_speed
|
- stir_speed
|
||||||
- temp
|
- temp
|
||||||
- temp_warning
|
|
||||||
- temp_target
|
- temp_target
|
||||||
|
- temp_warning
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
tempsensor:
|
tempsensor:
|
||||||
@@ -691,42 +710,41 @@ tempsensor:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_warning:
|
set_warning:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
status: status
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result:
|
result:
|
||||||
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -45,31 +45,6 @@ xrd_d7mate:
|
|||||||
title: connect参数
|
title: connect参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-start_from_string:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -85,11 +60,14 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
params:
|
params:
|
||||||
type: string
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: object
|
||||||
required:
|
required:
|
||||||
- params
|
- params
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: start_from_string参数
|
title: start_from_string参数
|
||||||
@@ -105,21 +83,18 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -130,38 +105,38 @@ xrd_d7mate:
|
|||||||
get_sample_down:
|
get_sample_down:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
sample_station: 1
|
int_input: int_input
|
||||||
|
sample_station: sample_station
|
||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
|
||||||
- int_input
|
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -179,21 +154,18 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -211,21 +183,18 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -238,26 +207,25 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -274,42 +242,35 @@ xrd_d7mate:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
start_theta: 10.0
|
start_theta: 10.0
|
||||||
goal_default:
|
goal_default:
|
||||||
end_theta: 80.0
|
end_theta: null
|
||||||
exp_time: 0.5
|
exp_time: null
|
||||||
increment: 0.02
|
increment: null
|
||||||
sample_id: Sample001
|
sample_id: null
|
||||||
start_theta: 10.0
|
start_theta: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 送样完成后,发送样品信息和采集参数
|
description: 送样完成后,发送样品信息和采集参数
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: SampleReadyInput_Feedback
|
title: SampleReadyInput_Feedback
|
||||||
type: object
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
end_theta:
|
end_theta:
|
||||||
description: 结束角度(≥5.5°,且必须大于start_theta)
|
description: 结束角度(≥5.5°,且必须大于start_theta)
|
||||||
minimum: 5.5
|
|
||||||
type: number
|
type: number
|
||||||
exp_time:
|
exp_time:
|
||||||
description: 曝光时间(0.1-5.0秒)
|
description: 曝光时间(0.1-5.0秒)
|
||||||
maximum: 5.0
|
|
||||||
minimum: 0.1
|
|
||||||
type: number
|
type: number
|
||||||
increment:
|
increment:
|
||||||
description: 角度增量(≥0.005)
|
description: 角度增量(≥0.005)
|
||||||
minimum: 0.005
|
|
||||||
type: number
|
type: number
|
||||||
sample_id:
|
sample_id:
|
||||||
description: 样品标识符
|
description: 样品标识符
|
||||||
type: string
|
type: string
|
||||||
start_theta:
|
start_theta:
|
||||||
description: 起始角度(≥5°)
|
description: 起始角度(≥5°)
|
||||||
minimum: 5.0
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- sample_id
|
- sample_id
|
||||||
@@ -320,19 +281,11 @@ xrd_d7mate:
|
|||||||
title: SampleReadyInput_Goal
|
title: SampleReadyInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SampleReadyInput_Result
|
title: SampleReadyInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: SampleReadyInput
|
title: send_sample_ready参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_power_off:
|
set_power_off:
|
||||||
@@ -340,26 +293,25 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -372,26 +324,25 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -405,18 +356,16 @@ xrd_d7mate:
|
|||||||
current: 30.0
|
current: 30.0
|
||||||
voltage: 40.0
|
voltage: 40.0
|
||||||
goal_default:
|
goal_default:
|
||||||
current: 30.0
|
current: null
|
||||||
voltage: 40.0
|
voltage: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 设置高压电源电压和电流
|
description: 设置高压电源电压和电流
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: VoltageCurrentInput_Feedback
|
title: VoltageCurrentInput_Feedback
|
||||||
type: object
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
current:
|
current:
|
||||||
@@ -431,19 +380,11 @@ xrd_d7mate:
|
|||||||
title: VoltageCurrentInput_Goal
|
title: VoltageCurrentInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: VoltageCurrentInput_Result
|
title: VoltageCurrentInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: VoltageCurrentInput
|
title: set_voltage_current参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
start:
|
start:
|
||||||
@@ -453,11 +394,12 @@ xrd_d7mate:
|
|||||||
end_theta: 80.0
|
end_theta: 80.0
|
||||||
exp_time: 0.1
|
exp_time: 0.1
|
||||||
increment: 0.05
|
increment: 0.05
|
||||||
sample_id: 样品名称
|
sample_id: ''
|
||||||
start_theta: 10.0
|
start_theta: 10.0
|
||||||
string: ''
|
string: ''
|
||||||
wait_minutes: 3.0
|
wait_minutes: 3.0
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
|
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
|
||||||
@@ -466,54 +408,42 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
end_theta:
|
end_theta:
|
||||||
|
default: 80.0
|
||||||
description: 结束角度(≥5.5°,且必须大于start_theta)
|
description: 结束角度(≥5.5°,且必须大于start_theta)
|
||||||
minimum: 5.5
|
type: number
|
||||||
type: string
|
|
||||||
exp_time:
|
exp_time:
|
||||||
|
default: 0.1
|
||||||
description: 曝光时间(0.1-5.0秒)
|
description: 曝光时间(0.1-5.0秒)
|
||||||
maximum: 5.0
|
type: number
|
||||||
minimum: 0.1
|
|
||||||
type: string
|
|
||||||
increment:
|
increment:
|
||||||
|
default: 0.05
|
||||||
description: 角度增量(≥0.005)
|
description: 角度增量(≥0.005)
|
||||||
minimum: 0.005
|
type: number
|
||||||
type: string
|
|
||||||
sample_id:
|
sample_id:
|
||||||
|
default: ''
|
||||||
description: 样品标识符
|
description: 样品标识符
|
||||||
type: string
|
type: string
|
||||||
start_theta:
|
start_theta:
|
||||||
|
default: 10.0
|
||||||
description: 起始角度(≥5°)
|
description: 起始角度(≥5°)
|
||||||
minimum: 5.0
|
type: number
|
||||||
type: string
|
|
||||||
string:
|
string:
|
||||||
|
default: ''
|
||||||
description: 字符串格式的参数输入,如果提供则优先解析使用
|
description: 字符串格式的参数输入,如果提供则优先解析使用
|
||||||
type: string
|
type: string
|
||||||
wait_minutes:
|
wait_minutes:
|
||||||
|
default: 3.0
|
||||||
description: 允许上样后等待分钟数
|
description: 允许上样后等待分钟数
|
||||||
minimum: 0.0
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required: []
|
||||||
- sample_id
|
|
||||||
- start_theta
|
|
||||||
- end_theta
|
|
||||||
- increment
|
|
||||||
- exp_time
|
|
||||||
title: StartWorkflow_Goal
|
title: StartWorkflow_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StartWorkflow_Result
|
title: StartWorkflow_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: StartWorkflow
|
title: start参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
start_auto_mode:
|
start_auto_mode:
|
||||||
@@ -521,17 +451,15 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
status: true
|
status: true
|
||||||
goal_default:
|
goal_default:
|
||||||
status: true
|
status: null
|
||||||
handles: {}
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 启动或停止自动模式
|
description: 启动或停止自动模式
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: BoolSingleInput_Feedback
|
title: BoolSingleInput_Feedback
|
||||||
type: object
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -542,25 +470,16 @@ xrd_d7mate:
|
|||||||
title: BoolSingleInput_Goal
|
title: BoolSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: BoolSingleInput_Result
|
title: BoolSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: BoolSingleInput
|
title: start_auto_mode参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
|
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
|
||||||
status_types:
|
status_types:
|
||||||
current_acquire_data: dict
|
current_acquire_data: dict
|
||||||
sample_down: dict
|
|
||||||
sample_request: dict
|
sample_request: dict
|
||||||
sample_status: dict
|
sample_status: dict
|
||||||
type: python
|
type: python
|
||||||
@@ -586,16 +505,13 @@ xrd_d7mate:
|
|||||||
properties:
|
properties:
|
||||||
current_acquire_data:
|
current_acquire_data:
|
||||||
type: object
|
type: object
|
||||||
sample_down:
|
|
||||||
type: object
|
|
||||||
sample_request:
|
sample_request:
|
||||||
type: object
|
type: object
|
||||||
sample_status:
|
sample_status:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- sample_request
|
|
||||||
- current_acquire_data
|
- current_acquire_data
|
||||||
|
- sample_request
|
||||||
- sample_status
|
- sample_status
|
||||||
- sample_down
|
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,26 +8,25 @@ zhida_gcms:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -77,31 +76,6 @@ zhida_gcms:
|
|||||||
title: connect参数
|
title: connect参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
get_methods:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -112,21 +86,18 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -144,21 +115,18 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -176,21 +144,18 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -203,26 +168,25 @@ zhida_gcms:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -234,35 +198,35 @@ zhida_gcms:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: string
|
string: string
|
||||||
|
text: text
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -273,36 +237,36 @@ zhida_gcms:
|
|||||||
start_with_csv_file:
|
start_with_csv_file:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
|
csv_file_path: csv_file_path
|
||||||
string: string
|
string: string
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
placeholder_keys: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
properties: {}
|
additionalProperties: true
|
||||||
required: []
|
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- string
|
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -343,8 +307,8 @@ zhida_gcms:
|
|||||||
version:
|
version:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- status
|
|
||||||
- methods
|
- methods
|
||||||
|
- status
|
||||||
- version
|
- version
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
85
unilabos/registry/resources/bioyond/YB_bottle.yaml
Normal file
85
unilabos/registry/resources/bioyond/YB_bottle.yaml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
YB_20ml_fenyeping:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_20ml_fenyeping
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_5ml_fenyeping:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_5ml_fenyeping
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_jia_yang_tou_da:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_jia_yang_tou_da
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_pei_ye_da_Bottle:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_pei_ye_da_Bottle
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_pei_ye_xiao_Bottle:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_pei_ye_xiao_Bottle
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_qiang_tou:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_qiang_tou
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_ye_Bottle:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
- YB_bottle
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_ye_Bottle
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
168
unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml
Normal file
168
unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
YB_100ml_yeti:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_100ml_yeti
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_20ml_fenyepingban:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_20ml_fenyepingban
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_5ml_fenyepingban:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_5ml_fenyepingban
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_6StockCarrier:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_6StockCarrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_6VialCarrier:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_6VialCarrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_gao_nian_ye_Bottle:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_gao_nian_ye_Bottle
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_gaonianye:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_gaonianye
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_jia_yang_tou_da_Carrier:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_jia_yang_tou_da_Carrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_peiyepingdaban:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_peiyepingdaban
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_peiyepingxiaoban:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_peiyepingxiaoban
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_qiang_tou_he:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_qiang_tou_he
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_shi_pei_qi_kuai:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_shi_pei_qi_kuai
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_ye:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_ye_Bottle_Carrier
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
YB_ye_100ml_Bottle:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_ye_100ml_Bottle
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
@@ -8,7 +8,6 @@ BIOYOND_PolymerStation_1BottleCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
BIOYOND_PolymerStation_1FlaskCarrier:
|
BIOYOND_PolymerStation_1FlaskCarrier:
|
||||||
category:
|
category:
|
||||||
@@ -20,7 +19,6 @@ BIOYOND_PolymerStation_1FlaskCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
BIOYOND_PolymerStation_6StockCarrier:
|
BIOYOND_PolymerStation_6StockCarrier:
|
||||||
category:
|
category:
|
||||||
@@ -32,7 +30,6 @@ BIOYOND_PolymerStation_6StockCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
BIOYOND_PolymerStation_8StockCarrier:
|
BIOYOND_PolymerStation_8StockCarrier:
|
||||||
category:
|
category:
|
||||||
@@ -44,5 +41,4 @@ BIOYOND_PolymerStation_8StockCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ BIOYOND_PolymerPreparationStation_Deck:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: 配液站.webp
|
icon: 配液站.webp
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
BIOYOND_PolymerReactionStation_Deck:
|
BIOYOND_PolymerReactionStation_Deck:
|
||||||
category:
|
category:
|
||||||
@@ -20,17 +19,26 @@ BIOYOND_PolymerReactionStation_Deck:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: 反应站.webp
|
icon: 反应站.webp
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
YB_Deck11:
|
BIOYOND_YB_Deck:
|
||||||
category:
|
category:
|
||||||
- deck
|
- deck
|
||||||
class:
|
class:
|
||||||
module: unilabos.resources.bioyond.decks:YB_Deck
|
module: unilabos.resources.bioyond.decks:YB_Deck
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
description: BIOYOND PolymerReactionStation Deck
|
description: BIOYOND ElectrolyteFormulationStation Deck
|
||||||
handles: []
|
handles: []
|
||||||
icon: 配液站.webp
|
icon: 配液站.webp
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
version: 1.0.0
|
||||||
|
CoincellDeck:
|
||||||
|
category:
|
||||||
|
- deck
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck
|
||||||
|
type: pylabrobot
|
||||||
|
description: YIHUA CoinCellAssembly Deck
|
||||||
|
handles: []
|
||||||
|
icon: koudian.webp
|
||||||
|
init_param_schema: {}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -1,24 +1,3 @@
|
|||||||
disposal:
|
|
||||||
category:
|
|
||||||
- disposal
|
|
||||||
- waste
|
|
||||||
- resource_container
|
|
||||||
class:
|
|
||||||
module: unilabos.resources.disposal:Disposal
|
|
||||||
type: unilabos
|
|
||||||
description: 废料处理位置,用于处理实验废料
|
|
||||||
handles:
|
|
||||||
- data_key: disposal_access
|
|
||||||
data_source: handle
|
|
||||||
data_type: fluid
|
|
||||||
handler_key: access
|
|
||||||
io_type: target
|
|
||||||
label: access
|
|
||||||
side: NORTH
|
|
||||||
icon: ''
|
|
||||||
init_param_schema: {}
|
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
|
||||||
hplc_plate:
|
hplc_plate:
|
||||||
category:
|
category:
|
||||||
- resource_container
|
- resource_container
|
||||||
@@ -40,56 +19,6 @@ hplc_plate:
|
|||||||
- 3.1416
|
- 3.1416
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
|
||||||
type: resource
|
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
|
version: 1.0.0
|
||||||
plate_96:
|
plate_96:
|
||||||
category:
|
category:
|
||||||
@@ -112,7 +41,6 @@ plate_96:
|
|||||||
- 0
|
- 0
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
plate_96_high:
|
plate_96_high:
|
||||||
category:
|
category:
|
||||||
@@ -135,35 +63,6 @@ plate_96_high:
|
|||||||
- 1.5708
|
- 1.5708
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
|
||||||
type: resource
|
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
|
version: 1.0.0
|
||||||
tiprack_96_high:
|
tiprack_96_high:
|
||||||
category:
|
category:
|
||||||
@@ -195,7 +94,6 @@ tiprack_96_high:
|
|||||||
- 1.5708
|
- 1.5708
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
tiprack_box:
|
tiprack_box:
|
||||||
category:
|
category:
|
||||||
@@ -227,5 +125,4 @@ tiprack_box:
|
|||||||
- 0
|
- 0
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ bottle_container:
|
|||||||
- 0
|
- 0
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
tube_container:
|
tube_container:
|
||||||
category:
|
category:
|
||||||
@@ -62,5 +61,4 @@ tube_container:
|
|||||||
- 0
|
- 0
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -12,5 +12,4 @@ TransformXYZDeck:
|
|||||||
mesh: liquid_transform_xyz
|
mesh: liquid_transform_xyz
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
|
||||||
type: device
|
type: device
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ OTDeck:
|
|||||||
mesh: opentrons_liquid_handler
|
mesh: opentrons_liquid_handler
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
|
||||||
type: device
|
type: device
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
hplc_station:
|
hplc_station:
|
||||||
category:
|
category:
|
||||||
@@ -28,5 +27,4 @@ hplc_station:
|
|||||||
mesh: hplc_station
|
mesh: hplc_station
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
|
||||||
type: device
|
type: device
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
6884
unilabos/registry/resources/opentrons/lab.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,5 +8,4 @@ Opentrons_96_adapter_Vb:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ appliedbiosystemsmicroamp_384_wellplate_40ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
biorad_384_wellplate_50ul:
|
biorad_384_wellplate_50ul:
|
||||||
category:
|
category:
|
||||||
@@ -20,7 +19,6 @@ biorad_384_wellplate_50ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
biorad_96_wellplate_200ul_pcr:
|
biorad_96_wellplate_200ul_pcr:
|
||||||
category:
|
category:
|
||||||
@@ -32,7 +30,6 @@ biorad_96_wellplate_200ul_pcr:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_12_wellplate_6point9ml_flat:
|
corning_12_wellplate_6point9ml_flat:
|
||||||
category:
|
category:
|
||||||
@@ -44,7 +41,6 @@ corning_12_wellplate_6point9ml_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_24_wellplate_3point4ml_flat:
|
corning_24_wellplate_3point4ml_flat:
|
||||||
category:
|
category:
|
||||||
@@ -56,7 +52,6 @@ corning_24_wellplate_3point4ml_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_384_wellplate_112ul_flat:
|
corning_384_wellplate_112ul_flat:
|
||||||
category:
|
category:
|
||||||
@@ -68,7 +63,6 @@ corning_384_wellplate_112ul_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_48_wellplate_1point6ml_flat:
|
corning_48_wellplate_1point6ml_flat:
|
||||||
category:
|
category:
|
||||||
@@ -80,7 +74,6 @@ corning_48_wellplate_1point6ml_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_6_wellplate_16point8ml_flat:
|
corning_6_wellplate_16point8ml_flat:
|
||||||
category:
|
category:
|
||||||
@@ -92,7 +85,6 @@ corning_6_wellplate_16point8ml_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
corning_96_wellplate_360ul_flat:
|
corning_96_wellplate_360ul_flat:
|
||||||
category:
|
category:
|
||||||
@@ -104,7 +96,6 @@ corning_96_wellplate_360ul_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_96_wellplate_100ul_pcr_full_skirt:
|
nest_96_wellplate_100ul_pcr_full_skirt:
|
||||||
category:
|
category:
|
||||||
@@ -136,7 +127,6 @@ nest_96_wellplate_100ul_pcr_full_skirt:
|
|||||||
- 1.5708
|
- 1.5708
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_96_wellplate_200ul_flat:
|
nest_96_wellplate_200ul_flat:
|
||||||
category:
|
category:
|
||||||
@@ -148,7 +138,6 @@ nest_96_wellplate_200ul_flat:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_96_wellplate_2ml_deep:
|
nest_96_wellplate_2ml_deep:
|
||||||
category:
|
category:
|
||||||
@@ -171,7 +160,6 @@ nest_96_wellplate_2ml_deep:
|
|||||||
- 1.5708
|
- 1.5708
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
thermoscientificnunc_96_wellplate_1300ul:
|
thermoscientificnunc_96_wellplate_1300ul:
|
||||||
category:
|
category:
|
||||||
@@ -183,7 +171,6 @@ thermoscientificnunc_96_wellplate_1300ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
thermoscientificnunc_96_wellplate_2000ul:
|
thermoscientificnunc_96_wellplate_2000ul:
|
||||||
category:
|
category:
|
||||||
@@ -195,7 +182,6 @@ thermoscientificnunc_96_wellplate_2000ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
usascientific_96_wellplate_2point4ml_deep:
|
usascientific_96_wellplate_2point4ml_deep:
|
||||||
category:
|
category:
|
||||||
@@ -207,5 +193,4 @@ usascientific_96_wellplate_2point4ml_deep:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ agilent_1_reservoir_290ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
axygen_1_reservoir_90ml:
|
axygen_1_reservoir_90ml:
|
||||||
category:
|
category:
|
||||||
@@ -20,7 +19,6 @@ axygen_1_reservoir_90ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_12_reservoir_15ml:
|
nest_12_reservoir_15ml:
|
||||||
category:
|
category:
|
||||||
@@ -32,7 +30,6 @@ nest_12_reservoir_15ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_1_reservoir_195ml:
|
nest_1_reservoir_195ml:
|
||||||
category:
|
category:
|
||||||
@@ -44,7 +41,6 @@ nest_1_reservoir_195ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
nest_1_reservoir_290ml:
|
nest_1_reservoir_290ml:
|
||||||
category:
|
category:
|
||||||
@@ -56,7 +52,6 @@ nest_1_reservoir_290ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
usascientific_12_reservoir_22ml:
|
usascientific_12_reservoir_22ml:
|
||||||
category:
|
category:
|
||||||
@@ -68,5 +63,4 @@ usascientific_12_reservoir_22ml:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ eppendorf_96_tiprack_1000ul_eptips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
eppendorf_96_tiprack_10ul_eptips:
|
eppendorf_96_tiprack_10ul_eptips:
|
||||||
category:
|
category:
|
||||||
@@ -20,7 +19,6 @@ eppendorf_96_tiprack_10ul_eptips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
geb_96_tiprack_1000ul:
|
geb_96_tiprack_1000ul:
|
||||||
category:
|
category:
|
||||||
@@ -32,7 +30,6 @@ geb_96_tiprack_1000ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
geb_96_tiprack_10ul:
|
geb_96_tiprack_10ul:
|
||||||
category:
|
category:
|
||||||
@@ -44,7 +41,6 @@ geb_96_tiprack_10ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_filtertiprack_1000ul:
|
opentrons_96_filtertiprack_1000ul:
|
||||||
category:
|
category:
|
||||||
@@ -75,7 +71,6 @@ opentrons_96_filtertiprack_1000ul:
|
|||||||
- 1.5708
|
- 1.5708
|
||||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||||
type: resource
|
type: resource
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_filtertiprack_10ul:
|
opentrons_96_filtertiprack_10ul:
|
||||||
category:
|
category:
|
||||||
@@ -87,7 +82,6 @@ opentrons_96_filtertiprack_10ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_filtertiprack_200ul:
|
opentrons_96_filtertiprack_200ul:
|
||||||
category:
|
category:
|
||||||
@@ -99,7 +93,6 @@ opentrons_96_filtertiprack_200ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_filtertiprack_20ul:
|
opentrons_96_filtertiprack_20ul:
|
||||||
category:
|
category:
|
||||||
@@ -111,7 +104,6 @@ opentrons_96_filtertiprack_20ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_tiprack_1000ul:
|
opentrons_96_tiprack_1000ul:
|
||||||
category:
|
category:
|
||||||
@@ -123,7 +115,6 @@ opentrons_96_tiprack_1000ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_tiprack_10ul:
|
opentrons_96_tiprack_10ul:
|
||||||
category:
|
category:
|
||||||
@@ -135,7 +126,6 @@ opentrons_96_tiprack_10ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_tiprack_20ul:
|
opentrons_96_tiprack_20ul:
|
||||||
category:
|
category:
|
||||||
@@ -147,7 +137,6 @@ opentrons_96_tiprack_20ul:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_tiprack_300ul:
|
opentrons_96_tiprack_300ul:
|
||||||
category:
|
category:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
|
opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
|
||||||
category:
|
category:
|
||||||
@@ -20,7 +19,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
|
opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
|
||||||
category:
|
category:
|
||||||
@@ -32,7 +30,6 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_15_tuberack_falcon_15ml_conical:
|
opentrons_15_tuberack_falcon_15ml_conical:
|
||||||
category:
|
category:
|
||||||
@@ -44,7 +41,6 @@ opentrons_15_tuberack_falcon_15ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_15_tuberack_nest_15ml_conical:
|
opentrons_15_tuberack_nest_15ml_conical:
|
||||||
category:
|
category:
|
||||||
@@ -56,7 +52,6 @@ opentrons_15_tuberack_nest_15ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_aluminumblock_generic_2ml_screwcap:
|
opentrons_24_aluminumblock_generic_2ml_screwcap:
|
||||||
category:
|
category:
|
||||||
@@ -68,7 +63,6 @@ opentrons_24_aluminumblock_generic_2ml_screwcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_aluminumblock_nest_1point5ml_snapcap:
|
opentrons_24_aluminumblock_nest_1point5ml_snapcap:
|
||||||
category:
|
category:
|
||||||
@@ -80,7 +74,6 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
|
opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
|
||||||
category:
|
category:
|
||||||
@@ -92,7 +85,6 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
|
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
|
||||||
category:
|
category:
|
||||||
@@ -104,7 +96,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
|
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
|
||||||
category:
|
category:
|
||||||
@@ -116,7 +107,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
|
opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
|
||||||
category:
|
category:
|
||||||
@@ -128,7 +118,6 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_generic_2ml_screwcap:
|
opentrons_24_tuberack_generic_2ml_screwcap:
|
||||||
category:
|
category:
|
||||||
@@ -140,7 +129,6 @@ opentrons_24_tuberack_generic_2ml_screwcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_nest_0point5ml_screwcap:
|
opentrons_24_tuberack_nest_0point5ml_screwcap:
|
||||||
category:
|
category:
|
||||||
@@ -152,7 +140,6 @@ opentrons_24_tuberack_nest_0point5ml_screwcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_nest_1point5ml_screwcap:
|
opentrons_24_tuberack_nest_1point5ml_screwcap:
|
||||||
category:
|
category:
|
||||||
@@ -164,7 +151,6 @@ opentrons_24_tuberack_nest_1point5ml_screwcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_nest_1point5ml_snapcap:
|
opentrons_24_tuberack_nest_1point5ml_snapcap:
|
||||||
category:
|
category:
|
||||||
@@ -176,7 +162,6 @@ opentrons_24_tuberack_nest_1point5ml_snapcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_nest_2ml_screwcap:
|
opentrons_24_tuberack_nest_2ml_screwcap:
|
||||||
category:
|
category:
|
||||||
@@ -188,7 +173,6 @@ opentrons_24_tuberack_nest_2ml_screwcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_24_tuberack_nest_2ml_snapcap:
|
opentrons_24_tuberack_nest_2ml_snapcap:
|
||||||
category:
|
category:
|
||||||
@@ -200,7 +184,6 @@ opentrons_24_tuberack_nest_2ml_snapcap:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_6_tuberack_falcon_50ml_conical:
|
opentrons_6_tuberack_falcon_50ml_conical:
|
||||||
category:
|
category:
|
||||||
@@ -212,7 +195,6 @@ opentrons_6_tuberack_falcon_50ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_6_tuberack_nest_50ml_conical:
|
opentrons_6_tuberack_nest_50ml_conical:
|
||||||
category:
|
category:
|
||||||
@@ -224,7 +206,6 @@ opentrons_6_tuberack_nest_50ml_conical:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
opentrons_96_well_aluminum_block:
|
opentrons_96_well_aluminum_block:
|
||||||
category:
|
category:
|
||||||
@@ -236,5 +217,4 @@ opentrons_96_well_aluminum_block:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -29,5 +29,4 @@ container:
|
|||||||
side: WEST
|
side: WEST
|
||||||
icon: Flask.webp
|
icon: Flask.webp
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ POST_PROCESS_Raw_1BottleCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
POST_PROCESS_Reaction_1BottleCarrier:
|
POST_PROCESS_Reaction_1BottleCarrier:
|
||||||
category:
|
category:
|
||||||
@@ -20,5 +19,4 @@ POST_PROCESS_Reaction_1BottleCarrier:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ post_process_deck:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ PRCXI_30mm_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Adapter:
|
PRCXI_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -22,7 +21,6 @@ PRCXI_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Deep10_Adapter:
|
PRCXI_Deep10_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -35,7 +33,6 @@ PRCXI_Deep10_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Deep300_Adapter:
|
PRCXI_Deep300_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -48,7 +45,6 @@ PRCXI_Deep300_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_PCR_Adapter:
|
PRCXI_PCR_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -61,7 +57,6 @@ PRCXI_PCR_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Reservoir_Adapter:
|
PRCXI_Reservoir_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -74,7 +69,6 @@ PRCXI_Reservoir_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Tip10_Adapter:
|
PRCXI_Tip10_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -87,7 +81,6 @@ PRCXI_Tip10_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Tip1250_Adapter:
|
PRCXI_Tip1250_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -100,7 +93,6 @@ PRCXI_Tip1250_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_Tip300_Adapter:
|
PRCXI_Tip300_Adapter:
|
||||||
category:
|
category:
|
||||||
@@ -113,5 +105,4 @@ PRCXI_Tip300_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ PRCXI_48_DeepWell:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_96_DeepWell:
|
PRCXI_96_DeepWell:
|
||||||
category:
|
category:
|
||||||
@@ -22,7 +21,6 @@ PRCXI_96_DeepWell:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_AGenBio_4_troughplate:
|
PRCXI_AGenBio_4_troughplate:
|
||||||
category:
|
category:
|
||||||
@@ -35,7 +33,6 @@ PRCXI_AGenBio_4_troughplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_BioER_96_wellplate:
|
PRCXI_BioER_96_wellplate:
|
||||||
category:
|
category:
|
||||||
@@ -48,7 +45,6 @@ PRCXI_BioER_96_wellplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_BioRad_384_wellplate:
|
PRCXI_BioRad_384_wellplate:
|
||||||
category:
|
category:
|
||||||
@@ -61,7 +57,6 @@ PRCXI_BioRad_384_wellplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_CellTreat_96_wellplate:
|
PRCXI_CellTreat_96_wellplate:
|
||||||
category:
|
category:
|
||||||
@@ -74,7 +69,6 @@ PRCXI_CellTreat_96_wellplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_PCR_Plate_200uL_nonskirted:
|
PRCXI_PCR_Plate_200uL_nonskirted:
|
||||||
category:
|
category:
|
||||||
@@ -87,7 +81,6 @@ PRCXI_PCR_Plate_200uL_nonskirted:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_PCR_Plate_200uL_semiskirted:
|
PRCXI_PCR_Plate_200uL_semiskirted:
|
||||||
category:
|
category:
|
||||||
@@ -100,7 +93,6 @@ PRCXI_PCR_Plate_200uL_semiskirted:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_PCR_Plate_200uL_skirted:
|
PRCXI_PCR_Plate_200uL_skirted:
|
||||||
category:
|
category:
|
||||||
@@ -113,7 +105,6 @@ PRCXI_PCR_Plate_200uL_skirted:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_nest_12_troughplate:
|
PRCXI_nest_12_troughplate:
|
||||||
category:
|
category:
|
||||||
@@ -126,7 +117,6 @@ PRCXI_nest_12_troughplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_nest_1_troughplate:
|
PRCXI_nest_1_troughplate:
|
||||||
category:
|
category:
|
||||||
@@ -139,5 +129,4 @@ PRCXI_nest_1_troughplate:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ PRCXI_1000uL_Tips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_10uL_Tips:
|
PRCXI_10uL_Tips:
|
||||||
category:
|
category:
|
||||||
@@ -22,7 +21,6 @@ PRCXI_10uL_Tips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_10ul_eTips:
|
PRCXI_10ul_eTips:
|
||||||
category:
|
category:
|
||||||
@@ -35,7 +33,6 @@ PRCXI_10ul_eTips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_1250uL_Tips:
|
PRCXI_1250uL_Tips:
|
||||||
category:
|
category:
|
||||||
@@ -48,7 +45,6 @@ PRCXI_1250uL_Tips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_200uL_Tips:
|
PRCXI_200uL_Tips:
|
||||||
category:
|
category:
|
||||||
@@ -61,7 +57,6 @@ PRCXI_200uL_Tips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
PRCXI_300ul_Tips:
|
PRCXI_300ul_Tips:
|
||||||
category:
|
category:
|
||||||
@@ -74,5 +69,4 @@ PRCXI_300ul_Tips:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ PRCXI_trash:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ PRCXI_EP_Adapter:
|
|||||||
handles: []
|
handles: []
|
||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
714
unilabos/registry/utils.py
Normal file
714
unilabos/registry/utils.py
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
"""
|
||||||
|
注册表工具函数
|
||||||
|
|
||||||
|
从 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
|
||||||
|
from unilabos.registry.decorators import Side, DataSource, normalize_enum_value
|
||||||
|
|
||||||
|
_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:
|
||||||
|
entry["side"] = normalize_enum_value(side, Side) or 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:
|
||||||
|
entry["data_source"] = normalize_enum_value(data_source, DataSource) or 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", ""),
|
||||||
|
}
|
||||||
|
_FIELD_ENUM_MAP = {"side": Side, "data_source": DataSource}
|
||||||
|
for opt_key in ("side", "data_key", "data_source", "description", "io_type"):
|
||||||
|
val = h.get(opt_key)
|
||||||
|
if val is not None:
|
||||||
|
if opt_key in _FIELD_ENUM_MAP:
|
||||||
|
val = normalize_enum_value(val, _FIELD_ENUM_MAP[opt_key]) or val
|
||||||
|
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 {}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import json
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from pylabrobot.resources import Container
|
from pylabrobot.resources import Container
|
||||||
from unilabos_msgs.msg import Resource
|
|
||||||
|
|
||||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
|
||||||
|
|
||||||
|
|
||||||
class RegularContainer(Container):
|
class RegularContainer(Container):
|
||||||
@@ -16,12 +12,14 @@ class RegularContainer(Container):
|
|||||||
kwargs["size_y"] = 0
|
kwargs["size_y"] = 0
|
||||||
if "size_z" not in kwargs:
|
if "size_z" not in kwargs:
|
||||||
kwargs["size_z"] = 0
|
kwargs["size_z"] = 0
|
||||||
|
if "category" not in kwargs:
|
||||||
|
kwargs["category"] = "container"
|
||||||
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.state = {}
|
super().__init__(*args, **kwargs)
|
||||||
super().__init__(*args, category="container", **kwargs)
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]):
|
def load_state(self, state: Dict[str, Any]):
|
||||||
self.state = state
|
super().load_state(state)
|
||||||
|
|
||||||
|
|
||||||
def get_regular_container(name="container"):
|
def get_regular_container(name="container"):
|
||||||
@@ -29,7 +27,6 @@ def get_regular_container(name="container"):
|
|||||||
r.category = "container"
|
r.category = "container"
|
||||||
return r
|
return r
|
||||||
|
|
||||||
#
|
|
||||||
# class RegularContainer(object):
|
# class RegularContainer(object):
|
||||||
# # 第一个参数必须是id传入
|
# # 第一个参数必须是id传入
|
||||||
# # noinspection PyShadowingBuiltins
|
# # noinspection PyShadowingBuiltins
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ def canonicalize_nodes_data(
|
|||||||
if sample_id:
|
if sample_id:
|
||||||
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
||||||
for k in list(node.keys()):
|
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"]:
|
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra", "machine_name"]:
|
||||||
v = node.pop(k)
|
v = node.pop(k)
|
||||||
node["config"][k] = v
|
node["config"][k] = v
|
||||||
if outer_host_node_id is not None:
|
if outer_host_node_id is not None:
|
||||||
@@ -288,6 +288,15 @@ def read_node_link_json(
|
|||||||
physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False)
|
physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False)
|
||||||
handle_communications(physical_setup_graph)
|
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
|
return physical_setup_graph, resource_tree_set, standardized_links
|
||||||
|
|
||||||
|
|
||||||
@@ -372,6 +381,15 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic
|
|||||||
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
|
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
|
||||||
handle_communications(physical_setup_graph)
|
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
|
return physical_setup_graph, resource_tree_set, standardized_links
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3681
unilabos/resources/lab_resources.py
Normal file
3681
unilabos/resources/lab_resources.py
Normal file
File diff suppressed because it is too large
Load Diff
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
1
unilabos/resources/opentrons_custom_labware_defs.json
Normal file
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
EXTRA_CLASS = "unilabos_resource_class"
|
EXTRA_CLASS = "unilabos_resource_class"
|
||||||
|
FRONTEND_POSE_EXTRA = "unilabos_frontend_pose_extra"
|
||||||
EXTRA_SAMPLE_UUID = "sample_uuid"
|
EXTRA_SAMPLE_UUID = "sample_uuid"
|
||||||
EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid"
|
EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid"
|
||||||
|
|
||||||
@@ -38,24 +39,52 @@ class LabSample(TypedDict):
|
|||||||
extra: Dict[str, Any]
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionSizeType(TypedDict):
|
||||||
|
depth: float
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionSize(BaseModel):
|
class ResourceDictPositionSize(BaseModel):
|
||||||
depth: float = Field(description="Depth", default=0.0) # z
|
depth: float = Field(description="Depth", default=0.0) # z
|
||||||
width: float = Field(description="Width", default=0.0) # x
|
width: float = Field(description="Width", default=0.0) # x
|
||||||
height: float = Field(description="Height", default=0.0) # y
|
height: float = Field(description="Height", default=0.0) # y
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionScaleType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionScale(BaseModel):
|
class ResourceDictPositionScale(BaseModel):
|
||||||
x: float = Field(description="x scale", default=0.0)
|
x: float = Field(description="x scale", default=0.0)
|
||||||
y: float = Field(description="y scale", default=0.0)
|
y: float = Field(description="y scale", default=0.0)
|
||||||
z: float = Field(description="z scale", default=0.0)
|
z: float = Field(description="z scale", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionObjectType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionObject(BaseModel):
|
class ResourceDictPositionObject(BaseModel):
|
||||||
x: float = Field(description="X coordinate", default=0.0)
|
x: float = Field(description="X coordinate", default=0.0)
|
||||||
y: float = Field(description="Y coordinate", default=0.0)
|
y: float = Field(description="Y coordinate", default=0.0)
|
||||||
z: float = Field(description="Z coordinate", default=0.0)
|
z: float = Field(description="Z coordinate", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionType(TypedDict):
|
||||||
|
size: ResourceDictPositionSizeType
|
||||||
|
scale: ResourceDictPositionScaleType
|
||||||
|
layout: Literal["2d", "x-y", "z-y", "x-z"]
|
||||||
|
position: ResourceDictPositionObjectType
|
||||||
|
position3d: ResourceDictPositionObjectType
|
||||||
|
rotation: ResourceDictPositionObjectType
|
||||||
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"]
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPosition(BaseModel):
|
class ResourceDictPosition(BaseModel):
|
||||||
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
||||||
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
||||||
@@ -72,6 +101,44 @@ class ResourceDictPosition(BaseModel):
|
|||||||
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
|
||||||
description="Cross section type", default="rectangle"
|
description="Cross section type", default="rectangle"
|
||||||
)
|
)
|
||||||
|
extra: Optional[Dict[str, Any]] = Field(description="Extra data", default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictType(TypedDict):
|
||||||
|
id: str
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
resource_schema: Dict[str, Any]
|
||||||
|
model: Dict[str, Any]
|
||||||
|
icon: str
|
||||||
|
parent_uuid: Optional[str]
|
||||||
|
parent: Optional["ResourceDictType"]
|
||||||
|
type: Union[Literal["device"], str]
|
||||||
|
klass: str
|
||||||
|
pose: ResourceDictPositionType
|
||||||
|
config: Dict[str, Any]
|
||||||
|
data: Dict[str, Any]
|
||||||
|
extra: Dict[str, Any]
|
||||||
|
machine_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictType(TypedDict):
|
||||||
|
id: str
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
resource_schema: Dict[str, Any]
|
||||||
|
model: Dict[str, Any]
|
||||||
|
icon: str
|
||||||
|
parent_uuid: Optional[str]
|
||||||
|
parent: Optional["ResourceDictType"]
|
||||||
|
type: Union[Literal["device"], str]
|
||||||
|
klass: str
|
||||||
|
pose: ResourceDictPositionType
|
||||||
|
config: Dict[str, Any]
|
||||||
|
data: Dict[str, Any]
|
||||||
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
@@ -93,6 +160,7 @@ class ResourceDict(BaseModel):
|
|||||||
config: Dict[str, Any] = Field(description="Resource configuration")
|
config: Dict[str, Any] = Field(description="Resource configuration")
|
||||||
data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data")
|
data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data")
|
||||||
extra: Dict[str, Any] = Field(description="Extra data, eg: slot index")
|
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")
|
@field_serializer("parent_uuid")
|
||||||
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
|
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
|
||||||
@@ -148,22 +216,30 @@ class ResourceDictInstance(object):
|
|||||||
self.typ = "dict"
|
self.typ = "dict"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_resource_instance_from_dict(cls, content: Dict[str, Any]) -> "ResourceDictInstance":
|
def get_resource_instance_from_dict(cls, content: ResourceDictType) -> "ResourceDictInstance":
|
||||||
"""从字典创建资源实例"""
|
"""从字典创建资源实例"""
|
||||||
if "id" not in content:
|
if "id" not in content:
|
||||||
content["id"] = content["name"]
|
content["id"] = content["name"]
|
||||||
if "uuid" not in content:
|
if "uuid" not in content:
|
||||||
content["uuid"] = str(uuid.uuid4())
|
content["uuid"] = str(uuid.uuid4())
|
||||||
if "description" in content and content["description"] is None:
|
if "description" in content and content["description"] is None:
|
||||||
|
# noinspection PyTypedDict
|
||||||
del content["description"]
|
del content["description"]
|
||||||
if "model" in content and content["model"] is None:
|
if "model" in content and content["model"] is None:
|
||||||
|
# noinspection PyTypedDict
|
||||||
del content["model"]
|
del content["model"]
|
||||||
|
# noinspection PyTypedDict
|
||||||
if "schema" in content and content["schema"] is None:
|
if "schema" in content and content["schema"] is None:
|
||||||
|
# noinspection PyTypedDict
|
||||||
del content["schema"]
|
del content["schema"]
|
||||||
|
# noinspection PyTypedDict
|
||||||
if "x" in content.get("position", {}):
|
if "x" in content.get("position", {}):
|
||||||
# 说明是老版本的position格式,转换成新的
|
# 说明是老版本的position格式,转换成新的
|
||||||
|
# noinspection PyTypedDict
|
||||||
content["position"] = {"position": content["position"]}
|
content["position"] = {"position": content["position"]}
|
||||||
|
# noinspection PyTypedDict
|
||||||
if not content.get("class"):
|
if not content.get("class"):
|
||||||
|
# noinspection PyTypedDict
|
||||||
content["class"] = ""
|
content["class"] = ""
|
||||||
if not content.get("config"): # todo: 后续从后端保证字段非空
|
if not content.get("config"): # todo: 后续从后端保证字段非空
|
||||||
content["config"] = {}
|
content["config"] = {}
|
||||||
@@ -174,16 +250,18 @@ class ResourceDictInstance(object):
|
|||||||
if "position" in content:
|
if "position" in content:
|
||||||
pose = content.get("pose", {})
|
pose = content.get("pose", {})
|
||||||
if "position" not in pose:
|
if "position" not in pose:
|
||||||
|
# noinspection PyTypedDict
|
||||||
if "position" in content["position"]:
|
if "position" in content["position"]:
|
||||||
|
# noinspection PyTypedDict
|
||||||
pose["position"] = content["position"]["position"]
|
pose["position"] = content["position"]["position"]
|
||||||
else:
|
else:
|
||||||
pose["position"] = {"x": 0, "y": 0, "z": 0}
|
pose["position"] = ResourceDictPositionObjectType(x=0, y=0, z=0)
|
||||||
if "size" not in pose:
|
if "size" not in pose:
|
||||||
pose["size"] = {
|
pose["size"] = ResourceDictPositionSizeType(
|
||||||
"width": content["config"].get("size_x", 0),
|
width= content["config"].get("size_x", 0),
|
||||||
"height": content["config"].get("size_y", 0),
|
height= content["config"].get("size_y", 0),
|
||||||
"depth": content["config"].get("size_z", 0),
|
depth= content["config"].get("size_z", 0),
|
||||||
}
|
)
|
||||||
content["pose"] = pose
|
content["pose"] = pose
|
||||||
try:
|
try:
|
||||||
res_dict = ResourceDict.model_validate(content)
|
res_dict = ResourceDict.model_validate(content)
|
||||||
@@ -351,7 +429,7 @@ class ResourceTreeSet(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False) -> "ResourceTreeSet":
|
def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False, old_size=False) -> "ResourceTreeSet":
|
||||||
"""
|
"""
|
||||||
从plr资源创建ResourceTreeSet
|
从plr资源创建ResourceTreeSet
|
||||||
"""
|
"""
|
||||||
@@ -365,13 +443,29 @@ class ResourceTreeSet(object):
|
|||||||
"tip_spot": "tip_spot",
|
"tip_spot": "tip_spot",
|
||||||
"tube": "tube",
|
"tube": "tube",
|
||||||
"bottle_carrier": "bottle_carrier",
|
"bottle_carrier": "bottle_carrier",
|
||||||
|
"material_hole": "material_hole",
|
||||||
|
"container": "container",
|
||||||
|
"material_plate": "material_plate",
|
||||||
|
"electrode_sheet": "electrode_sheet",
|
||||||
|
"warehouse": "warehouse",
|
||||||
|
"magazine_holder": "magazine_holder",
|
||||||
|
"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:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
elif source is None:
|
elif source is None:
|
||||||
return ""
|
return ""
|
||||||
else:
|
else:
|
||||||
print("转换pylabrobot的时候,出现未知类型", source)
|
logger.trace(f"转换pylabrobot的时候,出现未知类型 {source}")
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None):
|
def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None):
|
||||||
@@ -408,6 +502,7 @@ class ResourceTreeSet(object):
|
|||||||
"position3d": raw_pos,
|
"position3d": raw_pos,
|
||||||
"rotation": d["rotation"],
|
"rotation": d["rotation"],
|
||||||
"cross_section_type": d.get("cross_section_type", "rectangle"),
|
"cross_section_type": d.get("cross_section_type", "rectangle"),
|
||||||
|
"extra": extra.get(FRONTEND_POSE_EXTRA)
|
||||||
}
|
}
|
||||||
|
|
||||||
# 先构建当前节点的字典(不包含children)
|
# 先构建当前节点的字典(不包含children)
|
||||||
@@ -425,7 +520,7 @@ class ResourceTreeSet(object):
|
|||||||
k: v
|
k: v
|
||||||
for k, v in d.items()
|
for k, v in d.items()
|
||||||
if k
|
if k
|
||||||
not in [
|
not in ([
|
||||||
"name",
|
"name",
|
||||||
"children",
|
"children",
|
||||||
"parent_name",
|
"parent_name",
|
||||||
@@ -436,7 +531,15 @@ class ResourceTreeSet(object):
|
|||||||
"size_z",
|
"size_z",
|
||||||
"cross_section_type",
|
"cross_section_type",
|
||||||
"bottom_type",
|
"bottom_type",
|
||||||
]
|
] if not old_size else [
|
||||||
|
"name",
|
||||||
|
"children",
|
||||||
|
"parent_name",
|
||||||
|
"location",
|
||||||
|
"rotation",
|
||||||
|
"cross_section_type",
|
||||||
|
"bottom_type",
|
||||||
|
])
|
||||||
},
|
},
|
||||||
"data": states[d["name"]],
|
"data": states[d["name"]],
|
||||||
"extra": extra,
|
"extra": extra,
|
||||||
@@ -468,10 +571,17 @@ class ResourceTreeSet(object):
|
|||||||
trees.append(tree_instance)
|
trees.append(tree_instance)
|
||||||
return cls(trees)
|
return cls(trees)
|
||||||
|
|
||||||
def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]:
|
def to_plr_resources(
|
||||||
|
self, skip_devices: bool = True, requested_uuids: Optional[List[str]] = None
|
||||||
|
) -> List["PLRResource"]:
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 转换为 PLR 资源列表
|
将 ResourceTreeSet 转换为 PLR 资源列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skip_devices: 是否跳过 device 类型节点
|
||||||
|
requested_uuids: 若指定,则按此 UUID 顺序返回对应资源(用于批量查询时一一对应),
|
||||||
|
否则返回各树的根节点列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PLRResource]: PLR 资源实例列表
|
List[PLRResource]: PLR 资源实例列表
|
||||||
"""
|
"""
|
||||||
@@ -493,6 +603,7 @@ class ResourceTreeSet(object):
|
|||||||
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
||||||
all_states[node.res_content.name] = node.res_content.data
|
all_states[node.res_content.name] = node.res_content.data
|
||||||
name_to_extra[node.res_content.name] = node.res_content.extra
|
name_to_extra[node.res_content.name] = node.res_content.extra
|
||||||
|
name_to_extra[node.res_content.name][FRONTEND_POSE_EXTRA] = node.res_content.pose.extra
|
||||||
name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass
|
name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
||||||
@@ -526,6 +637,71 @@ class ResourceTreeSet(object):
|
|||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
# deserialize 会单独处理的元数据 key,不传给构造函数
|
||||||
|
_META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"}
|
||||||
|
# deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留
|
||||||
|
_DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"}
|
||||||
|
|
||||||
|
def remove_incompatible_params(plr_d: dict) -> None:
|
||||||
|
"""递归移除 PLR 类不接受的参数,避免 deserialize 报错。
|
||||||
|
- 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category)
|
||||||
|
- 对 TubeRack:将 ordering 转为 ordered_items
|
||||||
|
- 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip)
|
||||||
|
"""
|
||||||
|
if "type" in plr_d:
|
||||||
|
sub_cls = find_subclass(plr_d["type"], PLRResource)
|
||||||
|
if sub_cls is not None:
|
||||||
|
spec = inspect.signature(sub_cls)
|
||||||
|
valid_params = set(spec.parameters.keys())
|
||||||
|
# TubeRack 特殊处理:先转换 ordering,再参与后续过滤
|
||||||
|
if "ordering" not in valid_params and "ordering" in plr_d:
|
||||||
|
ordering = plr_d.pop("ordering", None)
|
||||||
|
if sub_cls.__name__ == "TubeRack":
|
||||||
|
plr_d["ordered_items"] = (
|
||||||
|
_ordering_to_ordered_items(plr_d, ordering)
|
||||||
|
if ordering
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
# 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key)
|
||||||
|
for key in list(plr_d.keys()):
|
||||||
|
if (
|
||||||
|
key not in _META_KEYS
|
||||||
|
and key not in _DESERIALIZE_PRESERVED_KEYS
|
||||||
|
and key not in valid_params
|
||||||
|
):
|
||||||
|
plr_d.pop(key, None)
|
||||||
|
for child in plr_d.get("children", []):
|
||||||
|
remove_incompatible_params(child)
|
||||||
|
|
||||||
|
def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict:
|
||||||
|
"""将 ordering 转为 ordered_items,从 children 构建 Tube 对象"""
|
||||||
|
from pylabrobot.resources import Tube, Coordinate
|
||||||
|
from pylabrobot.serializer import deserialize as plr_deserialize
|
||||||
|
|
||||||
|
children = plr_d.get("children", [])
|
||||||
|
ordered_items = {}
|
||||||
|
for idx, (ident, child_name) in enumerate(ordering.items()):
|
||||||
|
child_data = children[idx] if idx < len(children) else None
|
||||||
|
if child_data is None:
|
||||||
|
continue
|
||||||
|
loc_data = child_data.get("location")
|
||||||
|
loc = (
|
||||||
|
plr_deserialize(loc_data)
|
||||||
|
if loc_data
|
||||||
|
else Coordinate(0, 0, 0)
|
||||||
|
)
|
||||||
|
tube = Tube(
|
||||||
|
name=child_data.get("name", child_name or ident),
|
||||||
|
size_x=child_data.get("size_x", 10),
|
||||||
|
size_y=child_data.get("size_y", 10),
|
||||||
|
size_z=child_data.get("size_z", 50),
|
||||||
|
max_volume=child_data.get("max_volume", 1000),
|
||||||
|
)
|
||||||
|
tube.location = loc
|
||||||
|
ordered_items[ident] = tube
|
||||||
|
plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化
|
||||||
|
return ordered_items
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
@@ -545,9 +721,7 @@ class ResourceTreeSet(object):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
)
|
)
|
||||||
spec = inspect.signature(sub_cls)
|
remove_incompatible_params(plr_dict)
|
||||||
if "category" not in spec.parameters:
|
|
||||||
plr_dict.pop("category", None)
|
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.serializer import deserialize
|
from pylabrobot.serializer import deserialize
|
||||||
@@ -561,12 +735,24 @@ class ResourceTreeSet(object):
|
|||||||
plr_resources.append(plr_resource)
|
plr_resources.append(plr_resource)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换 PLR 资源失败: {e}")
|
logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
if requested_uuids:
|
||||||
|
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
||||||
|
result = []
|
||||||
|
for uid in requested_uuids:
|
||||||
|
if uid in tracker.uuid_to_resources:
|
||||||
|
result.append(tracker.uuid_to_resources[uid])
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"请求的 UUID {uid} 在资源树中未找到。"
|
||||||
|
f"可用 UUID 数量: {len(tracker.uuid_to_resources)}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -724,7 +910,8 @@ class ResourceTreeSet(object):
|
|||||||
if remote_root_type == "device":
|
if remote_root_type == "device":
|
||||||
# 情况1: 一级是 device
|
# 情况1: 一级是 device
|
||||||
if remote_root_id not in local_device_map:
|
if remote_root_id not in local_device_map:
|
||||||
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步")
|
if remote_root_id != "host_node":
|
||||||
|
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
local_device = local_device_map[remote_root_id]
|
local_device = local_device_map[remote_root_id]
|
||||||
@@ -771,14 +958,27 @@ class ResourceTreeSet(object):
|
|||||||
f"从远端同步了 {added_count} 个物料子树"
|
f"从远端同步了 {added_count} 个物料子树"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 情况2: 二级是物料(不是 device)
|
# 二级物料已存在,比较三级子节点是否缺失
|
||||||
if remote_child_name not in local_children_map:
|
local_material = local_children_map[remote_child_name]
|
||||||
# 引入整个子树
|
local_material_children_map = {child.res_content.name: child for child in
|
||||||
remote_child.res_content.parent = local_device.res_content
|
local_material.children}
|
||||||
local_device.children.append(remote_child)
|
added_count = 0
|
||||||
logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'")
|
for remote_sub in remote_child.children:
|
||||||
else:
|
remote_sub_name = remote_sub.res_content.name
|
||||||
logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过")
|
if remote_sub_name not in local_material_children_map:
|
||||||
|
remote_sub.res_content.parent = local_material.res_content
|
||||||
|
local_material.children.append(remote_sub)
|
||||||
|
added_count += 1
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"物料 '{remote_root_id}/{remote_child_name}/{remote_sub_name}' "
|
||||||
|
f"已存在,跳过"
|
||||||
|
)
|
||||||
|
if added_count > 0:
|
||||||
|
logger.info(
|
||||||
|
f"物料 '{remote_root_id}/{remote_child_name}': "
|
||||||
|
f"从远端同步了 {added_count} 个子物料"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 情况1: 一级节点是物料(不是 device)
|
# 情况1: 一级节点是物料(不是 device)
|
||||||
# 检查是否已存在
|
# 检查是否已存在
|
||||||
@@ -801,7 +1001,7 @@ class ResourceTreeSet(object):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def dump(self) -> List[List[Dict[str, Any]]]:
|
def dump(self, old_position=False) -> List[List[Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 序列化为嵌套列表格式
|
将 ResourceTreeSet 序列化为嵌套列表格式
|
||||||
|
|
||||||
@@ -817,6 +1017,10 @@ class ResourceTreeSet(object):
|
|||||||
# 获取树的所有节点并序列化
|
# 获取树的所有节点并序列化
|
||||||
tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()]
|
tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()]
|
||||||
result.append(tree_nodes)
|
result.append(tree_nodes)
|
||||||
|
if old_position:
|
||||||
|
for r in result:
|
||||||
|
for rr in r:
|
||||||
|
rr["position"] = rr["pose"]["position"]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ def ros2_device_node(
|
|||||||
# 从属性中自动发现可发布状态
|
# 从属性中自动发现可发布状态
|
||||||
if status_types is None:
|
if status_types is None:
|
||||||
status_types = {}
|
status_types = {}
|
||||||
if device_config is None:
|
assert device_config is not None, "device_config cannot be None"
|
||||||
raise ValueError("device_config cannot be None")
|
|
||||||
if action_value_mappings is None:
|
if action_value_mappings is None:
|
||||||
action_value_mappings = {}
|
action_value_mappings = {}
|
||||||
if hardware_interface is None:
|
if hardware_interface is None:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from io import StringIO
|
|||||||
from typing import Iterable, Any, Dict, Type, TypeVar, Union
|
from typing import Iterable, Any, Dict, Type, TypeVar, Union
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from dataclasses import asdict, is_dataclass
|
from dataclasses import asdict, is_dataclass
|
||||||
|
|
||||||
@@ -361,14 +362,7 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
|
|||||||
if hasattr(ros_msg, key):
|
if hasattr(ros_msg, key):
|
||||||
attr = getattr(ros_msg, key)
|
attr = getattr(ros_msg, key)
|
||||||
if isinstance(attr, (float, int, str, bool)):
|
if isinstance(attr, (float, int, str, bool)):
|
||||||
# 处理list类型的值,取第一个元素或抛出错误
|
setattr(ros_msg, key, type(attr)(value))
|
||||||
if isinstance(value, list):
|
|
||||||
if len(value) > 0:
|
|
||||||
setattr(ros_msg, key, type(attr)(value[0]))
|
|
||||||
else:
|
|
||||||
setattr(ros_msg, key, type(attr)()) # 使用默认值
|
|
||||||
else:
|
|
||||||
setattr(ros_msg, key, type(attr)(value))
|
|
||||||
elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable):
|
elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable):
|
||||||
td = ros_msg.SLOT_TYPES[ind].value_type
|
td = ros_msg.SLOT_TYPES[ind].value_type
|
||||||
if isinstance(td, NamespacedType):
|
if isinstance(td, NamespacedType):
|
||||||
@@ -381,35 +375,9 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
|
|||||||
setattr(ros_msg, key, []) # FIXME
|
setattr(ros_msg, key, []) # FIXME
|
||||||
elif "array.array" in str(type(attr)):
|
elif "array.array" in str(type(attr)):
|
||||||
if attr.typecode == "f" or attr.typecode == "d":
|
if attr.typecode == "f" or attr.typecode == "d":
|
||||||
# 如果是单个值,转换为列表
|
|
||||||
if value is None:
|
|
||||||
value = []
|
|
||||||
elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
||||||
value = [value]
|
|
||||||
setattr(ros_msg, key, [float(i) for i in value])
|
setattr(ros_msg, key, [float(i) for i in value])
|
||||||
else:
|
else:
|
||||||
# 对于整数数组,需要确保是序列且每个值在有效范围内
|
setattr(ros_msg, key, value)
|
||||||
if value is None:
|
|
||||||
value = []
|
|
||||||
elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
||||||
# 如果是单个值,转换为列表
|
|
||||||
value = [value]
|
|
||||||
# 确保每个整数值在有效范围内(-2147483648 到 2147483647)
|
|
||||||
converted_value = []
|
|
||||||
for i in value:
|
|
||||||
if i is None:
|
|
||||||
continue # 跳过 None 值
|
|
||||||
if isinstance(i, (int, float)):
|
|
||||||
int_val = int(i)
|
|
||||||
# 确保在 int32 范围内
|
|
||||||
if int_val < -2147483648:
|
|
||||||
int_val = -2147483648
|
|
||||||
elif int_val > 2147483647:
|
|
||||||
int_val = 2147483647
|
|
||||||
converted_value.append(int_val)
|
|
||||||
else:
|
|
||||||
converted_value.append(i)
|
|
||||||
setattr(ros_msg, key, converted_value)
|
|
||||||
else:
|
else:
|
||||||
nested_ros_msg = convert_to_ros_msg(type(attr)(), value)
|
nested_ros_msg = convert_to_ros_msg(type(attr)(), value)
|
||||||
setattr(ros_msg, key, nested_ros_msg)
|
setattr(ros_msg, key, nested_ros_msg)
|
||||||
@@ -749,6 +717,19 @@ def ros_field_type_to_json_schema(
|
|||||||
# return {'type': 'object', 'description': f'未知类型: {field_type}'}
|
# 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]:
|
def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
将 ROS 消息类转换为 JSON Schema
|
将 ROS 消息类转换为 JSON Schema
|
||||||
@@ -760,56 +741,23 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any
|
|||||||
Returns:
|
Returns:
|
||||||
对应的 JSON Schema 定义
|
对应的 JSON Schema 定义
|
||||||
"""
|
"""
|
||||||
schema = {"type": "object", "properties": {}, "required": []}
|
schema = ROS2MessageInstance(msg_class()).get_json_schema()
|
||||||
|
|
||||||
# 优先使用字段名作为标题,否则使用类名
|
|
||||||
schema["title"] = field_name
|
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
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, Any]:
|
def ros_action_to_json_schema(
|
||||||
|
action_class: Any, description="", previous_schema: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
将 ROS Action 类转换为 JSON Schema
|
将 ROS Action 类转换为 JSON Schema
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action_class: ROS Action 类
|
action_class: ROS Action 类
|
||||||
description: 描述
|
description: 描述
|
||||||
|
previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
完整的 JSON Schema 定义
|
完整的 JSON Schema 定义
|
||||||
@@ -843,9 +791,46 @@ def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, An
|
|||||||
"required": ["goal"],
|
"required": ["goal"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_strip_rosidl_descriptions(schema)
|
||||||
|
|
||||||
|
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description
|
||||||
|
if previous_schema:
|
||||||
|
_preserve_field_descriptions(schema, previous_schema)
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def _preserve_field_descriptions(
|
||||||
|
new_schema: Dict[str, Any], previous_schema: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_schema: 新生成的 schema(会被修改)
|
||||||
|
previous_schema: 之前的 schema
|
||||||
|
"""
|
||||||
|
for section in ["goal", "feedback", "result"]:
|
||||||
|
new_section = new_schema.get("properties", {}).get(section, {})
|
||||||
|
prev_section = previous_schema.get("properties", {}).get(section, {})
|
||||||
|
|
||||||
|
if not new_section or not prev_section:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_props = new_section.get("properties", {})
|
||||||
|
prev_props = prev_section.get("properties", {})
|
||||||
|
|
||||||
|
for field_name, field_schema in new_props.items():
|
||||||
|
if field_name in prev_props:
|
||||||
|
prev_field = prev_props[field_name]
|
||||||
|
# 保留字段的 description
|
||||||
|
if "description" in prev_field and prev_field["description"]:
|
||||||
|
field_schema["description"] = prev_field["description"]
|
||||||
|
# 保留字段的 title(用户自定义的中文名)
|
||||||
|
if "title" in prev_field and prev_field["title"]:
|
||||||
|
field_schema["title"] = prev_field["title"]
|
||||||
|
|
||||||
|
|
||||||
def convert_ros_action_to_jsonschema(
|
def convert_ros_action_to_jsonschema(
|
||||||
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
|
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ from unilabos_msgs.action import SendCmd
|
|||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
|
|
||||||
from unilabos.config.config import BasicConfig
|
from unilabos.config.config import BasicConfig
|
||||||
from unilabos.utils.decorator import get_topic_config, get_all_subscriptions
|
from unilabos.registry.decorators import get_topic_config
|
||||||
|
from unilabos.utils.decorator import get_all_subscriptions
|
||||||
|
|
||||||
from unilabos.resources.container import RegularContainer
|
from unilabos.resources.container import RegularContainer
|
||||||
from unilabos.resources.graphio import (
|
from unilabos.resources.graphio import (
|
||||||
@@ -57,6 +58,7 @@ from unilabos_msgs.msg import Resource # type: ignore
|
|||||||
|
|
||||||
from unilabos.resources.resource_tracker import (
|
from unilabos.resources.resource_tracker import (
|
||||||
DeviceNodeResourceTracker,
|
DeviceNodeResourceTracker,
|
||||||
|
ResourceDictType,
|
||||||
ResourceTreeSet,
|
ResourceTreeSet,
|
||||||
ResourceTreeInstance,
|
ResourceTreeInstance,
|
||||||
ResourceDictInstance,
|
ResourceDictInstance,
|
||||||
@@ -146,7 +148,7 @@ def init_wrapper(
|
|||||||
device_id: str,
|
device_id: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
driver_class: type[T],
|
driver_class: type[T],
|
||||||
device_config: ResourceTreeInstance,
|
device_config: ResourceDictInstance,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
hardware_interface: Dict[str, Any],
|
hardware_interface: Dict[str, Any],
|
||||||
@@ -194,9 +196,9 @@ class PropertyPublisher:
|
|||||||
self._value = None
|
self._value = None
|
||||||
try:
|
try:
|
||||||
self.publisher_ = node.create_publisher(msg_type, f"{name}", qos)
|
self.publisher_ = node.create_publisher(msg_type, f"{name}", qos)
|
||||||
except AttributeError as ex:
|
except Exception as e:
|
||||||
self.node.lab_logger().error(
|
self.node.lab_logger().error(
|
||||||
f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}"
|
f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}"
|
||||||
)
|
)
|
||||||
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
self.timer = node.create_timer(self.timer_period, self.publish_property)
|
||||||
self.__loop = ROS2DeviceNode.get_asyncio_loop()
|
self.__loop = ROS2DeviceNode.get_asyncio_loop()
|
||||||
@@ -279,6 +281,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self,
|
self,
|
||||||
driver_instance: T,
|
driver_instance: T,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -300,6 +303,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
self.driver_instance = driver_instance
|
self.driver_instance = driver_instance
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
|
self.registry_name = registry_name
|
||||||
self.uuid = device_uuid
|
self.uuid = device_uuid
|
||||||
self.publish_high_frequency = False
|
self.publish_high_frequency = False
|
||||||
self.callback_group = ReentrantCallbackGroup()
|
self.callback_group = ReentrantCallbackGroup()
|
||||||
@@ -416,7 +420,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
container_instance: RegularContainer = rts_plr_instances[0]
|
container_instance: RegularContainer = rts_plr_instances[0]
|
||||||
found_resources = self.resource_tracker.figure_resource({"name": container_instance.name}, try_mode=True)
|
found_resources = self.resource_tracker.figure_resource(
|
||||||
|
{"name": container_instance.name}, try_mode=True
|
||||||
|
)
|
||||||
if not len(found_resources):
|
if not len(found_resources):
|
||||||
self.resource_tracker.add_resource(container_instance)
|
self.resource_tracker.add_resource(container_instance)
|
||||||
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
||||||
@@ -456,7 +462,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
}
|
}
|
||||||
res.response = json.dumps(final_response)
|
res.response = json.dumps(final_response)
|
||||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||||
if hasattr(self.driver_instance, "create_resource"):
|
if hasattr(self.driver_instance, "create_resource") and self.node_name != "host_node":
|
||||||
create_resource_func = getattr(self.driver_instance, "create_resource")
|
create_resource_func = getattr(self.driver_instance, "create_resource")
|
||||||
try:
|
try:
|
||||||
ret = create_resource_func(
|
ret = create_resource_func(
|
||||||
@@ -565,9 +571,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
future.add_done_callback(done_cb)
|
future.add_done_callback(done_cb)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot")
|
self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot")
|
||||||
|
res.response = get_result_info_str(traceback.format_exc(), False, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.lab_logger().error("Host请求添加物料时出错")
|
self.lab_logger().error("Host请求添加物料时出错")
|
||||||
self.lab_logger().error(traceback.format_exc())
|
self.lab_logger().error(traceback.format_exc())
|
||||||
|
res.response = get_result_info_str(traceback.format_exc(), False, {})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
@@ -590,6 +598,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self.s2c_resource_tree, # type: ignore
|
self.s2c_resource_tree, # type: ignore
|
||||||
callback_group=self.callback_group,
|
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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 向全局在线设备注册表添加设备信息
|
# 向全局在线设备注册表添加设备信息
|
||||||
@@ -911,8 +925,24 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
if target_site is not None and sites is not None and site_names is not None:
|
if target_site is not None and sites is not None and site_names is not None:
|
||||||
site_index = sites.index(original_instance)
|
site_index = None
|
||||||
site_name = site_names[site_index]
|
try:
|
||||||
|
# sites 可能是 Resource 列表或 dict 列表 (如 PRCXI9300Deck)
|
||||||
|
# 只有itemized_carrier在使用,准备弃用
|
||||||
|
site_index = sites.index(original_instance)
|
||||||
|
except ValueError:
|
||||||
|
# dict 类型的 sites: 通过name匹配
|
||||||
|
for idx, site in enumerate(sites):
|
||||||
|
if original_instance.name == site["occupied_by"]:
|
||||||
|
site_index = idx
|
||||||
|
break
|
||||||
|
elif (original_instance.location.x == site["position"]["x"] and original_instance.location.y == site["position"]["y"] and original_instance.location.z == site["position"]["z"]):
|
||||||
|
site_index = idx
|
||||||
|
break
|
||||||
|
if site_index is None:
|
||||||
|
site_name = None
|
||||||
|
else:
|
||||||
|
site_name = site_names[site_index]
|
||||||
if site_name != target_site:
|
if site_name != target_site:
|
||||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
@@ -920,6 +950,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
parent_appended = True
|
parent_appended = True
|
||||||
|
|
||||||
# 加载状态
|
# 加载状态
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
original_instance._size_x = plr_resource._size_x
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
original_instance._size_y = plr_resource._size_y
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
original_instance._size_z = plr_resource._size_z
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
original_instance._local_size_z = plr_resource._local_size_z
|
||||||
original_instance.location = plr_resource.location
|
original_instance.location = plr_resource.location
|
||||||
original_instance.rotation = plr_resource.rotation
|
original_instance.rotation = plr_resource.rotation
|
||||||
original_instance.barcode = plr_resource.barcode
|
original_instance.barcode = plr_resource.barcode
|
||||||
@@ -980,7 +1018,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().info(f"确认资源云端 Add 结果: {response.response}")
|
self.lab_logger().trace(f"确认资源云端 Add 结果: {response.response}")
|
||||||
results.append(result)
|
results.append(result)
|
||||||
elif action == "update":
|
elif action == "update":
|
||||||
if tree_set is None:
|
if tree_set is None:
|
||||||
@@ -1006,7 +1044,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
self.lab_logger().trace(f"确认资源云端 Update 结果: {response.response}")
|
||||||
results.append(result)
|
results.append(result)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
result = _handle_remove(resources_uuid)
|
result = _handle_remove(resources_uuid)
|
||||||
@@ -1034,6 +1072,48 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
return res
|
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(
|
async def transfer_resource_to_another(
|
||||||
self,
|
self,
|
||||||
plr_resources: List["ResourcePLR"],
|
plr_resources: List["ResourcePLR"],
|
||||||
@@ -1152,6 +1232,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"machine_name": BasicConfig.machine_name,
|
"machine_name": BasicConfig.machine_name,
|
||||||
"type": "slave",
|
"type": "slave",
|
||||||
"edge_device_id": self.device_id,
|
"edge_device_id": self.device_id,
|
||||||
|
"registry_name": self.registry_name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -1175,22 +1256,40 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
return self._lab_logger
|
return self._lab_logger
|
||||||
|
|
||||||
def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0):
|
def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0):
|
||||||
"""创建ROS发布者"""
|
"""创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建。"""
|
||||||
# 检测装饰器配置(支持 get_{attr_name} 方法和 @property)
|
# 检测 @topic_config 装饰器配置
|
||||||
topic_config = {}
|
topic_config = {}
|
||||||
|
driver_class = type(self.driver_instance)
|
||||||
|
|
||||||
# 优先检测 get_{attr_name} 方法
|
# 区分 @property 和普通方法两种情况
|
||||||
if hasattr(self.driver_instance, f"get_{attr_name}"):
|
is_prop = hasattr(driver_class, attr_name) and isinstance(
|
||||||
getter_method = getattr(self.driver_instance, f"get_{attr_name}")
|
getattr(driver_class, attr_name), property
|
||||||
topic_config = get_topic_config(getter_method)
|
)
|
||||||
|
|
||||||
# 如果没有配置,检测 @property 装饰的属性
|
if is_prop:
|
||||||
|
# @property: 检测 fget 上的 @topic_config
|
||||||
|
class_attr = getattr(driver_class, attr_name)
|
||||||
|
if class_attr.fget is not None:
|
||||||
|
topic_config = get_topic_config(class_attr.fget)
|
||||||
|
else:
|
||||||
|
# 普通方法: 直接检测 attr_name 方法上的 @topic_config
|
||||||
|
if hasattr(self.driver_instance, attr_name):
|
||||||
|
method = getattr(self.driver_instance, attr_name)
|
||||||
|
if callable(method):
|
||||||
|
topic_config = get_topic_config(method)
|
||||||
|
|
||||||
|
# 没有 @topic_config 装饰器则跳过发布
|
||||||
if not topic_config:
|
if not topic_config:
|
||||||
driver_class = type(self.driver_instance)
|
return
|
||||||
if hasattr(driver_class, attr_name):
|
|
||||||
class_attr = getattr(driver_class, attr_name)
|
# 发布名称优先级: @topic_config(name=...) > get_ 前缀去除 > attr_name
|
||||||
if isinstance(class_attr, property) and class_attr.fget is not None:
|
cfg_name = topic_config.get("name")
|
||||||
topic_config = get_topic_config(class_attr.fget)
|
if cfg_name:
|
||||||
|
publish_name = cfg_name
|
||||||
|
elif attr_name.startswith("get_"):
|
||||||
|
publish_name = attr_name[4:]
|
||||||
|
else:
|
||||||
|
publish_name = attr_name
|
||||||
|
|
||||||
# 使用装饰器配置或默认值
|
# 使用装饰器配置或默认值
|
||||||
cfg_period = topic_config.get("period")
|
cfg_period = topic_config.get("period")
|
||||||
@@ -1203,10 +1302,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
# 获取属性值的方法
|
# 获取属性值的方法
|
||||||
def get_device_attr():
|
def get_device_attr():
|
||||||
try:
|
try:
|
||||||
if hasattr(self.driver_instance, f"get_{attr_name}"):
|
if is_prop:
|
||||||
return getattr(self.driver_instance, f"get_{attr_name}")()
|
|
||||||
else:
|
|
||||||
return getattr(self.driver_instance, attr_name)
|
return getattr(self.driver_instance, attr_name)
|
||||||
|
else:
|
||||||
|
return getattr(self.driver_instance, attr_name)()
|
||||||
except AttributeError as ex:
|
except AttributeError as ex:
|
||||||
if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"):
|
if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"):
|
||||||
self.lab_logger().error(
|
self.lab_logger().error(
|
||||||
@@ -1218,8 +1317,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
)
|
)
|
||||||
self.lab_logger().error(traceback.format_exc())
|
self.lab_logger().error(traceback.format_exc())
|
||||||
|
|
||||||
self._property_publishers[attr_name] = PropertyPublisher(
|
self._property_publishers[publish_name] = PropertyPublisher(
|
||||||
self, attr_name, get_device_attr, msg_type, period, print_publish, qos
|
self, publish_name, get_device_attr, msg_type, period, print_publish, qos
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_ros_action_server(self, action_name, action_value_mapping):
|
def create_ros_action_server(self, action_name, action_value_mapping):
|
||||||
@@ -1227,14 +1326,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
action_type = action_value_mapping["type"]
|
action_type = action_value_mapping["type"]
|
||||||
str_action_type = str(action_type)[8:-2]
|
str_action_type = str(action_type)[8:-2]
|
||||||
|
|
||||||
self._action_servers[action_name] = ActionServer(
|
try:
|
||||||
self,
|
self._action_servers[action_name] = ActionServer(
|
||||||
action_type,
|
self,
|
||||||
action_name,
|
action_type,
|
||||||
execute_callback=self._create_execute_callback(action_name, action_value_mapping),
|
action_name,
|
||||||
callback_group=self.callback_group,
|
execute_callback=self._create_execute_callback(action_name, action_value_mapping),
|
||||||
)
|
callback_group=self.callback_group,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().error(f"创建ActionServer失败,Device: {self.device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}")
|
||||||
|
return
|
||||||
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||||
|
|
||||||
def _setup_decorated_subscribers(self):
|
def _setup_decorated_subscribers(self):
|
||||||
@@ -1384,9 +1486,13 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if uuid_indices:
|
if uuid_indices:
|
||||||
uuids = [item[1] for item in uuid_indices]
|
uuids = [item[1] for item in uuid_indices]
|
||||||
resource_tree = await self.get_resource(uuids)
|
resource_tree = await self.get_resource(uuids)
|
||||||
plr_resources = resource_tree.to_plr_resources()
|
plr_resources = resource_tree.to_plr_resources(requested_uuids=uuids)
|
||||||
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
||||||
plr_resource = plr_resources[i]
|
try:
|
||||||
|
plr_resource = plr_resources[i]
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().error(f"资源查询结果: 共 {len(queried_resources)} 个资源,但查询结果只有 {len(plr_resources)} 个资源,索引为 {i} 的资源不存在")
|
||||||
|
raise e
|
||||||
if "sample_id" in resource_data:
|
if "sample_id" in resource_data:
|
||||||
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
||||||
queried_resources[idx] = plr_resource
|
queried_resources[idx] = plr_resource
|
||||||
@@ -1626,9 +1732,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
else:
|
else:
|
||||||
resolved_sample_uuids[sample_uuid] = material_uuid
|
resolved_sample_uuids[sample_uuid] = material_uuid
|
||||||
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
||||||
self.lab_logger().debug(
|
self.lab_logger().debug(f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}")
|
||||||
f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 处理单个 ResourceSlot
|
# 处理单个 ResourceSlot
|
||||||
@@ -1784,7 +1888,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 处理单个 ResourceSlot
|
# 处理单个 ResourceSlot
|
||||||
if arg_type == "unilabos.registry.placeholder_type:ResourceSlot":
|
_is_resource_slot = isinstance(arg_type, str) and arg_type.endswith(":ResourceSlot")
|
||||||
|
if _is_resource_slot:
|
||||||
resource_data = function_args[arg_name]
|
resource_data = function_args[arg_name]
|
||||||
if isinstance(resource_data, dict) and "id" in resource_data:
|
if isinstance(resource_data, dict) and "id" in resource_data:
|
||||||
try:
|
try:
|
||||||
@@ -1798,8 +1903,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
# 处理 ResourceSlot 列表
|
# 处理 ResourceSlot 列表
|
||||||
elif isinstance(arg_type, tuple) and len(arg_type) == 2:
|
elif isinstance(arg_type, tuple) and len(arg_type) == 2:
|
||||||
resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot"
|
if arg_type[0] == "list" and isinstance(arg_type[1], str) and arg_type[1].endswith(":ResourceSlot"):
|
||||||
if arg_type[0] == "list" and arg_type[1] == resource_slot_type:
|
|
||||||
resource_list = function_args[arg_name]
|
resource_list = function_args[arg_name]
|
||||||
if isinstance(resource_list, list):
|
if isinstance(resource_list, list):
|
||||||
try:
|
try:
|
||||||
@@ -2005,6 +2109,7 @@ class ROS2DeviceNode:
|
|||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
driver_params["device_id"] = device_id
|
driver_params["device_id"] = device_id
|
||||||
|
driver_params["registry_name"] = device_config.res_content.klass
|
||||||
driver_params["resource_tracker"] = self.resource_tracker
|
driver_params["resource_tracker"] = self.resource_tracker
|
||||||
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
||||||
if self._driver_instance is None:
|
if self._driver_instance is None:
|
||||||
@@ -2022,6 +2127,7 @@ class ROS2DeviceNode:
|
|||||||
children=children,
|
children=children,
|
||||||
driver_instance=self._driver_instance, # type: ignore
|
driver_instance=self._driver_instance, # type: ignore
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=device_config.res_content.klass,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2033,6 +2139,7 @@ class ROS2DeviceNode:
|
|||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=device_config.res_content.klass,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2041,6 +2148,7 @@ class ROS2DeviceNode:
|
|||||||
resource_tracker=self.resource_tracker,
|
resource_tracker=self.resource_tracker,
|
||||||
)
|
)
|
||||||
self._ros_node: BaseROS2DeviceNode
|
self._ros_node: BaseROS2DeviceNode
|
||||||
|
# 将注册表类型名传递给BaseROS2DeviceNode,用于slave上报
|
||||||
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
||||||
self.driver_instance._ros_node = self._ros_node # type: ignore
|
self.driver_instance._ros_node = self._ros_node # type: ignore
|
||||||
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
||||||
|
|||||||
@@ -4,14 +4,22 @@ import cv2
|
|||||||
from sensor_msgs.msg import Image
|
from sensor_msgs.msg import Image
|
||||||
from cv_bridge import CvBridge
|
from cv_bridge import CvBridge
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
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):
|
class VideoPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id='video_publisher', device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
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
|
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
controller_func: Callable,
|
controller_func: Callable,
|
||||||
update_rate: float,
|
update_rate: float,
|
||||||
inputs: Dict[str, Dict[str, type | str]],
|
inputs: Dict[str, Dict[str, type | str]],
|
||||||
@@ -51,6 +52,7 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from geometry_msgs.msg import Point
|
|||||||
from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
|
from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
|
||||||
from rclpy.service import Service
|
from rclpy.service import Service
|
||||||
from typing_extensions import TypedDict
|
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.msg import Resource # type: ignore
|
||||||
from unilabos_msgs.srv import (
|
from unilabos_msgs.srv import (
|
||||||
ResourceAdd,
|
ResourceAdd,
|
||||||
@@ -23,6 +24,7 @@ from unilabos_msgs.srv import (
|
|||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
|
|
||||||
|
from unilabos.registry.decorators import device, action, NodeType
|
||||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
from unilabos.resources.container import RegularContainer
|
from unilabos.resources.container import RegularContainer
|
||||||
@@ -30,12 +32,13 @@ from unilabos.resources.graphio import initialize_resource
|
|||||||
from unilabos.resources.registry import add_schema
|
from unilabos.resources.registry import add_schema
|
||||||
from unilabos.resources.resource_tracker import (
|
from unilabos.resources.resource_tracker import (
|
||||||
ResourceDict,
|
ResourceDict,
|
||||||
|
ResourceDictType,
|
||||||
ResourceDictInstance,
|
ResourceDictInstance,
|
||||||
ResourceTreeSet,
|
ResourceTreeSet,
|
||||||
ResourceTreeInstance,
|
ResourceTreeInstance,
|
||||||
RETURN_UNILABOS_SAMPLES,
|
RETURN_UNILABOS_SAMPLES,
|
||||||
JSON_UNILABOS_PARAM,
|
JSON_UNILABOS_PARAM,
|
||||||
PARAM_SAMPLE_UUIDS,
|
PARAM_SAMPLE_UUIDS, SampleUUIDsType, LabSample,
|
||||||
)
|
)
|
||||||
from unilabos.ros.initialize_device import initialize_device_from_dict
|
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||||
from unilabos.ros.msgs.message_converter import (
|
from unilabos.ros.msgs.message_converter import (
|
||||||
@@ -51,6 +54,7 @@ from unilabos.utils import logger
|
|||||||
from unilabos.utils.exception import DeviceClassInvalid
|
from unilabos.utils.exception import DeviceClassInvalid
|
||||||
from unilabos.utils.log import warning
|
from unilabos.utils.log import warning
|
||||||
from unilabos.utils.type_check import serialize_result_info
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
|
from unilabos.config.config import BasicConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from unilabos.app.ws_client import QueueItem
|
from unilabos.app.ws_client import QueueItem
|
||||||
@@ -63,7 +67,14 @@ class DeviceActionStatus:
|
|||||||
|
|
||||||
class TestResourceReturn(TypedDict):
|
class TestResourceReturn(TypedDict):
|
||||||
resources: List[List[ResourceDict]]
|
resources: List[List[ResourceDict]]
|
||||||
devices: List[DeviceSlot]
|
devices: List[Dict[str, Any]]
|
||||||
|
# unilabos_samples: List[LabSample]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateResourceReturn(TypedDict):
|
||||||
|
created_resource_tree: List[List[ResourceDict]]
|
||||||
|
liquid_input_resource_tree: List[Dict[str, Any]]
|
||||||
|
# unilabos_samples: List[LabSample]
|
||||||
|
|
||||||
|
|
||||||
class TestLatencyReturn(TypedDict):
|
class TestLatencyReturn(TypedDict):
|
||||||
@@ -78,6 +89,7 @@ class TestLatencyReturn(TypedDict):
|
|||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
@device(id="host_node", category=[], description="Host Node", icon="icon_device.webp")
|
||||||
class HostNode(BaseROS2DeviceNode):
|
class HostNode(BaseROS2DeviceNode):
|
||||||
"""
|
"""
|
||||||
主机节点类,负责管理设备、资源和控制器
|
主机节点类,负责管理设备、资源和控制器
|
||||||
@@ -248,6 +260,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name="host_node",
|
||||||
device_uuid=host_node_dict["uuid"],
|
device_uuid=host_node_dict["uuid"],
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
||||||
@@ -265,44 +278,45 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
||||||
"/devices/host_node/create_resource": ActionClient(
|
"/devices/host_node/create_resource": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.ResourceCreateFromOuterEasy,
|
ResourceCreateFromOuterEasy,
|
||||||
"/devices/host_node/create_resource",
|
"/devices/host_node/create_resource",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/create_resource_detailed": ActionClient(
|
"/devices/host_node/create_resource_detailed": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.ResourceCreateFromOuter,
|
ResourceCreateFromOuter,
|
||||||
"/devices/host_node/create_resource_detailed",
|
"/devices/host_node/create_resource_detailed",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/test_latency": ActionClient(
|
"/devices/host_node/test_latency": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.EmptyIn,
|
EmptyIn,
|
||||||
"/devices/host_node/test_latency",
|
"/devices/host_node/test_latency",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/test_resource": ActionClient(
|
"/devices/host_node/test_resource": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.EmptyIn,
|
EmptyIn,
|
||||||
"/devices/host_node/test_resource",
|
"/devices/host_node/test_resource",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/_execute_driver_command": ActionClient(
|
"/devices/host_node/_execute_driver_command": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.StrSingleInput,
|
StrSingleInput,
|
||||||
"/devices/host_node/_execute_driver_command",
|
"/devices/host_node/_execute_driver_command",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"/devices/host_node/_execute_driver_command_async": ActionClient(
|
"/devices/host_node/_execute_driver_command_async": ActionClient(
|
||||||
self,
|
self,
|
||||||
lab_registry.StrSingleInput,
|
StrSingleInput,
|
||||||
"/devices/host_node/_execute_driver_command_async",
|
"/devices/host_node/_execute_driver_command_async",
|
||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
} # 用来存储多个ActionClient实例
|
} # 用来存储多个ActionClient实例
|
||||||
self._action_value_mappings: Dict[str, Dict] = (
|
self._action_value_mappings: Dict[str, Dict] = {
|
||||||
{}
|
device_id: self._action_value_mappings
|
||||||
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
} # 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._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||||
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
||||||
@@ -319,10 +333,18 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self._discover_devices()
|
self._discover_devices()
|
||||||
|
|
||||||
# 初始化所有本机设备节点,多一次过滤,防止重复初始化
|
# 初始化所有本机设备节点,多一次过滤,防止重复初始化
|
||||||
|
local_machine = BasicConfig.machine_name
|
||||||
for device_config in devices_config.root_nodes:
|
for device_config in devices_config.root_nodes:
|
||||||
device_id = device_config.res_content.id
|
device_id = device_config.res_content.id
|
||||||
if device_config.res_content.type != "device":
|
if device_config.res_content.type != "device":
|
||||||
continue
|
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:
|
if device_id not in self.devices_names:
|
||||||
self.initialize_device(device_id, device_config)
|
self.initialize_device(device_id, device_config)
|
||||||
else:
|
else:
|
||||||
@@ -552,7 +574,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
liquid_type: list[str] = [],
|
liquid_type: list[str] = [],
|
||||||
liquid_volume: list[int] = [],
|
liquid_volume: list[int] = [],
|
||||||
slot_on_deck: str = "",
|
slot_on_deck: str = "",
|
||||||
):
|
) -> CreateResourceReturn:
|
||||||
# 暂不支持多对同名父子同时存在
|
# 暂不支持多对同名父子同时存在
|
||||||
res_creation_input = {
|
res_creation_input = {
|
||||||
"id": res_id.split("/")[-1],
|
"id": res_id.split("/")[-1],
|
||||||
@@ -605,6 +627,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
assert len(response) == 1, "Create Resource应当只返回一个结果"
|
assert len(response) == 1, "Create Resource应当只返回一个结果"
|
||||||
for i in response:
|
for i in response:
|
||||||
res = json.loads(i)
|
res = json.loads(i)
|
||||||
|
if "suc" in res:
|
||||||
|
raise ValueError(res.get("error"))
|
||||||
return res
|
return res
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
pass
|
pass
|
||||||
@@ -636,6 +660,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.device_machine_names[device_id] = "本地"
|
self.device_machine_names[device_id] = "本地"
|
||||||
self.devices_instances[device_id] = d
|
self.devices_instances[device_id] = d
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
|
self._action_value_mappings[device_id] = d._ros_node._action_value_mappings
|
||||||
|
# noinspection PyProtectedMember
|
||||||
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
||||||
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
||||||
"UniLabJsonCommand"
|
"UniLabJsonCommand"
|
||||||
@@ -644,7 +670,12 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
action_id = f"/devices/{device_id}/{action_name}"
|
action_id = f"/devices/{device_id}/{action_name}"
|
||||||
if action_id not in self._action_clients:
|
if action_id not in self._action_clients:
|
||||||
action_type = action_value_mapping["type"]
|
action_type = action_value_mapping["type"]
|
||||||
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
try:
|
||||||
|
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().error(
|
||||||
|
f"创建ActionClient失败,Device: {device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}")
|
||||||
|
continue
|
||||||
self.lab_logger().trace(
|
self.lab_logger().trace(
|
||||||
f"[Host Node] Created ActionClient (Local): {action_id}"
|
f"[Host Node] Created ActionClient (Local): {action_id}"
|
||||||
) # 子设备再创建用的是Discover发现的
|
) # 子设备再创建用的是Discover发现的
|
||||||
@@ -772,6 +803,17 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
u = uuid.UUID(item.job_id)
|
u = uuid.UUID(item.job_id)
|
||||||
device_id = item.device_id
|
device_id = item.device_id
|
||||||
action_name = item.action_name
|
action_name = item.action_name
|
||||||
|
|
||||||
|
if BasicConfig.test_mode:
|
||||||
|
action_id = f"/devices/{device_id}/{action_name}"
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[TEST MODE] 模拟执行: {action_id} (job={item.job_id[:8]}), 参数: {str(action_kwargs)[:500]}"
|
||||||
|
)
|
||||||
|
# 根据注册表 handles 构建模拟返回值
|
||||||
|
mock_return = self._build_test_mode_return(device_id, action_name, action_kwargs)
|
||||||
|
self._handle_test_mode_result(item, action_id, mock_return)
|
||||||
|
return
|
||||||
|
|
||||||
if action_type.startswith("UniLabJsonCommand"):
|
if action_type.startswith("UniLabJsonCommand"):
|
||||||
if action_name.startswith("auto-"):
|
if action_name.startswith("auto-"):
|
||||||
action_name = action_name[5:]
|
action_name = action_name[5:]
|
||||||
@@ -809,6 +851,51 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
)
|
)
|
||||||
future.add_done_callback(lambda f: self.goal_response_callback(item, action_id, f))
|
future.add_done_callback(lambda f: self.goal_response_callback(item, action_id, f))
|
||||||
|
|
||||||
|
def _build_test_mode_return(
|
||||||
|
self, device_id: str, action_name: str, action_kwargs: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
根据注册表 handles 的 output 定义构建测试模式的模拟返回值
|
||||||
|
|
||||||
|
根据 data_key 中 @flatten 的层数决定嵌套数组层数,叶子值为空字典。
|
||||||
|
例如: "vessel" → {}, "plate.@flatten" → [{}], "a.@flatten.@flatten" → [[{}]]
|
||||||
|
"""
|
||||||
|
mock_return: Dict[str, Any] = {"test_mode": True, "action_name": action_name}
|
||||||
|
action_mappings = self._action_value_mappings.get(device_id, {})
|
||||||
|
action_mapping = action_mappings.get(action_name, {})
|
||||||
|
handles = action_mapping.get("handles", {})
|
||||||
|
if isinstance(handles, dict):
|
||||||
|
for output_handle in handles.get("output", []):
|
||||||
|
data_key = output_handle.get("data_key", "")
|
||||||
|
handler_key = output_handle.get("handler_key", "")
|
||||||
|
# 根据 @flatten 层数构建嵌套数组,叶子为空字典
|
||||||
|
flatten_count = data_key.count("@flatten")
|
||||||
|
value: Any = {}
|
||||||
|
for _ in range(flatten_count):
|
||||||
|
value = [value]
|
||||||
|
mock_return[handler_key] = value
|
||||||
|
return mock_return
|
||||||
|
|
||||||
|
def _handle_test_mode_result(
|
||||||
|
self, item: "QueueItem", action_id: str, mock_return: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
测试模式下直接构建结果并走正常的结果回调流程(跳过 ROS)
|
||||||
|
"""
|
||||||
|
job_id = item.job_id
|
||||||
|
status = "success"
|
||||||
|
return_info = serialize_result_info("", True, mock_return)
|
||||||
|
|
||||||
|
self.lab_logger().info(f"[TEST MODE] Result for {action_id} ({job_id[:8]}): {status}")
|
||||||
|
|
||||||
|
from unilabos.app.web.controller import store_job_result
|
||||||
|
store_job_result(job_id, status, return_info, mock_return)
|
||||||
|
|
||||||
|
# 发布状态到桥接器
|
||||||
|
for bridge in self.bridges:
|
||||||
|
if hasattr(bridge, "publish_job_status"):
|
||||||
|
bridge.publish_job_status(mock_return, item, status, return_info)
|
||||||
|
|
||||||
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
|
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
|
||||||
"""目标响应回调"""
|
"""目标响应回调"""
|
||||||
goal_handle = future.result()
|
goal_handle = future.result()
|
||||||
@@ -1133,7 +1220,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
||||||
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
||||||
response.response = json.dumps(uuid_mapping)
|
response.response = json.dumps(uuid_mapping)
|
||||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree update completed, success: {success}")
|
||||||
|
|
||||||
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
@@ -1168,6 +1255,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
def _node_info_update_callback(self, request, response):
|
def _node_info_update_callback(self, request, response):
|
||||||
"""
|
"""
|
||||||
更新节点信息回调
|
更新节点信息回调
|
||||||
|
|
||||||
|
处理两种消息:
|
||||||
|
1. 首次上报(main_slave_run): 带 devices_config + registry_config,存储 action_value_mappings
|
||||||
|
2. 设备重注册(SYNC_SLAVE_NODE_INFO): 带 edge_device_id + registry_name,用 registry_name 索引已存储的 mappings
|
||||||
"""
|
"""
|
||||||
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
||||||
try:
|
try:
|
||||||
@@ -1179,12 +1270,65 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
info = info["SYNC_SLAVE_NODE_INFO"]
|
info = info["SYNC_SLAVE_NODE_INFO"]
|
||||||
machine_name = info["machine_name"]
|
machine_name = info["machine_name"]
|
||||||
edge_device_id = info["edge_device_id"]
|
edge_device_id = info["edge_device_id"]
|
||||||
|
registry_name = info.get("registry_name", "")
|
||||||
self.device_machine_names[edge_device_id] = machine_name
|
self.device_machine_names[edge_device_id] = machine_name
|
||||||
|
|
||||||
|
# 用 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", {})
|
||||||
|
)
|
||||||
|
if action_mappings:
|
||||||
|
self._action_value_mappings[edge_device_id] = action_mappings
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[Host Node] Loaded {len(action_mappings)} action mappings "
|
||||||
|
f"for remote device {edge_device_id} (registry: {registry_name})"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
devices_config = info.pop("devices_config")
|
devices_config = info.pop("devices_config")
|
||||||
registry_config = info.pop("registry_config")
|
registry_config = info.pop("registry_config")
|
||||||
if registry_config:
|
if registry_config:
|
||||||
http_client.resource_registry({"resources": registry_config})
|
http_client.resource_registry({"resources": registry_config})
|
||||||
|
|
||||||
|
# 存储 slave 的 registry_config,用于后续 SYNC_SLAVE_NODE_INFO 索引
|
||||||
|
for reg_name, reg_data in registry_config.items():
|
||||||
|
if isinstance(reg_data, dict) and "class" in reg_data:
|
||||||
|
self._slave_registry_configs[reg_name] = reg_data
|
||||||
|
|
||||||
|
# 解析 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", {})
|
||||||
|
)
|
||||||
|
if action_mappings:
|
||||||
|
self._action_value_mappings[device_id] = action_mappings
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[Host Node] Stored {len(action_mappings)} action mappings "
|
||||||
|
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}")
|
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
||||||
response.response = "OK"
|
response.response = "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1479,8 +1623,21 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@action(always_free=True, node_type=NodeType.MANUAL_CONFIRM, placeholder_keys={
|
||||||
|
"assignee_user_ids": "unilabos_manual_confirm"
|
||||||
|
}, goal_default={
|
||||||
|
"timeout_seconds": 3600,
|
||||||
|
"assignee_user_ids": []
|
||||||
|
})
|
||||||
|
def manual_confirm(self, timeout_seconds: int, assignee_user_ids: list[str], **kwargs) -> dict:
|
||||||
|
"""
|
||||||
|
timeout_seconds: 超时时间(秒),默认3600秒
|
||||||
|
"""
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def test_resource(
|
def test_resource(
|
||||||
self,
|
self,
|
||||||
|
sample_uuids: SampleUUIDsType,
|
||||||
resource: ResourceSlot = None,
|
resource: ResourceSlot = None,
|
||||||
resources: List[ResourceSlot] = None,
|
resources: List[ResourceSlot] = None,
|
||||||
device: DeviceSlot = None,
|
device: DeviceSlot = None,
|
||||||
@@ -1495,6 +1652,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
return {
|
return {
|
||||||
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
||||||
"devices": [device, *devices],
|
"devices": [device, *devices],
|
||||||
|
"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()]
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_pong_response(self, pong_data: dict):
|
def handle_pong_response(self, pong_data: dict):
|
||||||
@@ -1591,3 +1749,177 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}")
|
self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}")
|
||||||
self.lab_logger().error(traceback.format_exc())
|
self.lab_logger().error(traceback.format_exc())
|
||||||
return False
|
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)}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ from rclpy.callback_groups import ReentrantCallbackGroup
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class JointRepublisher(BaseROS2DeviceNode):
|
class JointRepublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,device_id,resource_tracker, **kwargs):
|
def __init__(self,device_id, registry_name, resource_tracker, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from unilabos.resources.graphio import initialize_resources
|
|||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
class ResourceMeshManager(BaseROS2DeviceNode):
|
class ResourceMeshManager(BaseROS2DeviceNode):
|
||||||
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", rate=50, **kwargs):
|
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", registry_name: str = "", rate=50, **kwargs):
|
||||||
"""初始化资源网格管理器节点
|
"""初始化资源网格管理器节点
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -37,6 +37,7 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeRe
|
|||||||
|
|
||||||
|
|
||||||
class ROS2SerialNode(BaseROS2DeviceNode):
|
class ROS2SerialNode(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
def __init__(self, device_id, registry_name, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
||||||
# 保存属性,以便在调用父类初始化前使用
|
# 保存属性,以便在调用父类初始化前使用
|
||||||
self.port = port
|
self.port = port
|
||||||
self.baudrate = baudrate
|
self.baudrate = baudrate
|
||||||
@@ -28,6 +28,7 @@ class ROS2SerialNode(BaseROS2DeviceNode):
|
|||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
|
registry_name=registry_name,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from unilabos.ros.msgs.message_converter import (
|
|||||||
convert_from_ros_msg_with_mapping,
|
convert_from_ros_msg_with_mapping,
|
||||||
)
|
)
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDictInstance
|
from unilabos.resources.resource_tracker import ResourceDictType, ResourceTreeSet, ResourceDictInstance
|
||||||
from unilabos.utils.type_check import get_result_info_str
|
from unilabos.utils.type_check import get_result_info_str
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -47,6 +47,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
*,
|
*,
|
||||||
driver_instance: "WorkstationBase",
|
driver_instance: "WorkstationBase",
|
||||||
device_id: str,
|
device_id: str,
|
||||||
|
registry_name: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -62,6 +63,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=driver_instance,
|
driver_instance=driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
|
registry_name=registry_name,
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
||||||
@@ -175,6 +177,103 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
||||||
return d
|
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):
|
def create_ros_action_server(self, action_name, action_value_mapping):
|
||||||
"""创建ROS动作服务器"""
|
"""创建ROS动作服务器"""
|
||||||
if action_name not in self.protocol_names:
|
if action_name not in self.protocol_names:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user