mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b420d1fa8e | ||
|
|
767e0fcdee | ||
|
|
84944396e9 | ||
|
|
bfcb214b53 | ||
|
|
ec4e6c6cfd | ||
|
|
53b6457a88 | ||
|
|
133dbf77bb | ||
|
|
5a564c0c05 | ||
|
|
d1fbea3b7d | ||
|
|
200ebaff31 | ||
|
|
9d034bd343 | ||
|
|
01ac3415ae | ||
|
|
74ae2a88ac | ||
|
|
f476b40983 | ||
|
|
2f69480f92 | ||
|
|
0cd11fa46b | ||
|
|
136bb1ded0 | ||
|
|
a4fd428dc3 | ||
|
|
9fa6b71368 | ||
|
|
35ada068cc | ||
|
|
22a02bdb06 | ||
|
|
4a427bde61 | ||
|
|
e638c33d89 | ||
|
|
af1adea5ea | ||
|
|
290c1fb60d | ||
|
|
455feb5c43 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is. The bug may results in:
|
||||||
|
- abnormal interruption of the program,
|
||||||
|
- systematic or randomized numerical error, or
|
||||||
|
- relatively low efficiency.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
.vscode
|
.vscode
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
service
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
@@ -229,6 +230,6 @@ CATKIN_IGNORE
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
local_config.py
|
/**/local_config.py
|
||||||
|
|
||||||
*.graphml
|
*.graphml
|
||||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
recursive-include unilabos/registry *.yaml
|
||||||
|
recursive-include unilabos/app/web *.html
|
||||||
|
recursive-include unilabos/app/web *.css
|
||||||
88
README.md
88
README.md
@@ -1 +1,89 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="docs/logo.png" alt="Uni-Lab Logo" width="200"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
# 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/network/members)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Configure Conda Environment
|
||||||
|
|
||||||
|
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new environment
|
||||||
|
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||||
|
mamba activate unilab
|
||||||
|
|
||||||
|
# Or update existing environment
|
||||||
|
# Where `[YOUR_OS]` can be `win64`, `linux-64`, `osx-64`, or `osx-arm64`.
|
||||||
|
conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||||
|
|
||||||
|
# Currently, you need to install the `unilabos_msgs` package
|
||||||
|
# You can download the system-specific package from the Release page
|
||||||
|
conda install ros-humble-unilabos-msgs-0.9.1-xxxxx.tar.bz2
|
||||||
|
|
||||||
|
# Install PyLabRobot and other prerequisites
|
||||||
|
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||||
|
cd plr_repo
|
||||||
|
pip install .[opentrons]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install Uni-Lab-OS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||||
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
|
# Install Uni-Lab-OS
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start Uni-Lab System:
|
||||||
|
|
||||||
|
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||||
|
|
||||||
|
## Message Format
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Project Statistics
|
||||||
|
|
||||||
|
### Stars Trend
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
## Contact Us
|
||||||
|
|
||||||
|
- 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.1-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)
|
||||||
BIN
docs/logo.png
Normal file
BIN
docs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
@@ -18,7 +18,7 @@ Uni-Lab支持Python格式的配置文件,它比YAML或JSON提供更多的灵
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# 配置类定义
|
# 配置类定义
|
||||||
@dataclass
|
|
||||||
class MQConfig:
|
class MQConfig:
|
||||||
"""MQTT 配置类"""
|
"""MQTT 配置类"""
|
||||||
lab_id: str = "YOUR_LAB_ID"
|
lab_id: str = "YOUR_LAB_ID"
|
||||||
@@ -34,7 +34,7 @@ class MQConfig:
|
|||||||
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@dataclass
|
|
||||||
class MQConfig:
|
class MQConfig:
|
||||||
"""MQTT 配置类"""
|
"""MQTT 配置类"""
|
||||||
lab_id: str = "7AAEDBEA" # 实验室唯一标识
|
lab_id: str = "7AAEDBEA" # 实验室唯一标识
|
||||||
@@ -46,9 +46,9 @@ class MQConfig:
|
|||||||
port: int = 8883
|
port: int = 8883
|
||||||
|
|
||||||
# 可以直接提供证书文件路径
|
# 可以直接提供证书文件路径
|
||||||
ca_file: str = "/path/to/ca.pem"
|
ca_file: str = "/path/to/ca.pem" # 相对config.py所在目录的路径
|
||||||
cert_file: str = "/path/to/cert.pem"
|
cert_file: str = "/path/to/cert.pem" # 相对config.py所在目录的路径
|
||||||
key_file: str = "/path/to/key.pem"
|
key_file: str = "/path/to/key.pem" # 相对config.py所在目录的路径
|
||||||
|
|
||||||
# 或者直接提供证书内容
|
# 或者直接提供证书内容
|
||||||
ca_content: str = ""
|
ca_content: str = ""
|
||||||
@@ -74,22 +74,18 @@ MQTT连接支持两种方式配置证书:
|
|||||||
配置ROS消息转换器需要加载的模块:
|
配置ROS消息转换器需要加载的模块:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@dataclass
|
|
||||||
class ROSConfig:
|
class ROSConfig:
|
||||||
"""ROS模块配置"""
|
"""ROS模块配置"""
|
||||||
modules: list = None
|
modules = [
|
||||||
|
"std_msgs.msg",
|
||||||
def __post_init__(self):
|
"geometry_msgs.msg",
|
||||||
if self.modules is None:
|
"control_msgs.msg",
|
||||||
self.modules = [
|
"control_msgs.action",
|
||||||
"std_msgs.msg",
|
"nav2_msgs.action",
|
||||||
"geometry_msgs.msg",
|
"unilabos_msgs.msg",
|
||||||
"control_msgs.msg",
|
"unilabos_msgs.action",
|
||||||
"control_msgs.action",
|
]
|
||||||
"nav2_msgs.action",
|
|
||||||
"unilabos_msgs.msg",
|
|
||||||
"unilabos_msgs.action",
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
您可以根据需要添加其他ROS模块。
|
您可以根据需要添加其他ROS模块。
|
||||||
@@ -106,14 +102,7 @@ class ROSConfig:
|
|||||||
unilab --config path/to/your/config.py
|
unilab --config path/to/your/config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 环境变量覆盖
|
如果您不涉及多环境开发,可以在unilabos的安装路径中手动添加local_config.py的文件
|
||||||
|
|
||||||
某些配置项可以通过环境变量进行覆盖,这在不同环境部署时特别有用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 设置环境变量覆盖配置
|
|
||||||
export UNILAB_LAB_ID="YOUR_LAB_ID"
|
|
||||||
export UNILAB_MQTT_BROKER="mqtt-broker-address"
|
|
||||||
|
|
||||||
# 启动Uni-Lab
|
# 启动Uni-Lab
|
||||||
python -m unilabos.app.main --config path/to/your/config.py
|
python -m unilabos.app.main --config path/to/your/config.py
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
mamba env create -f unilabos-<YOUR_OS>.yaml
|
mamba env create -f unilabos-<YOUR_OS>.yaml
|
||||||
mamba activate ilab
|
mamba activate unilab
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.8.0
|
version: 0.9.1
|
||||||
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.8.0"
|
version: "0.9.1"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
18
setup.py
18
setup.py
@@ -1,28 +1,18 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from glob import glob
|
|
||||||
import os
|
|
||||||
|
|
||||||
package_name = 'unilabos'
|
package_name = 'unilabos'
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.8.0',
|
version='0.9.1',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
# data_files=[
|
include_package_data=True,
|
||||||
# ('share/ament_index/resource_index/packages',
|
|
||||||
# ['resource/' + package_name]),
|
|
||||||
# ('share/' + package_name, ['package.xml']),
|
|
||||||
# # (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
|
|
||||||
# # (os.path.join('share', package_name, 'urdf'), glob('urdf/*')),
|
|
||||||
# # (os.path.join('share', package_name, 'meshes'), glob('meshes/*')),
|
|
||||||
# # (os.path.join('share', package_name, 'config'), glob('config/*'))
|
|
||||||
# ],
|
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
maintainer='Junhan Chang',
|
maintainer='Junhan Chang',
|
||||||
maintainer_email='changjh@pku.edu.cn',
|
maintainer_email='changjh@pku.edu.cn',
|
||||||
description='TODO: Package description',
|
description='',
|
||||||
license='TODO: License declaration',
|
license='GPL v3',
|
||||||
tests_require=['pytest'],
|
tests_require=['pytest'],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
|||||||
5
test/commands/resource_add.md
Normal file
5
test/commands/resource_add.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
使用plr_test.json启动,将Well加入Plate中
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: [ '{}' ] }"
|
||||||
|
```
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "HPLC",
|
"name": "HPLC",
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "hplc",
|
"class": "hplc.agilent",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 620.6111111111111,
|
"x": 620.6111111111111,
|
||||||
"y": 171,
|
"y": 171,
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "BottlesRack3",
|
"id": "BottlesRack3",
|
||||||
"name": "Revvity上样盘3",
|
"name": "上样盘3",
|
||||||
"parent": "Revvity",
|
"parent": "HPLC",
|
||||||
"type": "plate",
|
"type": "plate",
|
||||||
"class": null,
|
"class": null,
|
||||||
"position": {
|
"position": {
|
||||||
|
|||||||
@@ -6679,8 +6679,7 @@
|
|||||||
"plate_well_11_3",
|
"plate_well_11_3",
|
||||||
"plate_well_11_4",
|
"plate_well_11_4",
|
||||||
"plate_well_11_5",
|
"plate_well_11_5",
|
||||||
"plate_well_11_6",
|
"plate_well_11_6"
|
||||||
"plate_well_11_7"
|
|
||||||
],
|
],
|
||||||
"parent": "deck",
|
"parent": "deck",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
@@ -10508,45 +10507,6 @@
|
|||||||
"pending_liquids": [],
|
"pending_liquids": [],
|
||||||
"liquid_history": []
|
"liquid_history": []
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "plate_well_11_7",
|
|
||||||
"name": "plate_well_11_7",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "plate",
|
|
||||||
"type": "device",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 109.87,
|
|
||||||
"y": 7.77,
|
|
||||||
"z": 3.03
|
|
||||||
},
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": []
|
"links": []
|
||||||
|
|||||||
9590
test/experiments/plr_test_converted.json
Normal file
9590
test/experiments/plr_test_converted.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,14 @@
|
|||||||
"id": "Gripper1",
|
"id": "Gripper1",
|
||||||
"name": "假夹爪",
|
"name": "假夹爪",
|
||||||
"children": [
|
"children": [
|
||||||
|
"Plate1"
|
||||||
],
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "gripper.mock",
|
"class": "gripper.mock",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 620.6111111111111,
|
"x": 0,
|
||||||
"y": 171,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -23,18 +24,120 @@
|
|||||||
"name": "Plate1",
|
"name": "Plate1",
|
||||||
"children": [
|
"children": [
|
||||||
],
|
],
|
||||||
"parent": null,
|
"parent": "Gripper1",
|
||||||
"type": "plate",
|
"type": "plate",
|
||||||
"class": "nest_96_wellplate_2ml_deep",
|
"class": "nest_96_wellplate_100ul_pcr_full_skirt",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 620.6111111111111,
|
"x": 0,
|
||||||
"y": 171,
|
"y": 0,
|
||||||
"z": 0
|
"z": 69
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ot_joint_publisher",
|
||||||
|
"name": "ot_joint_publisher",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "lh_joint_publisher",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"lh_id":"deck",
|
||||||
|
"joint_config":
|
||||||
|
{
|
||||||
|
"joint_names":[
|
||||||
|
"first_joint",
|
||||||
|
"second_joint",
|
||||||
|
"third_joint",
|
||||||
|
"fourth_joint"
|
||||||
|
],
|
||||||
|
"y":{
|
||||||
|
"first_joint":{
|
||||||
|
"factor":-1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x":{
|
||||||
|
"second_joint":{
|
||||||
|
"factor":-1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z":{
|
||||||
|
"third_joint":{
|
||||||
|
"factor":1,
|
||||||
|
"offset":0.0
|
||||||
|
},
|
||||||
|
"fourth_joint":{
|
||||||
|
"factor":1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ot_joint_publisher",
|
||||||
|
"name": "ot_joint_publisher",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "lh_joint_publisher",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"lh_id":"deck",
|
||||||
|
"joint_config":
|
||||||
|
{
|
||||||
|
"joint_names":[
|
||||||
|
"first_joint",
|
||||||
|
"second_joint",
|
||||||
|
"third_joint",
|
||||||
|
"fourth_joint"
|
||||||
|
],
|
||||||
|
"y":{
|
||||||
|
"first_joint":{
|
||||||
|
"factor":-1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x":{
|
||||||
|
"second_joint":{
|
||||||
|
"factor":-1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"z":{
|
||||||
|
"third_joint":{
|
||||||
|
"factor":1,
|
||||||
|
"offset":0.0
|
||||||
|
},
|
||||||
|
"fourth_joint":{
|
||||||
|
"factor":1,
|
||||||
|
"offset":0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
|
|||||||
135
test/experiments/test_copy.json
Normal file
135
test/experiments/test_copy.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "PLR_STATION",
|
||||||
|
"name": "PLR_LH_TEST",
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "liquid_handler",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"data": {
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"_resource_child_name": "deck",
|
||||||
|
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backend": {
|
||||||
|
"type": "LiquidHandlerRvizBackend"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"children": [
|
||||||
|
"deck"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deck",
|
||||||
|
"name": "deck",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [
|
||||||
|
"teaching_carrier"
|
||||||
|
],
|
||||||
|
"parent": "PLR_STATION",
|
||||||
|
"type": "deck",
|
||||||
|
"class": "OTDeck",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "OTDeck",
|
||||||
|
"with_trash": false,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "teaching_carrier",
|
||||||
|
"name": "teaching_carrier",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [
|
||||||
|
"teaching_carrier_A1"
|
||||||
|
],
|
||||||
|
"parent": "deck",
|
||||||
|
"type": "plate",
|
||||||
|
"class": "opentrons_96_filtertiprack_1000ul",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 69
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "Resource",
|
||||||
|
"size_x": 127,
|
||||||
|
"size_y": 85,
|
||||||
|
"size_z": 0,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
},
|
||||||
|
"category": null,
|
||||||
|
"model": null
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "teaching_carrier_A1",
|
||||||
|
"name": "teaching_carrier_A1",
|
||||||
|
"sample_id": null,
|
||||||
|
"children": [],
|
||||||
|
"parent": "teaching_carrier",
|
||||||
|
"type": "device",
|
||||||
|
"class": "",
|
||||||
|
"position": {
|
||||||
|
"x": 10.87,
|
||||||
|
"y": 70.77,
|
||||||
|
"z": 9.47
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "TipSpot",
|
||||||
|
"size_x": 6.86,
|
||||||
|
"size_y": 6.86,
|
||||||
|
"size_z": 10.67,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
},
|
||||||
|
"category": "tip_spot",
|
||||||
|
"model": null,
|
||||||
|
"prototype_tip": {
|
||||||
|
"type": "Tip",
|
||||||
|
"total_tip_length": 39.2,
|
||||||
|
"has_filter": true,
|
||||||
|
"maximal_volume": 20.0,
|
||||||
|
"fitting_depth": 3.29
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquids": [],
|
||||||
|
"pending_liquids": [],
|
||||||
|
"liquid_history": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -56,6 +56,10 @@ 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
|
||||||
|
- pip:
|
||||||
|
- paho-mqtt
|
||||||
@@ -56,6 +56,10 @@ 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
|
||||||
|
- pip:
|
||||||
|
- paho-mqtt
|
||||||
@@ -58,6 +58,10 @@ 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
|
||||||
|
- pip:
|
||||||
|
- paho-mqtt
|
||||||
@@ -56,6 +56,10 @@ 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
|
||||||
|
- pip:
|
||||||
|
- paho-mqtt
|
||||||
@@ -7,11 +7,13 @@ from unilabos.utils import logger
|
|||||||
def start_backend(
|
def start_backend(
|
||||||
backend: str,
|
backend: str,
|
||||||
devices_config: dict = {},
|
devices_config: dict = {},
|
||||||
resources_config: dict = {},
|
resources_config: list = [],
|
||||||
graph=None,
|
graph=None,
|
||||||
controllers_config: dict = {},
|
controllers_config: dict = {},
|
||||||
bridges=[],
|
bridges=[],
|
||||||
without_host: bool = False,
|
without_host: bool = False,
|
||||||
|
visual: str = "None",
|
||||||
|
resources_mesh_config: dict = {},
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
if backend == "ros":
|
if backend == "ros":
|
||||||
@@ -29,7 +31,9 @@ def start_backend(
|
|||||||
|
|
||||||
backend_thread = threading.Thread(
|
backend_thread = threading.Thread(
|
||||||
target=main if not without_host else slave,
|
target=main if not without_host else slave,
|
||||||
args=(devices_config, resources_config, graph, controllers_config, bridges)
|
args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config),
|
||||||
|
name="backend_thread",
|
||||||
|
daemon=True,
|
||||||
)
|
)
|
||||||
backend_thread.start()
|
backend_thread.start()
|
||||||
logger.info(f"Backend {backend} started.")
|
logger.info(f"Backend {backend} started.")
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ def job_add(req: JobAddReq) -> JobData:
|
|||||||
req.data['action'] = action_name
|
req.data['action'] = action_name
|
||||||
if action_name == "execute_command_from_outer":
|
if action_name == "execute_command_from_outer":
|
||||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||||
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
elif "command" in action_kwargs:
|
||||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
|
action_kwargs = action_kwargs["command"]
|
||||||
|
# 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, server_info=req.server_info)
|
||||||
return JobData(jobId=req.job_id)
|
return JobData(jobId=req.job_id)
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import json
|
import threading
|
||||||
import yaml
|
import time
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
# 首先添加项目根目录到路径
|
# 首先添加项目根目录到路径
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
ilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||||
if ilabos_dir not in sys.path:
|
if unilabos_dir not in sys.path:
|
||||||
sys.path.append(ilabos_dir)
|
sys.path.append(unilabos_dir)
|
||||||
|
|
||||||
from unilabos.config.config import load_config, BasicConfig
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +62,28 @@ def parse_args():
|
|||||||
default=None,
|
default=None,
|
||||||
help="配置文件路径,支持.py格式的Python配置文件",
|
help="配置文件路径,支持.py格式的Python配置文件",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=8002,
|
||||||
|
help="信息页web服务的启动端口",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable_browser",
|
||||||
|
action='store_true',
|
||||||
|
help="是否在启动时关闭信息页",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--2d_vis",
|
||||||
|
action='store_true',
|
||||||
|
help="是否在pylabrobot实例启动时,同时启动可视化",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--visual",
|
||||||
|
choices=["rviz", "web", "disable"],
|
||||||
|
default="disable",
|
||||||
|
help="选择可视化工具: rviz, web",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -68,19 +93,28 @@ def main():
|
|||||||
args = parse_args()
|
args = parse_args()
|
||||||
args_dict = vars(args)
|
args_dict = vars(args)
|
||||||
|
|
||||||
# 加载配置文件 - 这里保持最先加载配置的逻辑
|
# 加载配置文件,优先加载config,然后从env读取
|
||||||
if args_dict.get("config"):
|
config_path = args_dict.get("config")
|
||||||
config_path = args_dict["config"]
|
if config_path is None:
|
||||||
|
config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None)
|
||||||
|
if config_path:
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
print_status(f"配置文件 {config_path} 不存在", "error")
|
print_status(f"配置文件 {config_path} 不存在", "error")
|
||||||
elif not config_path.endswith(".py"):
|
elif not config_path.endswith(".py"):
|
||||||
print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error")
|
print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error")
|
||||||
else:
|
else:
|
||||||
load_config(config_path)
|
load_config(config_path)
|
||||||
|
else:
|
||||||
|
print_status(f"启动 UniLab-OS时,配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning")
|
||||||
|
load_config(config_path)
|
||||||
|
|
||||||
# 设置BasicConfig参数
|
# 设置BasicConfig参数
|
||||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||||
|
machine_name = os.popen("hostname").read().strip()
|
||||||
|
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||||
|
BasicConfig.machine_name = machine_name
|
||||||
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
|
|
||||||
from unilabos.resources.graphio import (
|
from unilabos.resources.graphio import (
|
||||||
read_node_link_json,
|
read_node_link_json,
|
||||||
@@ -92,8 +126,8 @@ def main():
|
|||||||
from unilabos.app.mq import mqtt_client
|
from unilabos.app.mq import mqtt_client
|
||||||
from unilabos.registry.registry import build_registry
|
from unilabos.registry.registry import build_registry
|
||||||
from unilabos.app.backend import start_backend
|
from unilabos.app.backend import start_backend
|
||||||
from unilabos.web import http_client
|
from unilabos.app.web import http_client
|
||||||
from unilabos.web import start_server
|
from unilabos.app.web import start_server
|
||||||
|
|
||||||
# 显示启动横幅
|
# 显示启动横幅
|
||||||
print_unilab_banner(args_dict)
|
print_unilab_banner(args_dict)
|
||||||
@@ -101,6 +135,7 @@ def main():
|
|||||||
# 注册表
|
# 注册表
|
||||||
build_registry(args_dict["registry_path"])
|
build_registry(args_dict["registry_path"])
|
||||||
|
|
||||||
|
devices_and_resources = None
|
||||||
if args_dict["graph"] is not None:
|
if args_dict["graph"] is not None:
|
||||||
import unilabos.resources.graphio as graph_res
|
import unilabos.resources.graphio as graph_res
|
||||||
graph_res.physical_setup_graph = (
|
graph_res.physical_setup_graph = (
|
||||||
@@ -112,6 +147,7 @@ def main():
|
|||||||
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["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["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:
|
||||||
@@ -146,9 +182,29 @@ def main():
|
|||||||
signal.signal(signal.SIGINT, _exit)
|
signal.signal(signal.SIGINT, _exit)
|
||||||
signal.signal(signal.SIGTERM, _exit)
|
signal.signal(signal.SIGTERM, _exit)
|
||||||
mqtt_client.start()
|
mqtt_client.start()
|
||||||
|
args_dict["resources_mesh_config"] = {}
|
||||||
start_backend(**args_dict)
|
# web visiualize 2D
|
||||||
start_server()
|
if args_dict["visual"] != "disable":
|
||||||
|
enable_rviz = args_dict["visual"] == "rviz"
|
||||||
|
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)
|
||||||
|
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
||||||
|
start_backend(**args_dict)
|
||||||
|
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
||||||
|
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
|
||||||
|
))
|
||||||
|
server_thread.start()
|
||||||
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||||
|
resource_visualization.start()
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
start_backend(**args_dict)
|
||||||
|
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||||
|
else:
|
||||||
|
start_backend(**args_dict)
|
||||||
|
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,19 +1,24 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
import ssl, base64, hmac
|
import ssl
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
import tempfile
|
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, JobAddResp
|
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
|
||||||
|
|
||||||
|
from paho.mqtt.enums import CallbackAPIVersion
|
||||||
|
|
||||||
|
|
||||||
class MQTTClient:
|
class MQTTClient:
|
||||||
mqtt_disable = True
|
mqtt_disable = True
|
||||||
@@ -21,7 +26,8 @@ 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()}"
|
||||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||||
|
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
@@ -31,34 +37,45 @@ class MQTTClient:
|
|||||||
self.client.on_disconnect = self._on_disconnect
|
self.client.on_disconnect = self._on_disconnect
|
||||||
|
|
||||||
def _on_log(self, client, userdata, level, buf):
|
def _on_log(self, client, userdata, level, buf):
|
||||||
logger.info(f"[MQTT] log: {buf}")
|
# logger.info(f"[MQTT] log: {buf}")
|
||||||
|
pass
|
||||||
|
|
||||||
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):
|
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:
|
||||||
|
payload_json["data"] = {}
|
||||||
|
if "action" in payload_json:
|
||||||
|
payload_json["data"]["action"] = payload_json.pop("action")
|
||||||
|
if "action_kwargs" in payload_json:
|
||||||
|
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
|
||||||
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 JobAddResp(data=data)
|
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}")
|
||||||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
@@ -87,7 +104,7 @@ class MQTTClient:
|
|||||||
for temp_file in temp_files:
|
for temp_file in temp_files:
|
||||||
try:
|
try:
|
||||||
os.unlink(temp_file)
|
os.unlink(temp_file)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -142,7 +159,7 @@ class MQTTClient:
|
|||||||
if self.mqtt_disable:
|
if self.mqtt_disable:
|
||||||
return
|
return
|
||||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
|
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
|
||||||
address = f"labs/{MQConfig.lab_id}/devices"
|
address = f"labs/{MQConfig.lab_id}/devices/"
|
||||||
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}")
|
||||||
|
|
||||||
@@ -156,7 +173,7 @@ class MQTTClient:
|
|||||||
if self.mqtt_disable:
|
if self.mqtt_disable:
|
||||||
return
|
return
|
||||||
address = f"labs/{MQConfig.lab_id}/registry/"
|
address = f"labs/{MQConfig.lab_id}/registry/"
|
||||||
registry_data = json.dumps({device_id: device_info}, ensure_ascii = False, cls = TypeEncoder)
|
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
|
||||||
self.client.publish(address, registry_data, qos=2)
|
self.client.publish(address, registry_data, qos=2)
|
||||||
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
||||||
|
|
||||||
@@ -164,11 +181,30 @@ class MQTTClient:
|
|||||||
if self.mqtt_disable:
|
if self.mqtt_disable:
|
||||||
return
|
return
|
||||||
address = f"labs/{MQConfig.lab_id}/actions/"
|
address = f"labs/{MQConfig.lab_id}/actions/"
|
||||||
action_type_name = action_info["title"]
|
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||||
action_info["title"] = action_id
|
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||||
action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False)
|
|
||||||
self.client.publish(address, action_data, qos=2)
|
def send_ping(self, ping_id: str, timestamp: float):
|
||||||
logger.debug(f"Action data published: address: {address}, {action_data}")
|
"""发送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()
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ Web UI 模块
|
|||||||
提供了UniLab系统的Web界面功能
|
提供了UniLab系统的Web界面功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unilabos.web.pages import setup_web_pages
|
from unilabos.app.web.pages import setup_web_pages
|
||||||
from unilabos.web.server import setup_server, start_server
|
from unilabos.app.web.server import setup_server, start_server
|
||||||
from unilabos.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
from unilabos.web.api import setup_api_routes
|
from unilabos.app.web.api import setup_api_routes
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"setup_web_pages", # 设置Web页面
|
"setup_web_pages", # 设置Web页面
|
||||||
@@ -18,7 +18,7 @@ from unilabos.app.model import (
|
|||||||
JobPreintakeFinishReq,
|
JobPreintakeFinishReq,
|
||||||
JobFinishReq,
|
JobFinishReq,
|
||||||
)
|
)
|
||||||
from unilabos.web.utils.host_utils import get_host_node_info
|
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||||
|
|
||||||
# 创建API路由器
|
# 创建API路由器
|
||||||
api = APIRouter()
|
api = APIRouter()
|
||||||
@@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional
|
|||||||
import requests
|
import requests
|
||||||
from unilabos.utils.log import info
|
from unilabos.utils.log import info
|
||||||
from unilabos.config.config import MQConfig, HTTPConfig
|
from unilabos.config.config import MQConfig, HTTPConfig
|
||||||
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient:
|
class HTTPClient:
|
||||||
@@ -102,6 +103,30 @@ class HTTPClient:
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
|
||||||
|
"""
|
||||||
|
上传文件到服务器
|
||||||
|
|
||||||
|
使用multipart/form-data格式上传文件,类似curl -F "files=@filepath"
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 要上传的文件路径
|
||||||
|
scene: 上传场景,可选值为"user"或"models",默认为"models"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: API响应对象
|
||||||
|
"""
|
||||||
|
with open(file_path, "rb") as file:
|
||||||
|
files = {"files": file}
|
||||||
|
logger.info(f"上传文件: {file_path} 到 {scene}")
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||||
|
files=files,
|
||||||
|
headers={"Authorization": f"lab {self.auth}"},
|
||||||
|
timeout=30, # 上传文件可能需要更长的超时时间
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# 创建默认客户端实例
|
# 创建默认客户端实例
|
||||||
http_client = HTTPClient()
|
http_client = HTTPClient()
|
||||||
@@ -20,9 +20,9 @@ from unilabos.app.mq import mqtt_client
|
|||||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||||
from unilabos.utils.log import error
|
from unilabos.utils.log import error
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
from unilabos.web.utils.device_utils import get_registry_info
|
from unilabos.app.web.utils.device_utils import get_registry_info
|
||||||
from unilabos.web.utils.host_utils import get_host_node_info
|
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||||
from unilabos.web.utils.ros_utils import get_ros_node_info, update_ros_node_info
|
from unilabos.app.web.utils.ros_utils import get_ros_node_info, update_ros_node_info
|
||||||
|
|
||||||
# 设置Jinja2模板环境
|
# 设置Jinja2模板环境
|
||||||
template_dir = Path(__file__).parent / "templates"
|
template_dir = Path(__file__).parent / "templates"
|
||||||
@@ -92,19 +92,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
|||||||
|
|
||||||
# 获取已加载的设备
|
# 获取已加载的设备
|
||||||
if lab_registry:
|
if lab_registry:
|
||||||
# 设备类型
|
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
|
||||||
for device_id, device_info in lab_registry.device_type_registry.items():
|
|
||||||
msg = {
|
|
||||||
"id": device_id,
|
|
||||||
"name": device_info.get("name", "未命名"),
|
|
||||||
"file_path": device_info.get("file_path", ""),
|
|
||||||
"class_json": json.dumps(
|
|
||||||
device_info.get("class", {}), indent=4, ensure_ascii=False, cls=TypeEncoder
|
|
||||||
),
|
|
||||||
}
|
|
||||||
mqtt_client.publish_registry(device_id, device_info)
|
|
||||||
devices.append(msg)
|
|
||||||
|
|
||||||
# 资源类型
|
# 资源类型
|
||||||
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
||||||
resources.append(
|
resources.append(
|
||||||
@@ -13,8 +13,8 @@ from starlette.responses import Response
|
|||||||
|
|
||||||
from unilabos.utils.fastapi.log_adapter import setup_fastapi_logging
|
from unilabos.utils.fastapi.log_adapter import setup_fastapi_logging
|
||||||
from unilabos.utils.log import info, error
|
from unilabos.utils.log import info, error
|
||||||
from unilabos.web.api import setup_api_routes
|
from unilabos.app.web.api import setup_api_routes
|
||||||
from unilabos.web.pages import setup_web_pages
|
from unilabos.app.web.pages import setup_web_pages
|
||||||
|
|
||||||
# 创建FastAPI应用
|
# 创建FastAPI应用
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -96,17 +96,19 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>设备ID</th>
|
<th>设备ID</th>
|
||||||
<th>命名空间</th>
|
<th>命名空间</th>
|
||||||
|
<th>机器名称</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for device_id, device_info in host_node_info.devices.items() %}
|
{% for device_id, device_info in host_node_info.devices.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ device_id }}</td>
|
<td>{{ device_id }}</td>
|
||||||
<td>{{ device_info.namespace }}</td>
|
<td>{{ device_info.namespace }}</td>
|
||||||
|
<td>{{ device_info.machine_name }}</td>
|
||||||
<td><span class="status-badge online">{{ "在线" if device_info.is_online else "离线" }}</span></td>
|
<td><span class="status-badge online">{{ "在线" if device_info.is_online else "离线" }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="empty-state">没有发现已管理的设备</td>
|
<td colspan="4" class="empty-state">没有发现已管理的设备</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@@ -218,6 +220,7 @@
|
|||||||
<th>Device ID</th>
|
<th>Device ID</th>
|
||||||
<th>节点名称</th>
|
<th>节点名称</th>
|
||||||
<th>命名空间</th>
|
<th>命名空间</th>
|
||||||
|
<th>机器名称</th>
|
||||||
<th>状态项</th>
|
<th>状态项</th>
|
||||||
<th>动作数</th>
|
<th>动作数</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -227,6 +230,7 @@
|
|||||||
<td>{{ device_id }}</td>
|
<td>{{ device_id }}</td>
|
||||||
<td>{{ device_info.node_name }}</td>
|
<td>{{ device_info.node_name }}</td>
|
||||||
<td>{{ device_info.namespace }}</td>
|
<td>{{ device_info.namespace }}</td>
|
||||||
|
<td>{{ device_info.machine_name|default("本地") }}</td>
|
||||||
<td>{{ ros_node_info.device_topics.get(device_id, {})|length }}</td>
|
<td>{{ ros_node_info.device_topics.get(device_id, {})|length }}</td>
|
||||||
<td>{{ ros_node_info.device_actions.get(device_id, {})|length }} <span class="toggle-indicator">▼</span></td>
|
<td>{{ ros_node_info.device_actions.get(device_id, {})|length }} <span class="toggle-indicator">▼</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -329,7 +333,12 @@
|
|||||||
<tr id="device-info-{{ loop.index }}" class="detail-row" style="display: none;">
|
<tr id="device-info-{{ loop.index }}" class="detail-row" style="display: none;">
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<div class="content-full">
|
<div class="content-full">
|
||||||
<pre>{{ device.class_json }}</pre>
|
{% if device.class %}
|
||||||
|
<pre>{{ device.class | tojson(indent=4) }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<!-- 这里可以放占位内容,比如 -->
|
||||||
|
<pre>// No data</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if device.is_online %}
|
{% if device.is_online %}
|
||||||
<div class="status-badge"><span class="online-status">在线</span></div>
|
<div class="status-badge"><span class="online-status">在线</span></div>
|
||||||
@@ -362,7 +371,12 @@
|
|||||||
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
|
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
|
||||||
<button class="debug-btn" onclick="toggleDebugInfo(this, event)">调试</button>
|
<button class="debug-btn" onclick="toggleDebugInfo(this, event)">调试</button>
|
||||||
<div class="debug-info" style="display:none;">
|
<div class="debug-info" style="display:none;">
|
||||||
<pre>{{ action_info|tojson(indent=2) }}</pre>
|
{% if action_info %}
|
||||||
|
<pre>{{ action_info | tojson(indent=4) }}</pre>
|
||||||
|
{% else %}
|
||||||
|
<!-- 这里可以放占位内容,比如 -->
|
||||||
|
<pre>// No data</pre>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import traceback
|
|||||||
from typing import Dict, Any, Type, TypedDict, Optional
|
from typing import Dict, Any, Type, TypedDict, Optional
|
||||||
|
|
||||||
from rclpy.action import ActionClient, ActionServer
|
from rclpy.action import ActionClient, ActionServer
|
||||||
from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType
|
from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType, UnboundedString
|
||||||
|
|
||||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
@@ -74,7 +74,6 @@ def get_yaml_from_goal_type(goal_type) -> str:
|
|||||||
for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()):
|
for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()):
|
||||||
slot_name, slot_type = slot_info
|
slot_name, slot_type = slot_info
|
||||||
type_info = goal_type.SLOT_TYPES[ind]
|
type_info = goal_type.SLOT_TYPES[ind]
|
||||||
default_value = "unknown"
|
|
||||||
if isinstance(type_info, UnboundedSequence):
|
if isinstance(type_info, UnboundedSequence):
|
||||||
inner_type = type_info.value_type
|
inner_type = type_info.value_type
|
||||||
if isinstance(inner_type, NamespacedType):
|
if isinstance(inner_type, NamespacedType):
|
||||||
@@ -83,8 +82,10 @@ def get_yaml_from_goal_type(goal_type) -> str:
|
|||||||
default_value = [get_ros_msg_instance_as_dict(type_class())]
|
default_value = [get_ros_msg_instance_as_dict(type_class())]
|
||||||
elif isinstance(inner_type, BasicType):
|
elif isinstance(inner_type, BasicType):
|
||||||
default_value = [get_default_value_for_ros_type(inner_type.typename)]
|
default_value = [get_default_value_for_ros_type(inner_type.typename)]
|
||||||
|
elif isinstance(inner_type, UnboundedString):
|
||||||
|
default_value = [""]
|
||||||
else:
|
else:
|
||||||
default_value = "unknown"
|
default_value = []
|
||||||
elif isinstance(type_info, NamespacedType):
|
elif isinstance(type_info, NamespacedType):
|
||||||
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
|
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
|
||||||
type_class = msg_converter_manager.get_class(cls_name)
|
type_class = msg_converter_manager.get_class(cls_name)
|
||||||
@@ -93,6 +94,8 @@ def get_yaml_from_goal_type(goal_type) -> str:
|
|||||||
default_value = get_ros_msg_instance_as_dict(type_class())
|
default_value = get_ros_msg_instance_as_dict(type_class())
|
||||||
elif isinstance(type_info, BasicType):
|
elif isinstance(type_info, BasicType):
|
||||||
default_value = get_default_value_for_ros_type(type_info.typename)
|
default_value = get_default_value_for_ros_type(type_info.typename)
|
||||||
|
elif isinstance(type_info, UnboundedString):
|
||||||
|
default_value = ""
|
||||||
else:
|
else:
|
||||||
type_class = msg_converter_manager.search_class(slot_type, search_lower=True)
|
type_class = msg_converter_manager.search_class(slot_type, search_lower=True)
|
||||||
if type_class is not None:
|
if type_class is not None:
|
||||||
@@ -9,7 +9,7 @@ from typing import Dict, Any
|
|||||||
|
|
||||||
from unilabos.config.config import BasicConfig
|
from unilabos.config.config import BasicConfig
|
||||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
from unilabos.web.utils.action_utils import get_action_info
|
from unilabos.app.web.utils.action_utils import get_action_info
|
||||||
|
|
||||||
|
|
||||||
def get_host_node_info() -> Dict[str, Any]:
|
def get_host_node_info() -> Dict[str, Any]:
|
||||||
@@ -30,20 +30,19 @@ def get_host_node_info() -> Dict[str, Any]:
|
|||||||
return host_info
|
return host_info
|
||||||
host_info["available"] = True
|
host_info["available"] = True
|
||||||
host_info["devices"] = {
|
host_info["devices"] = {
|
||||||
device_id: {
|
edge_device_id: {
|
||||||
"namespace": namespace,
|
"namespace": namespace,
|
||||||
"is_online": f"{namespace}/{device_id}" in host_node._online_devices,
|
"is_online": f"{namespace}/{edge_device_id}" in host_node._online_devices,
|
||||||
"key": f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}",
|
"key": f"{namespace}/{edge_device_id}" if namespace.startswith("/") else f"/{namespace}/{edge_device_id}",
|
||||||
|
"machine_name": host_node.device_machine_names.get(edge_device_id, "未知"),
|
||||||
}
|
}
|
||||||
for device_id, namespace in host_node.devices_names.items()
|
for edge_device_id, namespace in host_node.devices_names.items()
|
||||||
}
|
}
|
||||||
# 获取已订阅的主题
|
# 获取已订阅的主题
|
||||||
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"] = {
|
host_info["action_clients"][action_id] = get_action_info(client, full_name=action_id)
|
||||||
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
|
||||||
@@ -7,11 +7,12 @@ ROS 工具函数模块
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from unilabos.web.utils.action_utils import get_action_info
|
from unilabos.app.web.utils.action_utils import get_action_info
|
||||||
|
|
||||||
# 存储 ROS 节点信息的全局变量
|
# 存储 ROS 节点信息的全局变量
|
||||||
ros_node_info = {"online_devices": {}, "device_topics": {}, "device_actions": {}}
|
ros_node_info = {"online_devices": {}, "device_topics": {}, "device_actions": {}}
|
||||||
|
|
||||||
|
|
||||||
def get_ros_node_info() -> Dict[str, Any]:
|
def get_ros_node_info() -> Dict[str, Any]:
|
||||||
"""获取 ROS 节点信息,包括设备节点、发布的状态和动作
|
"""获取 ROS 节点信息,包括设备节点、发布的状态和动作
|
||||||
|
|
||||||
@@ -35,6 +36,13 @@ def update_ros_node_info() -> Dict[str, Any]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from unilabos.ros.nodes.base_device_node import registered_devices
|
from unilabos.ros.nodes.base_device_node import registered_devices
|
||||||
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
|
|
||||||
|
# 尝试获取主机节点实例
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
device_machine_names = {}
|
||||||
|
if host_node:
|
||||||
|
device_machine_names = host_node.device_machine_names
|
||||||
|
|
||||||
for device_id, device_info in registered_devices.items():
|
for device_id, device_info in registered_devices.items():
|
||||||
# 设备基本信息
|
# 设备基本信息
|
||||||
@@ -42,6 +50,7 @@ def update_ros_node_info() -> Dict[str, Any]:
|
|||||||
"node_name": device_info["node_name"],
|
"node_name": device_info["node_name"],
|
||||||
"namespace": device_info["namespace"],
|
"namespace": device_info["namespace"],
|
||||||
"uuid": device_info["uuid"],
|
"uuid": device_info["uuid"],
|
||||||
|
"machine_name": device_machine_names.get(device_id, "本地"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 设备话题(状态)信息
|
# 设备话题(状态)信息
|
||||||
@@ -55,10 +64,7 @@ def update_ros_node_info() -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 设备动作信息
|
# 设备动作信息
|
||||||
result["device_actions"][device_id] = {
|
result["device_actions"][device_id] = {k: get_action_info(v, k) for k, v in device_info["actions"].items()}
|
||||||
k: get_action_info(v, k)
|
|
||||||
for k, v in device_info["actions"].items()
|
|
||||||
}
|
|
||||||
# 更新全局变量
|
# 更新全局变量
|
||||||
ros_node_info = result
|
ros_node_info = result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -6,7 +6,7 @@ def generate_clean_protocol(
|
|||||||
G: nx.DiGraph,
|
G: nx.DiGraph,
|
||||||
vessel: str, # Vessel to clean.
|
vessel: str, # Vessel to clean.
|
||||||
solvent: str, # Solvent to clean vessel with.
|
solvent: str, # Solvent to clean vessel with.
|
||||||
volume: float = 25000.0, # Optional. Volume of solvent to clean vessel with.
|
volume: float = 25.0, # Optional. Volume of solvent to clean vessel with.
|
||||||
temp: float = 25, # Optional. Temperature to heat vessel to while cleaning.
|
temp: float = 25, # Optional. Temperature to heat vessel to while cleaning.
|
||||||
repeats: int = 1, # Optional. Number of cleaning cycles to perform.
|
repeats: int = 1, # Optional. Number of cleaning cycles to perform.
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@@ -27,7 +27,7 @@ def generate_clean_protocol(
|
|||||||
from_vessel = f"flask_{solvent}"
|
from_vessel = f"flask_{solvent}"
|
||||||
waste_vessel = f"waste_workup"
|
waste_vessel = f"waste_workup"
|
||||||
|
|
||||||
transfer_flowrate = flowrate = 2500.0
|
transfer_flowrate = flowrate = 2.5
|
||||||
|
|
||||||
# 生成泵操作的动作序列
|
# 生成泵操作的动作序列
|
||||||
for i in range(repeats):
|
for i in range(repeats):
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ def generate_evaporate_protocol(
|
|||||||
|
|
||||||
# 生成泵操作的动作序列
|
# 生成泵操作的动作序列
|
||||||
pump_action_sequence = []
|
pump_action_sequence = []
|
||||||
reactor_volume = 500000.0
|
reactor_volume = 500.0
|
||||||
transfer_flowrate = flowrate = 2500.0
|
transfer_flowrate = flowrate = 2.5
|
||||||
|
|
||||||
# 开启冷凝器
|
# 开启冷凝器
|
||||||
pump_action_sequence.append({
|
pump_action_sequence.append({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ def generate_pump_protocol(
|
|||||||
from_vessel: str,
|
from_vessel: str,
|
||||||
to_vessel: str,
|
to_vessel: str,
|
||||||
volume: float,
|
volume: float,
|
||||||
flowrate: float = 500.0,
|
flowrate: float = 0.5,
|
||||||
transfer_flowrate: float = 0,
|
transfer_flowrate: float = 0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
@@ -141,11 +141,11 @@ def generate_pump_protocol_with_rinsing(
|
|||||||
time: float = 0,
|
time: float = 0,
|
||||||
viscous: bool = False,
|
viscous: bool = False,
|
||||||
rinsing_solvent: str = "air",
|
rinsing_solvent: str = "air",
|
||||||
rinsing_volume: float = 5000.0,
|
rinsing_volume: float = 5.0,
|
||||||
rinsing_repeats: int = 2,
|
rinsing_repeats: int = 2,
|
||||||
solid: bool = False,
|
solid: bool = False,
|
||||||
flowrate: float = 2500.0,
|
flowrate: float = 2.5,
|
||||||
transfer_flowrate: float = 500.0,
|
transfer_flowrate: float = 0.5,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
|
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
|
||||||
@@ -159,11 +159,11 @@ def generate_pump_protocol_with_rinsing(
|
|||||||
time (float, optional): Time over which to perform the transfer (default is 0).
|
time (float, optional): Time over which to perform the transfer (default is 0).
|
||||||
viscous (bool, optional): Indicates if the fluid is viscous (default is False).
|
viscous (bool, optional): Indicates if the fluid is viscous (default is False).
|
||||||
rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air").
|
rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air").
|
||||||
rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5000.0).
|
rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5.0).
|
||||||
rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2).
|
rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2).
|
||||||
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
|
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
|
||||||
flowrate (float, optional): The flow rate for the transfer (default is 2500.0). 最终注入容器B时的流速
|
flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速
|
||||||
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 500.0). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
|
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
|
||||||
@@ -172,7 +172,7 @@ def generate_pump_protocol_with_rinsing(
|
|||||||
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
|
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 100.0, rinsing_solvent="water")
|
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
|
||||||
"""
|
"""
|
||||||
air_vessel = "flask_air"
|
air_vessel = "flask_air"
|
||||||
waste_vessel = f"waste_workup"
|
waste_vessel = f"waste_workup"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def generate_separate_protocol(
|
|||||||
to_vessel: str, # Vessel to send product phase to.
|
to_vessel: str, # Vessel to send product phase to.
|
||||||
waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
|
waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
|
||||||
solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
|
solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
|
||||||
solvent_volume: float = 50000, # Optional. Volume of solvent to add.
|
solvent_volume: float = 50, # Optional. Volume of solvent to add (mL).
|
||||||
through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
|
through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
|
||||||
repeats: int = 1, # Optional. Number of separations to perform.
|
repeats: int = 1, # Optional. Number of separations to perform.
|
||||||
stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
|
stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
|
||||||
@@ -32,7 +32,7 @@ def generate_separate_protocol(
|
|||||||
|
|
||||||
# 生成泵操作的动作序列
|
# 生成泵操作的动作序列
|
||||||
pump_action_sequence = []
|
pump_action_sequence = []
|
||||||
reactor_volume = 500000.0
|
reactor_volume = 500.0
|
||||||
waste_vessel = waste_phase_to_vessel
|
waste_vessel = waste_phase_to_vessel
|
||||||
|
|
||||||
# TODO:通过物料管理系统找到溶剂的容器
|
# TODO:通过物料管理系统找到溶剂的容器
|
||||||
@@ -46,7 +46,7 @@ def generate_separate_protocol(
|
|||||||
separator_controller = f"{separation_vessel}_controller"
|
separator_controller = f"{separation_vessel}_controller"
|
||||||
separation_vessel_bottom = f"flask_{separation_vessel}"
|
separation_vessel_bottom = f"flask_{separation_vessel}"
|
||||||
|
|
||||||
transfer_flowrate = flowrate = 2500.0
|
transfer_flowrate = flowrate = 2.5
|
||||||
|
|
||||||
if from_vessel != separation_vessel:
|
if from_vessel != separation_vessel:
|
||||||
pump_action_sequence.append(
|
pump_action_sequence.append(
|
||||||
@@ -140,8 +140,8 @@ def generate_separate_protocol(
|
|||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"from_vessel": separation_vessel_bottom,
|
"from_vessel": separation_vessel_bottom,
|
||||||
"to_vessel": to_vessel,
|
"to_vessel": to_vessel,
|
||||||
"volume": 250000.0,
|
"volume": 250.0,
|
||||||
"time": 250000.0 / flowrate,
|
"time": 250.0 / flowrate,
|
||||||
# "transfer_flowrate": transfer_flowrate,
|
# "transfer_flowrate": transfer_flowrate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,8 +164,8 @@ def generate_separate_protocol(
|
|||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"from_vessel": separation_vessel_bottom,
|
"from_vessel": separation_vessel_bottom,
|
||||||
"to_vessel": waste_vessel,
|
"to_vessel": waste_vessel,
|
||||||
"volume": 250000.0,
|
"volume": 250.0,
|
||||||
"time": 250000.0 / flowrate,
|
"time": 250.0 / flowrate,
|
||||||
# "transfer_flowrate": transfer_flowrate,
|
# "transfer_flowrate": transfer_flowrate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,8 +179,8 @@ def generate_separate_protocol(
|
|||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"from_vessel": separation_vessel_bottom,
|
"from_vessel": separation_vessel_bottom,
|
||||||
"to_vessel": waste_vessel,
|
"to_vessel": waste_vessel,
|
||||||
"volume": 250000.0,
|
"volume": 250.0,
|
||||||
"time": 250000.0 / flowrate,
|
"time": 250.0 / flowrate,
|
||||||
# "transfer_flowrate": transfer_flowrate,
|
# "transfer_flowrate": transfer_flowrate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,8 +203,8 @@ def generate_separate_protocol(
|
|||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"from_vessel": separation_vessel_bottom,
|
"from_vessel": separation_vessel_bottom,
|
||||||
"to_vessel": to_vessel,
|
"to_vessel": to_vessel,
|
||||||
"volume": 250000.0,
|
"volume": 250.0,
|
||||||
"time": 250000.0 / flowrate,
|
"time": 250.0 / flowrate,
|
||||||
# "transfer_flowrate": transfer_flowrate,
|
# "transfer_flowrate": transfer_flowrate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,8 +221,8 @@ def generate_separate_protocol(
|
|||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"from_vessel": to_vessel,
|
"from_vessel": to_vessel,
|
||||||
"to_vessel": separation_vessel,
|
"to_vessel": separation_vessel,
|
||||||
"volume": 250000.0,
|
"volume": 250.0,
|
||||||
"time": 250000.0 / flowrate,
|
"time": 250.0 / flowrate,
|
||||||
# "transfer_flowrate": transfer_flowrate,
|
# "transfer_flowrate": transfer_flowrate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class BasicConfig:
|
|||||||
config_path = ""
|
config_path = ""
|
||||||
is_host_mode = True # 从registry.py移动过来
|
is_host_mode = True # 从registry.py移动过来
|
||||||
slave_no_host = False # 是否跳过rclient.wait_for_service()
|
slave_no_host = False # 是否跳过rclient.wait_for_service()
|
||||||
|
machine_name = "undefined"
|
||||||
|
vis_2d_enable = False
|
||||||
|
|
||||||
|
|
||||||
# MQTT配置
|
# MQTT配置
|
||||||
@@ -28,9 +30,9 @@ class MQConfig:
|
|||||||
key_content = ""
|
key_content = ""
|
||||||
|
|
||||||
# 指定
|
# 指定
|
||||||
ca_file = ""
|
ca_file = "" # 相对config.py所在目录的路径
|
||||||
cert_file = ""
|
cert_file = "" # 相对config.py所在目录的路径
|
||||||
key_file = ""
|
key_file = "" # 相对config.py所在目录的路径
|
||||||
|
|
||||||
|
|
||||||
# OSS上传配置
|
# OSS上传配置
|
||||||
@@ -75,49 +77,106 @@ def _update_config_from_module(module):
|
|||||||
# 需要先判断是否为相对路径
|
# 需要先判断是否为相对路径
|
||||||
if MQConfig.ca_file.startswith("."):
|
if MQConfig.ca_file.startswith("."):
|
||||||
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
|
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
|
||||||
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
|
if len(MQConfig.ca_file) != 0:
|
||||||
MQConfig.ca_content = f.read()
|
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
|
||||||
|
MQConfig.ca_content = f.read()
|
||||||
|
else:
|
||||||
|
logger.warning("Skipping CA file loading, ca_file is empty")
|
||||||
if len(MQConfig.cert_content) == 0:
|
if len(MQConfig.cert_content) == 0:
|
||||||
# 需要先判断是否为相对路径
|
# 需要先判断是否为相对路径
|
||||||
if MQConfig.cert_file.startswith("."):
|
if MQConfig.cert_file.startswith("."):
|
||||||
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
|
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
|
||||||
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
|
if len(MQConfig.ca_file) != 0:
|
||||||
MQConfig.cert_content = f.read()
|
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
|
||||||
|
MQConfig.cert_content = f.read()
|
||||||
|
else:
|
||||||
|
logger.warning("Skipping cert file loading, cert_file is empty")
|
||||||
if len(MQConfig.key_content) == 0:
|
if len(MQConfig.key_content) == 0:
|
||||||
# 需要先判断是否为相对路径
|
# 需要先判断是否为相对路径
|
||||||
if MQConfig.key_file.startswith("."):
|
if MQConfig.key_file.startswith("."):
|
||||||
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
|
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
|
||||||
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
|
if len(MQConfig.ca_file) != 0:
|
||||||
MQConfig.key_content = f.read()
|
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
|
||||||
|
MQConfig.key_content = f.read()
|
||||||
|
else:
|
||||||
|
logger.warning("Skipping key file loading, key_file is empty")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_config_from_env():
|
||||||
|
prefix = "UNILABOS."
|
||||||
|
for env_key, env_value in os.environ.items():
|
||||||
|
if not env_key.startswith(prefix):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
key_path = env_key[len(prefix):] # Remove UNILAB_ prefix
|
||||||
|
class_field = key_path.upper().split(".", 1)
|
||||||
|
if len(class_field) != 2:
|
||||||
|
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
class_key, field_key = class_field
|
||||||
|
# 遍历 globals 找匹配类(不区分大小写)
|
||||||
|
matched_cls = None
|
||||||
|
for name, obj in globals().items():
|
||||||
|
if name.upper() == class_key and isinstance(obj, type):
|
||||||
|
matched_cls = obj
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_cls is None:
|
||||||
|
logger.warning(f"[ENV] 未找到类:{class_key}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 查找类属性(不区分大小写)
|
||||||
|
matched_field = None
|
||||||
|
for attr in dir(matched_cls):
|
||||||
|
if attr.upper() == field_key:
|
||||||
|
matched_field = attr
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_field is None:
|
||||||
|
logger.warning(f"[ENV] 类 {matched_cls.__name__} 中未找到字段:{field_key}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_value = getattr(matched_cls, matched_field)
|
||||||
|
attr_type = type(current_value)
|
||||||
|
if attr_type == bool:
|
||||||
|
value = env_value.lower() in ("true", "1", "yes")
|
||||||
|
elif attr_type == int:
|
||||||
|
value = int(env_value)
|
||||||
|
elif attr_type == float:
|
||||||
|
value = float(env_value)
|
||||||
|
else:
|
||||||
|
value = env_value
|
||||||
|
setattr(matched_cls, matched_field, value)
|
||||||
|
logger.info(f"[ENV] 设置 {matched_cls.__name__}.{matched_field} = {value}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path=None):
|
def load_config(config_path=None):
|
||||||
# 如果提供了配置文件路径,从该文件导入配置
|
# 如果提供了配置文件路径,从该文件导入配置
|
||||||
if config_path:
|
if config_path:
|
||||||
|
_update_config_from_env() # 允许config_path被env设定后读取
|
||||||
BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path))
|
BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path))
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
logger.error(f"配置文件 {config_path} 不存在")
|
logger.error(f"[ENV] 配置文件 {config_path} 不存在")
|
||||||
return
|
exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module_name = "lab_" + os.path.basename(config_path).replace(".py", "")
|
module_name = "lab_" + os.path.basename(config_path).replace(".py", "")
|
||||||
spec = importlib.util.spec_from_file_location(module_name, config_path)
|
spec = importlib.util.spec_from_file_location(module_name, config_path)
|
||||||
if spec is None:
|
if spec is None:
|
||||||
logger.error(f"配置文件 {config_path} 错误")
|
logger.error(f"[ENV] 配置文件 {config_path} 错误")
|
||||||
return
|
return
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module) # type: ignore
|
spec.loader.exec_module(module) # type: ignore
|
||||||
_update_config_from_module(module)
|
_update_config_from_module(module)
|
||||||
logger.info(f"配置文件 {config_path} 加载成功")
|
logger.info(f"[ENV] 配置文件 {config_path} 加载成功")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
logger.error(f"[ENV] 加载配置文件 {config_path} 失败")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
try:
|
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||||
import unilabos.config.local_config as local_config # type: ignore
|
load_config(config_path)
|
||||||
|
|
||||||
_update_config_from_module(local_config)
|
|
||||||
logger.info("已加载默认配置 unilabos.config.local_config")
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|||||||
0
unilabos/device_mesh/__init__.py
Normal file
0
unilabos/device_mesh/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"first_joint": {
|
||||||
|
"child":"first_link",
|
||||||
|
"axis" : "-y"
|
||||||
|
},
|
||||||
|
"second_joint": {
|
||||||
|
"child":"second_link",
|
||||||
|
"axis" : "-x"
|
||||||
|
},
|
||||||
|
"third_joint": {
|
||||||
|
"child":"third_link",
|
||||||
|
"axis" : "-z"
|
||||||
|
},
|
||||||
|
"fourth_joint": {
|
||||||
|
"child":"fourth_link",
|
||||||
|
"axis" : "-z"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
|
||||||
|
<robot xmlns:xacro="http://ros.org/wiki/xacro">
|
||||||
|
|
||||||
|
<xacro:macro name="opentrons_liquid_handler"
|
||||||
|
params="parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0 mesh_path:=''">
|
||||||
|
|
||||||
|
|
||||||
|
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
|
||||||
|
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
|
||||||
|
<parent link="${parent_link}"/>
|
||||||
|
<child link="${station_name}${device_name}device_link"/>
|
||||||
|
<axis xyz="0 0 0"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name="${station_name}${device_name}device_link"/>
|
||||||
|
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
|
||||||
|
<origin xyz="-0.11565 0.496 0" rpy="0 0 0" />
|
||||||
|
<parent link="${station_name}${device_name}device_link"/>
|
||||||
|
<child link="${station_name}${device_name}main_link"/>
|
||||||
|
<axis xyz="0 0 0"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}main_link'>
|
||||||
|
<visual>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}first_link'>
|
||||||
|
<visual>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}second_link'>
|
||||||
|
<visual>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}third_link'>
|
||||||
|
<visual>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}fourth_link'>
|
||||||
|
<visual>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
|
||||||
|
<joint name='${station_name}${device_name}first_joint' type='prismatic'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}first_link"/>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<axis xyz="0 -1 0"/>
|
||||||
|
<limit effort="-1" lower="-0.2" upper="0.13" velocity="-1"/>
|
||||||
|
<dynamics damping="0.1"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}second_joint' type='prismatic'>
|
||||||
|
<parent link="${station_name}${device_name}first_link"/>
|
||||||
|
<child link="${station_name}${device_name}second_link"/>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<axis xyz="-1 0 0"/>
|
||||||
|
<limit effort="-1" lower="-0.15" upper="0.15" velocity="-1"/>
|
||||||
|
<dynamics damping="0.1"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}third_joint' type='prismatic'>
|
||||||
|
<parent link="${station_name}${device_name}second_link"/>
|
||||||
|
<child link="${station_name}${device_name}third_link"/>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<axis xyz="0 0 -1"/>
|
||||||
|
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
|
||||||
|
<dynamics damping="0.1"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}fourth_joint' type='prismatic'>
|
||||||
|
<parent link="${station_name}${device_name}second_link"/>
|
||||||
|
<child link="${station_name}${device_name}fourth_link"/>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<axis xyz="0 0 -1"/>
|
||||||
|
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
|
||||||
|
<dynamics damping="0.1"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}socketTypeGenericSbsFootprint'/>
|
||||||
|
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_10_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.1795 -0.1825 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_7_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.1795 -0.273 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_4_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.1795 -0.3635 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_1_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.1795 -0.454 0.07"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_11_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.312 -0.1825 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_8_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.312 -0.273 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_5_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.312 -0.3635 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_2_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.312 -0.454 0.07"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_9_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.4445 -0.273 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_6_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.4445 -0.3635 0.07"/>
|
||||||
|
</joint>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_3_60_1' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
|
||||||
|
<origin rpy="0 0 -1.57" xyz="0.4445 -0.454 0.07"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
|
||||||
|
<link name='${station_name}${device_name}socketTypeHEPAModule'/>
|
||||||
|
<joint name='${station_name}${device_name}socketTypeHEPAModule' type='fixed'>
|
||||||
|
<parent link="${station_name}${device_name}main_link"/>
|
||||||
|
<child link="${station_name}${device_name}socketTypeHEPAModule"/>
|
||||||
|
<origin rpy="0 0 0" xyz="0.31 -0.26 0.66"/>
|
||||||
|
</joint>
|
||||||
|
</xacro:macro>
|
||||||
|
</robot>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"private_param":
|
||||||
|
{
|
||||||
|
|
||||||
|
},
|
||||||
|
"public_param":
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"slider_joint": {
|
||||||
|
"child":"slider",
|
||||||
|
"axis" : "x"
|
||||||
|
}
|
||||||
|
}
|
||||||
136
unilabos/device_mesh/devices/slide_w140/macro_device.xacro
Normal file
136
unilabos/device_mesh/devices/slide_w140/macro_device.xacro
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<!-- =================================================================================== -->
|
||||||
|
<!-- | This document was autogenerated by xacro from slide.urdf | -->
|
||||||
|
<!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | -->
|
||||||
|
<!-- =================================================================================== -->
|
||||||
|
<!-- This URDF was automatically created by SolidWorks to URDF Exporter! Originally created by Stephen Brawner (brawner@gmail.com)
|
||||||
|
Commit Version: 1.6.0-4-g7f85cfe Build Version: 1.6.7995.38578
|
||||||
|
For more information, please see http://wiki.ros.org/sw_urdf_exporter -->
|
||||||
|
<robot xmlns:xacro="http://ros.org/wiki/xacro">
|
||||||
|
|
||||||
|
<xacro:macro name="slide_w140" params="mesh_path:='' length:=0.1 min_d:=0.1 max_d:=0.1 slider_d:=0.14 parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0" >
|
||||||
|
|
||||||
|
|
||||||
|
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
|
||||||
|
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
|
||||||
|
<parent link="${parent_link}"/>
|
||||||
|
<child link="${station_name}${device_name}device_link"/>
|
||||||
|
<axis xyz="0 0 0"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name="${station_name}${device_name}device_link"/>
|
||||||
|
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
|
||||||
|
<origin xyz="${-min_d} 0 0" rpy="0 0 0" />
|
||||||
|
<parent link="${station_name}${device_name}device_link"/>
|
||||||
|
<child link="${station_name}${device_name}base_link"/>
|
||||||
|
<axis xyz="0 0 0"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
<link name="${station_name}${device_name}base_link">
|
||||||
|
<inertial>
|
||||||
|
<origin rpy="0 0 0" xyz="-0.00073944 -0.070732 0.035966"/>
|
||||||
|
<mass value="0.88697"/>
|
||||||
|
<inertia ixx="0.0019162" ixy="-2.5282E-11" ixz="6.1083E-07" iyy="0.00066605" iyz="1.4627E-08" izz="0.0019573"/>
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="1 1 1 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link name="${station_name}${device_name}slider">
|
||||||
|
<inertial>
|
||||||
|
<origin rpy="0 0 0" xyz="0 -4.7184E-16 0.0095496"/>
|
||||||
|
<mass value="0.27913"/>
|
||||||
|
<inertia ixx="0.00048249" ixy="2.0866E-19" ixz="2.788E-21" iyy="0.00047014" iyz="1.1542E-17" izz="0.0009242"/>
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="1 1 1 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint name="${station_name}${device_name}slider_joint" type="prismatic">
|
||||||
|
<origin rpy="0 0 0" xyz="${min_d + slider_d/2} 0 0.0475"/>
|
||||||
|
<parent link="${station_name}${device_name}base_link"/>
|
||||||
|
<child link="${station_name}${device_name}slider"/>
|
||||||
|
<axis xyz="1 0 0"/>
|
||||||
|
<limit effort="100" lower="0" upper="${length}" velocity="1"/>
|
||||||
|
</joint>
|
||||||
|
<link name="${station_name}${device_name}length">
|
||||||
|
<inertial>
|
||||||
|
<origin rpy="0 0 0" xyz="0.005 -9.3044E-10 0.02803"/>
|
||||||
|
<mass value="0.050532"/>
|
||||||
|
<inertia ixx="8.098E-05" ixy="-1.2574E-19" ixz="3.1207E-20" iyy="4.4152E-06" iyz="-3.1294E-13" izz="7.7407E-05"/>
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="1 1 1 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint name="${station_name}${device_name}length_joint" type="fixed">
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<parent link="${station_name}${device_name}base_link"/>
|
||||||
|
<child link="${station_name}${device_name}length"/>
|
||||||
|
</joint>
|
||||||
|
<link name="${station_name}${device_name}slide_end">
|
||||||
|
<inertial>
|
||||||
|
<origin rpy="0 0 0" xyz="0.003 8.6044E-06 0.035593"/>
|
||||||
|
<mass value="0.055452"/>
|
||||||
|
<inertia ixx="9.9729E-05" ixy="-1.0228E-21" ixz="3.8344E-22" iyy="2.3158E-05" iyz="1.5613E-08" izz="7.6904E-05"/>
|
||||||
|
</inertial>
|
||||||
|
<visual>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="">
|
||||||
|
<color rgba="1 1 1 1"/>
|
||||||
|
</material>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin rpy="0 0 0" xyz="0 0 0"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint name="${station_name}${device_name}slide_end_joint" type="fixed">
|
||||||
|
<origin rpy="0 0 0" xyz="${length + max_d +min_d + slider_d} 0 0"/>
|
||||||
|
<parent link="${station_name}${device_name}base_link"/>
|
||||||
|
<child link="${station_name}${device_name}slide_end"/>
|
||||||
|
</joint>
|
||||||
|
|
||||||
|
</xacro:macro>
|
||||||
|
</robot>
|
||||||
BIN
unilabos/device_mesh/devices/slide_w140/meshes/base_link.STL
Executable file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/base_link.STL
Executable file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/base_link.fbx
Normal file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/base_link.fbx
Normal file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/length.STL
Executable file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/length.STL
Executable file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/length.fbx
Normal file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/length.fbx
Normal file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slide_end.STL
Executable file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slide_end.STL
Executable file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slide_end.fbx
Normal file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slide_end.fbx
Normal file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slider.STL
Executable file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slider.STL
Executable file
Binary file not shown.
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slider.fbx
Normal file
BIN
unilabos/device_mesh/devices/slide_w140/meshes/slider.fbx
Normal file
Binary file not shown.
12
unilabos/device_mesh/devices/slide_w140/param_config.json
Normal file
12
unilabos/device_mesh/devices/slide_w140/param_config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"private_param":
|
||||||
|
{
|
||||||
|
"min_d": 0.1 ,
|
||||||
|
"max_d": 0.1 ,
|
||||||
|
"slider_d": 0.14
|
||||||
|
},
|
||||||
|
"public_param":
|
||||||
|
{
|
||||||
|
"length" :0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
174
unilabos/device_mesh/resource_visalization.py
Normal file
174
unilabos/device_mesh/resource_visalization.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from launch import LaunchService
|
||||||
|
from launch import LaunchDescription
|
||||||
|
from launch_ros.actions import Node as nd
|
||||||
|
import xacro
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceVisualization:
|
||||||
|
def __init__(self, device: dict, resource: dict, enable_rviz: bool = True):
|
||||||
|
"""初始化资源可视化类
|
||||||
|
|
||||||
|
该类用于将设备和资源的3D模型可视化展示。通过解析设备和资源的配置信息,
|
||||||
|
从注册表中获取对应的3D模型文件,并使用ROS2和RViz进行可视化。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device (dict): 设备配置字典,包含设备的类型、位置等信息
|
||||||
|
resource (dict): 资源配置字典,包含资源的类型、位置等信息
|
||||||
|
registry (dict): 注册表字典,包含设备和资源类型的注册信息
|
||||||
|
enable_rviz (bool, optional): 是否启用RViz可视化. Defaults to True.
|
||||||
|
"""
|
||||||
|
self.launch_service = LaunchService()
|
||||||
|
self.launch_description = LaunchDescription()
|
||||||
|
self.resource_dict = resource
|
||||||
|
self.resource_model = {}
|
||||||
|
self.resource_type = ['deck', 'plate', 'container']
|
||||||
|
self.mesh_path = Path(__file__).parent.absolute()
|
||||||
|
self.enable_rviz = enable_rviz
|
||||||
|
registry = lab_registry
|
||||||
|
|
||||||
|
self.srdf_str = '''
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<robot name="minimal">
|
||||||
|
|
||||||
|
</robot>
|
||||||
|
'''
|
||||||
|
self.robot_state_str= '''<?xml version="1.0" ?>
|
||||||
|
<robot xmlns:xacro="http://ros.org/wiki/xacro" name="full_dev">
|
||||||
|
<link name="world"/>
|
||||||
|
</robot>
|
||||||
|
'''
|
||||||
|
self.root = etree.fromstring(self.robot_state_str)
|
||||||
|
|
||||||
|
xacro_uri = self.root.nsmap["xacro"]
|
||||||
|
|
||||||
|
# 遍历设备节点
|
||||||
|
for node in device.values():
|
||||||
|
if node['type'] == 'device' and node['class'] != '':
|
||||||
|
device_class = node['class']
|
||||||
|
# 检查设备类型是否在注册表中
|
||||||
|
if device_class not in registry.device_type_registry.keys():
|
||||||
|
raise ValueError(f"设备类型 {device_class} 未在注册表中注册")
|
||||||
|
elif node['type'] in self.resource_type:
|
||||||
|
# print(registry.resource_type_registry)
|
||||||
|
resource_class = node['class']
|
||||||
|
if resource_class not in registry.resource_type_registry.keys():
|
||||||
|
raise ValueError(f"资源类型 {resource_class} 未在注册表中注册")
|
||||||
|
elif "model" in registry.resource_type_registry[resource_class].keys():
|
||||||
|
model_config = registry.resource_type_registry[resource_class]['model']
|
||||||
|
if model_config['type'] == 'resource':
|
||||||
|
self.resource_model[node['id']] = {
|
||||||
|
'mesh': f"{str(self.mesh_path)}/resources/{model_config['mesh']}",
|
||||||
|
'mesh_tf': model_config['mesh_tf']}
|
||||||
|
if 'children_mesh' in model_config:
|
||||||
|
if model_config['children_mesh'] is not None:
|
||||||
|
self.resource_model[f"{node['id']}_"] = {
|
||||||
|
'mesh': f"{str(self.mesh_path)}/resources/{model_config['children_mesh']}",
|
||||||
|
'mesh_tf': model_config['children_mesh_tf']
|
||||||
|
}
|
||||||
|
elif model_config['type'] == 'device':
|
||||||
|
new_include = etree.SubElement(self.root, f"{{{xacro_uri}}}include")
|
||||||
|
new_include.set("filename", f"{str(self.mesh_path)}/devices/{model_config['mesh']}/macro_device.xacro")
|
||||||
|
new_dev = etree.SubElement(self.root, f"{{{xacro_uri}}}{model_config['mesh']}")
|
||||||
|
new_dev.set("parent_link", "world")
|
||||||
|
new_dev.set("mesh_path", str(self.mesh_path))
|
||||||
|
new_dev.set("device_name", node["id"]+"_")
|
||||||
|
new_dev.set("station_name", node["parent"]+'_')
|
||||||
|
new_dev.set("x",str(float(node["position"]["x"])/1000))
|
||||||
|
new_dev.set("y",str(float(node["position"]["y"])/1000))
|
||||||
|
new_dev.set("z",str(float(node["position"]["z"])/1000))
|
||||||
|
if "rotation" in node["config"]:
|
||||||
|
new_dev.set("r",str(float(node["config"]["rotation"]["z"])/1000))
|
||||||
|
else:
|
||||||
|
print("错误的注册表类型!")
|
||||||
|
re = etree.tostring(self.root, encoding="unicode")
|
||||||
|
doc = xacro.parse(re)
|
||||||
|
xacro.process_doc(doc)
|
||||||
|
self.urdf_str = doc.toxml()
|
||||||
|
|
||||||
|
|
||||||
|
def create_launch_description(self, urdf_str: str) -> LaunchDescription:
|
||||||
|
"""
|
||||||
|
创建launch描述,包含robot_state_publisher和move_group节点
|
||||||
|
|
||||||
|
Args:
|
||||||
|
urdf_str: URDF文本
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LaunchDescription: launch描述对象
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 解析URDF文件
|
||||||
|
robot_description = urdf_str
|
||||||
|
|
||||||
|
# 创建robot_state_publisher节点
|
||||||
|
robot_state_publisher = nd(
|
||||||
|
package='robot_state_publisher',
|
||||||
|
executable='robot_state_publisher',
|
||||||
|
name='robot_state_publisher',
|
||||||
|
output='screen',
|
||||||
|
parameters=[{
|
||||||
|
'robot_description': robot_description,
|
||||||
|
'use_sim_time': False
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# joint_state_publisher_node = nd(
|
||||||
|
# package='joint_state_publisher_gui', # 或 joint_state_publisher
|
||||||
|
# executable='joint_state_publisher_gui',
|
||||||
|
# name='joint_state_publisher',
|
||||||
|
# output='screen'
|
||||||
|
# )
|
||||||
|
# 创建move_group节点
|
||||||
|
move_group = nd(
|
||||||
|
package='moveit_ros_move_group',
|
||||||
|
executable='move_group',
|
||||||
|
output='screen',
|
||||||
|
parameters=[{
|
||||||
|
'robot_description': robot_description,
|
||||||
|
'robot_description_semantic': self.srdf_str,
|
||||||
|
'capabilities': '',
|
||||||
|
'disable_capabilities': '',
|
||||||
|
'monitor_dynamics': False,
|
||||||
|
'publish_monitored_planning_scene': True,
|
||||||
|
'publish_robot_description_semantic': True,
|
||||||
|
'publish_planning_scene': True,
|
||||||
|
'publish_geometry_updates': True,
|
||||||
|
'publish_state_updates': True,
|
||||||
|
'publish_transforms_updates': True,
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 将节点添加到launch描述中
|
||||||
|
self.launch_description.add_action(robot_state_publisher)
|
||||||
|
# self.launch_description.add_action(joint_state_publisher_node)
|
||||||
|
self.launch_description.add_action(move_group)
|
||||||
|
|
||||||
|
# 如果启用RViz,添加RViz节点
|
||||||
|
if self.enable_rviz:
|
||||||
|
rviz_node = nd(
|
||||||
|
package='rviz2',
|
||||||
|
executable='rviz2',
|
||||||
|
name='rviz2',
|
||||||
|
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
||||||
|
output='screen'
|
||||||
|
)
|
||||||
|
self.launch_description.add_action(rviz_node)
|
||||||
|
|
||||||
|
return self.launch_description
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
启动可视化服务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
urdf_str: URDF文件路径
|
||||||
|
"""
|
||||||
|
launch_description = self.create_launch_description(self.urdf_str)
|
||||||
|
self.launch_service.include_launch_description(launch_description)
|
||||||
|
self.launch_service.run()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"fileName": "generic_labware_tube_10_75",
|
||||||
|
"related": [
|
||||||
|
"generic_labware_0.5ml_screw_cap_tube",
|
||||||
|
"generic_labware_0.5ml_tube_rack",
|
||||||
|
"generic_labware_12_well_plate",
|
||||||
|
"sarstedt_14x200mm_tube",
|
||||||
|
"sarstedt_18x200mm_tube",
|
||||||
|
"generic_labware_1ml_tube_rack",
|
||||||
|
"generic_labware_24_well_plate",
|
||||||
|
"generic_labware_2ml_screw_cap_tube",
|
||||||
|
"generic_labware_5ml_screw_cap_tube",
|
||||||
|
"generic_labware_6_well_plate",
|
||||||
|
"generic_labware_96_well_square",
|
||||||
|
"generic_labware_96_well_pcr_plate_round",
|
||||||
|
"generic_labware_framedtiprack",
|
||||||
|
"generic_labware_plate_lid",
|
||||||
|
"generic_labware_reservoir",
|
||||||
|
"generic_labware_tip_box",
|
||||||
|
"generic_labware_tube_10_75"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="generic_labware_tube_10_75">
|
||||||
|
<link name='0_base_link'>
|
||||||
|
<visual name='visual'>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="meshes/0_base.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="clay" />
|
||||||
|
</visual>
|
||||||
|
</link>
|
||||||
|
</robot>
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"fileName": "tecan_nested_tip_rack",
|
||||||
|
"related": [
|
||||||
|
"tecan_techrom",
|
||||||
|
"tecan_holder_transfer_tool",
|
||||||
|
"tecan_fluent_9_grid_segment_cutout",
|
||||||
|
"tecan_fluent_centric_gripper",
|
||||||
|
"tecan_fluent_eccentric_gripper",
|
||||||
|
"tecan_evo100",
|
||||||
|
"tecan_fluent_mp_diti_nest_segment",
|
||||||
|
"tecan_fluent_4x100_trough",
|
||||||
|
"tecan_fluent_1080_extended",
|
||||||
|
"tecan_fluent_1_1_1000_trough",
|
||||||
|
"tecan_fluent_50ml_tube_runner_10_v2",
|
||||||
|
"tecan_fluent_15ml_tube_runner_16_v2",
|
||||||
|
"tecan_fluent_1_16_16_tube_runner",
|
||||||
|
"tecan_fluent_1.5ml_tube_runner_v2",
|
||||||
|
"tecan_fluent_1_24_10_tube_runner",
|
||||||
|
"tecan_fluent_1_24_13_tube_runner",
|
||||||
|
"tecan_fluent_3x320_reagent_trough_v2",
|
||||||
|
"tecan_fluent_32_tube_runner_v2",
|
||||||
|
"tecan_fluent_1_4_100_trough",
|
||||||
|
"tecan_fluent_2_grid_segment",
|
||||||
|
"tecan_fluent_2_4_100_trough_waste",
|
||||||
|
"tecan_fluent_3_grid_segment",
|
||||||
|
"tecan_fluent_nest_waste_segment_v2",
|
||||||
|
"tecan_fluent_320ml_reagent_trough",
|
||||||
|
"tecan_fluent_4_landscape_61mm_nest_segment",
|
||||||
|
"tecan_fluent_4_landscape_61mm_nest_segment_waste",
|
||||||
|
"tecan_fluent_4_landscape_7mm_nest_segment",
|
||||||
|
"tecan_fluent_4_landscape_7mm_nest_segment_waste",
|
||||||
|
"tecan_fluent_hotel_deck_4",
|
||||||
|
"tecan_fluent_480_extended",
|
||||||
|
"tecan_fluent_4x100_reagent_trough_v2",
|
||||||
|
"tecan_fluent_5_landscape_61mm_nest_segment",
|
||||||
|
"tecan_fluent_5_landscape_7mm_nest_segment",
|
||||||
|
"tecan_fluent_hotel_deck_5",
|
||||||
|
"tecan_fluent_6_grid_segment",
|
||||||
|
"tecan_fluent_nest_landscape_segment_v2",
|
||||||
|
"tecan_fluent_6_landscape_7mm_nest_segment",
|
||||||
|
"tecan_fluent_deck_segment_6_v2",
|
||||||
|
"tecan_fluent_fca_diti_segment_v2",
|
||||||
|
"tecan_fluent_6_nest_incubator",
|
||||||
|
"tecan_fluent_plate_nest",
|
||||||
|
"tecan_fluent_780_extended",
|
||||||
|
"tecan_fluent_plate_holder",
|
||||||
|
"tecan_fluent_8_grid_segment",
|
||||||
|
"tecan_fluent_8_grid_segment_evo",
|
||||||
|
"tecan_fluent_hotel_deck_9",
|
||||||
|
"tecan_carousel",
|
||||||
|
"tecan_carousel_stacker_10",
|
||||||
|
"tecan_carousel_stacker_25",
|
||||||
|
"tecan_carousel_stacker_6",
|
||||||
|
"tecan_fluent_coolheat_microplate_segment_v2",
|
||||||
|
"tecan_fluent_fca_diti_tray",
|
||||||
|
"tecan_fluent_trough_waste",
|
||||||
|
"tecan_fluent_id_left",
|
||||||
|
"tecan_fluent_id_middle",
|
||||||
|
"tecan_fluent_lower_6_grid_v2",
|
||||||
|
"tecan_fluent_mc384_nest",
|
||||||
|
"tecan_fluent_mca_44mm_nest",
|
||||||
|
"tecan_fluent_deck_segment_4_v2",
|
||||||
|
"tecan_fluent_mca_base_segment_384_v2",
|
||||||
|
"tecan_fluent_waste_module",
|
||||||
|
"tecan_fluent_reagent_block",
|
||||||
|
"tecan_fluent_tube_grippers",
|
||||||
|
"tecan_fluent_washstation_waste_v2",
|
||||||
|
"tecan_carrier_additive_trough_3_pce_max_100ml",
|
||||||
|
"tecan_carrier_384_well_mp_3_pos_accessible_roma",
|
||||||
|
"tecan_carrier_rack_3_diti_width_6",
|
||||||
|
"tecan_transport_box_diti_tray_1000ul",
|
||||||
|
"tecan_transport_box_diti_tray_200ul",
|
||||||
|
"tecan_magicprep_ngs_sample_deck",
|
||||||
|
"tecan_fluent_shelf_large",
|
||||||
|
"tecan_fluent_shelf_small",
|
||||||
|
"tecan_spacer_29_9_te_chrom",
|
||||||
|
"tecan_teshake_adapter_2",
|
||||||
|
"tecan_teshake_base",
|
||||||
|
"tecan_tevacs_base",
|
||||||
|
"tecan_tevacs_plate_park",
|
||||||
|
"tecan_tevacs_spacer",
|
||||||
|
"tecan_tevacs_vacuum",
|
||||||
|
"tecan_tip_box",
|
||||||
|
"tecan_nested_tip_rack"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="tecan_nested_tip_rack">
|
||||||
|
<link name='plate_link'>
|
||||||
|
<visual name='visual'>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="meshes/plate.stl"/>
|
||||||
|
</geometry>
|
||||||
|
<material name="clay" />
|
||||||
|
</visual>
|
||||||
|
</link>
|
||||||
|
</robot>
|
||||||
BIN
unilabos/device_mesh/resources/tecan_nested_tip_rack/plate.png
Normal file
BIN
unilabos/device_mesh/resources/tecan_nested_tip_rack/plate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
387
unilabos/device_mesh/view_robot.rviz
Normal file
387
unilabos/device_mesh/view_robot.rviz
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
Panels:
|
||||||
|
- Class: rviz_common/Displays
|
||||||
|
Help Height: 138
|
||||||
|
Name: Displays
|
||||||
|
Property Tree Widget:
|
||||||
|
Expanded:
|
||||||
|
- /Global Options1
|
||||||
|
- /TF1
|
||||||
|
- /TF1/Tree1
|
||||||
|
- /RobotModel1
|
||||||
|
- /PlanningScene1
|
||||||
|
- /PlanningScene1/Scene Geometry1
|
||||||
|
- /RobotState1
|
||||||
|
- /RobotState1/Links1
|
||||||
|
- /MotionPlanning1
|
||||||
|
- /MotionPlanning1/Scene Geometry1
|
||||||
|
- /MotionPlanning1/Scene Robot1
|
||||||
|
- /MotionPlanning1/Planning Request1
|
||||||
|
Splitter Ratio: 0.5
|
||||||
|
Tree Height: 345
|
||||||
|
- Class: rviz_common/Selection
|
||||||
|
Name: Selection
|
||||||
|
- Class: rviz_common/Tool Properties
|
||||||
|
Expanded:
|
||||||
|
- /2D Goal Pose1
|
||||||
|
- /Publish Point1
|
||||||
|
Name: Tool Properties
|
||||||
|
Splitter Ratio: 0.5886790156364441
|
||||||
|
- Class: rviz_common/Views
|
||||||
|
Expanded:
|
||||||
|
- /Current View1
|
||||||
|
Name: Views
|
||||||
|
Splitter Ratio: 0.5
|
||||||
|
Visualization Manager:
|
||||||
|
Class: ""
|
||||||
|
Displays:
|
||||||
|
- Alpha: 0.5
|
||||||
|
Cell Size: 1
|
||||||
|
Class: rviz_default_plugins/Grid
|
||||||
|
Color: 160; 160; 164
|
||||||
|
Enabled: true
|
||||||
|
Line Style:
|
||||||
|
Line Width: 0.029999999329447746
|
||||||
|
Value: Lines
|
||||||
|
Name: Grid
|
||||||
|
Normal Cell Count: 0
|
||||||
|
Offset:
|
||||||
|
X: 0
|
||||||
|
Y: 0
|
||||||
|
Z: 0
|
||||||
|
Plane: XY
|
||||||
|
Plane Cell Count: 10
|
||||||
|
Reference Frame: <Fixed Frame>
|
||||||
|
Value: true
|
||||||
|
- Class: rviz_default_plugins/TF
|
||||||
|
Enabled: false
|
||||||
|
Frame Timeout: 15
|
||||||
|
Frames:
|
||||||
|
All Enabled: false
|
||||||
|
Marker Scale: 1
|
||||||
|
Name: TF
|
||||||
|
Show Arrows: true
|
||||||
|
Show Axes: true
|
||||||
|
Show Names: false
|
||||||
|
Tree:
|
||||||
|
{}
|
||||||
|
Update Interval: 0
|
||||||
|
Value: false
|
||||||
|
- Alpha: 1
|
||||||
|
Class: rviz_default_plugins/RobotModel
|
||||||
|
Collision Enabled: false
|
||||||
|
Description File: ""
|
||||||
|
Description Source: Topic
|
||||||
|
Description Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /robot_description
|
||||||
|
Enabled: false
|
||||||
|
Links:
|
||||||
|
All Links Enabled: true
|
||||||
|
Expand Joint Details: false
|
||||||
|
Expand Link Details: false
|
||||||
|
Expand Tree: false
|
||||||
|
Link Tree Style: Links in Alphabetic Order
|
||||||
|
Mass Properties:
|
||||||
|
Inertia: false
|
||||||
|
Mass: false
|
||||||
|
Name: RobotModel
|
||||||
|
TF Prefix: ""
|
||||||
|
Update Interval: 0
|
||||||
|
Value: false
|
||||||
|
Visual Enabled: true
|
||||||
|
- Class: moveit_rviz_plugin/PlanningScene
|
||||||
|
Enabled: false
|
||||||
|
Move Group Namespace: ""
|
||||||
|
Name: PlanningScene
|
||||||
|
Planning Scene Topic: /monitored_planning_scene
|
||||||
|
Robot Description: robot_description
|
||||||
|
Scene Geometry:
|
||||||
|
Scene Alpha: 0.8999999761581421
|
||||||
|
Scene Color: 50; 230; 50
|
||||||
|
Scene Display Time: 0.009999999776482582
|
||||||
|
Show Scene Geometry: true
|
||||||
|
Voxel Coloring: Z-Axis
|
||||||
|
Voxel Rendering: Occupied Voxels
|
||||||
|
Scene Robot:
|
||||||
|
Attached Body Color: 150; 50; 150
|
||||||
|
Links:
|
||||||
|
All Links Enabled: true
|
||||||
|
Expand Joint Details: false
|
||||||
|
Expand Link Details: false
|
||||||
|
Expand Tree: false
|
||||||
|
Link Tree Style: Links in Alphabetic Order
|
||||||
|
Robot Alpha: 1
|
||||||
|
Show Robot Collision: false
|
||||||
|
Show Robot Visual: false
|
||||||
|
Value: false
|
||||||
|
- Attached Body Color: 150; 50; 150
|
||||||
|
Class: moveit_rviz_plugin/RobotState
|
||||||
|
Collision Enabled: false
|
||||||
|
Enabled: false
|
||||||
|
Links:
|
||||||
|
All Links Enabled: true
|
||||||
|
Expand Joint Details: false
|
||||||
|
Expand Link Details: false
|
||||||
|
Expand Tree: false
|
||||||
|
Link Tree Style: Links in Alphabetic Order
|
||||||
|
Name: RobotState
|
||||||
|
Robot Alpha: 1
|
||||||
|
Robot Description: robot_description
|
||||||
|
Robot State Topic: display_robot_state
|
||||||
|
Show All Links: true
|
||||||
|
Show Highlights: true
|
||||||
|
Value: false
|
||||||
|
Visual Enabled: true
|
||||||
|
- Acceleration_Scaling_Factor: 0.1
|
||||||
|
Class: moveit_rviz_plugin/MotionPlanning
|
||||||
|
Enabled: true
|
||||||
|
Move Group Namespace: ""
|
||||||
|
MoveIt_Allow_Approximate_IK: false
|
||||||
|
MoveIt_Allow_External_Program: false
|
||||||
|
MoveIt_Allow_Replanning: false
|
||||||
|
MoveIt_Allow_Sensor_Positioning: false
|
||||||
|
MoveIt_Planning_Attempts: 10
|
||||||
|
MoveIt_Planning_Time: 5
|
||||||
|
MoveIt_Use_Cartesian_Path: false
|
||||||
|
MoveIt_Use_Constraint_Aware_IK: false
|
||||||
|
MoveIt_Workspace:
|
||||||
|
Center:
|
||||||
|
X: 0
|
||||||
|
Y: 0
|
||||||
|
Z: 0
|
||||||
|
Size:
|
||||||
|
X: 2
|
||||||
|
Y: 2
|
||||||
|
Z: 2
|
||||||
|
Name: MotionPlanning
|
||||||
|
Planned Path:
|
||||||
|
Color Enabled: false
|
||||||
|
Interrupt Display: false
|
||||||
|
Links:
|
||||||
|
All Links Enabled: true
|
||||||
|
Expand Joint Details: false
|
||||||
|
Expand Link Details: false
|
||||||
|
Expand Tree: false
|
||||||
|
Link Tree Style: Links in Alphabetic Order
|
||||||
|
PLR_STATION_deck_device_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_first_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_fourth_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_main_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_second_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_socketTypeGenericSbsFootprint:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_socketTypeHEPAModule:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_third_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
world:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Loop Animation: false
|
||||||
|
Robot Alpha: 0.5
|
||||||
|
Robot Color: 150; 50; 150
|
||||||
|
Show Robot Collision: false
|
||||||
|
Show Robot Visual: true
|
||||||
|
Show Trail: false
|
||||||
|
State Display Time: 3x
|
||||||
|
Trail Step Size: 1
|
||||||
|
Trajectory Topic: /display_planned_path
|
||||||
|
Use Sim Time: false
|
||||||
|
Planning Metrics:
|
||||||
|
Payload: 1
|
||||||
|
Show Joint Torques: false
|
||||||
|
Show Manipulability: false
|
||||||
|
Show Manipulability Index: false
|
||||||
|
Show Weight Limit: false
|
||||||
|
TextHeight: 0.07999999821186066
|
||||||
|
Planning Request:
|
||||||
|
Colliding Link Color: 255; 0; 0
|
||||||
|
Goal State Alpha: 1
|
||||||
|
Goal State Color: 250; 128; 0
|
||||||
|
Interactive Marker Size: 0
|
||||||
|
Joint Violation Color: 255; 0; 255
|
||||||
|
Planning Group: ""
|
||||||
|
Query Goal State: false
|
||||||
|
Query Start State: false
|
||||||
|
Show Workspace: false
|
||||||
|
Start State Alpha: 1
|
||||||
|
Start State Color: 0; 255; 0
|
||||||
|
Planning Scene Topic: /monitored_planning_scene
|
||||||
|
Robot Description: robot_description
|
||||||
|
Scene Geometry:
|
||||||
|
Scene Alpha: 0.8999999761581421
|
||||||
|
Scene Color: 50; 230; 50
|
||||||
|
Scene Display Time: 0.009999999776482582
|
||||||
|
Show Scene Geometry: true
|
||||||
|
Voxel Coloring: Z-Axis
|
||||||
|
Voxel Rendering: Occupied Voxels
|
||||||
|
Scene Robot:
|
||||||
|
Attached Body Color: 150; 50; 150
|
||||||
|
Links:
|
||||||
|
All Links Enabled: true
|
||||||
|
Expand Joint Details: false
|
||||||
|
Expand Link Details: false
|
||||||
|
Expand Tree: false
|
||||||
|
Link Tree Style: Links in Alphabetic Order
|
||||||
|
PLR_STATION_deck_device_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_first_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_fourth_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_main_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_second_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
PLR_STATION_deck_socketTypeGenericSbsFootprint:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_socketTypeHEPAModule:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
PLR_STATION_deck_third_link:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Value: true
|
||||||
|
world:
|
||||||
|
Alpha: 1
|
||||||
|
Show Axes: false
|
||||||
|
Show Trail: false
|
||||||
|
Robot Alpha: 1
|
||||||
|
Show Robot Collision: false
|
||||||
|
Show Robot Visual: true
|
||||||
|
Value: true
|
||||||
|
Velocity_Scaling_Factor: 0.1
|
||||||
|
Enabled: true
|
||||||
|
Global Options:
|
||||||
|
Background Color: 48; 48; 48
|
||||||
|
Fixed Frame: world
|
||||||
|
Frame Rate: 30
|
||||||
|
Name: root
|
||||||
|
Tools:
|
||||||
|
- Class: rviz_default_plugins/Interact
|
||||||
|
Hide Inactive Objects: true
|
||||||
|
- Class: rviz_default_plugins/MoveCamera
|
||||||
|
- Class: rviz_default_plugins/Select
|
||||||
|
- Class: rviz_default_plugins/FocusCamera
|
||||||
|
- Class: rviz_default_plugins/Measure
|
||||||
|
Line color: 128; 128; 0
|
||||||
|
- Class: rviz_default_plugins/SetInitialPose
|
||||||
|
Covariance x: 0.25
|
||||||
|
Covariance y: 0.25
|
||||||
|
Covariance yaw: 0.06853891909122467
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /initialpose
|
||||||
|
- Class: rviz_default_plugins/SetGoal
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /goal_pose
|
||||||
|
- Class: rviz_default_plugins/PublishPoint
|
||||||
|
Single click: true
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /clicked_point
|
||||||
|
Transformation:
|
||||||
|
Current:
|
||||||
|
Class: rviz_default_plugins/TF
|
||||||
|
Value: true
|
||||||
|
Views:
|
||||||
|
Current:
|
||||||
|
Class: rviz_default_plugins/Orbit
|
||||||
|
Distance: 1.0284695625305176
|
||||||
|
Enable Stereo Rendering:
|
||||||
|
Stereo Eye Separation: 0.05999999865889549
|
||||||
|
Stereo Focal Distance: 1
|
||||||
|
Swap Stereo Eyes: false
|
||||||
|
Value: false
|
||||||
|
Focal Point:
|
||||||
|
X: 0.29730814695358276
|
||||||
|
Y: 0.21228469908237457
|
||||||
|
Z: 0.20008830726146698
|
||||||
|
Focal Shape Fixed Size: true
|
||||||
|
Focal Shape Size: 0.05000000074505806
|
||||||
|
Invert Z Axis: false
|
||||||
|
Name: Current View
|
||||||
|
Near Clip Distance: 0.009999999776482582
|
||||||
|
Pitch: 0.38979560136795044
|
||||||
|
Target Frame: <Fixed Frame>
|
||||||
|
Value: Orbit (rviz)
|
||||||
|
Yaw: 0.06074193865060806
|
||||||
|
Saved: ~
|
||||||
|
Window Geometry:
|
||||||
|
Displays:
|
||||||
|
collapsed: false
|
||||||
|
Height: 1656
|
||||||
|
Hide Left Dock: false
|
||||||
|
Hide Right Dock: true
|
||||||
|
MotionPlanning:
|
||||||
|
collapsed: false
|
||||||
|
MotionPlanning - Trajectory Slider:
|
||||||
|
collapsed: false
|
||||||
|
QMainWindow State: 000000ff00000000fd0000000400000000000003a3000005dcfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b000000b000fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000006e000002510000018200fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000007a00fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e006701000002cb0000037f000002b800ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b000003870000013200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000627000005dc00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
|
||||||
|
Selection:
|
||||||
|
collapsed: false
|
||||||
|
Tool Properties:
|
||||||
|
collapsed: false
|
||||||
|
Views:
|
||||||
|
collapsed: true
|
||||||
|
Width: 2518
|
||||||
|
X: 385
|
||||||
|
Y: 120
|
||||||
@@ -77,9 +77,6 @@ class UrArmTask():
|
|||||||
if n > retry:
|
if n > retry:
|
||||||
raise Exception('Can not connect to the arm info server!')
|
raise Exception('Can not connect to the arm info server!')
|
||||||
|
|
||||||
self.pose_data = {}
|
|
||||||
self.pose_file = 'C:\\auto\\unilabos\\unilabos\\devices\\agv\\pose.json'
|
|
||||||
self.reload_pose()
|
|
||||||
self.dash_c.stop()
|
self.dash_c.stop()
|
||||||
|
|
||||||
def arm_init(self):
|
def arm_init(self):
|
||||||
|
|||||||
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
304
unilabos/devices/laiyu_add_solid/laiyu.py
Normal file
304
unilabos/devices/laiyu_add_solid/laiyu.py
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
class Laiyu:
|
||||||
|
@property
|
||||||
|
def status(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, port, baudrate=115200, timeout=0.5):
|
||||||
|
"""
|
||||||
|
初始化串口参数,默认波特率115200,8位数据位、1位停止位、无校验
|
||||||
|
"""
|
||||||
|
self.ser = serial.Serial(port, baudrate=baudrate, timeout=timeout)
|
||||||
|
|
||||||
|
def calculate_crc(self, data: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
计算Modbus CRC-16,返回低字节和高字节(little-endian)
|
||||||
|
"""
|
||||||
|
crc = 0xFFFF
|
||||||
|
for pos in data:
|
||||||
|
crc ^= pos
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x0001:
|
||||||
|
crc = (crc >> 1) ^ 0xA001
|
||||||
|
else:
|
||||||
|
crc >>= 1
|
||||||
|
return crc.to_bytes(2, byteorder='little')
|
||||||
|
|
||||||
|
def send_command(self, command: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
构造完整指令帧(加上CRC校验),发送指令后一直等待设备响应,直至响应结束或超时(最大3分钟)
|
||||||
|
"""
|
||||||
|
crc = self.calculate_crc(command)
|
||||||
|
full_command = command + crc
|
||||||
|
# 清空接收缓存
|
||||||
|
self.ser.reset_input_buffer()
|
||||||
|
self.ser.write(full_command)
|
||||||
|
print("发送指令:", full_command.hex().upper()) # 打印发送的指令帧
|
||||||
|
|
||||||
|
# 持续等待响应,直到连续0.5秒没有新数据或超时(3分钟)
|
||||||
|
start_time = time.time()
|
||||||
|
last_data_time = time.time()
|
||||||
|
response = bytearray()
|
||||||
|
while True:
|
||||||
|
if self.ser.in_waiting > 0:
|
||||||
|
new_data = self.ser.read(self.ser.in_waiting)
|
||||||
|
response.extend(new_data)
|
||||||
|
last_data_time = time.time()
|
||||||
|
# 如果已有数据,并且0.5秒内无新数据,则认为响应结束
|
||||||
|
if response and (time.time() - last_data_time) > 0.5:
|
||||||
|
break
|
||||||
|
# 超过最大等待时间,退出循环
|
||||||
|
if time.time() - start_time > 180:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
return bytes(response)
|
||||||
|
|
||||||
|
def pick_powder_tube(self, int_input: int) -> bytes:
|
||||||
|
"""
|
||||||
|
拿取粉筒指令:
|
||||||
|
- 功能码06
|
||||||
|
- 寄存器地址0x0037(取粉筒)
|
||||||
|
- 数据:粉筒编号(如1代表A,2代表B,以此类推)
|
||||||
|
示例:拿取A粉筒指令帧:01 06 00 37 00 01 + CRC
|
||||||
|
"""
|
||||||
|
slave_addr = 0x01
|
||||||
|
function_code = 0x06
|
||||||
|
register_addr = 0x0037
|
||||||
|
# 数据部分:粉筒编号转换为2字节大端
|
||||||
|
data = int_input.to_bytes(2, byteorder='big')
|
||||||
|
command = bytes([slave_addr, function_code]) + register_addr.to_bytes(2, byteorder='big') + data
|
||||||
|
return self.send_command(command)
|
||||||
|
|
||||||
|
def put_powder_tube(self, int_input: int) -> bytes:
|
||||||
|
"""
|
||||||
|
放回粉筒指令:
|
||||||
|
- 功能码06
|
||||||
|
- 寄存器地址0x0038(放回粉筒)
|
||||||
|
- 数据:粉筒编号
|
||||||
|
示例:放回A粉筒指令帧:01 06 00 38 00 01 + CRC
|
||||||
|
"""
|
||||||
|
slave_addr = 0x01
|
||||||
|
function_code = 0x06
|
||||||
|
register_addr = 0x0038
|
||||||
|
data = int_input.to_bytes(2, byteorder='big')
|
||||||
|
command = bytes([slave_addr, function_code]) + register_addr.to_bytes(2, byteorder='big') + data
|
||||||
|
return self.send_command(command)
|
||||||
|
|
||||||
|
def reset(self) -> bytes:
|
||||||
|
"""
|
||||||
|
重置指令:
|
||||||
|
- 功能码 0x06
|
||||||
|
- 寄存器地址 0x0042 (示例中用了 00 42)
|
||||||
|
- 数据 0x0001
|
||||||
|
示例发送:01 06 00 42 00 01 E8 1E
|
||||||
|
"""
|
||||||
|
slave_addr = 0x01
|
||||||
|
function_code = 0x06
|
||||||
|
register_addr = 0x0042 # 对应示例中的 00 42
|
||||||
|
payload = (0x0001).to_bytes(2, 'big') # 重置命令
|
||||||
|
|
||||||
|
cmd = (
|
||||||
|
bytes([slave_addr, function_code])
|
||||||
|
+ register_addr.to_bytes(2, 'big')
|
||||||
|
+ payload
|
||||||
|
)
|
||||||
|
return self.send_command(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def move_to_xyz(self, x: float, y: float, z: float) -> bytes:
|
||||||
|
"""
|
||||||
|
移动到指定位置指令:
|
||||||
|
- 功能码10(写多个寄存器)
|
||||||
|
- 寄存器起始地址0x0030
|
||||||
|
- 寄存器数量:3个(x,y,z)
|
||||||
|
- 字节计数:6
|
||||||
|
- 数据:x,y,z各2字节,单位为0.1mm(例如1mm对应数值10)
|
||||||
|
示例帧:01 10 00 30 00 03 06 00C8 02BC 02EE + CRC
|
||||||
|
"""
|
||||||
|
slave_addr = 0x01
|
||||||
|
function_code = 0x10
|
||||||
|
register_addr = 0x0030
|
||||||
|
num_registers = 3
|
||||||
|
byte_count = num_registers * 2 # 6字节
|
||||||
|
|
||||||
|
# 将mm转换为0.1mm单位(乘以10),转换为2字节大端表示
|
||||||
|
x_val = int(x * 10)
|
||||||
|
y_val = int(y * 10)
|
||||||
|
z_val = int(z * 10)
|
||||||
|
data = x_val.to_bytes(2, 'big') + y_val.to_bytes(2, 'big') + z_val.to_bytes(2, 'big')
|
||||||
|
|
||||||
|
command = (bytes([slave_addr, function_code]) +
|
||||||
|
register_addr.to_bytes(2, 'big') +
|
||||||
|
num_registers.to_bytes(2, 'big') +
|
||||||
|
byte_count.to_bytes(1, 'big') +
|
||||||
|
data)
|
||||||
|
return self.send_command(command)
|
||||||
|
|
||||||
|
def discharge(self, float_in: float) -> bytes:
|
||||||
|
"""
|
||||||
|
出料指令:
|
||||||
|
- 使用写多个寄存器命令(功能码 0x10)
|
||||||
|
- 寄存器起始地址设为 0x0039
|
||||||
|
- 寄存器数量为 0x0002(两个寄存器:出料质量和误差范围)
|
||||||
|
- 字节计数为 0x04(每个寄存器2字节,共4字节)
|
||||||
|
- 数据:出料质量(单位0.1mg,例如10mg对应100,即0x0064)、误差范围固定为0x0005
|
||||||
|
示例发送帧:01 10 00 39 0002 04 00640005 + CRC
|
||||||
|
"""
|
||||||
|
mass = float_in
|
||||||
|
slave_addr = 0x01
|
||||||
|
function_code = 0x10 # 修改为写多个寄存器的功能码
|
||||||
|
start_register = 0x0039 # 寄存器起始地址
|
||||||
|
quantity = 0x0002 # 寄存器数量
|
||||||
|
byte_count = 0x04 # 字节数:2寄存器*2字节=4
|
||||||
|
mass_val = int(mass * 10) # 质量转换,单位0.1mg
|
||||||
|
error_margin = 5 # 固定误差范围,0x0005
|
||||||
|
|
||||||
|
command = (bytes([slave_addr, function_code]) +
|
||||||
|
start_register.to_bytes(2, 'big') +
|
||||||
|
quantity.to_bytes(2, 'big') +
|
||||||
|
byte_count.to_bytes(1, 'big') +
|
||||||
|
mass_val.to_bytes(2, 'big') +
|
||||||
|
error_margin.to_bytes(2, 'big'))
|
||||||
|
return self.send_command(command)
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
示例:这个是标智96孔板的坐标转换,但是不同96孔板的坐标可能不同
|
||||||
|
所以需要根据实际情况进行修改
|
||||||
|
'''
|
||||||
|
|
||||||
|
def move_to_plate(self, string):
|
||||||
|
#只接受两位数的str,比如a1,a2,b1,b2
|
||||||
|
# 解析位置字符串
|
||||||
|
if len(string) != 2 and len(string) != 3:
|
||||||
|
raise ValueError("Invalid plate position")
|
||||||
|
if not string[0].isalpha() or not string[1:].isdigit():
|
||||||
|
raise ValueError("Invalid plate position")
|
||||||
|
a = string[0] # 字母部分s
|
||||||
|
b = string[1:] # 数字部分
|
||||||
|
|
||||||
|
if a.isalpha():
|
||||||
|
a = ord(a.lower()) - ord('a') + 1
|
||||||
|
else:
|
||||||
|
print('1')
|
||||||
|
raise ValueError("Invalid plate position")
|
||||||
|
a = int(a)
|
||||||
|
b = int(b)
|
||||||
|
# max a = 8, max b = 12, 否则报错
|
||||||
|
if a > 8 or b > 12:
|
||||||
|
print('2')
|
||||||
|
raise ValueError("Invalid plate position")
|
||||||
|
# 计算移动到指定位置的坐标
|
||||||
|
# a=1, x=3.0; a=12, x=220.0
|
||||||
|
# b=1, y=62.0; b=8, y=201.0
|
||||||
|
# z = 110.0
|
||||||
|
x = float((b-1) * (220-4.0)/11 + 4.0)
|
||||||
|
y = float((a-1) * (201.0-62.0)/7 + 62.0)
|
||||||
|
z = 110.0
|
||||||
|
# 移动到指定位置
|
||||||
|
resp_move = self.move_to_xyz(x, y, z)
|
||||||
|
print("移动位置响应:", resp_move.hex().upper())
|
||||||
|
# 打印移动到指定位置的坐标
|
||||||
|
print(f"移动到位置:{string},坐标:x={x:.2f}, y={y:.2f}, z={z:.2f}")
|
||||||
|
return resp_move
|
||||||
|
|
||||||
|
def add_powder_tube(self, powder_tube_number, target_tube_position, compound_mass):
|
||||||
|
# 拿取粉筒
|
||||||
|
resp_pick = self.pick_powder_tube(powder_tube_number)
|
||||||
|
print("拿取粉筒响应:", resp_pick.hex().upper())
|
||||||
|
time.sleep(1)
|
||||||
|
# 移动到指定位置
|
||||||
|
self.move_to_plate(target_tube_position)
|
||||||
|
time.sleep(1)
|
||||||
|
# 出料,设定质量
|
||||||
|
resp_discharge = self.discharge(compound_mass)
|
||||||
|
print("出料响应:", resp_discharge.hex().upper())
|
||||||
|
# 使用modbus协议读取实际出料质量
|
||||||
|
# 样例 01 06 00 40 00 64 89 F5,其中 00 64 是实际出料质量,换算为十进制为100,代表10 mg
|
||||||
|
# 从resp_discharge读取实际出料质量
|
||||||
|
# 提取字节4和字节5的两个字节
|
||||||
|
actual_mass_raw = int.from_bytes(resp_discharge[4:6], byteorder='big')
|
||||||
|
# 根据说明,将读取到的数据转换为实际出料质量(mg),这里除以10,例如:0x0064 = 100,转换后为10 mg
|
||||||
|
actual_mass_mg = actual_mass_raw / 10
|
||||||
|
print(f"孔位{target_tube_position},实际出料质量:{actual_mass_mg}mg")
|
||||||
|
time.sleep(1)
|
||||||
|
# 放回粉筒
|
||||||
|
resp_put = self.put_powder_tube(powder_tube_number)
|
||||||
|
print("放回粉筒响应:", resp_put.hex().upper())
|
||||||
|
print(f"放回粉筒{powder_tube_number}")
|
||||||
|
resp_reset = self.reset()
|
||||||
|
return actual_mass_mg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
样例:对单个粉筒进行称量
|
||||||
|
'''
|
||||||
|
|
||||||
|
modbus = Laiyu(port="COM25")
|
||||||
|
|
||||||
|
mass_test = modbus.add_powder_tube(1, 'h12', 6.0)
|
||||||
|
print(f"实际出料质量:{mass_test}mg")
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
样例: 对一份excel文件记录的化合物进行称量
|
||||||
|
'''
|
||||||
|
|
||||||
|
excel_file = r"C:\auto\laiyu\test1.xlsx"
|
||||||
|
# 定义输出文件路径,用于记录实际加样多少
|
||||||
|
output_file = r"C:\auto\laiyu\test_output.xlsx"
|
||||||
|
|
||||||
|
# 定义物料名称和料筒位置关系
|
||||||
|
compound_positions = {
|
||||||
|
'XPhos': '1',
|
||||||
|
'Cu(OTf)2': '2',
|
||||||
|
'CuSO4': '3',
|
||||||
|
'PPh3': '4',
|
||||||
|
}
|
||||||
|
|
||||||
|
# read excel file
|
||||||
|
# excel_file = r"C:\auto\laiyu\test.xlsx"
|
||||||
|
df = pd.read_excel(excel_file, sheet_name='Sheet1')
|
||||||
|
# 读取Excel文件中的数据
|
||||||
|
# 遍历每一行数据
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
# 获取物料名称和质量
|
||||||
|
copper_name = row['copper']
|
||||||
|
copper_mass = row['copper_mass']
|
||||||
|
ligand_name = row['ligand']
|
||||||
|
ligand_mass = row['ligand_mass']
|
||||||
|
target_tube_position = row['position']
|
||||||
|
# 获取物料位置 from compound_positions
|
||||||
|
copper_position = compound_positions.get(copper_name)
|
||||||
|
ligand_position = compound_positions.get(ligand_name)
|
||||||
|
# 判断物料位置是否存在
|
||||||
|
if copper_position is None:
|
||||||
|
print(f"物料位置不存在:{copper_name}")
|
||||||
|
continue
|
||||||
|
if ligand_position is None:
|
||||||
|
print(f"物料位置不存在:{ligand_name}")
|
||||||
|
continue
|
||||||
|
# 加铜
|
||||||
|
copper_actual_mass = modbus.add_powder_tube(int(copper_position), target_tube_position, copper_mass)
|
||||||
|
time.sleep(1)
|
||||||
|
# 加配体
|
||||||
|
ligand_actual_mass = modbus.add_powder_tube(int(ligand_position), target_tube_position, ligand_mass)
|
||||||
|
time.sleep(1)
|
||||||
|
# 保存至df
|
||||||
|
df.at[index, 'copper_actual_mass'] = copper_actual_mass
|
||||||
|
df.at[index, 'ligand_actual_mass'] = ligand_actual_mass
|
||||||
|
|
||||||
|
# 保存修改后的数据到新的Excel文件
|
||||||
|
df.to_excel(output_file, index=False)
|
||||||
|
print(f"已保存到文件:{output_file}")
|
||||||
|
|
||||||
|
# 关闭串口
|
||||||
|
modbus.ser.close()
|
||||||
|
print("串口已关闭")
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
355
unilabos/devices/liquid_handling/liquid_handler_abstract.py
Normal file
355
unilabos/devices/liquid_handling/liquid_handler_abstract.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Sequence, Optional, Literal, Union, Iterator
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
from pylabrobot.liquid_handling import LiquidHandler
|
||||||
|
from pylabrobot.resources import (
|
||||||
|
Resource,
|
||||||
|
TipRack,
|
||||||
|
Container,
|
||||||
|
Coordinate,
|
||||||
|
Well
|
||||||
|
)
|
||||||
|
|
||||||
|
class LiquidHandlerAbstract(LiquidHandler):
|
||||||
|
"""Extended LiquidHandler with additional operations."""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# REMOVE LIQUID --------------------------------------------------
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
async def remove_liquid(
|
||||||
|
self,
|
||||||
|
vols: List[float],
|
||||||
|
sources: Sequence[Container],
|
||||||
|
waste_liquid: Optional[Container] = None,
|
||||||
|
*,
|
||||||
|
use_channels: Optional[List[int]] = None,
|
||||||
|
flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
offsets: Optional[List[Coordinate]] = None,
|
||||||
|
liquid_height: Optional[List[Optional[float]]] = None,
|
||||||
|
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||||
|
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||||||
|
delays: Optional[List[int]] = None,
|
||||||
|
is_96_well: Optional[bool] = False,
|
||||||
|
top: Optional[List(float)] = None,
|
||||||
|
none_keys: List[str] = []
|
||||||
|
):
|
||||||
|
"""A complete *remove* (aspirate → waste) operation."""
|
||||||
|
trash = self.deck.get_trash_area()
|
||||||
|
try:
|
||||||
|
if is_96_well:
|
||||||
|
pass # This mode is not verified
|
||||||
|
else:
|
||||||
|
if len(vols) != len(sources):
|
||||||
|
raise ValueError("Length of `vols` must match `sources`.")
|
||||||
|
|
||||||
|
for src, vol in zip(sources, vols):
|
||||||
|
self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||||
|
tip = next(self.current_tip)
|
||||||
|
await self.pick_up_tips(tip)
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[src],
|
||||||
|
vols=[vol],
|
||||||
|
use_channels=use_channels, # only aspirate96 used, default to None
|
||||||
|
flow_rates=[flow_rates[0]] if flow_rates else None,
|
||||||
|
offsets=[offsets[0]] if offsets 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,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
await self.custom_delay(seconds=delays[0] if delays else 0)
|
||||||
|
await self.dispense(
|
||||||
|
resources=waste_liquid,
|
||||||
|
vols=[vol],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[flow_rates[1]] if flow_rates else None,
|
||||||
|
offsets=[offsets[1]] if offsets 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,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
await self.discard_tips() # For now, each of tips is discarded after use
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Liquid removal failed: {e}") from e
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# ADD LIQUID -----------------------------------------------------
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
async def add_liquid(
|
||||||
|
self,
|
||||||
|
asp_vols: Union[List[float], float],
|
||||||
|
dis_vols: Union[List[float], float],
|
||||||
|
reagent_sources: Sequence[Container],
|
||||||
|
targets: Sequence[Container],
|
||||||
|
*,
|
||||||
|
use_channels: Optional[List[int]] = None,
|
||||||
|
flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
offsets: Optional[List[Coordinate]] = None,
|
||||||
|
liquid_height: Optional[List[Optional[float]]] = None,
|
||||||
|
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||||
|
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||||||
|
is_96_well: bool = False,
|
||||||
|
delays: Optional[List[int]] = None,
|
||||||
|
mix_time: Optional[int] = None,
|
||||||
|
mix_vol: Optional[int] = None,
|
||||||
|
mix_rate: Optional[int] = None,
|
||||||
|
mix_liquid_height: Optional[float] = None,
|
||||||
|
none_keys: List[str] = []
|
||||||
|
):
|
||||||
|
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_96_well:
|
||||||
|
pass # This mode is not verified.
|
||||||
|
else:
|
||||||
|
if len(asp_vols) != len(targets):
|
||||||
|
raise ValueError("Length of `vols` must match `targets`.")
|
||||||
|
tip = next(self.current_tip)
|
||||||
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
|
for _ in range(len(targets)):
|
||||||
|
await self.aspirate(
|
||||||
|
resources=reagent_sources,
|
||||||
|
vols=[asp_vols[_]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[flow_rates[0]] if flow_rates else None,
|
||||||
|
offsets=[offsets[0]] if offsets 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,
|
||||||
|
spread=spread
|
||||||
|
)
|
||||||
|
if delays is not None:
|
||||||
|
await self.custom_delay(seconds=delays[0])
|
||||||
|
await self.dispense(
|
||||||
|
resources=[targets[_]],
|
||||||
|
vols=[dis_vols[_]],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[flow_rates[1]] if flow_rates else None,
|
||||||
|
offsets=[offsets[1]] if offsets else None,
|
||||||
|
blow_out_air_volume=[blow_out_air_volume[1]] if blow_out_air_volume else None,
|
||||||
|
liquid_height=[liquid_height[1]] if liquid_height else None,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
if delays is not None:
|
||||||
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
await self.mix(
|
||||||
|
targets=targets[_],
|
||||||
|
mix_time=mix_time,
|
||||||
|
mix_vol=mix_vol,
|
||||||
|
offsets=offsets if offsets else None,
|
||||||
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
|
mix_rate=mix_rate if mix_rate else None)
|
||||||
|
if delays is not None:
|
||||||
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
await self.touch_tip(targets[_])
|
||||||
|
await self.discard_tips()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Liquid addition failed: {e}") from e
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# TRANSFER LIQUID ------------------------------------------------
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
async def transfer_liquid(
|
||||||
|
self,
|
||||||
|
asp_vols: Union[List[float], float],
|
||||||
|
dis_vols: Union[List[float], float],
|
||||||
|
sources: Sequence[Container],
|
||||||
|
targets: Sequence[Container],
|
||||||
|
tip_racks: Sequence[TipRack],
|
||||||
|
*,
|
||||||
|
use_channels: Optional[List[int]] = None,
|
||||||
|
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||||
|
offsets: Optional[List[Coordinate]] = 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(int)] = 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] = []
|
||||||
|
):
|
||||||
|
"""Transfer liquid from each *source* well/plate to the corresponding *target*.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asp_vols, dis_vols
|
||||||
|
Single volume (µL) or list matching the number of transfers.
|
||||||
|
sources, targets
|
||||||
|
Same‑length sequences of containers (wells or plates). In 96‑well mode
|
||||||
|
each must contain exactly one plate.
|
||||||
|
tip_racks
|
||||||
|
One or more TipRacks providing fresh tips.
|
||||||
|
is_96_well
|
||||||
|
Set *True* to use the 96‑channel head.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 96‑channel head mode
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if is_96_well:
|
||||||
|
pass # This mode is not verified
|
||||||
|
else:
|
||||||
|
if not (len(asp_vols) == len(sources) and len(dis_vols) == len(targets)):
|
||||||
|
raise ValueError("`sources`, `targets`, and `vols` must have the same length.")
|
||||||
|
|
||||||
|
tip_iter = self.iter_tips(tip_racks)
|
||||||
|
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in (
|
||||||
|
zip(sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates)):
|
||||||
|
tip = next(tip_iter)
|
||||||
|
await self.pick_up_tips(tip)
|
||||||
|
# Aspirate from source
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[src],
|
||||||
|
vols=[asp_vol],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[asp_flow_rate],
|
||||||
|
offsets=offsets,
|
||||||
|
liquid_height=liquid_height,
|
||||||
|
blow_out_air_volume=blow_out_air_volume,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
self.custom_delay(seconds=delays[0] if delays else 0)
|
||||||
|
# Dispense into target
|
||||||
|
await self.dispense(
|
||||||
|
resources=[tgt],
|
||||||
|
vols=[dis_vol],
|
||||||
|
use_channels=use_channels,
|
||||||
|
flow_rates=[dis_flow_rate],
|
||||||
|
offsets=offsets,
|
||||||
|
liquid_height=liquid_height,
|
||||||
|
blow_out_air_volume=blow_out_air_volume,
|
||||||
|
spread=spread,
|
||||||
|
)
|
||||||
|
await self.mix(
|
||||||
|
targets=[tgt],
|
||||||
|
mix_time=mix_times[0] if mix_times else None,
|
||||||
|
mix_vol=mix_vol[0] if mix_vol else None,
|
||||||
|
mix_rate=mix_rate[0] if mix_rate else None,
|
||||||
|
)
|
||||||
|
if touch_tip:
|
||||||
|
await self.touch_tip(tgt)
|
||||||
|
await self.discard_tips()
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"Liquid transfer failed: {exc}") from exc
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Helper utilities
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
|
||||||
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
|
"""
|
||||||
|
seconds: seconds to wait
|
||||||
|
msg: information to be printed
|
||||||
|
"""
|
||||||
|
if seconds != None and seconds > 0:
|
||||||
|
if msg:
|
||||||
|
print(f"Waiting time: {msg}")
|
||||||
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
|
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
||||||
|
await asyncio.sleep(seconds)
|
||||||
|
if msg:
|
||||||
|
print(f"Done: {msg}")
|
||||||
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
|
async def touch_tip(self,
|
||||||
|
targets: Sequence[Container],
|
||||||
|
):
|
||||||
|
"""Touch the tip to the side of the well."""
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[targets],
|
||||||
|
vols=[0],
|
||||||
|
use_channels=None,
|
||||||
|
flow_rates=None,
|
||||||
|
offsets=[Coordinate(x=-targets.get_size_x()/2,y=0,z=0)],
|
||||||
|
liquid_height=None,
|
||||||
|
blow_out_air_volume=None
|
||||||
|
)
|
||||||
|
#await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[targets],
|
||||||
|
vols=[0],
|
||||||
|
use_channels=None,
|
||||||
|
flow_rates=None,
|
||||||
|
offsets=[Coordinate(x=targets.get_size_x()/2,y=0,z=0)],
|
||||||
|
liquid_height=None,
|
||||||
|
blow_out_air_volume=None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def mix(
|
||||||
|
self,
|
||||||
|
targets: Sequence[Container],
|
||||||
|
mix_time: int = None,
|
||||||
|
mix_vol: Optional[int] = None,
|
||||||
|
height_to_bottom: Optional[float] = None,
|
||||||
|
offsets: Optional[Coordinate] = None,
|
||||||
|
mix_rate: Optional[float] = None,
|
||||||
|
none_keys: List[str] = []
|
||||||
|
):
|
||||||
|
if mix_time is None: # No mixing required
|
||||||
|
return
|
||||||
|
"""Mix the liquid in the target wells."""
|
||||||
|
for _ in range(mix_time):
|
||||||
|
await self.aspirate(
|
||||||
|
resources=[targets],
|
||||||
|
vols=[mix_vol],
|
||||||
|
flow_rates=[mix_rate] if mix_rate else None,
|
||||||
|
offsets=[offsets] if offsets else None,
|
||||||
|
liquid_height=[height_to_bottom] if height_to_bottom else None,
|
||||||
|
)
|
||||||
|
await self.custom_delay(seconds=1)
|
||||||
|
await self.dispense(
|
||||||
|
resources=[targets],
|
||||||
|
vols=[mix_vol],
|
||||||
|
flow_rates=[mix_rate] if mix_rate else None,
|
||||||
|
offsets=[offsets] if offsets else None,
|
||||||
|
liquid_height=[height_to_bottom] if height_to_bottom else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||||||
|
"""Yield tips from a list of TipRacks one-by-one until depleted."""
|
||||||
|
for rack in tip_racks:
|
||||||
|
for tip in rack:
|
||||||
|
yield tip
|
||||||
|
raise RuntimeError("Out of tips!")
|
||||||
|
|
||||||
|
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
||||||
|
"""Set the tip racks for the liquid handler."""
|
||||||
|
self.tip_racks = tip_racks
|
||||||
|
tip_iter = self.iter_tips(tip_racks)
|
||||||
|
self.current_tip = tip_iter
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
well : Well
|
||||||
|
The target well.
|
||||||
|
dis_to_top : float
|
||||||
|
Height in mm to move to relative to the well top.
|
||||||
|
channel : int
|
||||||
|
Pipetting channel to move (default: 0).
|
||||||
|
"""
|
||||||
|
await self.prepare_for_manual_channel_operation(channel=channel)
|
||||||
|
abs_loc = well.get_absolute_location()
|
||||||
|
well_height = well.get_absolute_size_z()
|
||||||
|
await self.move_channel_x(channel, abs_loc.x)
|
||||||
|
await self.move_channel_y(channel, abs_loc.y)
|
||||||
|
await self.move_channel_z(channel, abs_loc.z + well_height + dis_to_top)
|
||||||
|
|
||||||
0
unilabos/devices/ros_dev/__init__.py
Normal file
0
unilabos/devices/ros_dev/__init__.py
Normal file
208
unilabos/devices/ros_dev/liquid_handler_joint_publisher.py
Normal file
208
unilabos/devices/ros_dev/liquid_handler_joint_publisher.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import copy
|
||||||
|
import rclpy
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from rclpy.executors import MultiThreadedExecutor
|
||||||
|
from rclpy.action import ActionServer
|
||||||
|
from sensor_msgs.msg import JointState
|
||||||
|
from unilabos_msgs.action import SendCmd
|
||||||
|
from rclpy.action.server import ServerGoalHandle
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
from tf_transformations import quaternion_from_euler
|
||||||
|
from tf2_ros import TransformBroadcaster, Buffer, TransformListener
|
||||||
|
|
||||||
|
|
||||||
|
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||||
|
def __init__(self,device_id:str, joint_config:dict, lh_id:str,resource_tracker, rate=50):
|
||||||
|
super().__init__(
|
||||||
|
driver_instance=self,
|
||||||
|
device_id=device_id,
|
||||||
|
status_types={},
|
||||||
|
action_value_mappings={},
|
||||||
|
hardware_interface={},
|
||||||
|
print_publish=False,
|
||||||
|
resource_tracker=resource_tracker,
|
||||||
|
)
|
||||||
|
|
||||||
|
# joint_config_dict = {
|
||||||
|
# "joint_names":[
|
||||||
|
# "first_joint",
|
||||||
|
# "second_joint",
|
||||||
|
# "third_joint",
|
||||||
|
# "fourth_joint"
|
||||||
|
# ],
|
||||||
|
# "y":{
|
||||||
|
# "first_joint":{
|
||||||
|
# "factor":-1,
|
||||||
|
# "offset":0.0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "x":{
|
||||||
|
# "second_joint":{
|
||||||
|
# "factor":-1,
|
||||||
|
# "offset":0.0
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# "z":{
|
||||||
|
# "third_joint":{
|
||||||
|
# "factor":1,
|
||||||
|
# "offset":0.0
|
||||||
|
# },
|
||||||
|
# "fourth_joint":{
|
||||||
|
# "factor":1,
|
||||||
|
# "offset":0.0
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
self.j_msg = JointState()
|
||||||
|
self.lh_id = lh_id
|
||||||
|
# self.j_msg.name = joint_names
|
||||||
|
self.joint_config = joint_config
|
||||||
|
self.j_msg.position = [0.0 for i in range(len(joint_config['joint_names']))]
|
||||||
|
self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config['joint_names']]
|
||||||
|
# self.joint_config = joint_config_dict
|
||||||
|
# self.j_msg.position = [0.0 for i in range(len(joint_config_dict['joint_names']))]
|
||||||
|
# self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config_dict['joint_names']]
|
||||||
|
self.rate = rate
|
||||||
|
self.tf_buffer = Buffer()
|
||||||
|
self.tf_listener = TransformListener(self.tf_buffer, self)
|
||||||
|
self.j_pub = self.create_publisher(JointState,'/joint_states',10)
|
||||||
|
self.create_timer(0.02,self.lh_joint_pub_callback)
|
||||||
|
self.j_action = ActionServer(
|
||||||
|
self,
|
||||||
|
SendCmd,
|
||||||
|
"joint",
|
||||||
|
self.lh_joint_action_callback,
|
||||||
|
result_timeout=5000
|
||||||
|
)
|
||||||
|
|
||||||
|
def lh_joint_action_callback(self,goal_handle: ServerGoalHandle):
|
||||||
|
"""Move a single joint
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: A JSON-formatted string that includes joint_name, speed, position
|
||||||
|
|
||||||
|
joint_name (str): The name of the joint to move
|
||||||
|
speed (float): The speed of the movement, speed > 0
|
||||||
|
position (float): The position to move to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
result = SendCmd.Result()
|
||||||
|
cmd_str = str(goal_handle.request.command).replace('\'','\"')
|
||||||
|
# goal_handle.execute()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd_dict = json.loads(cmd_str)
|
||||||
|
self.move_joints(**cmd_dict)
|
||||||
|
result.success = True
|
||||||
|
goal_handle.succeed()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
goal_handle.abort()
|
||||||
|
result.success = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
def inverse_kinematics(self, x, y, z,
|
||||||
|
x_joint:dict,
|
||||||
|
y_joint:dict,
|
||||||
|
z_joint:dict ):
|
||||||
|
"""
|
||||||
|
将x、y、z坐标转换为对应关节的位置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (float): x坐标
|
||||||
|
y (float): y坐标
|
||||||
|
z (float): z坐标
|
||||||
|
x_joint (dict): x轴关节配置,包含plus和offset
|
||||||
|
y_joint (dict): y轴关节配置,包含plus和offset
|
||||||
|
z_joint (dict): z轴关节配置,包含plus和offset
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 关节名称和对应位置的字典
|
||||||
|
"""
|
||||||
|
joint_positions = copy.deepcopy(self.j_msg.position)
|
||||||
|
|
||||||
|
# 处理x轴关节
|
||||||
|
for joint_name, config in x_joint.items():
|
||||||
|
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||||
|
joint_positions[index] = x * config["factor"] + config["offset"]
|
||||||
|
|
||||||
|
# 处理y轴关节
|
||||||
|
for joint_name, config in y_joint.items():
|
||||||
|
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||||
|
joint_positions[index] = y * config["factor"] + config["offset"]
|
||||||
|
|
||||||
|
# 处理z轴关节
|
||||||
|
for joint_name, config in z_joint.items():
|
||||||
|
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||||
|
joint_positions[index] = z * config["factor"] + config["offset"]
|
||||||
|
|
||||||
|
|
||||||
|
return joint_positions
|
||||||
|
|
||||||
|
|
||||||
|
def move_joints(self, resource_name, link_name, speed, x_joint=None, y_joint=None, z_joint=None):
|
||||||
|
|
||||||
|
transform = self.tf_buffer.lookup_transform(
|
||||||
|
link_name,
|
||||||
|
resource_name,
|
||||||
|
rclpy.time.Time()
|
||||||
|
)
|
||||||
|
x,y,z = transform.transform.translation.x, transform.transform.translation.y, transform.transform.translation.z
|
||||||
|
if x_joint is None:
|
||||||
|
x_joint_config = next(iter(self.joint_config['x'].items()))
|
||||||
|
elif x_joint in self.joint_config['x']:
|
||||||
|
x_joint_config = self.joint_config['x'][x_joint]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"x_joint {x_joint} not in joint_config['x']")
|
||||||
|
if y_joint is None:
|
||||||
|
y_joint_config = next(iter(self.joint_config['y'].items()))
|
||||||
|
elif y_joint in self.joint_config['y']:
|
||||||
|
y_joint_config = self.joint_config['y'][y_joint]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"y_joint {y_joint} not in joint_config['y']")
|
||||||
|
if z_joint is None:
|
||||||
|
z_joint_config = next(iter(self.joint_config['z'].items()))
|
||||||
|
elif z_joint in self.joint_config['z']:
|
||||||
|
z_joint_config = self.joint_config['z'][z_joint]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"z_joint {z_joint} not in joint_config['z']")
|
||||||
|
joint_positions_target = self.inverse_kinematics(x,y,z,x_joint_config,y_joint_config,z_joint_config)
|
||||||
|
|
||||||
|
loop_flag = 0
|
||||||
|
|
||||||
|
|
||||||
|
while loop_flag < len(self.joint_config['joint_names']):
|
||||||
|
loop_flag = 0
|
||||||
|
for i in range(len(self.joint_config['joint_names'])):
|
||||||
|
distance = joint_positions_target[i] - self.j_msg.position[i]
|
||||||
|
if distance == 0:
|
||||||
|
loop_flag += 1
|
||||||
|
continue
|
||||||
|
minus_flag = distance/abs(distance)
|
||||||
|
if abs(distance) > speed/self.rate:
|
||||||
|
self.j_msg.position[i] += minus_flag * speed/self.rate
|
||||||
|
else :
|
||||||
|
self.j_msg.position[i] = joint_positions_target[i]
|
||||||
|
loop_flag += 1
|
||||||
|
|
||||||
|
|
||||||
|
# 发布关节状态
|
||||||
|
self.lh_joint_pub_callback()
|
||||||
|
time.sleep(1/self.rate)
|
||||||
|
|
||||||
|
|
||||||
|
def lh_joint_pub_callback(self):
|
||||||
|
self.j_msg.header.stamp = self.get_clock().now().to_msg()
|
||||||
|
self.j_pub.publish(self.j_msg)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
15
unilabos/devices/zhida_hplc/possible_status.txt
Normal file
15
unilabos/devices/zhida_hplc/possible_status.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
(base) PS C:\Users\dell\Desktop> python zhida.py getstatus
|
||||||
|
{
|
||||||
|
"result": "RUN",
|
||||||
|
"message": "AcqTime:3.321049min Vial:1"
|
||||||
|
}
|
||||||
|
(base) PS C:\Users\dell\Desktop> python zhida.py getstatus
|
||||||
|
{
|
||||||
|
"result": "NOTREADY",
|
||||||
|
"message": "AcqTime:0min Vial:1"
|
||||||
|
}
|
||||||
|
(base) PS C:\Users\dell\Desktop> python zhida.py getstatus
|
||||||
|
{
|
||||||
|
"result": "PRERUN",
|
||||||
|
"message": "AcqTime:0min Vial:1"
|
||||||
|
}
|
||||||
112
unilabos/devices/zhida_hplc/zhida.py
Normal file
112
unilabos/devices/zhida_hplc/zhida.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class ZhidaClient:
|
||||||
|
def __init__(self, host='192.168.1.47', port=5792, timeout=10.0):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.timeout = timeout
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""建立 TCP 连接,并设置超时用于后续 recv/send。"""
|
||||||
|
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
|
||||||
|
# 确保后续 recv/send 都会在 timeout 秒后抛 socket.timeout
|
||||||
|
self.sock.settimeout(self.timeout)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭连接。"""
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _send_command(self, cmd: dict) -> dict:
|
||||||
|
"""
|
||||||
|
发送一条命令,接收 raw bytes,直到能成功 json.loads。
|
||||||
|
"""
|
||||||
|
if not self.sock:
|
||||||
|
raise ConnectionError("Not connected")
|
||||||
|
|
||||||
|
# 1) 发送 JSON 命令
|
||||||
|
payload = json.dumps(cmd, ensure_ascii=False).encode('utf-8')
|
||||||
|
# 如果服务端需要换行分隔,也可以加上: payload += b'\n'
|
||||||
|
self.sock.sendall(payload)
|
||||||
|
|
||||||
|
# 2) 循环 recv,直到能成功解析完整 JSON
|
||||||
|
buffer = bytearray()
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = self.sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
# 对端关闭
|
||||||
|
break
|
||||||
|
buffer.extend(chunk)
|
||||||
|
# 尝试解码、解析
|
||||||
|
text = buffer.decode('utf-8', errors='strict')
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 继续 recv
|
||||||
|
pass
|
||||||
|
except socket.timeout:
|
||||||
|
raise TimeoutError("recv() timed out after {:.1f}s".format(self.timeout))
|
||||||
|
# 可选:防止死循环,整个循环时长超过 2×timeout 就报错
|
||||||
|
if time.time() - start > self.timeout * 2:
|
||||||
|
raise TimeoutError("No complete JSON received after {:.1f}s".format(time.time() - start))
|
||||||
|
|
||||||
|
raise ConnectionError("Connection closed before JSON could be parsed")
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def xxx() -> 类型:
|
||||||
|
# return xxxxxx
|
||||||
|
|
||||||
|
# def send_command(self, ):
|
||||||
|
# self.xxx = dict[xxx]
|
||||||
|
|
||||||
|
# 示例响应回复:
|
||||||
|
# {
|
||||||
|
# "result": "RUN",
|
||||||
|
# "message": "AcqTime:3.321049min Vial:1"
|
||||||
|
# }
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> dict:
|
||||||
|
return self._send_command({"command": "getstatus"})["result"]
|
||||||
|
|
||||||
|
# def get_status(self) -> dict:
|
||||||
|
# print(self._send_command({"command": "getstatus"}))
|
||||||
|
# return self._send_command({"command": "getstatus"})
|
||||||
|
|
||||||
|
def get_methods(self) -> dict:
|
||||||
|
return self._send_command({"command": "getmethods"})
|
||||||
|
|
||||||
|
def start(self, text) -> dict:
|
||||||
|
b64 = base64.b64encode(text.encode('utf-8')).decode('ascii')
|
||||||
|
return self._send_command({"command": "start", "message": b64})
|
||||||
|
|
||||||
|
def abort(self) -> dict:
|
||||||
|
return self._send_command({"command": "abort"})
|
||||||
|
|
||||||
|
"""
|
||||||
|
a,b,c
|
||||||
|
1,2,4
|
||||||
|
2,4,5
|
||||||
|
"""
|
||||||
|
|
||||||
|
client = ZhidaClient()
|
||||||
|
# 连接
|
||||||
|
client.connect()
|
||||||
|
# 获取状态
|
||||||
|
print(client.status)
|
||||||
|
|
||||||
|
|
||||||
|
# 命令格式:python zhida.py <subcommand> [options]
|
||||||
2
unilabos/devices/zhida_hplc/zhida_test_1.csv
Normal file
2
unilabos/devices/zhida_hplc/zhida_test_1.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
SampleName,AcqMethod,RackCode,VialPos,SmplInjVol,OutputFile
|
||||||
|
Sample001,1028-10ul-10min.M,CStk1-01,1,10,DataSET1
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
io_snrd:
|
io_snrd:
|
||||||
|
description: IO Board with 16 IOs
|
||||||
class:
|
class:
|
||||||
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||||
type: python
|
type: python
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
serial:
|
serial:
|
||||||
|
description: Serial communication interface, used when sharing same serial port for multiple devices
|
||||||
class:
|
class:
|
||||||
module: unilabos.ros.nodes.presets:ROS2SerialNode
|
module: unilabos.ros.nodes.presets.serial_node:ROS2SerialNode
|
||||||
type: ros2
|
type: ros2
|
||||||
schema:
|
schema:
|
||||||
properties: {}
|
properties: {}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# 光学表征设备:红外、紫外可见、拉曼等
|
# 光学表征设备:红外、紫外可见、拉曼等
|
||||||
raman_home_made:
|
raman_home_made:
|
||||||
|
description: Raman spectroscopy device
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
|
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
|
||||||
type: python
|
type: python
|
||||||
@@ -18,6 +19,49 @@ raman_home_made:
|
|||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- status
|
- status
|
||||||
|
additionalProperties: false
|
||||||
|
type: object
|
||||||
|
hplc.agilent:
|
||||||
|
description: HPLC device
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||||
|
type: python
|
||||||
|
status_types:
|
||||||
|
device_status: String
|
||||||
|
could_run: Bool
|
||||||
|
driver_init_ok: Bool
|
||||||
|
is_running: Bool
|
||||||
|
finish_status: String
|
||||||
|
status_text: String
|
||||||
|
action_value_mappings:
|
||||||
|
execute_command_from_outer:
|
||||||
|
type: SendCmd
|
||||||
|
goal:
|
||||||
|
command: command
|
||||||
|
feedback: {}
|
||||||
|
result:
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
device_status:
|
||||||
|
type: string
|
||||||
|
could_run:
|
||||||
|
type: boolean
|
||||||
|
driver_init_ok:
|
||||||
|
type: boolean
|
||||||
|
is_running:
|
||||||
|
type: boolean
|
||||||
|
finish_status:
|
||||||
|
type: string
|
||||||
|
status_text:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- device_status
|
||||||
|
- could_run
|
||||||
|
- driver_init_ok
|
||||||
|
- is_running
|
||||||
|
- finish_status
|
||||||
|
- status_text
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
type: object
|
type: object
|
||||||
56
unilabos/registry/devices/laiyu_add_solid.yaml
Normal file
56
unilabos/registry/devices/laiyu_add_solid.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
laiyu_add_solid:
|
||||||
|
description: Laiyu Add Solid
|
||||||
|
class:
|
||||||
|
module: unilabos.devices.laiyu_add_solid.laiyu:Laiyu
|
||||||
|
type: python
|
||||||
|
status_types: {}
|
||||||
|
action_value_mappings:
|
||||||
|
move_to_xyz:
|
||||||
|
type: Point3DSeparateInput
|
||||||
|
goal:
|
||||||
|
x: x
|
||||||
|
y: y
|
||||||
|
z: z
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
pick_powder_tube:
|
||||||
|
type: IntSingleInput
|
||||||
|
goal:
|
||||||
|
int_input: int_input
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
put_powder_tube:
|
||||||
|
type: IntSingleInput
|
||||||
|
goal:
|
||||||
|
int_input: int_input
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
reset:
|
||||||
|
type: EmptyIn
|
||||||
|
goal: {}
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
add_powder_tube:
|
||||||
|
type: SolidDispenseAddPowderTube
|
||||||
|
goal:
|
||||||
|
powder_tube_number: powder_tube_number
|
||||||
|
target_tube_position: target_tube_position
|
||||||
|
compound_mass: compound_mass
|
||||||
|
feedback: {}
|
||||||
|
result:
|
||||||
|
actual_mass_mg: actual_mass_mg
|
||||||
|
move_to_plate:
|
||||||
|
type: StrSingleInput
|
||||||
|
goal:
|
||||||
|
string: string
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
discharge:
|
||||||
|
type: FloatSingleInput
|
||||||
|
goal:
|
||||||
|
float_input: float_input
|
||||||
|
feedback: {}
|
||||||
|
result: {}
|
||||||
|
|
||||||
|
schema:
|
||||||
|
properties: {}
|
||||||
@@ -1,10 +1,96 @@
|
|||||||
liquid_handler:
|
liquid_handler:
|
||||||
|
description: Liquid handler device controlled by pylabrobot
|
||||||
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:
|
||||||
@@ -162,11 +248,11 @@ liquid_handler:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
status:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: 液体处理仪器当前状态
|
description: 液体处理仪器当前状态
|
||||||
required:
|
required:
|
||||||
- status
|
- name
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
liquid_handler.revvity:
|
liquid_handler.revvity:
|
||||||
@@ -186,4 +272,3 @@ liquid_handler.revvity:
|
|||||||
status: status
|
status: status
|
||||||
result:
|
result:
|
||||||
success: success
|
success: success
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
separator.homemade:
|
separator.homemade:
|
||||||
|
description: Separator device with homemade grbl controller
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.separator.homemade_grbl_conductivity:Separator_Controller
|
module: unilabos.devices.separator.homemade_grbl_conductivity:SeparatorController
|
||||||
type: python
|
type: python
|
||||||
status_types:
|
status_types:
|
||||||
sensordata: Float64
|
sensordata: Float64
|
||||||
@@ -11,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:
|
||||||
@@ -39,6 +40,7 @@ separator.homemade:
|
|||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
rotavap.one:
|
rotavap.one:
|
||||||
|
description: Rotavap device
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
|
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
|
||||||
type: python
|
type: python
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
syringe_pump_with_valve.runze:
|
syringe_pump_with_valve.runze:
|
||||||
|
description: Runze Syringe pump with valve
|
||||||
class:
|
class:
|
||||||
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
|
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
|
||||||
type: python
|
type: python
|
||||||
|
hardware_interface:
|
||||||
|
name: hardware_interface
|
||||||
|
read: send_command
|
||||||
|
write: send_command
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -25,11 +30,13 @@ syringe_pump_with_valve.runze:
|
|||||||
|
|
||||||
|
|
||||||
solenoid_valve.mock:
|
solenoid_valve.mock:
|
||||||
|
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
|
||||||
|
|
||||||
solenoid_valve:
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user