mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-07 23:45:10 +00:00
Compare commits
67 Commits
v0.9.0
...
0bdb888108
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdb888108 | ||
|
|
b0ffe7df90 | ||
|
|
c7e610c1da | ||
|
|
3f4d247127 | ||
|
|
6ae77e0408 | ||
|
|
bab4b1d67a | ||
|
|
12c17ec26e | ||
|
|
6577fe12eb | ||
|
|
f1fee5fad9 | ||
|
|
9b3377aedb | ||
|
|
526327727d | ||
|
|
aaa86314e3 | ||
|
|
6a14104e6b | ||
|
|
ab0c4b708b | ||
|
|
c0b7f2decd | ||
|
|
b6c9530c61 | ||
|
|
8698821c52 | ||
|
|
3f53f88390 | ||
|
|
e840516ba4 | ||
|
|
146d8c5296 | ||
|
|
6573c9e02e | ||
|
|
c7b9c6a825 | ||
|
|
48c43d3303 | ||
|
|
55be5e8188 | ||
|
|
1b9f3c666d | ||
|
|
097114d38c | ||
|
|
5bec899479 | ||
|
|
5e86112ebf | ||
|
|
24ecb13b79 | ||
|
|
2573d34713 | ||
|
|
106d71e1db | ||
|
|
3c2a4a64ac | ||
|
|
1e00a66a65 | ||
|
|
46da42deef | ||
|
|
101c1bc3cc | ||
|
|
a62112ae26 | ||
|
|
dd5a7cab75 | ||
|
|
39de3ac58e | ||
|
|
b99969278c | ||
|
|
b957ad2f71 | ||
|
|
e1a7c3a103 | ||
|
|
e63c15997c | ||
|
|
c5a495f409 | ||
|
|
5b240cb0ea | ||
|
|
147b8f47c0 | ||
|
|
6d2489af5f | ||
|
|
807dcdd226 | ||
|
|
8a29bc5597 | ||
|
|
6f6c70ee57 | ||
|
|
478a85951c | ||
|
|
0f2555c90c | ||
|
|
d2dda6ee03 | ||
|
|
208540b307 | ||
|
|
cb7c56a1d9 | ||
|
|
ea2e9c3e3a | ||
|
|
0452a68180 | ||
|
|
90a0f3db9b | ||
|
|
055d120ba8 | ||
|
|
a948f09f60 | ||
|
|
6f69df440c | ||
|
|
b420d1fa8e | ||
|
|
767e0fcdee | ||
|
|
84944396e9 | ||
|
|
bfcb214b53 | ||
|
|
ec4e6c6cfd | ||
|
|
53b6457a88 | ||
|
|
133dbf77bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
.vscode
|
.vscode
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
service
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -4,83 +4,86 @@
|
|||||||
|
|
||||||
# Uni-Lab-OS
|
# Uni-Lab-OS
|
||||||
|
|
||||||
|
<!-- Language switcher -->
|
||||||
|
**English** | [中文](README_zh.md)
|
||||||
|
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
Uni-Lab Operating System is a platform for laboratory automation, designed to connect and control various experimental equipment, enabling automation and standardization of experimental workflows.
|
||||||
|
|
||||||
## 核心特点
|
## Key Features
|
||||||
|
|
||||||
- 多设备集成管理
|
- Multi-device integration management
|
||||||
- 自动化实验流程
|
- Automated experimental workflows
|
||||||
- 云端连接能力
|
- Cloud connectivity capabilities
|
||||||
- 灵活的配置系统
|
- Flexible configuration system
|
||||||
- 支持多种实验协议
|
- Support for multiple experimental protocols
|
||||||
|
|
||||||
## 文档
|
## Documentation
|
||||||
|
|
||||||
详细文档可在以下位置找到:
|
Detailed documentation can be found at:
|
||||||
|
|
||||||
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||||
|
|
||||||
## 快速开始
|
## Quick Start
|
||||||
|
|
||||||
1. 配置Conda环境
|
1. Configure Conda Environment
|
||||||
|
|
||||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建新环境
|
# Create new environment
|
||||||
mamba env create -f unilabos-[YOUR_OS].yaml
|
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
|
||||||
# 或更新现有环境
|
# Or update existing environment
|
||||||
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
# Where `[YOUR_OS]` can be `win64`, `linux-64`, `osx-64`, or `osx-arm64`.
|
||||||
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||||
|
|
||||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
# Currently, you need to install the `unilabos_msgs` package
|
||||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
# You can download the system-specific package from the Release page
|
||||||
conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2
|
conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
|
||||||
|
|
||||||
# 安装PyLabRobot等前置
|
# Install PyLabRobot and other prerequisites
|
||||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
cd plr_repo
|
cd plr_repo
|
||||||
pip install .[opentrons]
|
pip install .[opentrons]
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 安装 Uni-Lab-OS:
|
2. Install Uni-Lab-OS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# Clone the repository
|
||||||
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||||
cd Uni-Lab-OS
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
# 安装 Uni-Lab-OS
|
# Install Uni-Lab-OS
|
||||||
pip install .
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 启动 Uni-Lab 系统:
|
3. Start Uni-Lab System:
|
||||||
|
|
||||||
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||||
|
|
||||||
## 消息格式
|
## Message Format
|
||||||
|
|
||||||
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) page.
|
||||||
|
|
||||||
## 许可证
|
## License
|
||||||
|
|
||||||
此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## 项目统计
|
## Project Statistics
|
||||||
|
|
||||||
### Stars 趋势
|
### Stars Trend
|
||||||
|
|
||||||
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
||||||
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 联系我们
|
## Contact Us
|
||||||
|
|
||||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
89
README_zh.md
Normal file
89
README_zh.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="docs/logo.png" alt="Uni-Lab Logo" width="200"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Uni-Lab-OS
|
||||||
|
|
||||||
|
<!-- Language switcher -->
|
||||||
|
[English](README.md) | **中文**
|
||||||
|
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
|
Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||||
|
|
||||||
|
## 核心特点
|
||||||
|
|
||||||
|
- 多设备集成管理
|
||||||
|
- 自动化实验流程
|
||||||
|
- 云端连接能力
|
||||||
|
- 灵活的配置系统
|
||||||
|
- 支持多种实验协议
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
详细文档可在以下位置找到:
|
||||||
|
|
||||||
|
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
1. 配置Conda环境
|
||||||
|
|
||||||
|
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建新环境
|
||||||
|
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||||
|
mamba activate unilab
|
||||||
|
|
||||||
|
# 或更新现有环境
|
||||||
|
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
||||||
|
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||||
|
|
||||||
|
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||||
|
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||||
|
conda install ros-humble-unilabos-msgs-0.9.2-xxxxx.tar.bz2
|
||||||
|
|
||||||
|
# 安装PyLabRobot等前置
|
||||||
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
|
cd plr_repo
|
||||||
|
pip install .[opentrons]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装 Uni-Lab-OS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||||
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
|
# 安装 Uni-Lab-OS
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动 Uni-Lab 系统:
|
||||||
|
|
||||||
|
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||||
|
|
||||||
|
## 消息格式
|
||||||
|
|
||||||
|
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
||||||
|
|
||||||
|
## 项目统计
|
||||||
|
|
||||||
|
### Stars 趋势
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
||||||
|
<img src="https://api.star-history.com/svg?repos=dptech-corp/Uni-Lab-OS&type=Date" alt="Star History Chart" width="600">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## 联系我们
|
||||||
|
|
||||||
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.9.0
|
version: 0.9.2
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
folder: ros-humble-unilabos-msgs/src/work
|
folder: ros-humble-unilabos-msgs/src/work
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.9.0"
|
version: "0.9.2"
|
||||||
|
|
||||||
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.9.0',
|
version='0.9.2',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
使用plr_test.json启动,将Well加入Plate中
|
使用plr_test.json启动,将Well加入Plate中
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ros2 action send_goal /devices/host_node/add_resource_from_outer unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
||||||
```
|
```
|
||||||
22
test/experiments/biomek.json
Normal file
22
test/experiments/biomek.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "BIOMEK",
|
||||||
|
"name": "BIOMEK",
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "liquid_handler.biomek",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"children": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
1710
test/experiments/plr_test_converted_slim.json
Normal file
1710
test/experiments/plr_test_converted_slim.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,8 @@ dependencies:
|
|||||||
- ros-humble-moveit-servo
|
- ros-humble-moveit-servo
|
||||||
# simulation
|
# simulation
|
||||||
- ros-humble-simulation
|
- ros-humble-simulation
|
||||||
|
- ros-humble-tf-transformations
|
||||||
|
- transforms3d
|
||||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||||
# ilab equipments
|
# ilab equipments
|
||||||
# - ros-humble-unilabos-msgs
|
# - ros-humble-unilabos-msgs
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ dependencies:
|
|||||||
# - ros-humble-moveit-servo
|
# - ros-humble-moveit-servo
|
||||||
# simulation
|
# simulation
|
||||||
- ros-humble-simulation
|
- ros-humble-simulation
|
||||||
|
- ros-humble-tf-transformations
|
||||||
|
- transforms3d
|
||||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||||
# ilab equipments
|
# ilab equipments
|
||||||
# - ros-humble-unilabos-msgs
|
# - ros-humble-unilabos-msgs
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ dependencies:
|
|||||||
- ros-humble-moveit-servo
|
- ros-humble-moveit-servo
|
||||||
# simulation
|
# simulation
|
||||||
- ros-humble-simulation
|
- ros-humble-simulation
|
||||||
|
- ros-humble-tf-transformations
|
||||||
|
- transforms3d
|
||||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||||
# ilab equipments
|
# ilab equipments
|
||||||
# - ros-humble-unilabos-msgs
|
# - ros-humble-unilabos-msgs
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ dependencies:
|
|||||||
- ros-humble-moveit-servo
|
- ros-humble-moveit-servo
|
||||||
# simulation
|
# simulation
|
||||||
- ros-humble-simulation # ignored because of NO python3.11 package in WIN64
|
- ros-humble-simulation # ignored because of NO python3.11 package in WIN64
|
||||||
|
- ros-humble-tf-transformations
|
||||||
|
- transforms3d
|
||||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||||
# ilab equipments
|
# ilab equipments
|
||||||
# ros-humble-unilabos-msgs
|
# ros-humble-unilabos-msgs
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ def job_add(req: JobAddReq) -> JobData:
|
|||||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||||
elif "command" in action_kwargs:
|
elif "command" in action_kwargs:
|
||||||
action_kwargs = action_kwargs["command"]
|
action_kwargs = action_kwargs["command"]
|
||||||
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
|
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id, server_info=req.server_info)
|
||||||
return JobData(jobId=req.job_id)
|
return JobData(jobId=req.job_id)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from copy import deepcopy
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from unilabos.resources.graphio import tree_to_list
|
||||||
|
|
||||||
# 首先添加项目根目录到路径
|
# 首先添加项目根目录到路径
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||||
@@ -18,7 +20,6 @@ if unilabos_dir not in sys.path:
|
|||||||
|
|
||||||
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
|
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
|
||||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||||
from unilabos.device_mesh.resource_visalization import ResourceVisualization
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
@@ -145,19 +146,19 @@ def main():
|
|||||||
else read_graphml(args_dict["graph"])
|
else read_graphml(args_dict["graph"])
|
||||||
)
|
)
|
||||||
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
||||||
args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||||
|
args_dict["resources_config"] = list(devices_and_resources.values())
|
||||||
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
||||||
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
|
|
||||||
|
|
||||||
args_dict["graph"] = graph_res.physical_setup_graph
|
args_dict["graph"] = graph_res.physical_setup_graph
|
||||||
else:
|
else:
|
||||||
if args_dict["devices"] is None or args_dict["resources"] is None:
|
if args_dict["devices"] is None or args_dict["resources"] is None:
|
||||||
print_status("Either graph or devices and resources must be provided.", "error")
|
print_status("Either graph or devices and resources must be provided.", "error")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
|
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
|
||||||
args_dict["resources_config"] = initialize_resources(
|
# args_dict["resources_config"] = initialize_resources(
|
||||||
list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
# list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||||
)
|
# )
|
||||||
|
args_dict["resources_config"] = list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||||
|
|
||||||
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
|
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
|
||||||
for i in args_dict["resources_config"]:
|
for i in args_dict["resources_config"]:
|
||||||
@@ -188,11 +189,12 @@ def main():
|
|||||||
if args_dict["visual"] != "disable":
|
if args_dict["visual"] != "disable":
|
||||||
enable_rviz = args_dict["visual"] == "rviz"
|
enable_rviz = args_dict["visual"] == "rviz"
|
||||||
if devices_and_resources is not None:
|
if devices_and_resources is not None:
|
||||||
|
from unilabos.device_mesh.resource_visalization import ResourceVisualization # 此处开启后,logger会变更为INFO,有需要请调整
|
||||||
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
|
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
|
||||||
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
||||||
open_browser=not args_dict["disable_browser"]
|
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
|
||||||
))
|
))
|
||||||
server_thread.start()
|
server_thread.start()
|
||||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||||
@@ -201,10 +203,10 @@ def main():
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
start_server(open_browser=not args_dict["disable_browser"])
|
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
start_server(open_browser=not args_dict["disable_browser"])
|
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ class Resp(BaseModel):
|
|||||||
class JobAddReq(BaseModel):
|
class JobAddReq(BaseModel):
|
||||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||||
job_id: str = Field(examples=["sfsfsfeq"], description="goal uuid")
|
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
||||||
node_id: str = Field(examples=["sfsfsfeq"], description="node uuid")
|
node_id: str = Field(examples=["node_id"], description="node uuid")
|
||||||
|
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
||||||
|
|
||||||
|
|
||||||
class JobStepFinishReq(BaseModel):
|
class JobStepFinishReq(BaseModel):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Optional
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
@@ -12,7 +13,7 @@ import tempfile
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from unilabos.config.config import MQConfig
|
from unilabos.config.config import MQConfig
|
||||||
from unilabos.app.controler import devices, job_add
|
from unilabos.app.controler import job_add
|
||||||
from unilabos.app.model import JobAddReq
|
from unilabos.app.model import JobAddReq
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
@@ -26,6 +27,7 @@ class MQTTClient:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.mqtt_disable = not MQConfig.lab_id
|
self.mqtt_disable = not MQConfig.lab_id
|
||||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||||
|
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
@@ -42,20 +44,14 @@ class MQTTClient:
|
|||||||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||||
isok, data = devices()
|
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
|
||||||
if not isok:
|
|
||||||
logger.error("[MQTT] on_connect ErrorHostNotInit")
|
|
||||||
return
|
|
||||||
|
|
||||||
def _on_message(self, client, userdata, msg) -> None:
|
def _on_message(self, client, userdata, msg) -> None:
|
||||||
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||||
try:
|
try:
|
||||||
payload_str = msg.payload.decode("utf-8")
|
payload_str = msg.payload.decode("utf-8")
|
||||||
payload_json = json.loads(payload_str)
|
payload_json = json.loads(payload_str)
|
||||||
logger.debug(f"Topic: {msg.topic}")
|
|
||||||
logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False))
|
|
||||||
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
||||||
logger.debug("job_add", type(payload_json), payload_json)
|
|
||||||
if "data" not in payload_json:
|
if "data" not in payload_json:
|
||||||
payload_json["data"] = {}
|
payload_json["data"] = {}
|
||||||
if "action" in payload_json:
|
if "action" in payload_json:
|
||||||
@@ -65,6 +61,14 @@ class MQTTClient:
|
|||||||
job_req = JobAddReq.model_validate(payload_json)
|
job_req = JobAddReq.model_validate(payload_json)
|
||||||
data = job_add(job_req)
|
data = job_add(job_req)
|
||||||
return
|
return
|
||||||
|
elif msg.topic == f"labs/{MQConfig.lab_id}/pong/":
|
||||||
|
# 处理pong响应,通知HostNode
|
||||||
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
|
|
||||||
|
host_instance = HostNode.get_instance(0)
|
||||||
|
if host_instance:
|
||||||
|
host_instance.handle_pong_response(payload_json)
|
||||||
|
return
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||||
@@ -160,10 +164,12 @@ class MQTTClient:
|
|||||||
self.client.publish(address, json.dumps(status), qos=2)
|
self.client.publish(address, json.dumps(status), qos=2)
|
||||||
logger.critical(f"Device status published: address: {address}, {status}")
|
logger.critical(f"Device status published: address: {address}, {status}")
|
||||||
|
|
||||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str):
|
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
||||||
if self.mqtt_disable:
|
if self.mqtt_disable:
|
||||||
return
|
return
|
||||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
|
if return_info is None:
|
||||||
|
return_info = "{}"
|
||||||
|
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
|
||||||
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
||||||
|
|
||||||
def publish_registry(self, device_id: str, device_info: dict):
|
def publish_registry(self, device_id: str, device_info: dict):
|
||||||
@@ -181,6 +187,28 @@ class MQTTClient:
|
|||||||
self.client.publish(address, json.dumps(action_info), qos=2)
|
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||||
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||||
|
|
||||||
|
def send_ping(self, ping_id: str, timestamp: float):
|
||||||
|
"""发送ping消息到服务端"""
|
||||||
|
if self.mqtt_disable:
|
||||||
|
return
|
||||||
|
address = f"labs/{MQConfig.lab_id}/ping/"
|
||||||
|
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
|
||||||
|
self.client.publish(address, json.dumps(ping_data), qos=2)
|
||||||
|
|
||||||
|
def setup_pong_subscription(self):
|
||||||
|
"""设置pong消息订阅"""
|
||||||
|
if self.mqtt_disable:
|
||||||
|
return
|
||||||
|
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
|
||||||
|
self.client.subscribe(pong_topic, 0)
|
||||||
|
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||||
|
|
||||||
|
def handle_pong(self, pong_data: dict):
|
||||||
|
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
||||||
|
logger.debug(f"Pong received: {pong_data}")
|
||||||
|
# 这里会被HostNode的ping-pong处理逻辑调用
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
mqtt_client = MQTTClient()
|
mqtt_client = MQTTClient()
|
||||||
|
|
||||||
|
|||||||
@@ -30,18 +30,18 @@ class HTTPClient:
|
|||||||
self.auth = MQConfig.lab_id
|
self.auth = MQConfig.lab_id
|
||||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||||
|
|
||||||
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
添加资源
|
添加资源
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resources: 要添加的资源列表
|
resources: 要添加的资源列表
|
||||||
|
database_process_later: 后台处理资源
|
||||||
Returns:
|
Returns:
|
||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource/",
|
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
|
||||||
json=resources,
|
json=resources,
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"lab {self.auth}"},
|
||||||
timeout=5,
|
timeout=5,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def get_host_node_info() -> Dict[str, Any]:
|
|||||||
host_info["subscribed_topics"] = sorted(list(host_node._subscribed_topics))
|
host_info["subscribed_topics"] = sorted(list(host_node._subscribed_topics))
|
||||||
# 获取动作客户端信息
|
# 获取动作客户端信息
|
||||||
for action_id, client in host_node._action_clients.items():
|
for action_id, client in host_node._action_clients.items():
|
||||||
host_info["action_clients"] = {action_id: get_action_info(client, full_name=action_id)}
|
host_info["action_clients"][action_id] = get_action_info(client, full_name=action_id)
|
||||||
|
|
||||||
# 获取设备状态
|
# 获取设备状态
|
||||||
host_info["device_status"] = host_node.device_status
|
host_info["device_status"] = host_node.device_status
|
||||||
|
|||||||
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
1098
unilabos/devices/liquid_handling/biomek.py
Normal file
1098
unilabos/devices/liquid_handling/biomek.py
Normal file
File diff suppressed because it is too large
Load Diff
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
File diff suppressed because one or more lines are too long
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
1006
unilabos/devices/liquid_handling/biomek_test.py
Normal file
1006
unilabos/devices/liquid_handling/biomek_test.py
Normal file
File diff suppressed because it is too large
Load Diff
3994
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
3994
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,21 +6,29 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from pylabrobot.liquid_handling import LiquidHandler
|
from pylabrobot.liquid_handling import LiquidHandler
|
||||||
from pylabrobot.resources import (
|
from pylabrobot.resources import Resource, TipRack, Container, Coordinate, Well
|
||||||
Resource,
|
|
||||||
TipRack,
|
|
||||||
Container,
|
|
||||||
Coordinate,
|
|
||||||
Well
|
|
||||||
)
|
|
||||||
|
|
||||||
class DPLiquidHandler(LiquidHandler):
|
|
||||||
|
class LiquidHandlerAbstract(LiquidHandler):
|
||||||
"""Extended LiquidHandler with additional operations."""
|
"""Extended LiquidHandler with additional operations."""
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# REMOVE LIQUID --------------------------------------------------
|
# REMOVE LIQUID --------------------------------------------------
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
async def create_protocol(
|
||||||
|
self,
|
||||||
|
protocol_name: str,
|
||||||
|
protocol_description: str,
|
||||||
|
protocol_version: str,
|
||||||
|
protocol_author: str,
|
||||||
|
protocol_date: str,
|
||||||
|
protocol_type: str,
|
||||||
|
none_keys: List[str] = [],
|
||||||
|
):
|
||||||
|
"""Create a new protocol with the given metadata."""
|
||||||
|
pass
|
||||||
|
|
||||||
async def remove_liquid(
|
async def remove_liquid(
|
||||||
self,
|
self,
|
||||||
vols: List[float],
|
vols: List[float],
|
||||||
@@ -35,26 +43,26 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||||||
delays: Optional[List[int]] = None,
|
delays: Optional[List[int]] = None,
|
||||||
is_96_well: Optional[bool] = False,
|
is_96_well: Optional[bool] = False,
|
||||||
top: Optional[List(float)] = None,
|
top: Optional[List[float]] = None,
|
||||||
none_keys: List[str] = []
|
none_keys: List[str] = [],
|
||||||
):
|
):
|
||||||
"""A complete *remove* (aspirate → waste) operation."""
|
"""A complete *remove* (aspirate → waste) operation."""
|
||||||
trash = self.deck.get_trash_area()
|
trash = self.deck.get_trash_area()
|
||||||
try:
|
try:
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified
|
pass # This mode is not verified
|
||||||
else:
|
else:
|
||||||
if len(vols) != len(sources):
|
if len(vols) != len(sources):
|
||||||
raise ValueError("Length of `vols` must match `sources`.")
|
raise ValueError("Length of `vols` must match `sources`.")
|
||||||
|
|
||||||
for src, vol in zip(sources, vols):
|
for src, vol in zip(sources, vols):
|
||||||
self.move_to(src, dis_to_top=top[0] if top else 0)
|
await self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||||
tip = next(self.current_tip)
|
tip = next(self.current_tip)
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[src],
|
resources=[src],
|
||||||
vols=[vol],
|
vols=[vol],
|
||||||
use_channels=use_channels, # only aspirate96 used, default to None
|
use_channels=use_channels, # only aspirate96 used, default to None
|
||||||
flow_rates=[flow_rates[0]] if flow_rates else None,
|
flow_rates=[flow_rates[0]] if flow_rates else None,
|
||||||
offsets=[offsets[0]] if offsets else None,
|
offsets=[offsets[0]] if offsets else None,
|
||||||
liquid_height=[liquid_height[0]] if liquid_height else None,
|
liquid_height=[liquid_height[0]] if liquid_height else None,
|
||||||
@@ -64,15 +72,15 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
await self.custom_delay(seconds=delays[0] if delays else 0)
|
await self.custom_delay(seconds=delays[0] if delays else 0)
|
||||||
await self.dispense(
|
await self.dispense(
|
||||||
resources=waste_liquid,
|
resources=waste_liquid,
|
||||||
vols=[vol],
|
vols=[vol],
|
||||||
use_channels=use_channels,
|
use_channels=use_channels,
|
||||||
flow_rates=[flow_rates[1]] if flow_rates else None,
|
flow_rates=[flow_rates[1]] if flow_rates else None,
|
||||||
offsets=[offsets[1]] if offsets else None,
|
offsets=[offsets[1]] if offsets else None,
|
||||||
liquid_height=[liquid_height[1]] if liquid_height else None,
|
liquid_height=[liquid_height[1]] if liquid_height else None,
|
||||||
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
|
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
|
||||||
spread=spread,
|
spread=spread,
|
||||||
)
|
)
|
||||||
await self.discard_tips() # For now, each of tips is discarded after use
|
await self.discard_tips() # For now, each of tips is discarded after use
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Liquid removal failed: {e}") from e
|
raise RuntimeError(f"Liquid removal failed: {e}") from e
|
||||||
@@ -100,13 +108,13 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
mix_vol: Optional[int] = None,
|
mix_vol: Optional[int] = None,
|
||||||
mix_rate: Optional[int] = None,
|
mix_rate: Optional[int] = None,
|
||||||
mix_liquid_height: Optional[float] = None,
|
mix_liquid_height: Optional[float] = None,
|
||||||
none_keys: List[str] = []
|
none_keys: List[str] = [],
|
||||||
):
|
):
|
||||||
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
|
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified.
|
pass # This mode is not verified.
|
||||||
else:
|
else:
|
||||||
if len(asp_vols) != len(targets):
|
if len(asp_vols) != len(targets):
|
||||||
raise ValueError("Length of `vols` must match `targets`.")
|
raise ValueError("Length of `vols` must match `targets`.")
|
||||||
@@ -122,7 +130,7 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
offsets=[offsets[0]] if offsets else None,
|
offsets=[offsets[0]] if offsets else None,
|
||||||
liquid_height=[liquid_height[0]] if liquid_height else None,
|
liquid_height=[liquid_height[0]] if liquid_height else None,
|
||||||
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
|
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
|
||||||
spread=spread
|
spread=spread,
|
||||||
)
|
)
|
||||||
if delays is not None:
|
if delays is not None:
|
||||||
await self.custom_delay(seconds=delays[0])
|
await self.custom_delay(seconds=delays[0])
|
||||||
@@ -144,7 +152,8 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
mix_vol=mix_vol,
|
mix_vol=mix_vol,
|
||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None)
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
|
)
|
||||||
if delays is not None:
|
if delays is not None:
|
||||||
await self.custom_delay(seconds=delays[1])
|
await self.custom_delay(seconds=delays[1])
|
||||||
await self.touch_tip(targets[_])
|
await self.touch_tip(targets[_])
|
||||||
@@ -158,13 +167,13 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
async def transfer_liquid(
|
async def transfer_liquid(
|
||||||
self,
|
self,
|
||||||
asp_vols: Union[List[float], float],
|
|
||||||
dis_vols: Union[List[float], float],
|
|
||||||
sources: Sequence[Container],
|
sources: Sequence[Container],
|
||||||
targets: Sequence[Container],
|
targets: Sequence[Container],
|
||||||
tip_racks: Sequence[TipRack],
|
tip_racks: Sequence[TipRack],
|
||||||
*,
|
*,
|
||||||
use_channels: Optional[List[int]] = None,
|
use_channels: Optional[List[int]] = None,
|
||||||
|
asp_vols: Union[List[float], float],
|
||||||
|
dis_vols: Union[List[float], float],
|
||||||
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
offsets: Optional[List[Coordinate]] = None,
|
offsets: Optional[List[Coordinate]] = None,
|
||||||
@@ -179,7 +188,7 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
mix_rate: Optional[int] = None,
|
mix_rate: Optional[int] = None,
|
||||||
mix_liquid_height: Optional[float] = None,
|
mix_liquid_height: Optional[float] = None,
|
||||||
delays: Optional[List[int]] = None,
|
delays: Optional[List[int]] = None,
|
||||||
none_keys: List[str] = []
|
none_keys: List[str] = [],
|
||||||
):
|
):
|
||||||
"""Transfer liquid from each *source* well/plate to the corresponding *target*.
|
"""Transfer liquid from each *source* well/plate to the corresponding *target*.
|
||||||
|
|
||||||
@@ -201,14 +210,15 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
# 96‑channel head mode
|
# 96‑channel head mode
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified
|
pass # This mode is not verified
|
||||||
else:
|
else:
|
||||||
if not (len(asp_vols) == len(sources) and len(dis_vols) == len(targets)):
|
if not (len(asp_vols) == len(sources) and len(dis_vols) == len(targets)):
|
||||||
raise ValueError("`sources`, `targets`, and `vols` must have the same length.")
|
raise ValueError("`sources`, `targets`, and `vols` must have the same length.")
|
||||||
|
|
||||||
tip_iter = self.iter_tips(tip_racks)
|
tip_iter = self.iter_tips(tip_racks)
|
||||||
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in (
|
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in zip(
|
||||||
zip(sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates)):
|
sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates
|
||||||
|
):
|
||||||
tip = next(tip_iter)
|
tip = next(tip_iter)
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
# Aspirate from source
|
# Aspirate from source
|
||||||
@@ -247,9 +257,9 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise RuntimeError(f"Liquid transfer failed: {exc}") from exc
|
raise RuntimeError(f"Liquid transfer failed: {exc}") from exc
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# Helper utilities
|
# Helper utilities
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
"""
|
"""
|
||||||
@@ -266,28 +276,26 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
print(f"Done: {msg}")
|
print(f"Done: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
async def touch_tip(self,
|
async def touch_tip(self, targets: Sequence[Container]):
|
||||||
targets: Sequence[Container],
|
|
||||||
):
|
|
||||||
"""Touch the tip to the side of the well."""
|
"""Touch the tip to the side of the well."""
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[targets],
|
resources=[targets],
|
||||||
vols=[0],
|
vols=[0],
|
||||||
use_channels=None,
|
use_channels=None,
|
||||||
flow_rates=None,
|
flow_rates=None,
|
||||||
offsets=[Coordinate(x=-targets.get_size_x()/2,y=0,z=0)],
|
offsets=[Coordinate(x=-targets.get_size_x() / 2, y=0, z=0)],
|
||||||
liquid_height=None,
|
liquid_height=None,
|
||||||
blow_out_air_volume=None
|
blow_out_air_volume=None,
|
||||||
)
|
)
|
||||||
#await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
# await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[targets],
|
resources=[targets],
|
||||||
vols=[0],
|
vols=[0],
|
||||||
use_channels=None,
|
use_channels=None,
|
||||||
flow_rates=None,
|
flow_rates=None,
|
||||||
offsets=[Coordinate(x=targets.get_size_x()/2,y=0,z=0)],
|
offsets=[Coordinate(x=targets.get_size_x() / 2, y=0, z=0)],
|
||||||
liquid_height=None,
|
liquid_height=None,
|
||||||
blow_out_air_volume=None
|
blow_out_air_volume=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mix(
|
async def mix(
|
||||||
@@ -298,9 +306,9 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
height_to_bottom: Optional[float] = None,
|
height_to_bottom: Optional[float] = None,
|
||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
none_keys: List[str] = []
|
none_keys: List[str] = [],
|
||||||
):
|
):
|
||||||
if mix_time is None: # No mixing required
|
if mix_time is None: # No mixing required
|
||||||
return
|
return
|
||||||
"""Mix the liquid in the target wells."""
|
"""Mix the liquid in the target wells."""
|
||||||
for _ in range(mix_time):
|
for _ in range(mix_time):
|
||||||
@@ -333,7 +341,7 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
tip_iter = self.iter_tips(tip_racks)
|
tip_iter = self.iter_tips(tip_racks)
|
||||||
self.current_tip = tip_iter
|
self.current_tip = tip_iter
|
||||||
|
|
||||||
async def move_to(self, well: Well, dis_to_top: float = 0 , channel: int = 0):
|
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
|
||||||
"""
|
"""
|
||||||
Move a single channel to a specific well with a given z-height.
|
Move a single channel to a specific well with a given z-height.
|
||||||
|
|
||||||
@@ -352,4 +360,3 @@ class DPLiquidHandler(LiquidHandler):
|
|||||||
await self.move_channel_x(channel, abs_loc.x)
|
await self.move_channel_x(channel, abs_loc.x)
|
||||||
await self.move_channel_y(channel, abs_loc.y)
|
await self.move_channel_y(channel, abs_loc.y)
|
||||||
await self.move_channel_z(channel, abs_loc.z + well_height + dis_to_top)
|
await self.move_channel_z(channel, abs_loc.z + well_height + dis_to_top)
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Sequence, Optional, List, Union, Literal
|
||||||
|
json_path = "/Users/guangxinzhang/Documents/Deep Potential/opentrons/convert/protocols/enriched_steps/sci-lucif-assay4.json"
|
||||||
|
|
||||||
|
with open(json_path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
transfer_example = data[0]
|
||||||
|
#print(transfer_example)
|
||||||
|
|
||||||
|
|
||||||
|
temp_protocol = []
|
||||||
|
TipLocation = "BC1025F" # Assuming this is a fixed tip location for the transfer
|
||||||
|
sources = transfer_example["sources"] # Assuming sources is a list of Container objects
|
||||||
|
targets = transfer_example["targets"] # Assuming targets is a list of Container objects
|
||||||
|
tip_racks = transfer_example["tip_racks"] # Assuming tip_racks is a list of TipRack objects
|
||||||
|
asp_vols = transfer_example["asp_vols"] # Assuming asp_vols is a list of volumes
|
||||||
|
solvent = "PBS"
|
||||||
|
|
||||||
|
def transfer_liquid(
|
||||||
|
#self,
|
||||||
|
sources,#: Sequence[Container],
|
||||||
|
targets,#: Sequence[Container],
|
||||||
|
tip_racks,#: Sequence[TipRack],
|
||||||
|
TipLocation,
|
||||||
|
# *,
|
||||||
|
# use_channels: Optional[List[int]] = None,
|
||||||
|
asp_vols: Union[List[float], float],
|
||||||
|
solvent: Optional[str] = None,
|
||||||
|
# dis_vols: Union[List[float], float],
|
||||||
|
# asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
# dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
# offsets,#: Optional[List[]] = None,
|
||||||
|
# touch_tip: bool = False,
|
||||||
|
# liquid_height: Optional[List[Optional[float]]] = None,
|
||||||
|
# blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||||
|
# spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
|
# is_96_well: bool = False,
|
||||||
|
# mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||||||
|
# mix_times,#: Optional[list() = None,
|
||||||
|
# mix_vol: Optional[int] = None,
|
||||||
|
# mix_rate: Optional[int] = None,
|
||||||
|
# mix_liquid_height: Optional[float] = None,
|
||||||
|
# delays: Optional[List[int]] = None,
|
||||||
|
# none_keys: List[str] = []
|
||||||
|
):
|
||||||
|
# -------- Build Biomek transfer step --------
|
||||||
|
# 1) Construct default parameter scaffold (values mirror Biomek “Transfer” block).
|
||||||
|
|
||||||
|
transfer_params = {
|
||||||
|
"Span8": False,
|
||||||
|
"Pod": "Pod1",
|
||||||
|
"items": {}, # to be filled below
|
||||||
|
"Wash": False,
|
||||||
|
"Dynamic?": True,
|
||||||
|
"AutoSelectActiveWashTechnique": False,
|
||||||
|
"ActiveWashTechnique": "",
|
||||||
|
"ChangeTipsBetweenDests": False,
|
||||||
|
"ChangeTipsBetweenSources": False,
|
||||||
|
"DefaultCaption": "", # filled after we know first pair/vol
|
||||||
|
"UseExpression": False,
|
||||||
|
"LeaveTipsOn": False,
|
||||||
|
"MandrelExpression": "",
|
||||||
|
"Repeats": "1",
|
||||||
|
"RepeatsByVolume": False,
|
||||||
|
"Replicates": "1",
|
||||||
|
"ShowTipHandlingDetails": False,
|
||||||
|
"ShowTransferDetails": True,
|
||||||
|
"Solvent": "Water",
|
||||||
|
"Span8Wash": False,
|
||||||
|
"Span8WashVolume": "2",
|
||||||
|
"Span8WasteVolume": "1",
|
||||||
|
"SplitVolume": False,
|
||||||
|
"SplitVolumeCleaning": False,
|
||||||
|
"Stop": "Destinations",
|
||||||
|
"TipLocation": "BC1025F",
|
||||||
|
"UseCurrentTips": False,
|
||||||
|
"UseDisposableTips": True,
|
||||||
|
"UseFixedTips": False,
|
||||||
|
"UseJIT": True,
|
||||||
|
"UseMandrelSelection": True,
|
||||||
|
"UseProbes": [True, True, True, True, True, True, True, True],
|
||||||
|
"WashCycles": "1",
|
||||||
|
"WashVolume": "110%",
|
||||||
|
"Wizard": False
|
||||||
|
}
|
||||||
|
|
||||||
|
items: dict = {}
|
||||||
|
for idx, (src, dst) in enumerate(zip(sources, targets)):
|
||||||
|
items[str(idx)] = {
|
||||||
|
"Source": str(src),
|
||||||
|
"Destination": str(dst),
|
||||||
|
"Volume": asp_vols[idx]
|
||||||
|
}
|
||||||
|
transfer_params["items"] = items
|
||||||
|
transfer_params["Solvent"] = solvent if solvent else "Water"
|
||||||
|
transfer_params["TipLocation"] = TipLocation
|
||||||
|
|
||||||
|
if len(tip_racks) == 1:
|
||||||
|
transfer_params['UseCurrentTips'] = True
|
||||||
|
elif len(tip_racks) > 1:
|
||||||
|
transfer_params["ChangeTipsBetweenDests"] = True
|
||||||
|
|
||||||
|
return transfer_params
|
||||||
|
|
||||||
|
action = transfer_liquid(sources=sources,targets=targets,tip_racks=tip_racks, asp_vols=asp_vols,solvent = solvent, TipLocation=TipLocation)
|
||||||
|
print(json.dumps(action,indent=2))
|
||||||
|
# print(action)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
"transfer": {
|
||||||
|
|
||||||
|
"items": {},
|
||||||
|
"Wash": false,
|
||||||
|
"Dynamic?": true,
|
||||||
|
"AutoSelectActiveWashTechnique": false,
|
||||||
|
"ActiveWashTechnique": "",
|
||||||
|
"ChangeTipsBetweenDests": true,
|
||||||
|
"ChangeTipsBetweenSources": false,
|
||||||
|
"DefaultCaption": "Transfer 100 µL from P13 to P3",
|
||||||
|
"UseExpression": false,
|
||||||
|
"LeaveTipsOn": false,
|
||||||
|
"MandrelExpression": "",
|
||||||
|
"Repeats": "1",
|
||||||
|
"RepeatsByVolume": false,
|
||||||
|
"Replicates": "1",
|
||||||
|
"ShowTipHandlingDetails": true,
|
||||||
|
"ShowTransferDetails": true,
|
||||||
|
|
||||||
|
"Span8Wash": false,
|
||||||
|
"Span8WashVolume": "2",
|
||||||
|
"Span8WasteVolume": "1",
|
||||||
|
"SplitVolume": false,
|
||||||
|
"SplitVolumeCleaning": false,
|
||||||
|
"Stop": "Destinations",
|
||||||
|
"TipLocation": "BC1025F",
|
||||||
|
"UseCurrentTips": false,
|
||||||
|
"UseDisposableTips": false,
|
||||||
|
"UseFixedTips": false,
|
||||||
|
"UseJIT": true,
|
||||||
|
"UseMandrelSelection": true,
|
||||||
|
"UseProbes": [true, true, true, true, true, true, true, true],
|
||||||
|
"WashCycles": "3",
|
||||||
|
"WashVolume": "110%",
|
||||||
|
"Wizard": false
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,22 +5,22 @@ class SolenoidValveMock:
|
|||||||
def __init__(self, port: str = "COM6"):
|
def __init__(self, port: str = "COM6"):
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
self._valve_position = "OPEN"
|
self._valve_position = "OPEN"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valve_position(self) -> str:
|
def valve_position(self) -> str:
|
||||||
return self._valve_position
|
return self._valve_position
|
||||||
|
|
||||||
def get_valve_position(self) -> str:
|
def get_valve_position(self) -> str:
|
||||||
return self._valve_position
|
return self._valve_position
|
||||||
|
|
||||||
def set_valve_position(self, position):
|
def set_valve_position(self, position):
|
||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
self._valve_position = position
|
self._valve_position = position
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import time
|
|||||||
class VacuumPumpMock:
|
class VacuumPumpMock:
|
||||||
def __init__(self, port: str = "COM6"):
|
def __init__(self, port: str = "COM6"):
|
||||||
self._status = "OPEN"
|
self._status = "OPEN"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
def get_status(self) -> str:
|
def get_status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
def set_status(self, position):
|
def set_status(self, position):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
self._status = position
|
self._status = position
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,97 @@
|
|||||||
liquid_handler:
|
liquid_handler:
|
||||||
description: Liquid handler device controlled by pylabrobot
|
description: Liquid handler device controlled by pylabrobot
|
||||||
|
icon: icon_yiyezhan.webp
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.liquid_handling:LiquidHandler
|
module: unilabos.devices.liquid_handling.liquid_handler_abstract:LiquidHandlerAbstract
|
||||||
type: python
|
type: python
|
||||||
status_types:
|
status_types:
|
||||||
name: String
|
name: String
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
|
remove:
|
||||||
|
type: LiquidHandlerRemove
|
||||||
|
goal:
|
||||||
|
vols: vols
|
||||||
|
sources: sources
|
||||||
|
waste_liquid: waste_liquid
|
||||||
|
use_channels: use_channels
|
||||||
|
flow_rates: flow_rates
|
||||||
|
offsets: offsets
|
||||||
|
liquid_height: liquid_height
|
||||||
|
blow_out_air_volume: blow_out_air_volume
|
||||||
|
spread: spread
|
||||||
|
delays: delays
|
||||||
|
is_96_well: is_96_well
|
||||||
|
top: top
|
||||||
|
none_keys: none_keys
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
add_liquid:
|
||||||
|
type: LiquidHandlerAdd
|
||||||
|
goal:
|
||||||
|
asp_vols: asp_vols
|
||||||
|
dis_vols: dis_vols
|
||||||
|
reagent_sources: reagent_sources
|
||||||
|
targets: targets
|
||||||
|
use_channels: use_channels
|
||||||
|
flow_rates: flow_rates
|
||||||
|
offsets: offsets
|
||||||
|
liquid_height: liquid_height
|
||||||
|
blow_out_air_volume: blow_out_air_volume
|
||||||
|
spread: spread
|
||||||
|
is_96_well: is_96_well
|
||||||
|
mix_time: mix_time
|
||||||
|
mix_vol: mix_vol
|
||||||
|
mix_rate: mix_rate
|
||||||
|
mix_liquid_height: mix_liquid_height
|
||||||
|
none_keys: none_keys
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
transfer_liquid:
|
||||||
|
type: LiquidHandlerTransfer
|
||||||
|
goal:
|
||||||
|
asp_vols: asp_vols
|
||||||
|
dis_vols: dis_vols
|
||||||
|
sources: sources
|
||||||
|
targets: targets
|
||||||
|
tip_racks: tip_racks
|
||||||
|
use_channels: use_channels
|
||||||
|
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
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
mix:
|
||||||
|
type: LiquidHandlerMix
|
||||||
|
goal:
|
||||||
|
targets: targets
|
||||||
|
mix_time: mix_time
|
||||||
|
mix_vol: mix_vol
|
||||||
|
height_to_bottom: height_to_bottom
|
||||||
|
offsets: offsets
|
||||||
|
mix_rate: mix_rate
|
||||||
|
none_keys: none_keys
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
move_to:
|
||||||
|
type: LiquidHandlerMoveTo
|
||||||
|
goal:
|
||||||
|
well: well
|
||||||
|
dis_to_top: dis_to_top
|
||||||
|
channel: channel
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
aspirate:
|
aspirate:
|
||||||
type: LiquidHandlerAspirate
|
type: LiquidHandlerAspirate
|
||||||
goal:
|
goal:
|
||||||
@@ -160,6 +246,21 @@ liquid_handler:
|
|||||||
target_vols: target_vols
|
target_vols: target_vols
|
||||||
aspiration_flow_rate: aspiration_flow_rate
|
aspiration_flow_rate: aspiration_flow_rate
|
||||||
dispense_flow_rates: dispense_flow_rates
|
dispense_flow_rates: dispense_flow_rates
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- handler_key: liquid-input
|
||||||
|
label: Liquid Input
|
||||||
|
data_type: resource
|
||||||
|
io_type: target
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
output:
|
||||||
|
- handler_key: liquid-output
|
||||||
|
label: Liquid Output
|
||||||
|
data_type: resource
|
||||||
|
io_type: source
|
||||||
|
data_source: executor
|
||||||
|
data_key: liquid
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -170,55 +271,51 @@ liquid_handler:
|
|||||||
- name
|
- name
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
dp_liquid_handler:
|
liquid_handler.revvity:
|
||||||
description: 通用液体处理
|
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.liquid_handling.action_definition:DPLiquidHandler
|
module: unilabos.devices.liquid_handling.revvity:Revvity
|
||||||
type: python
|
type: python
|
||||||
status_types:
|
status_types:
|
||||||
status: String
|
status: String
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
remove_liquid:
|
run:
|
||||||
type: DPLiquidHandlerRemoveLiquid
|
type: WorkStationRun
|
||||||
goal:
|
goal:
|
||||||
vols: vols
|
wf_name: file_path
|
||||||
sources: sources
|
params: params
|
||||||
waste_liquid: waste_liquid
|
resource: resource
|
||||||
use_channels: use_channels
|
feedback:
|
||||||
flow_rates: flow_rates
|
status: status
|
||||||
offsets: offsets
|
result:
|
||||||
liquid_height: liquid_height
|
success: success
|
||||||
blow_out_air_volume: blow_out_air_volume
|
|
||||||
spread: spread
|
liquid_handler.biomek:
|
||||||
delays: delays
|
description: Biomek液体处理器设备,基于pylabrobot控制
|
||||||
is_96_well: is_96_well
|
icon: icon_yiyezhan.webp
|
||||||
top: top
|
class:
|
||||||
|
module: unilabos.devices.liquid_handling.biomek:LiquidHandlerBiomek
|
||||||
|
type: python
|
||||||
|
status_types: {}
|
||||||
|
action_value_mappings:
|
||||||
|
create_protocol:
|
||||||
|
type: LiquidHandlerProtocolCreation
|
||||||
|
goal:
|
||||||
|
protocol_name: protocol_name
|
||||||
|
protocol_description: protocol_description
|
||||||
|
protocol_version: protocol_version
|
||||||
|
protocol_author: protocol_author
|
||||||
|
protocol_date: protocol_date
|
||||||
|
protocol_type: protocol_type
|
||||||
none_keys: none_keys
|
none_keys: none_keys
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
add_liquid:
|
run_protocol:
|
||||||
type: DPLiquidHandlerAddLiquid
|
type: EmptyIn
|
||||||
goal:
|
goal: {}
|
||||||
asp_vols: asp_vols
|
|
||||||
dis_vols: dis_vols
|
|
||||||
reagent_sources: reagent_sources
|
|
||||||
targets: targets
|
|
||||||
use_channels: use_channels
|
|
||||||
flow_rates: flow_rates
|
|
||||||
offsets: offsets
|
|
||||||
liquid_height: liquid_height
|
|
||||||
blow_out_air_volume: blow_out_air_volume
|
|
||||||
spread: spread
|
|
||||||
is_96_well: is_96_well
|
|
||||||
mix_time: mix_time
|
|
||||||
mix_vol: mix_vol
|
|
||||||
mix_rate: mix_rate
|
|
||||||
mix_liquid_height: mix_liquid_height
|
|
||||||
none_keys: none_keys
|
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
transfer_liquid:
|
transfer_liquid:
|
||||||
type: DPLiquidHandlerTransferLiquid
|
type: LiquidHandlerTransfer
|
||||||
goal:
|
goal:
|
||||||
asp_vols: asp_vols
|
asp_vols: asp_vols
|
||||||
dis_vols: dis_vols
|
dis_vols: dis_vols
|
||||||
@@ -243,68 +340,122 @@ dp_liquid_handler:
|
|||||||
none_keys: none_keys
|
none_keys: none_keys
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
custom_delay:
|
handles:
|
||||||
type: DPLiquidHandlerCustomDelay
|
input:
|
||||||
|
- handler_key: liquid-input
|
||||||
|
label: Liquid Input
|
||||||
|
data_type: resource
|
||||||
|
io_type: target
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
output:
|
||||||
|
- handler_key: liquid-output
|
||||||
|
label: Liquid Output
|
||||||
|
data_type: resource
|
||||||
|
io_type: source
|
||||||
|
data_source: executor
|
||||||
|
data_key: liquid
|
||||||
|
transfer_biomek:
|
||||||
|
type: LiquidHandlerTransferBiomek
|
||||||
goal:
|
goal:
|
||||||
seconds: seconds
|
source: source
|
||||||
msg: msg
|
target: target
|
||||||
|
tip_rack: tip_rack
|
||||||
|
volume: volume
|
||||||
|
aspirate_techniques: aspirate_techniques
|
||||||
|
dispense_techniques: dispense_techniques
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
touch_tip:
|
handles:
|
||||||
type: DPLiquidHandlerTouchTip
|
input:
|
||||||
|
- handler_key: sources
|
||||||
|
label: sources
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
- handler_key: targets
|
||||||
|
label: targets
|
||||||
|
data_type: resource
|
||||||
|
data_source: executor
|
||||||
|
data_key: liquid
|
||||||
|
- handler_key: tip_rack
|
||||||
|
label: tip_rack
|
||||||
|
data_type: resource
|
||||||
|
data_source: executor
|
||||||
|
data_key: liquid
|
||||||
|
output:
|
||||||
|
- handler_key: sources_out
|
||||||
|
label: sources
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
- handler_key: targets_out
|
||||||
|
label: targets
|
||||||
|
data_type: resource
|
||||||
|
data_source: executor
|
||||||
|
data_key: liquid
|
||||||
|
oscillation_biomek:
|
||||||
|
type: LiquidHandlerOscillateBiomek
|
||||||
goal:
|
goal:
|
||||||
targets: targets
|
rpm: rpm
|
||||||
|
time: time
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
mix:
|
handles:
|
||||||
type: DPLiquidHandlerMix
|
input:
|
||||||
|
- handler_key: plate
|
||||||
|
label: plate
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
output:
|
||||||
|
- handler_key: plate_out
|
||||||
|
label: plate
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
move_biomek:
|
||||||
|
type: LiquidHandlerMoveBiomek
|
||||||
goal:
|
goal:
|
||||||
targets: targets
|
source: resource
|
||||||
mix_time: mix_time
|
target: target
|
||||||
mix_vol: mix_vol
|
feedback: {}
|
||||||
height_to_bottom: height_to_bottom
|
result:
|
||||||
offsets: offsets
|
name: name
|
||||||
mix_rate: mix_rate
|
handles:
|
||||||
none_keys: none_keys
|
input:
|
||||||
feedback: {}
|
- handler_key: sources
|
||||||
result: {}
|
label: sources
|
||||||
set_tiprack:
|
data_type: resource
|
||||||
type: DPLiquidHandlerSetTiprack
|
data_source: handle
|
||||||
goal:
|
data_key: liquid
|
||||||
tip_racks: tip_racks
|
output:
|
||||||
feedback: {}
|
- handler_key: targets
|
||||||
result: {}
|
label: targets
|
||||||
move_to:
|
data_type: resource
|
||||||
type: DPLiquidHandlerMoveTo
|
data_source: handle
|
||||||
goal:
|
data_key: liquid
|
||||||
well: well
|
incubation_biomek:
|
||||||
dis_to_top: dis_to_top
|
type: LiquidHandlerIncubateBiomek
|
||||||
channel: channel
|
goal:
|
||||||
|
time: time
|
||||||
feedback: {}
|
feedback: {}
|
||||||
result: {}
|
result: {}
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- handler_key: plate
|
||||||
|
label: plate
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
|
output:
|
||||||
|
- handler_key: plate_out
|
||||||
|
label: plate
|
||||||
|
data_type: resource
|
||||||
|
data_source: handle
|
||||||
|
data_key: liquid
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties: {}
|
||||||
name:
|
required: []
|
||||||
type: string
|
additionalProperties: false
|
||||||
description: 物料名
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
|
|
||||||
liquid_handler.revvity:
|
|
||||||
class:
|
|
||||||
module: unilabos.devices.liquid_handling.revvity:Revvity
|
|
||||||
type: python
|
|
||||||
status_types:
|
|
||||||
status: String
|
|
||||||
action_value_mappings:
|
|
||||||
run:
|
|
||||||
type: WorkStationRun
|
|
||||||
goal:
|
|
||||||
wf_name: file_path
|
|
||||||
params: params
|
|
||||||
resource: resource
|
|
||||||
feedback:
|
|
||||||
status: status
|
|
||||||
result:
|
|
||||||
success: success
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ separator.homemade:
|
|||||||
goal:
|
goal:
|
||||||
stir_time: stir_time,
|
stir_time: stir_time,
|
||||||
stir_speed: stir_speed
|
stir_speed: stir_speed
|
||||||
settling_time": settling_time
|
settling_time: settling_time
|
||||||
feedback:
|
feedback:
|
||||||
status: status
|
status: status
|
||||||
result:
|
result:
|
||||||
|
|||||||
@@ -23,20 +23,51 @@ syringe_pump_with_valve.runze:
|
|||||||
type: string
|
type: string
|
||||||
description: The position of the valve
|
description: The position of the valve
|
||||||
required:
|
required:
|
||||||
- status
|
- status
|
||||||
- position
|
- position
|
||||||
- valve_position
|
- valve_position
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
|
|
||||||
solenoid_valve.mock:
|
solenoid_valve.mock:
|
||||||
description: Mock solenoid valve
|
description: Mock solenoid valve
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
|
module: unilabos.devices.pump_and_valve.solenoid_valve_mock:SolenoidValveMock
|
||||||
type: python
|
type: python
|
||||||
|
status_types:
|
||||||
|
status: String
|
||||||
|
valve_position: String
|
||||||
|
action_value_mappings:
|
||||||
|
open:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
close:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- handler_key: fluid-input
|
||||||
|
label: Fluid Input
|
||||||
|
data_type: fluid
|
||||||
|
output:
|
||||||
|
- handler_key: fluid-output
|
||||||
|
label: Fluid Output
|
||||||
|
data_type: fluid
|
||||||
|
init_param_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
port:
|
||||||
|
type: string
|
||||||
|
description: "通信端口"
|
||||||
|
default: "COM6"
|
||||||
|
required:
|
||||||
|
- port
|
||||||
|
|
||||||
solenoid_valve:
|
solenoid_valve:
|
||||||
description: Solenoid valve
|
description: Solenoid valve
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||||
type: python
|
type: python
|
||||||
|
|||||||
@@ -3,9 +3,95 @@ vacuum_pump.mock:
|
|||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||||
type: python
|
type: python
|
||||||
|
status_types:
|
||||||
|
status: String
|
||||||
|
action_value_mappings:
|
||||||
|
open:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
close:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
set_status:
|
||||||
|
type: StrSingleInput
|
||||||
|
goal:
|
||||||
|
string: string
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- handler_key: fluid-input
|
||||||
|
label: Fluid Input
|
||||||
|
data_type: fluid
|
||||||
|
io_type: target
|
||||||
|
data_source: handle
|
||||||
|
data_key: fluid_in
|
||||||
|
output:
|
||||||
|
- handler_key: fluid-output
|
||||||
|
label: Fluid Output
|
||||||
|
data_type: fluid
|
||||||
|
io_type: source
|
||||||
|
data_source: executor
|
||||||
|
data_key: fluid_out
|
||||||
|
init_param_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
port:
|
||||||
|
type: string
|
||||||
|
description: "通信端口"
|
||||||
|
default: "COM6"
|
||||||
|
required:
|
||||||
|
- port
|
||||||
|
|
||||||
gas_source.mock:
|
gas_source.mock:
|
||||||
description: Mock gas source
|
description: Mock gas source
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||||
type: python
|
type: python
|
||||||
|
status_types:
|
||||||
|
status: String
|
||||||
|
action_value_mappings:
|
||||||
|
open:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
close:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
set_status:
|
||||||
|
type: StrSingleInput
|
||||||
|
goal:
|
||||||
|
string: string
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- handler_key: fluid-input
|
||||||
|
label: Fluid Input
|
||||||
|
data_type: fluid
|
||||||
|
io_type: target
|
||||||
|
data_source: handle
|
||||||
|
data_key: fluid_in
|
||||||
|
output:
|
||||||
|
- handler_key: fluid-output
|
||||||
|
label: Fluid Output
|
||||||
|
data_type: fluid
|
||||||
|
io_type: source
|
||||||
|
data_source: executor
|
||||||
|
data_key: fluid_out
|
||||||
|
init_param_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
port:
|
||||||
|
type: string
|
||||||
|
description: "通信端口"
|
||||||
|
default: "COM6"
|
||||||
|
required:
|
||||||
|
- port
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ workstation:
|
|||||||
module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode
|
module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode
|
||||||
type: ros2
|
type: ros2
|
||||||
schema:
|
schema:
|
||||||
properties: {}
|
properties: {}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,10 +6,9 @@ from typing import Any
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from unilabos.utils import logger
|
|
||||||
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema
|
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema
|
||||||
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.decorator import singleton
|
from unilabos.utils.decorator import singleton
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
|
||||||
|
|
||||||
DEFAULT_PATHS = [Path(__file__).absolute().parent]
|
DEFAULT_PATHS = [Path(__file__).absolute().parent]
|
||||||
|
|
||||||
@@ -21,43 +19,14 @@ class Registry:
|
|||||||
self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值
|
self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值
|
||||||
if registry_paths:
|
if registry_paths:
|
||||||
self.registry_paths.extend(registry_paths)
|
self.registry_paths.extend(registry_paths)
|
||||||
action_type = self._replace_type_with_class(
|
self.ResourceCreateFromOuter = self._replace_type_with_class(
|
||||||
"ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer"
|
"ResourceCreateFromOuter", "host_node", f"动作 create_resource_detailed"
|
||||||
)
|
)
|
||||||
schema = ros_action_to_json_schema(action_type)
|
self.ResourceCreateFromOuterEasy = self._replace_type_with_class(
|
||||||
self.device_type_registry = {
|
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
|
||||||
"host_node": {
|
)
|
||||||
"description": "UniLabOS主机节点",
|
self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"")
|
||||||
"class": {
|
self.device_type_registry = {}
|
||||||
"module": "unilabos.ros.nodes.presets.host_node",
|
|
||||||
"type": "python",
|
|
||||||
"status_types": {},
|
|
||||||
"action_value_mappings": {
|
|
||||||
"add_resource_from_outer": {
|
|
||||||
"type": msg_converter_manager.search_class("ResourceCreateFromOuter"),
|
|
||||||
"goal": {
|
|
||||||
"resources": "resources",
|
|
||||||
"device_ids": "device_ids",
|
|
||||||
"bind_parent_ids": "bind_parent_ids",
|
|
||||||
"bind_locations": "bind_locations",
|
|
||||||
"other_calling_params": "other_calling_params",
|
|
||||||
},
|
|
||||||
"feedback": {},
|
|
||||||
"result": {
|
|
||||||
"success": "success"
|
|
||||||
},
|
|
||||||
"schema": schema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schema": {
|
|
||||||
"properties": {},
|
|
||||||
"additionalProperties": False,
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"file_path": "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.resource_type_registry = {}
|
self.resource_type_registry = {}
|
||||||
self._setup_called = False # 跟踪setup是否已调用
|
self._setup_called = False # 跟踪setup是否已调用
|
||||||
# 其他状态变量
|
# 其他状态变量
|
||||||
@@ -69,9 +38,83 @@ class Registry:
|
|||||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 标记setup已被调用
|
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||||
self._setup_called = True
|
|
||||||
|
|
||||||
|
self.device_type_registry.update(
|
||||||
|
{
|
||||||
|
"host_node": {
|
||||||
|
"description": "UniLabOS主机节点",
|
||||||
|
"class": {
|
||||||
|
"module": "unilabos.ros.nodes.presets.host_node",
|
||||||
|
"type": "python",
|
||||||
|
"status_types": {},
|
||||||
|
"action_value_mappings": {
|
||||||
|
"create_resource_detailed": {
|
||||||
|
"type": self.ResourceCreateFromOuter,
|
||||||
|
"goal": {
|
||||||
|
"resources": "resources",
|
||||||
|
"device_ids": "device_ids",
|
||||||
|
"bind_parent_ids": "bind_parent_ids",
|
||||||
|
"bind_locations": "bind_locations",
|
||||||
|
"other_calling_params": "other_calling_params",
|
||||||
|
},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"success": "success"},
|
||||||
|
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter),
|
||||||
|
"goal_default": yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
|
||||||
|
),
|
||||||
|
"handles": {},
|
||||||
|
},
|
||||||
|
"create_resource": {
|
||||||
|
"type": self.ResourceCreateFromOuterEasy,
|
||||||
|
"goal": {
|
||||||
|
"res_id": "res_id",
|
||||||
|
"class_name": "class_name",
|
||||||
|
"parent": "parent",
|
||||||
|
"device_id": "device_id",
|
||||||
|
"bind_locations": "bind_locations",
|
||||||
|
"liquid_input_slot": "liquid_input_slot[]",
|
||||||
|
"liquid_type": "liquid_type[]",
|
||||||
|
"liquid_volume": "liquid_volume[]",
|
||||||
|
"slot_on_deck": "slot_on_deck",
|
||||||
|
},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"success": "success"},
|
||||||
|
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy),
|
||||||
|
"goal_default": yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
|
||||||
|
),
|
||||||
|
"handles": {
|
||||||
|
"output": [{
|
||||||
|
"handler_key": "Labware",
|
||||||
|
"label": "Labware",
|
||||||
|
"data_type": "resource",
|
||||||
|
"data_source": "handle",
|
||||||
|
"data_key": "liquid"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"test_latency": {
|
||||||
|
"type": self.EmptyIn,
|
||||||
|
"goal": {},
|
||||||
|
"feedback": {},
|
||||||
|
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
|
||||||
|
"schema": ros_action_to_json_schema(self.EmptyIn),
|
||||||
|
"goal_default": {},
|
||||||
|
"handles": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"icon": "icon_device.webp",
|
||||||
|
"registry_type": "device",
|
||||||
|
"handles": [],
|
||||||
|
"init_param_schema": {},
|
||||||
|
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
|
||||||
|
"file_path": "/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
||||||
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
||||||
for i, path in enumerate(self.registry_paths):
|
for i, path in enumerate(self.registry_paths):
|
||||||
@@ -81,6 +124,8 @@ class Registry:
|
|||||||
self.load_device_types(path)
|
self.load_device_types(path)
|
||||||
self.load_resource_types(path)
|
self.load_resource_types(path)
|
||||||
logger.info("[UniLab Registry] 注册表设置完成")
|
logger.info("[UniLab Registry] 注册表设置完成")
|
||||||
|
# 标记setup已被调用
|
||||||
|
self._setup_called = True
|
||||||
|
|
||||||
def load_resource_types(self, path: os.PathLike):
|
def load_resource_types(self, path: os.PathLike):
|
||||||
abs_path = Path(path).absolute()
|
abs_path = Path(path).absolute()
|
||||||
@@ -96,6 +141,13 @@ class Registry:
|
|||||||
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
if "description" not in resource_info:
|
if "description" not in resource_info:
|
||||||
resource_info["description"] = ""
|
resource_info["description"] = ""
|
||||||
|
if "icon" not in resource_info:
|
||||||
|
resource_info["icon"] = ""
|
||||||
|
if "handles" not in resource_info:
|
||||||
|
resource_info["handles"] = []
|
||||||
|
if "init_param_schema" not in resource_info:
|
||||||
|
resource_info["init_param_schema"] = {}
|
||||||
|
resource_info["registry_type"] = "resource"
|
||||||
self.resource_type_registry.update(data)
|
self.resource_type_registry.update(data)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
||||||
@@ -145,6 +197,7 @@ class Registry:
|
|||||||
)
|
)
|
||||||
current_device_number = len(self.device_type_registry) + 1
|
current_device_number = len(self.device_type_registry) + 1
|
||||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||||
|
|
||||||
for i, file in enumerate(files):
|
for i, file in enumerate(files):
|
||||||
data = yaml.safe_load(open(file, encoding="utf-8"))
|
data = yaml.safe_load(open(file, encoding="utf-8"))
|
||||||
if data:
|
if data:
|
||||||
@@ -154,6 +207,13 @@ class Registry:
|
|||||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
if "description" not in device_config:
|
if "description" not in device_config:
|
||||||
device_config["description"] = ""
|
device_config["description"] = ""
|
||||||
|
if "icon" not in device_config:
|
||||||
|
device_config["icon"] = ""
|
||||||
|
if "handles" not in device_config:
|
||||||
|
device_config["handles"] = []
|
||||||
|
if "init_param_schema" not in device_config:
|
||||||
|
device_config["init_param_schema"] = {}
|
||||||
|
device_config["registry_type"] = "device"
|
||||||
if "class" in device_config:
|
if "class" in device_config:
|
||||||
# 处理状态类型
|
# 处理状态类型
|
||||||
if "status_types" in device_config["class"]:
|
if "status_types" in device_config["class"]:
|
||||||
@@ -165,12 +225,21 @@ class Registry:
|
|||||||
# 处理动作值映射
|
# 处理动作值映射
|
||||||
if "action_value_mappings" in device_config["class"]:
|
if "action_value_mappings" in device_config["class"]:
|
||||||
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
|
||||||
|
if "handles" not in action_config:
|
||||||
|
action_config["handles"] = []
|
||||||
if "type" in action_config:
|
if "type" in action_config:
|
||||||
action_config["type"] = self._replace_type_with_class(
|
action_config["type"] = self._replace_type_with_class(
|
||||||
action_config["type"], device_id, f"动作 {action_name}"
|
action_config["type"], device_id, f"动作 {action_name}"
|
||||||
)
|
)
|
||||||
action_config["goal_default"] = yaml.safe_load(io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal)))
|
if action_config["type"] is not None:
|
||||||
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
action_config["goal_default"] = yaml.safe_load(
|
||||||
|
io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal))
|
||||||
|
)
|
||||||
|
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换"
|
||||||
|
)
|
||||||
|
|
||||||
self.device_type_registry.update(data)
|
self.device_type_registry.update(data)
|
||||||
|
|
||||||
@@ -188,13 +257,17 @@ class Registry:
|
|||||||
def obtain_registry_device_info(self):
|
def obtain_registry_device_info(self):
|
||||||
devices = []
|
devices = []
|
||||||
for device_id, device_info in self.device_type_registry.items():
|
for device_id, device_info in self.device_type_registry.items():
|
||||||
msg = {
|
msg = {"id": device_id, **device_info}
|
||||||
"id": device_id,
|
|
||||||
**device_info
|
|
||||||
}
|
|
||||||
devices.append(msg)
|
devices.append(msg)
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
def obtain_registry_resource_info(self):
|
||||||
|
resources = []
|
||||||
|
for resource_id, resource_info in self.resource_type_registry.items():
|
||||||
|
msg = {"id": resource_id, **resource_info}
|
||||||
|
resources.append(msg)
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
# 全局单例实例
|
# 全局单例实例
|
||||||
lab_registry = Registry()
|
lab_registry = Registry()
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
agilent_1_reservoir_290ml:
|
agilent_1_reservoir_290ml:
|
||||||
description: Agilent 1 reservoir 290ml
|
description: Agilent 1 reservoir 290ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:agilent_1_reservoir_290ml
|
module: pylabrobot.resources.opentrons.reservoirs:agilent_1_reservoir_290ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|
||||||
axygen_1_reservoir_90ml:
|
axygen_1_reservoir_90ml:
|
||||||
description: Axygen 1 reservoir 90ml
|
description: Axygen 1 reservoir 90ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:axygen_1_reservoir_90ml
|
module: pylabrobot.resources.opentrons.reservoirs:axygen_1_reservoir_90ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|
||||||
nest_12_reservoir_15ml:
|
nest_12_reservoir_15ml:
|
||||||
description: Nest 12 reservoir 15ml
|
description: Nest 12 reservoir 15ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:nest_12_reservoir_15ml
|
module: pylabrobot.resources.opentrons.reservoirs:nest_12_reservoir_15ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|
||||||
nest_1_reservoir_195ml:
|
nest_1_reservoir_195ml:
|
||||||
description: Nest 1 reservoir 195ml
|
description: Nest 1 reservoir 195ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_195ml
|
module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_195ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|
||||||
nest_1_reservoir_290ml:
|
nest_1_reservoir_290ml:
|
||||||
description: Nest 1 reservoir 290ml
|
description: Nest 1 reservoir 290ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_290ml
|
module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_290ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|
||||||
usascientific_12_reservoir_22ml:
|
usascientific_12_reservoir_22ml:
|
||||||
description: USAScientific 12 reservoir 22ml
|
description: USAScientific 12 reservoir 22ml
|
||||||
class:
|
class:
|
||||||
module: pylabrobot.resources.opentrons.reserviors:usascientific_12_reservoir_22ml
|
module: pylabrobot.resources.opentrons.reservoirs:usascientific_12_reservoir_22ml
|
||||||
type: pylabrobot
|
type: pylabrobot
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ def dict_from_graph(graph: nx.Graph) -> dict:
|
|||||||
def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||||
# 将节点转换为字典,以便通过 ID 快速查找
|
# 将节点转换为字典,以便通过 ID 快速查找
|
||||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||||
|
id_list = [node["id"] for node in nodes_list]
|
||||||
|
|
||||||
# 初始化每个节点的 children 为包含节点字典的列表
|
# 初始化每个节点的 children 为包含节点字典的列表
|
||||||
for node in nodes_list:
|
for node in nodes_list:
|
||||||
@@ -196,7 +197,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
|||||||
|
|
||||||
# 找到根节点并返回
|
# 找到根节点并返回
|
||||||
root_nodes = [
|
root_nodes = [
|
||||||
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan]
|
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or node.get("parent", node.get("parent_name")) not in id_list
|
||||||
]
|
]
|
||||||
|
|
||||||
# 如果存在多个根节点,返回所有根节点
|
# 如果存在多个根节点,返回所有根节点
|
||||||
@@ -421,7 +422,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]:
|
def initialize_resource(resource_config: dict) -> list[dict]:
|
||||||
"""Initializes a resource based on its configuration.
|
"""Initializes a resource based on its configuration.
|
||||||
|
|
||||||
If the config is detailed, then do nothing;
|
If the config is detailed, then do nothing;
|
||||||
@@ -433,6 +434,7 @@ def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
from unilabos.registry.registry import lab_registry
|
||||||
resource_class_config = resource_config.get("class", None)
|
resource_class_config = resource_config.get("class", None)
|
||||||
if resource_class_config is None:
|
if resource_class_config is None:
|
||||||
return [resource_config]
|
return [resource_config]
|
||||||
@@ -476,11 +478,8 @@ def initialize_resources(resources_config) -> list[dict]:
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unilabos.registry.registry import lab_registry
|
|
||||||
resources = []
|
resources = []
|
||||||
for resource_config in resources_config:
|
for resource_config in resources_config:
|
||||||
if resource_config["parent"] == "tip_rack" or resource_config["parent"] == "plate_well":
|
resources.extend(initialize_resource(resource_config))
|
||||||
continue
|
|
||||||
resources.extend(initialize_resource(resource_config, lab_registry))
|
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ _msg_converter: Dict[Type, Any] = {
|
|||||||
Bool: lambda x: Bool(data=bool(x)),
|
Bool: lambda x: Bool(data=bool(x)),
|
||||||
str: str,
|
str: str,
|
||||||
String: lambda x: String(data=str(x)),
|
String: lambda x: String(data=str(x)),
|
||||||
Point: lambda x: Point(x=x.x, y=x.y, z=x.z),
|
Point: lambda x: Point(x=x.x, y=x.y, z=x.z) if not isinstance(x, dict) else Point(x=x.get("x", 0), y=x.get("y", 0), z=x.get("z", 0)),
|
||||||
Resource: lambda x: Resource(
|
Resource: lambda x: Resource(
|
||||||
id=x.get("id", ""),
|
id=x.get("id", ""),
|
||||||
name=x.get("name", ""),
|
name=x.get("name", ""),
|
||||||
@@ -348,10 +348,16 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
|
|||||||
if isinstance(td, NamespacedType):
|
if isinstance(td, NamespacedType):
|
||||||
target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}")
|
target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}")
|
||||||
setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value])
|
setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value])
|
||||||
|
elif isinstance(td, UnboundedString):
|
||||||
|
setattr(ros_msg, key, value)
|
||||||
else:
|
else:
|
||||||
|
logger.warning(f"Not Supported type: {td}")
|
||||||
setattr(ros_msg, key, []) # FIXME
|
setattr(ros_msg, key, []) # FIXME
|
||||||
elif "array.array" in str(type(attr)):
|
elif "array.array" in str(type(attr)):
|
||||||
setattr(ros_msg, key, value)
|
if attr.typecode == "f":
|
||||||
|
setattr(ros_msg, key, [float(i) for i in value])
|
||||||
|
else:
|
||||||
|
setattr(ros_msg, key, 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)
|
||||||
@@ -574,6 +580,7 @@ basic_type_map = {
|
|||||||
'int64': {'type': 'integer'},
|
'int64': {'type': 'integer'},
|
||||||
'uint64': {'type': 'integer', 'minimum': 0},
|
'uint64': {'type': 'integer', 'minimum': 0},
|
||||||
'double': {'type': 'number'},
|
'double': {'type': 'number'},
|
||||||
|
'float': {'type': 'number'},
|
||||||
'float32': {'type': 'number'},
|
'float32': {'type': 'number'},
|
||||||
'float64': {'type': 'number'},
|
'float64': {'type': 'number'},
|
||||||
'string': {'type': 'string'},
|
'string': {'type': 'string'},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -18,16 +19,29 @@ from rclpy.service import Service
|
|||||||
from unilabos_msgs.action import SendCmd
|
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.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \
|
from unilabos.resources.graphio import (
|
||||||
initialize_resources
|
convert_resources_to_type,
|
||||||
|
convert_resources_from_type,
|
||||||
|
resource_ulab_to_plr,
|
||||||
|
initialize_resources,
|
||||||
|
dict_to_tree,
|
||||||
|
resource_plr_to_ulab,
|
||||||
|
tree_to_list,
|
||||||
|
)
|
||||||
from unilabos.ros.msgs.message_converter import (
|
from unilabos.ros.msgs.message_converter import (
|
||||||
convert_to_ros_msg,
|
convert_to_ros_msg,
|
||||||
convert_from_ros_msg,
|
convert_from_ros_msg,
|
||||||
convert_from_ros_msg_with_mapping,
|
convert_from_ros_msg_with_mapping,
|
||||||
convert_to_ros_msg_with_mapping, ros_action_to_json_schema,
|
convert_to_ros_msg_with_mapping,
|
||||||
)
|
)
|
||||||
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
|
from unilabos_msgs.srv import (
|
||||||
SerialCommand # type: ignore
|
ResourceAdd,
|
||||||
|
ResourceGet,
|
||||||
|
ResourceDelete,
|
||||||
|
ResourceUpdate,
|
||||||
|
ResourceList,
|
||||||
|
SerialCommand,
|
||||||
|
) # type: ignore
|
||||||
from unilabos_msgs.msg import Resource # type: ignore
|
from unilabos_msgs.msg import Resource # type: ignore
|
||||||
|
|
||||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
@@ -35,7 +49,7 @@ from unilabos.ros.x.rclpyx import get_event_loop
|
|||||||
from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||||
from unilabos.utils.async_util import run_async_func
|
from unilabos.utils.async_util import run_async_func
|
||||||
from unilabos.utils.log import info, debug, warning, error, critical, logger
|
from unilabos.utils.log import info, debug, warning, error, critical, logger
|
||||||
from unilabos.utils.type_check import get_type_class, TypeEncoder
|
from unilabos.utils.type_check import get_type_class, TypeEncoder, serialize_result_info
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -290,7 +304,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self.create_ros_action_server(action_name, action_value_mapping)
|
self.create_ros_action_server(action_name, action_value_mapping)
|
||||||
|
|
||||||
# 创建线程池执行器
|
# 创建线程池执行器
|
||||||
self._executor = ThreadPoolExecutor(max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}")
|
self._executor = ThreadPoolExecutor(
|
||||||
|
max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# 创建资源管理客户端
|
# 创建资源管理客户端
|
||||||
self._resource_clients: Dict[str, Client] = {
|
self._resource_clients: Dict[str, Client] = {
|
||||||
@@ -311,7 +327,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
# 物料传输到对应的node节点
|
# 物料传输到对应的node节点
|
||||||
rclient = self.create_client(ResourceAdd, "/resources/add")
|
rclient = self.create_client(ResourceAdd, "/resources/add")
|
||||||
rclient.wait_for_service()
|
rclient.wait_for_service()
|
||||||
|
rclient2 = self.create_client(ResourceAdd, "/resources/add")
|
||||||
|
rclient2.wait_for_service()
|
||||||
request = ResourceAdd.Request()
|
request = ResourceAdd.Request()
|
||||||
|
request2 = ResourceAdd.Request()
|
||||||
command_json = json.loads(req.command)
|
command_json = json.loads(req.command)
|
||||||
namespace = command_json["namespace"]
|
namespace = command_json["namespace"]
|
||||||
bind_parent_id = command_json["bind_parent_id"]
|
bind_parent_id = command_json["bind_parent_id"]
|
||||||
@@ -320,11 +339,25 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
other_calling_param = command_json["other_calling_param"]
|
other_calling_param = command_json["other_calling_param"]
|
||||||
resources = command_json["resource"]
|
resources = command_json["resource"]
|
||||||
initialize_full = other_calling_param.pop("initialize_full", False)
|
initialize_full = other_calling_param.pop("initialize_full", False)
|
||||||
|
# 用来增加液体
|
||||||
|
ADD_LIQUID_TYPE = other_calling_param.pop("ADD_LIQUID_TYPE", [])
|
||||||
|
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
||||||
|
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
||||||
|
slot = other_calling_param.pop("slot", -1)
|
||||||
|
if slot >= 0: # slot为负数的时候采用assign方法
|
||||||
|
other_calling_param["slot"] = slot
|
||||||
# 本地拿到这个物料,可能需要先做初始化?
|
# 本地拿到这个物料,可能需要先做初始化?
|
||||||
if isinstance(resources, list):
|
if isinstance(resources, list):
|
||||||
if initialize_full:
|
if (
|
||||||
|
len(resources) == 1 and isinstance(resources[0], list) and not initialize_full
|
||||||
|
): # 取消,不存在的情况
|
||||||
|
# 预先initialize过,以整组的形式传入
|
||||||
|
request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]]
|
||||||
|
elif initialize_full:
|
||||||
resources = initialize_resources(resources)
|
resources = initialize_resources(resources)
|
||||||
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
||||||
|
else:
|
||||||
|
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
||||||
else:
|
else:
|
||||||
if initialize_full:
|
if initialize_full:
|
||||||
resources = initialize_resources([resources])
|
resources = initialize_resources([resources])
|
||||||
@@ -332,31 +365,70 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
response = rclient.call(request)
|
response = rclient.call(request)
|
||||||
# 应该先add_resource了
|
# 应该先add_resource了
|
||||||
res.response = "OK"
|
res.response = "OK"
|
||||||
|
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||||
|
if hasattr(self.driver_instance, "create_resource"):
|
||||||
|
create_resource_func = getattr(self.driver_instance, "create_resource")
|
||||||
|
create_resource_func(
|
||||||
|
resource_tracker=self.resource_tracker,
|
||||||
|
resources=request.resources,
|
||||||
|
bind_parent_id=bind_parent_id,
|
||||||
|
bind_location=location,
|
||||||
|
liquid_input_slot=LIQUID_INPUT_SLOT,
|
||||||
|
liquid_type=ADD_LIQUID_TYPE,
|
||||||
|
liquid_volume=LIQUID_VOLUME,
|
||||||
|
slot_on_deck=slot,
|
||||||
|
)
|
||||||
|
return res
|
||||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
||||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||||
request.resources = [convert_to_ros_msg(Resource, resources)]
|
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
from pylabrobot.resources.deck import Deck
|
from pylabrobot.resources.deck import Deck
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.resources import OTDeck
|
from pylabrobot.resources import OTDeck
|
||||||
|
from pylabrobot.resources import Plate
|
||||||
|
|
||||||
contain_model = not isinstance(resource, Deck)
|
contain_model = not isinstance(resource, Deck)
|
||||||
if isinstance(resource, ResourcePLR):
|
if isinstance(resource, ResourcePLR):
|
||||||
# resources.list()
|
# resources.list()
|
||||||
plr_instance = resource_ulab_to_plr(resources, contain_model)
|
resources_tree = dict_to_tree(copy.deepcopy({r["id"]: r for r in resources}))
|
||||||
|
plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model)
|
||||||
|
if isinstance(plr_instance, Plate):
|
||||||
|
empty_liquid_info_in = [(None, 0)] * plr_instance.num_items
|
||||||
|
for liquid_type, liquid_volume, liquid_input_slot in zip(
|
||||||
|
ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
|
||||||
|
):
|
||||||
|
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
|
||||||
|
plr_instance.set_well_liquids(empty_liquid_info_in)
|
||||||
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
|
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
|
||||||
resource.assign_child_at_slot(plr_instance, **other_calling_param)
|
resource.assign_child_at_slot(plr_instance, **other_calling_param)
|
||||||
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param)
|
else:
|
||||||
|
_discard_slot = other_calling_param.pop("slot", -1)
|
||||||
|
resource.assign_child_resource(
|
||||||
|
plr_instance,
|
||||||
|
Coordinate(location["x"], location["y"], location["z"]),
|
||||||
|
**other_calling_param,
|
||||||
|
)
|
||||||
|
request2.resources = [
|
||||||
|
convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])
|
||||||
|
]
|
||||||
|
rclient2.call(request2)
|
||||||
# 发送给ResourceMeshManager
|
# 发送给ResourceMeshManager
|
||||||
action_client = ActionClient(
|
action_client = ActionClient(
|
||||||
self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group
|
self,
|
||||||
|
SendCmd,
|
||||||
|
"/devices/resource_mesh_manager/add_resource_mesh",
|
||||||
|
callback_group=self.callback_group,
|
||||||
)
|
)
|
||||||
goal = SendCmd.Goal()
|
goal = SendCmd.Goal()
|
||||||
goal.command = json.dumps({
|
goal.command = json.dumps(
|
||||||
"resources": resources,
|
{
|
||||||
"bind_parent_id": bind_parent_id,
|
"resources": resources,
|
||||||
})
|
"bind_parent_id": bind_parent_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
||||||
|
|
||||||
def done_cb(*args):
|
def done_cb(*args):
|
||||||
@@ -373,10 +445,16 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
self._service_server: Dict[str, Service] = {
|
self._service_server: Dict[str, Service] = {
|
||||||
"query_host_name": self.create_service(
|
"query_host_name": self.create_service(
|
||||||
SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group
|
SerialCommand,
|
||||||
|
f"/srv{self.namespace}/query_host_name",
|
||||||
|
query_host_name_cb,
|
||||||
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"append_resource": self.create_service(
|
"append_resource": self.create_service(
|
||||||
SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group
|
SerialCommand,
|
||||||
|
f"/srv{self.namespace}/append_resource",
|
||||||
|
append_resource,
|
||||||
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +482,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
# 加入全局注册表
|
# 加入全局注册表
|
||||||
registered_devices[self.device_id] = device_info
|
registered_devices[self.device_id] = device_info
|
||||||
from unilabos.config.config import BasicConfig
|
from unilabos.config.config import BasicConfig
|
||||||
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
|
|
||||||
if not BasicConfig.is_host_mode:
|
if not BasicConfig.is_host_mode:
|
||||||
sclient = self.create_client(SerialCommand, "/node_info_update")
|
sclient = self.create_client(SerialCommand, "/node_info_update")
|
||||||
# 启动线程执行发送任务
|
# 启动线程执行发送任务
|
||||||
@@ -411,19 +491,29 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
target=self.send_slave_node_info,
|
target=self.send_slave_node_info,
|
||||||
args=(sclient,),
|
args=(sclient,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"ROSDevice{self.device_id}_send_slave_node_info"
|
name=f"ROSDevice{self.device_id}_send_slave_node_info",
|
||||||
).start()
|
).start()
|
||||||
|
else:
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if host_node is not None:
|
||||||
|
host_node.device_machine_names[self.device_id] = "本地"
|
||||||
|
|
||||||
def send_slave_node_info(self, sclient):
|
def send_slave_node_info(self, sclient):
|
||||||
sclient.wait_for_service()
|
sclient.wait_for_service()
|
||||||
request = SerialCommand.Request()
|
request = SerialCommand.Request()
|
||||||
from unilabos.config.config import BasicConfig
|
from unilabos.config.config import BasicConfig
|
||||||
request.command = json.dumps({
|
|
||||||
"SYNC_SLAVE_NODE_INFO": {
|
request.command = json.dumps(
|
||||||
"machine_name": BasicConfig.machine_name,
|
{
|
||||||
"type": "slave",
|
"SYNC_SLAVE_NODE_INFO": {
|
||||||
"edge_device_id": self.device_id
|
"machine_name": BasicConfig.machine_name,
|
||||||
}}, ensure_ascii=False, cls=TypeEncoder)
|
"type": "slave",
|
||||||
|
"edge_device_id": self.device_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
cls=TypeEncoder,
|
||||||
|
)
|
||||||
|
|
||||||
# 发送异步请求并等待结果
|
# 发送异步请求并等待结果
|
||||||
future = sclient.call_async(request)
|
future = sclient.call_async(request)
|
||||||
@@ -481,10 +571,26 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
self.lab_logger().debug(f"发布动作: {action_name}, 类型: {str_action_type}")
|
self.lab_logger().debug(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||||
|
|
||||||
|
def get_real_function(self, instance, attr_name):
|
||||||
|
if hasattr(instance.__class__, attr_name):
|
||||||
|
obj = getattr(instance.__class__, attr_name)
|
||||||
|
if isinstance(obj, property):
|
||||||
|
return lambda *args, **kwargs: obj.fset(instance, *args, **kwargs), get_type_hints(obj.fset)
|
||||||
|
obj = getattr(instance, attr_name)
|
||||||
|
return obj, get_type_hints(obj)
|
||||||
|
else:
|
||||||
|
obj = getattr(instance, attr_name)
|
||||||
|
return obj, get_type_hints(obj)
|
||||||
|
|
||||||
def _create_execute_callback(self, action_name, action_value_mapping):
|
def _create_execute_callback(self, action_name, action_value_mapping):
|
||||||
"""创建动作执行回调函数"""
|
"""创建动作执行回调函数"""
|
||||||
|
|
||||||
async def execute_callback(goal_handle: ServerGoalHandle):
|
async def execute_callback(goal_handle: ServerGoalHandle):
|
||||||
|
# 初始化结果信息变量
|
||||||
|
execution_error = ""
|
||||||
|
execution_success = False
|
||||||
|
action_return_value = None
|
||||||
|
|
||||||
self.lab_logger().info(f"执行动作: {action_name}")
|
self.lab_logger().info(f"执行动作: {action_name}")
|
||||||
goal = goal_handle.request
|
goal = goal_handle.request
|
||||||
|
|
||||||
@@ -495,22 +601,21 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
for i, action in enumerate(self._action_value_mappings["sequence"]):
|
for i, action in enumerate(self._action_value_mappings["sequence"]):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
||||||
getattr(self.driver_instance, action)(**kwargs)
|
self.get_real_function(self.driver_instance, action)[0](**kwargs)
|
||||||
else:
|
else:
|
||||||
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
||||||
getattr(self.driver_instance, action)()
|
self.get_real_function(self.driver_instance, action)[0]()
|
||||||
|
|
||||||
action_paramtypes = get_type_hints(
|
action_paramtypes = get_type_hints(
|
||||||
getattr(self.driver_instance, self._action_value_mappings["sequence"][0])
|
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
|
||||||
)
|
)[1]
|
||||||
else:
|
else:
|
||||||
ACTION = getattr(self.driver_instance, action_name)
|
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||||
action_paramtypes = get_type_hints(ACTION)
|
|
||||||
|
|
||||||
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||||
self.lab_logger().debug(f"接收到原始目标: {action_kwargs}")
|
self.lab_logger().debug(f"接收到原始目标: {action_kwargs}")
|
||||||
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
||||||
if action_name != "add_resource_from_outer":
|
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||||
for k, v in goal.get_fields_and_field_types().items():
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}")
|
self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}")
|
||||||
@@ -525,7 +630,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
current_resources.extend(response.resources)
|
current_resources.extend(response.resources)
|
||||||
else:
|
else:
|
||||||
r = ResourceGet.Request()
|
r = ResourceGet.Request()
|
||||||
r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"]
|
r.id = (
|
||||||
|
action_kwargs[k]["id"]
|
||||||
|
if v == "unilabos_msgs/Resource"
|
||||||
|
else action_kwargs[k][0]["id"]
|
||||||
|
)
|
||||||
r.with_children = True
|
r.with_children = True
|
||||||
response = await self._resource_clients["resource_get"].call_async(r)
|
response = await self._resource_clients["resource_get"].call_async(r)
|
||||||
current_resources.extend(response.resources)
|
current_resources.extend(response.resources)
|
||||||
@@ -548,7 +657,19 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if asyncio.iscoroutinefunction(ACTION):
|
if asyncio.iscoroutinefunction(ACTION):
|
||||||
try:
|
try:
|
||||||
self.lab_logger().info(f"异步执行动作 {ACTION}")
|
self.lab_logger().info(f"异步执行动作 {ACTION}")
|
||||||
future = ROS2DeviceNode.run_async_func(ACTION, **action_kwargs)
|
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
|
||||||
|
|
||||||
|
def _handle_future_exception(fut):
|
||||||
|
nonlocal execution_error, execution_success, action_return_value
|
||||||
|
try:
|
||||||
|
action_return_value = fut.result()
|
||||||
|
execution_success = True
|
||||||
|
except Exception as e:
|
||||||
|
execution_error = traceback.format_exc()
|
||||||
|
error(f"异步任务 {ACTION.__name__} 报错了")
|
||||||
|
error(traceback.format_exc())
|
||||||
|
|
||||||
|
future.add_done_callback(_handle_future_exception)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
|
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
@@ -557,9 +678,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
future = self._executor.submit(ACTION, **action_kwargs)
|
future = self._executor.submit(ACTION, **action_kwargs)
|
||||||
|
|
||||||
def _handle_future_exception(fut):
|
def _handle_future_exception(fut):
|
||||||
|
nonlocal execution_error, execution_success, action_return_value
|
||||||
try:
|
try:
|
||||||
fut.result()
|
action_return_value = fut.result()
|
||||||
|
execution_success = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
execution_error = traceback.format_exc()
|
||||||
error(f"同步任务 {ACTION.__name__} 报错了")
|
error(f"同步任务 {ACTION.__name__} 报错了")
|
||||||
error(traceback.format_exc())
|
error(traceback.format_exc())
|
||||||
|
|
||||||
@@ -609,7 +733,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
del future
|
del future
|
||||||
|
|
||||||
# 向Host更新物料当前状态
|
# 向Host更新物料当前状态
|
||||||
if action_name != "add_resource_from_outer":
|
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||||
for k, v in goal.get_fields_and_field_types().items():
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
continue
|
continue
|
||||||
@@ -650,6 +774,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
for attr_name in result_msg_types.keys():
|
for attr_name in result_msg_types.keys():
|
||||||
if attr_name in ["success", "reached_goal"]:
|
if attr_name in ["success", "reached_goal"]:
|
||||||
setattr(result_msg, attr_name, True)
|
setattr(result_msg, attr_name, True)
|
||||||
|
elif attr_name == "return_info":
|
||||||
|
setattr(result_msg, attr_name, serialize_result_info(execution_error, execution_success, action_return_value))
|
||||||
|
|
||||||
self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
|
self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
|
||||||
return result_msg
|
return result_msg
|
||||||
@@ -695,8 +821,8 @@ class ROS2DeviceNode:
|
|||||||
return cls._loop
|
return cls._loop
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run_async_func(cls, func, **kwargs):
|
def run_async_func(cls, func, trace_error=True, **kwargs):
|
||||||
return run_async_func(func, loop=cls._loop, **kwargs)
|
return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def driver_instance(self):
|
def driver_instance(self):
|
||||||
@@ -748,7 +874,11 @@ class ROS2DeviceNode:
|
|||||||
self.resource_tracker = DeviceNodeResourceTracker()
|
self.resource_tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
# use_pylabrobot_creator 使用 cls的包路径检测
|
# use_pylabrobot_creator 使用 cls的包路径检测
|
||||||
use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler"
|
use_pylabrobot_creator = (
|
||||||
|
driver_class.__module__.startswith("pylabrobot")
|
||||||
|
or driver_class.__name__ == "LiquidHandlerAbstract"
|
||||||
|
or driver_class.__name__ == "LiquidHandlerBiomek"
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
||||||
# 创建设备类实例
|
# 创建设备类实例
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
|
|||||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||||
from rclpy.service import Service
|
from rclpy.service import Service
|
||||||
from unilabos_msgs.msg import Resource # type: ignore
|
from unilabos_msgs.msg import Resource # type: ignore
|
||||||
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
|
from unilabos_msgs.srv import (
|
||||||
SerialCommand # type: ignore
|
ResourceAdd,
|
||||||
|
ResourceGet,
|
||||||
|
ResourceDelete,
|
||||||
|
ResourceUpdate,
|
||||||
|
ResourceList,
|
||||||
|
SerialCommand,
|
||||||
|
) # type: ignore
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
|
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
from unilabos.resources.graphio import initialize_resource
|
||||||
from unilabos.resources.registry import add_schema
|
from unilabos.resources.registry import add_schema
|
||||||
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 (
|
||||||
@@ -86,6 +93,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.__class__._instance = self
|
self.__class__._instance = self
|
||||||
|
|
||||||
# 初始化配置
|
# 初始化配置
|
||||||
|
self.server_latest_timestamp = 0.0 #
|
||||||
self.devices_config = devices_config
|
self.devices_config = devices_config
|
||||||
self.resources_config = resources_config
|
self.resources_config = resources_config
|
||||||
self.physical_setup_graph = physical_setup_graph
|
self.physical_setup_graph = physical_setup_graph
|
||||||
@@ -99,9 +107,32 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 创建设备、动作客户端和目标存储
|
# 创建设备、动作客户端和目标存储
|
||||||
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
||||||
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
||||||
self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射
|
self.device_machine_names: Dict[str, str] = {
|
||||||
self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例
|
device_id: "本地",
|
||||||
self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
} # 存储设备ID到机器名称的映射
|
||||||
|
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
||||||
|
"/devices/host_node/create_resource": ActionClient(
|
||||||
|
self,
|
||||||
|
lab_registry.ResourceCreateFromOuterEasy,
|
||||||
|
"/devices/host_node/create_resource",
|
||||||
|
callback_group=self.callback_group,
|
||||||
|
),
|
||||||
|
"/devices/host_node/create_resource_detailed": ActionClient(
|
||||||
|
self,
|
||||||
|
lab_registry.ResourceCreateFromOuter,
|
||||||
|
"/devices/host_node/create_resource_detailed",
|
||||||
|
callback_group=self.callback_group,
|
||||||
|
),
|
||||||
|
"/devices/host_node/test_latency": ActionClient(
|
||||||
|
self,
|
||||||
|
lab_registry.EmptyIn,
|
||||||
|
"/devices/host_node/test_latency",
|
||||||
|
callback_group=self.callback_group,
|
||||||
|
),
|
||||||
|
} # 用来存储多个ActionClient实例
|
||||||
|
self._action_value_mappings: Dict[str, Dict] = (
|
||||||
|
{}
|
||||||
|
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
||||||
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 # 上次设备发现的时间
|
||||||
@@ -115,9 +146,12 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
|
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
|
||||||
|
|
||||||
from unilabos.app.mq import mqtt_client
|
from unilabos.app.mq import mqtt_client
|
||||||
for device_config in lab_registry.obtain_registry_device_info():
|
|
||||||
mqtt_client.publish_registry(device_config["id"], device_config)
|
|
||||||
|
|
||||||
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
|
mqtt_client.publish_registry(device_info["id"], device_info)
|
||||||
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
|
mqtt_client.publish_registry(resource_info["id"], resource_info)
|
||||||
|
time.sleep(1) # 等待MQTT连接稳定
|
||||||
# 首次发现网络中的设备
|
# 首次发现网络中的设备
|
||||||
self._discover_devices()
|
self._discover_devices()
|
||||||
|
|
||||||
@@ -141,12 +175,40 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
].items():
|
].items():
|
||||||
controller_config["update_rate"] = update_rate
|
controller_config["update_rate"] = update_rate
|
||||||
self.initialize_controller(controller_id, controller_config)
|
self.initialize_controller(controller_id, controller_config)
|
||||||
|
resources_config.insert(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"id": "host_node",
|
||||||
|
"name": "host_node",
|
||||||
|
"parent": None,
|
||||||
|
"type": "device",
|
||||||
|
"class": "host_node",
|
||||||
|
"position": {"x": 0, "y": 0, "z": 0},
|
||||||
|
"config": {},
|
||||||
|
"data": {},
|
||||||
|
"children": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resource_with_parent_name = []
|
||||||
|
resource_ids_to_instance = {i["id"]: i for i in resources_config}
|
||||||
|
for res in resources_config:
|
||||||
|
if res.get("parent") and res.get("type") == "device" and res.get("class"):
|
||||||
|
parent_id = res.get("parent")
|
||||||
|
parent_res = resource_ids_to_instance[parent_id]
|
||||||
|
if parent_res.get("type") == "device" and parent_res.get("class"):
|
||||||
|
resource_with_parent_name.append(copy.deepcopy(res))
|
||||||
|
resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
|
||||||
|
continue
|
||||||
|
resource_with_parent_name.append(copy.deepcopy(res))
|
||||||
try:
|
try:
|
||||||
for bridge in self.bridges:
|
for bridge in self.bridges:
|
||||||
if hasattr(bridge, "resource_add"):
|
if hasattr(bridge, "resource_add"):
|
||||||
self.lab_logger().info("[Host Node-Resource] Adding resources to bridge.")
|
resource_start_time = time.time()
|
||||||
bridge.resource_add(add_schema(resources_config))
|
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name), True)
|
||||||
|
resource_end_time = time.time()
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||||
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
||||||
self.lab_logger().error(traceback.format_exc())
|
self.lab_logger().error(traceback.format_exc())
|
||||||
@@ -156,6 +218,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加ping-pong相关属性
|
||||||
|
self._ping_responses = {} # 存储ping响应
|
||||||
|
self._ping_lock = threading.Lock()
|
||||||
|
|
||||||
self.lab_logger().info("[Host Node] Host node initialized.")
|
self.lab_logger().info("[Host Node] Host node initialized.")
|
||||||
HostNode._ready_event.set()
|
HostNode._ready_event.set()
|
||||||
|
|
||||||
@@ -191,7 +257,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
# 如果是新设备,记录并创建ActionClient
|
# 如果是新设备,记录并创建ActionClient
|
||||||
if edge_device_id not in self.devices_names:
|
if edge_device_id not in self.devices_names:
|
||||||
self.lab_logger().info(f"[Host Node] Discovered new device: {device_key}")
|
self.lab_logger().info(f"[Host Node] Discovered new device: {edge_device_id}")
|
||||||
self.devices_names[edge_device_id] = namespace
|
self.devices_names[edge_device_id] = namespace
|
||||||
self._create_action_clients_for_device(device_id, namespace)
|
self._create_action_clients_for_device(device_id, namespace)
|
||||||
self._online_devices.add(device_key)
|
self._online_devices.add(device_key)
|
||||||
@@ -200,7 +266,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
target=self._send_re_register,
|
target=self._send_re_register,
|
||||||
args=(sclient,),
|
args=(sclient,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||||
).start()
|
).start()
|
||||||
elif device_key not in self._online_devices:
|
elif device_key not in self._online_devices:
|
||||||
# 设备重新上线
|
# 设备重新上线
|
||||||
@@ -211,7 +277,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
target=self._send_re_register,
|
target=self._send_re_register,
|
||||||
args=(sclient,),
|
args=(sclient,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
# 检测离线设备
|
# 检测离线设备
|
||||||
@@ -255,7 +321,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self, action_type, action_id, callback_group=self.callback_group
|
self, action_type, action_id, callback_group=self.callback_group
|
||||||
)
|
)
|
||||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
||||||
action_name = action_id[len(namespace) + 1:]
|
action_name = action_id[len(namespace) + 1 :]
|
||||||
edge_device_id = namespace[9:]
|
edge_device_id = namespace[9:]
|
||||||
# from unilabos.app.mq import mqtt_client
|
# from unilabos.app.mq import mqtt_client
|
||||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||||
@@ -268,30 +334,84 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
||||||
|
|
||||||
def add_resource_from_outer(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]):
|
def create_resource_detailed(
|
||||||
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params):
|
self,
|
||||||
|
resources: list["Resource"],
|
||||||
|
device_ids: list[str],
|
||||||
|
bind_parent_ids: list[str],
|
||||||
|
bind_locations: list[Point],
|
||||||
|
other_calling_params: list[str],
|
||||||
|
):
|
||||||
|
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(
|
||||||
|
resources, device_ids, bind_parent_ids, bind_locations, other_calling_params
|
||||||
|
):
|
||||||
# 这里要求device_id传入必须是edge_device_id
|
# 这里要求device_id传入必须是edge_device_id
|
||||||
namespace = "/devices/" + device_id
|
namespace = "/devices/" + device_id
|
||||||
srv_address = f"/srv{namespace}/append_resource"
|
srv_address = f"/srv{namespace}/append_resource"
|
||||||
sclient = self.create_client(SerialCommand, srv_address)
|
sclient = self.create_client(SerialCommand, srv_address)
|
||||||
sclient.wait_for_service()
|
sclient.wait_for_service()
|
||||||
request = SerialCommand.Request()
|
request = SerialCommand.Request()
|
||||||
request.command = json.dumps({
|
request.command = json.dumps(
|
||||||
"resource": resource,
|
{
|
||||||
"namespace": namespace,
|
"resource": resource, # 单个/单组 可为 list[list[Resource]]
|
||||||
"edge_device_id": device_id,
|
"namespace": namespace,
|
||||||
"bind_parent_id": bind_parent_id,
|
"edge_device_id": device_id,
|
||||||
"bind_location": {
|
"bind_parent_id": bind_parent_id,
|
||||||
"x": bind_location.x,
|
"bind_location": {
|
||||||
"y": bind_location.y,
|
"x": bind_location.x,
|
||||||
"z": bind_location.z,
|
"y": bind_location.y,
|
||||||
|
"z": bind_location.z,
|
||||||
|
},
|
||||||
|
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
||||||
},
|
},
|
||||||
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
ensure_ascii=False,
|
||||||
}, ensure_ascii=False)
|
)
|
||||||
response = sclient.call(request)
|
response = sclient.call(request)
|
||||||
pass
|
pass
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_resource(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
res_id: str,
|
||||||
|
class_name: str,
|
||||||
|
parent: str,
|
||||||
|
bind_locations: Point,
|
||||||
|
liquid_input_slot: list[int],
|
||||||
|
liquid_type: list[str],
|
||||||
|
liquid_volume: list[int],
|
||||||
|
slot_on_deck: int,
|
||||||
|
):
|
||||||
|
init_new_res = initialize_resource(
|
||||||
|
{
|
||||||
|
"name": res_id,
|
||||||
|
"class": class_name,
|
||||||
|
"parent": parent,
|
||||||
|
"position": {
|
||||||
|
"x": bind_locations.x,
|
||||||
|
"y": bind_locations.y,
|
||||||
|
"z": bind_locations.z,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
) # flatten的格式
|
||||||
|
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||||
|
device_ids = [device_id]
|
||||||
|
bind_parent_id = [parent]
|
||||||
|
bind_location = [bind_locations]
|
||||||
|
other_calling_param = [
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"ADD_LIQUID_TYPE": liquid_type,
|
||||||
|
"LIQUID_VOLUME": liquid_volume,
|
||||||
|
"LIQUID_INPUT_SLOT": liquid_input_slot,
|
||||||
|
"initialize_full": False,
|
||||||
|
"slot": slot_on_deck,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.create_resource_detailed(resources, device_ids, bind_parent_id, bind_location, other_calling_param)
|
||||||
|
|
||||||
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
根据配置初始化设备,
|
根据配置初始化设备,
|
||||||
@@ -319,7 +439,9 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
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)
|
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
||||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的
|
self.lab_logger().debug(
|
||||||
|
f"[Host Node] Created ActionClient (Local): {action_id}"
|
||||||
|
) # 子设备再创建用的是Discover发现的
|
||||||
# from unilabos.app.mq import mqtt_client
|
# from unilabos.app.mq import mqtt_client
|
||||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||||
# mqtt_client.publish_actions(action_name, {
|
# mqtt_client.publish_actions(action_name, {
|
||||||
@@ -419,7 +541,12 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def send_goal(
|
def send_goal(
|
||||||
self, device_id: str, action_name: str, action_kwargs: Dict[str, Any], goal_uuid: Optional[str] = None
|
self,
|
||||||
|
device_id: str,
|
||||||
|
action_name: str,
|
||||||
|
action_kwargs: Dict[str, Any],
|
||||||
|
goal_uuid: Optional[str] = None,
|
||||||
|
server_info: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
向设备发送目标请求
|
向设备发送目标请求
|
||||||
@@ -431,6 +558,8 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
goal_uuid: 目标UUID,如果为None则自动生成
|
goal_uuid: 目标UUID,如果为None则自动生成
|
||||||
"""
|
"""
|
||||||
action_id = f"/devices/{device_id}/{action_name}"
|
action_id = f"/devices/{device_id}/{action_name}"
|
||||||
|
if action_name == "test_latency" and server_info is not None:
|
||||||
|
self.server_latest_timestamp = server_info.get("send_timestamp", 0.0)
|
||||||
if action_id not in self._action_clients:
|
if action_id not in self._action_clients:
|
||||||
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
|
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
|
||||||
return
|
return
|
||||||
@@ -485,13 +614,21 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
"""获取结果回调"""
|
"""获取结果回调"""
|
||||||
result_msg = future.result().result
|
result_msg = future.result().result
|
||||||
result_data = convert_from_ros_msg(result_msg)
|
result_data = convert_from_ros_msg(result_msg)
|
||||||
|
status = "success"
|
||||||
|
try:
|
||||||
|
ret = json.loads(result_data.get("return_info", "{}")) # 确保返回信息是有效的JSON
|
||||||
|
suc = ret.get("suc", False)
|
||||||
|
if not suc:
|
||||||
|
status = "failed"
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
status = "failed"
|
||||||
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success")
|
self.lab_logger().info(f"[Host Node] Result for {action_id} ({uuid_str}): success")
|
||||||
self.lab_logger().debug(f"[Host Node] Result data: {result_data}")
|
self.lab_logger().debug(f"[Host Node] Result data: {result_data}")
|
||||||
|
|
||||||
if uuid_str:
|
if uuid_str:
|
||||||
for bridge in self.bridges:
|
for bridge in self.bridges:
|
||||||
if hasattr(bridge, "publish_job_status"):
|
if hasattr(bridge, "publish_job_status"):
|
||||||
bridge.publish_job_status(result_data, uuid_str, "success")
|
bridge.publish_job_status(result_data, uuid_str, status, result_data.get("return_info", "{}"))
|
||||||
|
|
||||||
def cancel_goal(self, goal_uuid: str) -> None:
|
def cancel_goal(self, goal_uuid: str) -> None:
|
||||||
"""取消目标"""
|
"""取消目标"""
|
||||||
@@ -725,3 +862,147 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# 这里可以实现返回资源列表的逻辑
|
# 这里可以实现返回资源列表的逻辑
|
||||||
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def test_latency(self):
|
||||||
|
"""
|
||||||
|
测试网络延迟的action实现
|
||||||
|
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||||
|
"""
|
||||||
|
import uuid as uuid_module
|
||||||
|
|
||||||
|
self.lab_logger().info("=" * 60)
|
||||||
|
self.lab_logger().info("开始网络延迟测试...")
|
||||||
|
|
||||||
|
# 记录任务开始执行的时间
|
||||||
|
task_start_time = time.time()
|
||||||
|
|
||||||
|
# 进行5次ping-pong测试
|
||||||
|
ping_results = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
self.lab_logger().info(f"第{i+1}/5次ping-pong测试...")
|
||||||
|
|
||||||
|
# 生成唯一的ping ID
|
||||||
|
ping_id = str(uuid_module.uuid4())
|
||||||
|
|
||||||
|
# 记录发送时间
|
||||||
|
send_timestamp = time.time()
|
||||||
|
|
||||||
|
# 发送ping
|
||||||
|
from unilabos.app.mq import mqtt_client
|
||||||
|
|
||||||
|
mqtt_client.send_ping(ping_id, send_timestamp)
|
||||||
|
|
||||||
|
# 等待pong响应
|
||||||
|
timeout = 10.0
|
||||||
|
start_wait_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_wait_time < timeout:
|
||||||
|
with self._ping_lock:
|
||||||
|
if ping_id in self._ping_responses:
|
||||||
|
pong_data = self._ping_responses.pop(ping_id)
|
||||||
|
break
|
||||||
|
time.sleep(0.001)
|
||||||
|
else:
|
||||||
|
self.lab_logger().error(f"❌ 第{i+1}次测试超时")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算本次测试结果
|
||||||
|
receive_timestamp = time.time()
|
||||||
|
client_timestamp = pong_data["client_timestamp"]
|
||||||
|
server_timestamp = pong_data["server_timestamp"]
|
||||||
|
|
||||||
|
# 往返时间
|
||||||
|
rtt_ms = (receive_timestamp - send_timestamp) * 1000
|
||||||
|
|
||||||
|
# 客户端与服务端时间差(客户端时间 - 服务端时间)
|
||||||
|
# 假设网络延迟对称,取中间点的服务端时间
|
||||||
|
mid_point_time = send_timestamp + (receive_timestamp - send_timestamp) / 2
|
||||||
|
time_diff_ms = (mid_point_time - server_timestamp) * 1000
|
||||||
|
|
||||||
|
ping_results.append({"rtt_ms": rtt_ms, "time_diff_ms": time_diff_ms})
|
||||||
|
|
||||||
|
self.lab_logger().info(f"✅ 第{i+1}次: 往返时间={rtt_ms:.2f}ms, 时间差={time_diff_ms:.2f}ms")
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if not ping_results:
|
||||||
|
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
||||||
|
return {"status": "all_timeout"}
|
||||||
|
|
||||||
|
# 统计分析
|
||||||
|
rtts = [r["rtt_ms"] for r in ping_results]
|
||||||
|
time_diffs = [r["time_diff_ms"] for r in ping_results]
|
||||||
|
|
||||||
|
avg_rtt_ms = sum(rtts) / len(rtts)
|
||||||
|
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
||||||
|
max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||||
|
|
||||||
|
self.lab_logger().info("-" * 50)
|
||||||
|
self.lab_logger().info("[测试统计]")
|
||||||
|
self.lab_logger().info(f"有效测试次数: {len(ping_results)}/5")
|
||||||
|
self.lab_logger().info(f"平均往返时间: {avg_rtt_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"平均时间差: {avg_time_diff_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"时间差范围: {min(time_diffs):.2f}ms ~ {max(time_diffs):.2f}ms")
|
||||||
|
self.lab_logger().info(f"最大时间误差: ±{max_time_diff_error_ms:.2f}ms")
|
||||||
|
|
||||||
|
# 计算任务执行延迟
|
||||||
|
if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0:
|
||||||
|
self.lab_logger().info("-" * 50)
|
||||||
|
self.lab_logger().info("[任务执行延迟分析]")
|
||||||
|
self.lab_logger().info(f"服务端任务下发时间: {self.server_latest_timestamp:.6f}")
|
||||||
|
self.lab_logger().info(f"客户端任务开始时间: {task_start_time:.6f}")
|
||||||
|
|
||||||
|
# 原始时间差(不考虑时间同步误差)
|
||||||
|
raw_delay_ms = (task_start_time - self.server_latest_timestamp) * 1000
|
||||||
|
|
||||||
|
# 考虑时间同步误差后的延迟(用平均时间差校正)
|
||||||
|
corrected_delay_ms = raw_delay_ms - avg_time_diff_ms
|
||||||
|
|
||||||
|
self.lab_logger().info(f"📊 原始时间差: {raw_delay_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"🔧 时间同步校正: {avg_time_diff_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"⏰ 实际任务延迟: {corrected_delay_ms:.2f}ms")
|
||||||
|
self.lab_logger().info(f"📏 误差范围: ±{max_time_diff_error_ms:.2f}ms")
|
||||||
|
|
||||||
|
# 给出延迟范围
|
||||||
|
min_delay = corrected_delay_ms - max_time_diff_error_ms
|
||||||
|
max_delay = corrected_delay_ms + max_time_diff_error_ms
|
||||||
|
self.lab_logger().info(f"📋 延迟范围: {min_delay:.2f}ms ~ {max_delay:.2f}ms")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.lab_logger().warning("⚠️ 无法获取服务端任务下发时间,跳过任务延迟分析")
|
||||||
|
corrected_delay_ms = -1
|
||||||
|
|
||||||
|
self.lab_logger().info("=" * 60)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_rtt_ms": avg_rtt_ms,
|
||||||
|
"avg_time_diff_ms": avg_time_diff_ms,
|
||||||
|
"max_time_error_ms": max_time_diff_error_ms,
|
||||||
|
"task_delay_ms": corrected_delay_ms if corrected_delay_ms > 0 else -1,
|
||||||
|
"raw_delay_ms": (
|
||||||
|
raw_delay_ms if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0 else -1
|
||||||
|
),
|
||||||
|
"test_count": len(ping_results),
|
||||||
|
"status": "success",
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_pong_response(self, pong_data: dict):
|
||||||
|
"""
|
||||||
|
处理pong响应
|
||||||
|
"""
|
||||||
|
ping_id = pong_data.get("ping_id")
|
||||||
|
if ping_id:
|
||||||
|
with self._ping_lock:
|
||||||
|
self._ping_responses[ping_id] = pong_data
|
||||||
|
|
||||||
|
# 详细信息合并为一条日志
|
||||||
|
client_timestamp = pong_data.get("client_timestamp", 0)
|
||||||
|
server_timestamp = pong_data.get("server_timestamp", 0)
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
self.lab_logger().debug(
|
||||||
|
f"📨 Pong | ID:{ping_id[:8]}.. | C→S→C: {client_timestamp:.3f}→{server_timestamp:.3f}→{current_time:.3f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.lab_logger().warning("⚠️ 收到无效的Pong响应(缺少ping_id)")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from asyncio import get_event_loop
|
|||||||
from unilabos.utils.log import error
|
from unilabos.utils.log import error
|
||||||
|
|
||||||
|
|
||||||
def run_async_func(func, *, loop=None, **kwargs):
|
def run_async_func(func, *, loop=None, trace_error=True, **kwargs):
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop = get_event_loop()
|
loop = get_event_loop()
|
||||||
|
|
||||||
@@ -17,5 +17,6 @@ def run_async_func(func, *, loop=None, **kwargs):
|
|||||||
error(traceback.format_exc())
|
error(traceback.format_exc())
|
||||||
|
|
||||||
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
||||||
future.add_done_callback(_handle_future_exception)
|
if trace_error:
|
||||||
return future
|
future.add_done_callback(_handle_future_exception)
|
||||||
|
return future
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import collections
|
import collections.abc
|
||||||
import json
|
import json
|
||||||
from typing import get_origin, get_args
|
from typing import get_origin, get_args
|
||||||
|
|
||||||
@@ -21,3 +21,46 @@ class TypeEncoder(json.JSONEncoder):
|
|||||||
return str(obj)[8:-2]
|
return str(obj)[8:-2]
|
||||||
return super().default(obj)
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ResultInfoEncoder(json.JSONEncoder):
|
||||||
|
"""专门用于处理任务执行结果信息的JSON编码器"""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
# 优先处理类型对象
|
||||||
|
if isinstance(obj, type):
|
||||||
|
return str(obj)[8:-2]
|
||||||
|
|
||||||
|
# 对于无法序列化的对象,统一转换为字符串
|
||||||
|
try:
|
||||||
|
# 尝试调用 __dict__ 或者其他序列化方法
|
||||||
|
if hasattr(obj, "__dict__"):
|
||||||
|
return obj.__dict__
|
||||||
|
elif hasattr(obj, "_asdict"): # namedtuple
|
||||||
|
return obj._asdict()
|
||||||
|
elif hasattr(obj, "to_dict"):
|
||||||
|
return obj.to_dict()
|
||||||
|
elif hasattr(obj, "dict"):
|
||||||
|
return obj.dict()
|
||||||
|
else:
|
||||||
|
# 如果都不行,转换为字符串
|
||||||
|
return str(obj)
|
||||||
|
except Exception:
|
||||||
|
# 如果转换失败,直接返回字符串表示
|
||||||
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_result_info(error: str, suc: bool, return_value=None) -> str:
|
||||||
|
"""
|
||||||
|
序列化任务执行结果信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: 错误信息字符串
|
||||||
|
suc: 是否成功的布尔值
|
||||||
|
return_value: 返回值,可以是任何类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON字符串格式的结果信息
|
||||||
|
"""
|
||||||
|
result_info = {"error": error, "suc": suc, "return_value": return_value}
|
||||||
|
|
||||||
|
return json.dumps(result_info, ensure_ascii=False, cls=ResultInfoEncoder)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ endif()
|
|||||||
|
|
||||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
|
add_compile_options(-include cstdint)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# find dependencies
|
# find dependencies
|
||||||
@@ -28,6 +29,8 @@ set(action_files
|
|||||||
"action/HeatChillStart.action"
|
"action/HeatChillStart.action"
|
||||||
"action/HeatChillStop.action"
|
"action/HeatChillStop.action"
|
||||||
|
|
||||||
|
"action/LiquidHandlerProtocolCreation.action"
|
||||||
|
|
||||||
"action/LiquidHandlerAspirate.action"
|
"action/LiquidHandlerAspirate.action"
|
||||||
"action/LiquidHandlerDiscardTips.action"
|
"action/LiquidHandlerDiscardTips.action"
|
||||||
"action/LiquidHandlerDispense.action"
|
"action/LiquidHandlerDispense.action"
|
||||||
@@ -43,14 +46,15 @@ set(action_files
|
|||||||
"action/LiquidHandlerStamp.action"
|
"action/LiquidHandlerStamp.action"
|
||||||
"action/LiquidHandlerTransfer.action"
|
"action/LiquidHandlerTransfer.action"
|
||||||
|
|
||||||
"action/DPLiquidHandlerAddLiquid.action"
|
"action/LiquidHandlerTransferBiomek.action"
|
||||||
"action/DPLiquidHandlerCustomDelay.action"
|
"action/LiquidHandlerIncubateBiomek.action"
|
||||||
"action/DPLiquidHandlerMix.action"
|
"action/LiquidHandlerMoveBiomek.action"
|
||||||
"action/DPLiquidHandlerMoveTo.action"
|
"action/LiquidHandlerOscillateBiomek.action"
|
||||||
"action/DPLiquidHandlerRemoveLiquid.action"
|
|
||||||
"action/DPLiquidHandlerSetTiprack.action"
|
"action/LiquidHandlerAdd.action"
|
||||||
"action/DPLiquidHandlerTouchTip.action"
|
"action/LiquidHandlerMix.action"
|
||||||
"action/DPLiquidHandlerTransferLiquid.action"
|
"action/LiquidHandlerMoveTo.action"
|
||||||
|
"action/LiquidHandlerRemove.action"
|
||||||
|
|
||||||
"action/EmptyIn.action"
|
"action/EmptyIn.action"
|
||||||
"action/FloatSingleInput.action"
|
"action/FloatSingleInput.action"
|
||||||
@@ -59,9 +63,10 @@ set(action_files
|
|||||||
"action/Point3DSeparateInput.action"
|
"action/Point3DSeparateInput.action"
|
||||||
|
|
||||||
"action/ResourceCreateFromOuter.action"
|
"action/ResourceCreateFromOuter.action"
|
||||||
|
"action/ResourceCreateFromOuterEasy.action"
|
||||||
|
|
||||||
"action/SolidDispenseAddPowderTube.action"
|
"action/SolidDispenseAddPowderTube.action"
|
||||||
|
|
||||||
"action/PumpTransfer.action"
|
"action/PumpTransfer.action"
|
||||||
"action/Clean.action"
|
"action/Clean.action"
|
||||||
"action/Separate.action"
|
"action/Separate.action"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ string from_repo_position
|
|||||||
Resource to_repo
|
Resource to_repo
|
||||||
string to_repo_position
|
string to_repo_position
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ float64 volume # Optional. Volume of solvent to clean vessel with.
|
|||||||
float64 temp # Optional. Temperature to heat vessel to while cleaning.
|
float64 temp # Optional. Temperature to heat vessel to while cleaning.
|
||||||
int32 repeats # Optional. Number of cleaning cycles to perform.
|
int32 repeats # Optional. Number of cleaning cycles to perform.
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
float64 seconds
|
|
||||||
string msg
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Resource[] tip_racks
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Resource[] targets
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
float64[] asp_vols
|
|
||||||
float64[] dis_vols
|
|
||||||
Resource[] sources
|
|
||||||
Resource[] targets
|
|
||||||
Resource[] tip_racks
|
|
||||||
int32[] use_channels
|
|
||||||
float64[] asp_flow_rates
|
|
||||||
float64[] dis_flow_rates
|
|
||||||
geometry_msgs/Point[] offsets
|
|
||||||
bool touch_tip
|
|
||||||
float64[] liquid_height
|
|
||||||
float64[] blow_out_air_volume
|
|
||||||
string spread
|
|
||||||
bool is_96_well
|
|
||||||
string mix_stage
|
|
||||||
int32[] mix_times
|
|
||||||
int32 mix_vol
|
|
||||||
int32 mix_rate
|
|
||||||
float64 mix_liquid_height
|
|
||||||
int32[] delays
|
|
||||||
string[] none_keys
|
|
||||||
---
|
|
||||||
bool success
|
|
||||||
---
|
|
||||||
# 反馈
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
---
|
---
|
||||||
@@ -3,6 +3,7 @@ string vessel
|
|||||||
string gas
|
string gas
|
||||||
int32 repeats
|
int32 repeats
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ float64 temp
|
|||||||
float64 time
|
float64 time
|
||||||
float64 stir_speed
|
float64 stir_speed
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
float64 float_in
|
float64 float_in
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -6,6 +6,7 @@ bool stir
|
|||||||
float64 stir_speed
|
float64 stir_speed
|
||||||
string purpose
|
string purpose
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
@@ -3,6 +3,7 @@ string vessel
|
|||||||
float64 temp
|
float64 temp
|
||||||
string purpose
|
string purpose
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Organic
|
# Organic
|
||||||
string vessel
|
string vessel
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
int32 int_input
|
int32 int_input
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -15,6 +15,7 @@ int32 mix_rate
|
|||||||
float64 mix_liquid_height
|
float64 mix_liquid_height
|
||||||
string[] none_keys
|
string[] none_keys
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈
|
# 反馈
|
||||||
@@ -5,7 +5,8 @@ float64[] flow_rates
|
|||||||
geometry_msgs/Point[] offsets
|
geometry_msgs/Point[] offsets
|
||||||
float64[] liquid_height
|
float64[] liquid_height
|
||||||
float64[] blow_out_air_volume
|
float64[] blow_out_air_volume
|
||||||
string spread="wide"
|
string spread
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
int32[] use_channels
|
int32[] use_channels
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -5,9 +5,10 @@ int32[] use_channels
|
|||||||
float64[] flow_rates
|
float64[] flow_rates
|
||||||
geometry_msgs/Point[] offsets
|
geometry_msgs/Point[] offsets
|
||||||
int32[] blow_out_air_volume
|
int32[] blow_out_air_volume
|
||||||
string spread="wide"
|
string spread
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
|
|||||||
bool allow_nonzero_volume
|
bool allow_nonzero_volume
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -5,6 +5,7 @@ geometry_msgs/Point offset
|
|||||||
bool allow_nonzero_volume
|
bool allow_nonzero_volume
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
6
unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
Normal file
6
unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
int32 time
|
||||||
|
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
bool success
|
||||||
|
---
|
||||||
@@ -6,6 +6,7 @@ geometry_msgs/Point[] offsets
|
|||||||
float64 mix_rate
|
float64 mix_rate
|
||||||
string[] none_keys
|
string[] none_keys
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈
|
# 反馈
|
||||||
7
unilabos_msgs/action/LiquidHandlerMoveBiomek.action
Normal file
7
unilabos_msgs/action/LiquidHandlerMoveBiomek.action
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
string source
|
||||||
|
string target
|
||||||
|
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
bool success
|
||||||
|
---
|
||||||
@@ -12,6 +12,7 @@ string put_direction
|
|||||||
float64 pickup_distance_from_top
|
float64 pickup_distance_from_top
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -13,6 +13,7 @@ string put_direction
|
|||||||
float64 pickup_distance_from_top
|
float64 pickup_distance_from_top
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -12,6 +12,7 @@ string get_direction
|
|||||||
string put_direction
|
string put_direction
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -2,6 +2,7 @@ Resource well
|
|||||||
float64 dis_to_top
|
float64 dis_to_top
|
||||||
int32 channel
|
int32 channel
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈
|
# 反馈
|
||||||
7
unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
Normal file
7
unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
int32 rpm
|
||||||
|
int32 time
|
||||||
|
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
bool success
|
||||||
|
---
|
||||||
@@ -5,6 +5,7 @@ int32[] use_channels
|
|||||||
geometry_msgs/Point[] offsets
|
geometry_msgs/Point[] offsets
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -4,6 +4,7 @@ Resource tip_rack
|
|||||||
geometry_msgs/Point offset
|
geometry_msgs/Point offset
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
10
unilabos_msgs/action/LiquidHandlerProtocolCreation.action
Normal file
10
unilabos_msgs/action/LiquidHandlerProtocolCreation.action
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
string protocol_name
|
||||||
|
string protocol_description
|
||||||
|
string protocol_version
|
||||||
|
string protocol_author
|
||||||
|
string protocol_date
|
||||||
|
string protocol_type
|
||||||
|
string[] none_keys
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
---
|
||||||
@@ -12,6 +12,7 @@ bool is_96_well
|
|||||||
float64[] top
|
float64[] top
|
||||||
string[] none_keys
|
string[] none_keys
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈
|
# 反馈
|
||||||
@@ -4,6 +4,7 @@ int32[] use_channels
|
|||||||
bool allow_nonzero_volume
|
bool allow_nonzero_volume
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
bool allow_nonzero_volume
|
bool allow_nonzero_volume
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -7,6 +7,7 @@ float64 aspiration_flow_rate
|
|||||||
float64 dispense_flow_rate
|
float64 dispense_flow_rate
|
||||||
---
|
---
|
||||||
# 结果字段
|
# 结果字段
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
# 反馈字段
|
# 反馈字段
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
# Bio
|
float64[] asp_vols
|
||||||
Resource source
|
float64[] dis_vols
|
||||||
|
Resource[] sources
|
||||||
Resource[] targets
|
Resource[] targets
|
||||||
float64 source_vol
|
Resource[] tip_racks
|
||||||
float64[] ratios
|
int32[] use_channels
|
||||||
float64[] target_vols
|
float64[] asp_flow_rates
|
||||||
float64 aspiration_flow_rate
|
float64[] dis_flow_rates
|
||||||
float64[] dispense_flow_rates
|
geometry_msgs/Point[] offsets
|
||||||
|
bool touch_tip
|
||||||
|
float64[] liquid_height
|
||||||
|
float64[] blow_out_air_volume
|
||||||
|
string spread
|
||||||
|
bool is_96_well
|
||||||
|
string mix_stage
|
||||||
|
int32[] mix_times
|
||||||
|
int32 mix_vol
|
||||||
|
int32 mix_rate
|
||||||
|
float64 mix_liquid_height
|
||||||
|
int32[] delays
|
||||||
|
string[] none_keys
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
|
# 反馈
|
||||||
11
unilabos_msgs/action/LiquidHandlerTransferBiomek.action
Normal file
11
unilabos_msgs/action/LiquidHandlerTransferBiomek.action
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
string source
|
||||||
|
string target
|
||||||
|
string tip_rack
|
||||||
|
float64 volume
|
||||||
|
string aspirate_technique
|
||||||
|
string dispense_technique
|
||||||
|
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
bool success
|
||||||
|
---
|
||||||
@@ -2,5 +2,6 @@ float64 x
|
|||||||
float64 y
|
float64 y
|
||||||
float64 z
|
float64 z
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -10,6 +10,7 @@ float64 rinsing_volume
|
|||||||
int32 rinsing_repeats
|
int32 rinsing_repeats
|
||||||
bool solid
|
bool solid
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ string[] bind_parent_ids
|
|||||||
geometry_msgs/Point[] bind_locations
|
geometry_msgs/Point[] bind_locations
|
||||||
string[] other_calling_params
|
string[] other_calling_params
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
13
unilabos_msgs/action/ResourceCreateFromOuterEasy.action
Normal file
13
unilabos_msgs/action/ResourceCreateFromOuterEasy.action
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
string res_id
|
||||||
|
string device_id
|
||||||
|
string class_name
|
||||||
|
string parent
|
||||||
|
geometry_msgs/Point bind_locations
|
||||||
|
int32[] liquid_input_slot
|
||||||
|
string[] liquid_type
|
||||||
|
float32[] liquid_volume
|
||||||
|
int32 slot_on_deck
|
||||||
|
---
|
||||||
|
string return_info
|
||||||
|
bool success
|
||||||
|
---
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Simple
|
# Simple
|
||||||
string command
|
string command
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ float64 stir_time # Optional. Time stir for after adding solvent, before separat
|
|||||||
float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
|
float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
|
||||||
float64 settling_time # Optional. Time
|
float64 settling_time # Optional. Time
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ int32 powder_tube_number
|
|||||||
string target_tube_position
|
string target_tube_position
|
||||||
float64 compound_mass
|
float64 compound_mass
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
float64 actual_mass_mg
|
float64 actual_mass_mg
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -3,6 +3,7 @@ float64 stir_time
|
|||||||
float64 stir_speed
|
float64 stir_speed
|
||||||
float64 settling_time
|
float64 settling_time
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
string string
|
string string
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
@@ -3,6 +3,7 @@ string wf_name
|
|||||||
string params
|
string params
|
||||||
Resource resource
|
Resource resource
|
||||||
---
|
---
|
||||||
|
string return_info
|
||||||
bool success
|
bool success
|
||||||
---
|
---
|
||||||
string status
|
string status
|
||||||
|
|||||||
Reference in New Issue
Block a user