mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-10 09:45:11 +00:00
Compare commits
37 Commits
v0.8.0
...
ca15173717
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca15173717 | ||
|
|
94e79418b9 | ||
|
|
02b4750637 | ||
|
|
4e636b91b4 | ||
|
|
3af1964328 | ||
|
|
f661049823 | ||
|
|
a79c26b9a4 | ||
|
|
8cf51ee8d3 | ||
|
|
8c81dab7e1 | ||
|
|
ff76bb1a76 | ||
|
|
6f69df440c | ||
|
|
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
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
service
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
@@ -229,6 +230,6 @@ CATKIN_IGNORE
|
||||
|
||||
.DS_Store
|
||||
|
||||
local_config.py
|
||||
/**/local_config.py
|
||||
|
||||
*.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
|
||||
90
README.md
90
README.md
@@ -1 +1,89 @@
|
||||
# Uni-Lab-OS
|
||||
<div align="center">
|
||||
<img src="docs/logo.png" alt="Uni-Lab Logo" width="200"/>
|
||||
</div>
|
||||
|
||||
# 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
|
||||
|
||||
# 配置类定义
|
||||
@dataclass
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "YOUR_LAB_ID"
|
||||
@@ -34,7 +34,7 @@ class MQConfig:
|
||||
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "7AAEDBEA" # 实验室唯一标识
|
||||
@@ -46,9 +46,9 @@ class MQConfig:
|
||||
port: int = 8883
|
||||
|
||||
# 可以直接提供证书文件路径
|
||||
ca_file: str = "/path/to/ca.pem"
|
||||
cert_file: str = "/path/to/cert.pem"
|
||||
key_file: str = "/path/to/key.pem"
|
||||
ca_file: str = "/path/to/ca.pem" # 相对config.py所在目录的路径
|
||||
cert_file: str = "/path/to/cert.pem" # 相对config.py所在目录的路径
|
||||
key_file: str = "/path/to/key.pem" # 相对config.py所在目录的路径
|
||||
|
||||
# 或者直接提供证书内容
|
||||
ca_content: str = ""
|
||||
@@ -74,22 +74,18 @@ MQTT连接支持两种方式配置证书:
|
||||
配置ROS消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
|
||||
class ROSConfig:
|
||||
"""ROS模块配置"""
|
||||
modules: list = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.modules is None:
|
||||
self.modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
"control_msgs.msg",
|
||||
"control_msgs.action",
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
]
|
||||
modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
"control_msgs.msg",
|
||||
"control_msgs.action",
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
]
|
||||
```
|
||||
|
||||
您可以根据需要添加其他ROS模块。
|
||||
@@ -106,14 +102,7 @@ class ROSConfig:
|
||||
unilab --config path/to/your/config.py
|
||||
```
|
||||
|
||||
## 环境变量覆盖
|
||||
|
||||
某些配置项可以通过环境变量进行覆盖,这在不同环境部署时特别有用:
|
||||
|
||||
```bash
|
||||
# 设置环境变量覆盖配置
|
||||
export UNILAB_LAB_ID="YOUR_LAB_ID"
|
||||
export UNILAB_MQTT_BROKER="mqtt-broker-address"
|
||||
如果您不涉及多环境开发,可以在unilabos的安装路径中手动添加local_config.py的文件
|
||||
|
||||
# 启动Uni-Lab
|
||||
python -m unilabos.app.main --config path/to/your/config.py
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
```shell
|
||||
mamba env create -f unilabos-<YOUR_OS>.yaml
|
||||
mamba activate ilab
|
||||
mamba activate unilab
|
||||
```
|
||||
|
||||
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.8.0
|
||||
version: 0.9.1
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.8.0"
|
||||
version: "0.9.1"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
18
setup.py
18
setup.py
@@ -1,28 +1,18 @@
|
||||
from setuptools import setup, find_packages
|
||||
from glob import glob
|
||||
import os
|
||||
|
||||
package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.8.0',
|
||||
version='0.9.1',
|
||||
packages=find_packages(),
|
||||
# data_files=[
|
||||
# ('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/*'))
|
||||
# ],
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
zip_safe=True,
|
||||
maintainer='Junhan Chang',
|
||||
maintainer_email='changjh@pku.edu.cn',
|
||||
description='TODO: Package description',
|
||||
license='TODO: License declaration',
|
||||
description='',
|
||||
license='GPL v3',
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'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",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "hplc",
|
||||
"class": "hplc.agilent",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
@@ -19,8 +19,8 @@
|
||||
},
|
||||
{
|
||||
"id": "BottlesRack3",
|
||||
"name": "Revvity上样盘3",
|
||||
"parent": "Revvity",
|
||||
"name": "上样盘3",
|
||||
"parent": "HPLC",
|
||||
"type": "plate",
|
||||
"class": null,
|
||||
"position": {
|
||||
|
||||
29
test/experiments/MockChiller.json
Normal file
29
test/experiments/MockChiller.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockChiller1",
|
||||
"name": "模拟冷却器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockChiller",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"power_on": false,
|
||||
"is_cooling": false,
|
||||
"is_heating": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
30
test/experiments/MockFilter.json
Normal file
30
test/experiments/MockFilter.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockFilter1",
|
||||
"name": "模拟过滤器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockFilter",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"is_filtering": false,
|
||||
"filter_efficiency": 95.0,
|
||||
"flow_rate": 0.0,
|
||||
"pressure_drop": 0.0,
|
||||
"filter_life": 100.0,
|
||||
"power_on": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
30
test/experiments/MockHeater.json
Normal file
30
test/experiments/MockHeater.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockHeater1",
|
||||
"name": "模拟加热器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockHeater",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"power_on": false,
|
||||
"is_heating": false,
|
||||
"heating_power": 0.0,
|
||||
"max_temperature": 300.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
33
test/experiments/MockPump.json
Normal file
33
test/experiments/MockPump.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockPump1",
|
||||
"name": "模拟泵设备",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_pump",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"power_state": "Off",
|
||||
"pump_state": "Stopped",
|
||||
"flow_rate": 0.0,
|
||||
"target_flow_rate": 0.0,
|
||||
"pressure": 0.0,
|
||||
"total_volume": 0.0,
|
||||
"direction": "Forward",
|
||||
"max_flow_rate": 100.0,
|
||||
"max_pressure": 10.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
34
test/experiments/MockRotavap.json
Normal file
34
test/experiments/MockRotavap.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockRotavap1",
|
||||
"name": "模拟旋转蒸发器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockRotavap",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"power_state": "Off",
|
||||
"rotate_state": "Stopped",
|
||||
"rotate_time": 0.0,
|
||||
"rotate_speed": 0.0,
|
||||
"pump_state": "Stopped",
|
||||
"pump_time": 0.0,
|
||||
"vacuum_level": 1013.25,
|
||||
"temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"success": "True"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
29
test/experiments/MockSeperator.json
Normal file
29
test/experiments/MockSeperator.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockSeparator1",
|
||||
"name": "模拟分离器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockSeparator",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"power_state": "Off",
|
||||
"settling_time": 0.0,
|
||||
"valve_state": "Closed",
|
||||
"shake_time": 0.0,
|
||||
"shake_status": "Not Shaking"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
25
test/experiments/MockSolenoidValve.json
Normal file
25
test/experiments/MockSolenoidValve.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockSolenoidValve1",
|
||||
"name": "模拟电磁阀",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockSolenoidValve",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"valve_status": "Closed"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
34
test/experiments/MockStirrer.json
Normal file
34
test/experiments/MockStirrer.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockStirrer1",
|
||||
"name": "模拟搅拌器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockStirrer",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"power_state": "Off",
|
||||
"stir_speed": 0.0,
|
||||
"target_stir_speed": 0.0,
|
||||
"stir_state": "Stopped",
|
||||
"temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"heating_state": "Off",
|
||||
"heating_power": 0.0,
|
||||
"max_stir_speed": 2000.0,
|
||||
"max_temperature": 300.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
31
test/experiments/MockVacuum.json
Normal file
31
test/experiments/MockVacuum.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockVacuum1",
|
||||
"name": "模拟真空泵",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "MockVacuum",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"power_state": "Off",
|
||||
"pump_state": "Stopped",
|
||||
"vacuum_level": 1013.25,
|
||||
"target_vacuum": 50.0,
|
||||
"pump_speed": 0.0,
|
||||
"pump_efficiency": 95.0,
|
||||
"max_pump_speed": 100.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
@@ -6679,8 +6679,7 @@
|
||||
"plate_well_11_3",
|
||||
"plate_well_11_4",
|
||||
"plate_well_11_5",
|
||||
"plate_well_11_6",
|
||||
"plate_well_11_7"
|
||||
"plate_well_11_6"
|
||||
],
|
||||
"parent": "deck",
|
||||
"type": "device",
|
||||
@@ -10508,45 +10507,6 @@
|
||||
"pending_liquids": [],
|
||||
"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": []
|
||||
|
||||
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",
|
||||
"name": "假夹爪",
|
||||
"children": [
|
||||
"Plate1"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "gripper.mock",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
@@ -23,18 +24,120 @@
|
||||
"name": "Plate1",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"parent": "Gripper1",
|
||||
"type": "plate",
|
||||
"class": "nest_96_wellplate_2ml_deep",
|
||||
"class": "nest_96_wellplate_100ul_pcr_full_skirt",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 69
|
||||
},
|
||||
"config": {
|
||||
},
|
||||
"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": [
|
||||
|
||||
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
|
||||
# simulation
|
||||
- ros-humble-simulation
|
||||
- ros-humble-tf-transformations
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
@@ -56,6 +56,10 @@ dependencies:
|
||||
# - ros-humble-moveit-servo
|
||||
# simulation
|
||||
- ros-humble-simulation
|
||||
- ros-humble-tf-transformations
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
@@ -58,6 +58,10 @@ dependencies:
|
||||
- ros-humble-moveit-servo
|
||||
# simulation
|
||||
- ros-humble-simulation
|
||||
- ros-humble-tf-transformations
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
@@ -56,6 +56,10 @@ dependencies:
|
||||
- ros-humble-moveit-servo
|
||||
# simulation
|
||||
- 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
|
||||
# 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(
|
||||
backend: str,
|
||||
devices_config: dict = {},
|
||||
resources_config: dict = {},
|
||||
resources_config: list = [],
|
||||
graph=None,
|
||||
controllers_config: dict = {},
|
||||
bridges=[],
|
||||
without_host: bool = False,
|
||||
visual: str = "None",
|
||||
resources_mesh_config: dict = {},
|
||||
**kwargs
|
||||
):
|
||||
if backend == "ros":
|
||||
@@ -29,7 +31,9 @@ def start_backend(
|
||||
|
||||
backend_thread = threading.Thread(
|
||||
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()
|
||||
logger.info(f"Backend {backend} started.")
|
||||
|
||||
@@ -29,6 +29,8 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
req.data['action'] = action_name
|
||||
if action_name == "execute_command_from_outer":
|
||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||
print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
|
||||
elif "command" in action_kwargs:
|
||||
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)
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
import threading
|
||||
import time
|
||||
from copy import deepcopy
|
||||
|
||||
import yaml
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if ilabos_dir not in sys.path:
|
||||
sys.path.append(ilabos_dir)
|
||||
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if unilabos_dir not in sys.path:
|
||||
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
|
||||
|
||||
|
||||
@@ -58,7 +62,28 @@ def parse_args():
|
||||
default=None,
|
||||
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()
|
||||
|
||||
|
||||
@@ -68,19 +93,28 @@ def main():
|
||||
args = parse_args()
|
||||
args_dict = vars(args)
|
||||
|
||||
# 加载配置文件 - 这里保持最先加载配置的逻辑
|
||||
if args_dict.get("config"):
|
||||
config_path = args_dict["config"]
|
||||
# 加载配置文件,优先加载config,然后从env读取
|
||||
config_path = args_dict.get("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):
|
||||
print_status(f"配置文件 {config_path} 不存在", "error")
|
||||
elif not config_path.endswith(".py"):
|
||||
print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error")
|
||||
else:
|
||||
load_config(config_path)
|
||||
else:
|
||||
print_status(f"启动 UniLab-OS时,配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning")
|
||||
load_config(config_path)
|
||||
|
||||
# 设置BasicConfig参数
|
||||
BasicConfig.is_host_mode = not args_dict.get("without_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 (
|
||||
read_node_link_json,
|
||||
@@ -92,8 +126,8 @@ def main():
|
||||
from unilabos.app.mq import mqtt_client
|
||||
from unilabos.registry.registry import build_registry
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.web import http_client
|
||||
from unilabos.web import start_server
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
@@ -101,6 +135,7 @@ def main():
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"])
|
||||
|
||||
devices_and_resources = None
|
||||
if args_dict["graph"] is not None:
|
||||
import unilabos.resources.graphio as graph_res
|
||||
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["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
||||
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
|
||||
|
||||
args_dict["graph"] = graph_res.physical_setup_graph
|
||||
else:
|
||||
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.SIGTERM, _exit)
|
||||
mqtt_client.start()
|
||||
|
||||
start_backend(**args_dict)
|
||||
start_server()
|
||||
args_dict["resources_mesh_config"] = {}
|
||||
# web visiualize 2D
|
||||
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__":
|
||||
|
||||
@@ -51,8 +51,9 @@ class Resp(BaseModel):
|
||||
class JobAddReq(BaseModel):
|
||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||
job_id: str = Field(examples=["sfsfsfeq"], description="goal uuid")
|
||||
node_id: str = Field(examples=["sfsfsfeq"], description="node uuid")
|
||||
job_id: str = Field(examples=["job_id"], description="goal 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):
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import ssl, base64, hmac
|
||||
import ssl
|
||||
import base64
|
||||
import hmac
|
||||
from hashlib import sha1
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from unilabos.config.config import MQConfig
|
||||
from unilabos.app.controler import devices, job_add
|
||||
from unilabos.app.model import JobAddReq, JobAddResp
|
||||
from unilabos.app.controler import job_add
|
||||
from unilabos.app.model import JobAddReq
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
from paho.mqtt.enums import CallbackAPIVersion
|
||||
|
||||
|
||||
class MQTTClient:
|
||||
mqtt_disable = True
|
||||
@@ -21,7 +26,8 @@ class MQTTClient:
|
||||
def __init__(self):
|
||||
self.mqtt_disable = not MQConfig.lab_id
|
||||
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()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
@@ -31,34 +37,45 @@ class MQTTClient:
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
|
||||
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):
|
||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||
isok, data = devices()
|
||||
if not isok:
|
||||
logger.error("[MQTT] on_connect ErrorHostNotInit")
|
||||
return
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
def _on_message(self, client, userdata, msg) -> None:
|
||||
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
try:
|
||||
payload_str = msg.payload.decode("utf-8")
|
||||
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/":
|
||||
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)
|
||||
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:
|
||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||||
logger.error(traceback.format_exc())
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||||
if rc != 0:
|
||||
@@ -87,7 +104,7 @@ class MQTTClient:
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
@@ -142,7 +159,7 @@ class MQTTClient:
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
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)
|
||||
logger.critical(f"Device status published: address: {address}, {status}")
|
||||
|
||||
@@ -151,12 +168,12 @@ class MQTTClient:
|
||||
return
|
||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status}
|
||||
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
||||
|
||||
|
||||
def publish_registry(self, device_id: str, device_info: dict):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
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)
|
||||
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
||||
|
||||
@@ -164,11 +181,30 @@ class MQTTClient:
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/actions/"
|
||||
action_type_name = action_info["title"]
|
||||
action_info["title"] = action_id
|
||||
action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False)
|
||||
self.client.publish(address, action_data, qos=2)
|
||||
logger.debug(f"Action data published: address: {address}, {action_data}")
|
||||
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||
|
||||
def send_ping(self, ping_id: str, timestamp: float):
|
||||
"""发送ping消息到服务端"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/ping/"
|
||||
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
|
||||
self.client.publish(address, json.dumps(ping_data), qos=2)
|
||||
|
||||
def setup_pong_subscription(self):
|
||||
"""设置pong消息订阅"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
|
||||
self.client.subscribe(pong_topic, 0)
|
||||
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||
|
||||
def handle_pong(self, pong_data: dict):
|
||||
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
||||
logger.debug(f"Pong received: {pong_data}")
|
||||
# 这里会被HostNode的ping-pong处理逻辑调用
|
||||
pass
|
||||
|
||||
|
||||
mqtt_client = MQTTClient()
|
||||
|
||||
@@ -4,10 +4,10 @@ Web UI 模块
|
||||
提供了UniLab系统的Web界面功能
|
||||
"""
|
||||
|
||||
from unilabos.web.pages import setup_web_pages
|
||||
from unilabos.web.server import setup_server, start_server
|
||||
from unilabos.web.client import http_client
|
||||
from unilabos.web.api import setup_api_routes
|
||||
from unilabos.app.web.pages import setup_web_pages
|
||||
from unilabos.app.web.server import setup_server, start_server
|
||||
from unilabos.app.web.client import http_client
|
||||
from unilabos.app.web.api import setup_api_routes
|
||||
|
||||
__all__ = [
|
||||
"setup_web_pages", # 设置Web页面
|
||||
@@ -18,7 +18,7 @@ from unilabos.app.model import (
|
||||
JobPreintakeFinishReq,
|
||||
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 = APIRouter()
|
||||
@@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional
|
||||
import requests
|
||||
from unilabos.utils.log import info
|
||||
from unilabos.config.config import MQConfig, HTTPConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
class HTTPClient:
|
||||
@@ -102,6 +103,30 @@ class HTTPClient:
|
||||
)
|
||||
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()
|
||||
@@ -20,9 +20,9 @@ from unilabos.app.mq import mqtt_client
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||
from unilabos.utils.log import error
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
from unilabos.web.utils.device_utils import get_registry_info
|
||||
from unilabos.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.device_utils import get_registry_info
|
||||
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||
from unilabos.app.web.utils.ros_utils import get_ros_node_info, update_ros_node_info
|
||||
|
||||
# 设置Jinja2模板环境
|
||||
template_dir = Path(__file__).parent / "templates"
|
||||
@@ -92,19 +92,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
|
||||
# 获取已加载的设备
|
||||
if lab_registry:
|
||||
# 设备类型
|
||||
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)
|
||||
|
||||
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
|
||||
# 资源类型
|
||||
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
||||
resources.append(
|
||||
@@ -13,8 +13,8 @@ from starlette.responses import Response
|
||||
|
||||
from unilabos.utils.fastapi.log_adapter import setup_fastapi_logging
|
||||
from unilabos.utils.log import info, error
|
||||
from unilabos.web.api import setup_api_routes
|
||||
from unilabos.web.pages import setup_web_pages
|
||||
from unilabos.app.web.api import setup_api_routes
|
||||
from unilabos.app.web.pages import setup_web_pages
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
@@ -96,17 +96,19 @@
|
||||
<tr>
|
||||
<th>设备ID</th>
|
||||
<th>命名空间</th>
|
||||
<th>机器名称</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
{% for device_id, device_info in host_node_info.devices.items() %}
|
||||
<tr>
|
||||
<td>{{ device_id }}</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>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="empty-state">没有发现已管理的设备</td>
|
||||
<td colspan="4" class="empty-state">没有发现已管理的设备</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -218,6 +220,7 @@
|
||||
<th>Device ID</th>
|
||||
<th>节点名称</th>
|
||||
<th>命名空间</th>
|
||||
<th>机器名称</th>
|
||||
<th>状态项</th>
|
||||
<th>动作数</th>
|
||||
</tr>
|
||||
@@ -227,6 +230,7 @@
|
||||
<td>{{ device_id }}</td>
|
||||
<td>{{ device_info.node_name }}</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_actions.get(device_id, {})|length }} <span class="toggle-indicator">▼</span></td>
|
||||
</tr>
|
||||
@@ -329,8 +333,13 @@
|
||||
<tr id="device-info-{{ loop.index }}" class="detail-row" style="display: none;">
|
||||
<td colspan="5">
|
||||
<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 %}
|
||||
<div class="status-badge"><span class="online-status">在线</span></div>
|
||||
{% endif %}
|
||||
@@ -362,7 +371,12 @@
|
||||
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
|
||||
<button class="debug-btn" onclick="toggleDebugInfo(this, event)">调试</button>
|
||||
<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>
|
||||
|
||||
@@ -8,7 +8,7 @@ import traceback
|
||||
from typing import Dict, Any, Type, TypedDict, Optional
|
||||
|
||||
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.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()):
|
||||
slot_name, slot_type = slot_info
|
||||
type_info = goal_type.SLOT_TYPES[ind]
|
||||
default_value = "unknown"
|
||||
if isinstance(type_info, UnboundedSequence):
|
||||
inner_type = type_info.value_type
|
||||
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())]
|
||||
elif isinstance(inner_type, BasicType):
|
||||
default_value = [get_default_value_for_ros_type(inner_type.typename)]
|
||||
elif isinstance(inner_type, UnboundedString):
|
||||
default_value = [""]
|
||||
else:
|
||||
default_value = "unknown"
|
||||
default_value = []
|
||||
elif isinstance(type_info, NamespacedType):
|
||||
cls_name = ".".join(type_info.namespaces) + ":" + type_info.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())
|
||||
elif isinstance(type_info, BasicType):
|
||||
default_value = get_default_value_for_ros_type(type_info.typename)
|
||||
elif isinstance(type_info, UnboundedString):
|
||||
default_value = ""
|
||||
else:
|
||||
type_class = msg_converter_manager.search_class(slot_type, search_lower=True)
|
||||
if type_class is not None:
|
||||
@@ -9,7 +9,7 @@ from typing import Dict, Any
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
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]:
|
||||
@@ -30,20 +30,19 @@ def get_host_node_info() -> Dict[str, Any]:
|
||||
return host_info
|
||||
host_info["available"] = True
|
||||
host_info["devices"] = {
|
||||
device_id: {
|
||||
edge_device_id: {
|
||||
"namespace": namespace,
|
||||
"is_online": f"{namespace}/{device_id}" in host_node._online_devices,
|
||||
"key": f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}",
|
||||
"is_online": f"{namespace}/{edge_device_id}" in host_node._online_devices,
|
||||
"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))
|
||||
# 获取动作客户端信息
|
||||
for action_id, client in host_node._action_clients.items():
|
||||
host_info["action_clients"] = {
|
||||
action_id: get_action_info(client, full_name=action_id)
|
||||
}
|
||||
host_info["action_clients"][action_id] = get_action_info(client, full_name=action_id)
|
||||
|
||||
# 获取设备状态
|
||||
host_info["device_status"] = host_node.device_status
|
||||
@@ -7,11 +7,12 @@ ROS 工具函数模块
|
||||
import traceback
|
||||
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_node_info = {"online_devices": {}, "device_topics": {}, "device_actions": {}}
|
||||
|
||||
|
||||
def get_ros_node_info() -> Dict[str, Any]:
|
||||
"""获取 ROS 节点信息,包括设备节点、发布的状态和动作
|
||||
|
||||
@@ -35,6 +36,13 @@ def update_ros_node_info() -> Dict[str, Any]:
|
||||
|
||||
try:
|
||||
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():
|
||||
# 设备基本信息
|
||||
@@ -42,6 +50,7 @@ def update_ros_node_info() -> Dict[str, Any]:
|
||||
"node_name": device_info["node_name"],
|
||||
"namespace": device_info["namespace"],
|
||||
"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] = {
|
||||
k: get_action_info(v, k)
|
||||
for k, v in device_info["actions"].items()
|
||||
}
|
||||
result["device_actions"][device_id] = {k: get_action_info(v, k) for k, v in device_info["actions"].items()}
|
||||
# 更新全局变量
|
||||
ros_node_info = result
|
||||
except Exception as e:
|
||||
@@ -6,7 +6,7 @@ def generate_clean_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str, # Vessel to clean.
|
||||
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.
|
||||
repeats: int = 1, # Optional. Number of cleaning cycles to perform.
|
||||
) -> list[dict]:
|
||||
@@ -27,7 +27,7 @@ def generate_clean_protocol(
|
||||
from_vessel = f"flask_{solvent}"
|
||||
waste_vessel = f"waste_workup"
|
||||
|
||||
transfer_flowrate = flowrate = 2500.0
|
||||
transfer_flowrate = flowrate = 2.5
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
for i in range(repeats):
|
||||
|
||||
@@ -24,8 +24,8 @@ def generate_evaporate_protocol(
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
pump_action_sequence = []
|
||||
reactor_volume = 500000.0
|
||||
transfer_flowrate = flowrate = 2500.0
|
||||
reactor_volume = 500.0
|
||||
transfer_flowrate = flowrate = 2.5
|
||||
|
||||
# 开启冷凝器
|
||||
pump_action_sequence.append({
|
||||
|
||||
@@ -7,7 +7,7 @@ def generate_pump_protocol(
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
flowrate: float = 500.0,
|
||||
flowrate: float = 0.5,
|
||||
transfer_flowrate: float = 0,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
@@ -141,11 +141,11 @@ def generate_pump_protocol_with_rinsing(
|
||||
time: float = 0,
|
||||
viscous: bool = False,
|
||||
rinsing_solvent: str = "air",
|
||||
rinsing_volume: float = 5000.0,
|
||||
rinsing_volume: float = 5.0,
|
||||
rinsing_repeats: int = 2,
|
||||
solid: bool = False,
|
||||
flowrate: float = 2500.0,
|
||||
transfer_flowrate: float = 500.0,
|
||||
flowrate: float = 2.5,
|
||||
transfer_flowrate: float = 0.5,
|
||||
) -> 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.
|
||||
@@ -159,11 +159,11 @@ def generate_pump_protocol_with_rinsing(
|
||||
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).
|
||||
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).
|
||||
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时的流速
|
||||
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 500.0). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
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 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
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"
|
||||
waste_vessel = f"waste_workup"
|
||||
|
||||
@@ -11,7 +11,7 @@ def generate_separate_protocol(
|
||||
to_vessel: str, # Vessel to send product 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_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'.
|
||||
repeats: int = 1, # Optional. Number of separations to perform.
|
||||
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 = []
|
||||
reactor_volume = 500000.0
|
||||
reactor_volume = 500.0
|
||||
waste_vessel = waste_phase_to_vessel
|
||||
|
||||
# TODO:通过物料管理系统找到溶剂的容器
|
||||
@@ -46,7 +46,7 @@ def generate_separate_protocol(
|
||||
separator_controller = f"{separation_vessel}_controller"
|
||||
separation_vessel_bottom = f"flask_{separation_vessel}"
|
||||
|
||||
transfer_flowrate = flowrate = 2500.0
|
||||
transfer_flowrate = flowrate = 2.5
|
||||
|
||||
if from_vessel != separation_vessel:
|
||||
pump_action_sequence.append(
|
||||
@@ -140,8 +140,8 @@ def generate_separate_protocol(
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": to_vessel,
|
||||
"volume": 250000.0,
|
||||
"time": 250000.0 / flowrate,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
@@ -164,8 +164,8 @@ def generate_separate_protocol(
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": waste_vessel,
|
||||
"volume": 250000.0,
|
||||
"time": 250000.0 / flowrate,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
@@ -179,8 +179,8 @@ def generate_separate_protocol(
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": waste_vessel,
|
||||
"volume": 250000.0,
|
||||
"time": 250000.0 / flowrate,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
@@ -203,8 +203,8 @@ def generate_separate_protocol(
|
||||
"action_kwargs": {
|
||||
"from_vessel": separation_vessel_bottom,
|
||||
"to_vessel": to_vessel,
|
||||
"volume": 250000.0,
|
||||
"time": 250000.0 / flowrate,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
@@ -221,8 +221,8 @@ def generate_separate_protocol(
|
||||
"action_kwargs": {
|
||||
"from_vessel": to_vessel,
|
||||
"to_vessel": separation_vessel,
|
||||
"volume": 250000.0,
|
||||
"time": 250000.0 / flowrate,
|
||||
"volume": 250.0,
|
||||
"time": 250.0 / flowrate,
|
||||
# "transfer_flowrate": transfer_flowrate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class BasicConfig:
|
||||
config_path = ""
|
||||
is_host_mode = True # 从registry.py移动过来
|
||||
slave_no_host = False # 是否跳过rclient.wait_for_service()
|
||||
machine_name = "undefined"
|
||||
vis_2d_enable = False
|
||||
|
||||
|
||||
# MQTT配置
|
||||
@@ -28,9 +30,9 @@ class MQConfig:
|
||||
key_content = ""
|
||||
|
||||
# 指定
|
||||
ca_file = ""
|
||||
cert_file = ""
|
||||
key_file = ""
|
||||
ca_file = "" # 相对config.py所在目录的路径
|
||||
cert_file = "" # 相对config.py所在目录的路径
|
||||
key_file = "" # 相对config.py所在目录的路径
|
||||
|
||||
|
||||
# OSS上传配置
|
||||
@@ -75,49 +77,106 @@ def _update_config_from_module(module):
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.ca_file.startswith("."):
|
||||
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
|
||||
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.ca_content = f.read()
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
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 MQConfig.cert_file.startswith("."):
|
||||
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
|
||||
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.cert_content = f.read()
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
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 MQConfig.key_file.startswith("."):
|
||||
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
|
||||
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.key_content = f.read()
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
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):
|
||||
# 如果提供了配置文件路径,从该文件导入配置
|
||||
if config_path:
|
||||
_update_config_from_env() # 允许config_path被env设定后读取
|
||||
BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path))
|
||||
if not os.path.exists(config_path):
|
||||
logger.error(f"配置文件 {config_path} 不存在")
|
||||
return
|
||||
logger.error(f"[ENV] 配置文件 {config_path} 不存在")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
module_name = "lab_" + os.path.basename(config_path).replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, config_path)
|
||||
if spec is None:
|
||||
logger.error(f"配置文件 {config_path} 错误")
|
||||
logger.error(f"[ENV] 配置文件 {config_path} 错误")
|
||||
return
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
_update_config_from_module(module)
|
||||
logger.info(f"配置文件 {config_path} 加载成功")
|
||||
logger.info(f"[ENV] 配置文件 {config_path} 加载成功")
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
||||
logger.error(f"[ENV] 加载配置文件 {config_path} 失败")
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
else:
|
||||
try:
|
||||
import unilabos.config.local_config as local_config # type: ignore
|
||||
|
||||
_update_config_from_module(local_config)
|
||||
logger.info("已加载默认配置 unilabos.config.local_config")
|
||||
except ImportError:
|
||||
pass
|
||||
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||
load_config(config_path)
|
||||
|
||||
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
|
||||
170
unilabos/devices/Mock/MockChiller/MockChiller.py
Normal file
170
unilabos/devices/Mock/MockChiller/MockChiller.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockChiller:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
self._current_temperature: float = 25.0 # 室温开始
|
||||
self._target_temperature: float = 25.0
|
||||
self._status: str = "Idle"
|
||||
self._is_cooling: bool = False
|
||||
self._is_heating: bool = False
|
||||
self._power_on: bool = False
|
||||
|
||||
# 模拟温度变化的线程
|
||||
self._temperature_thread = None
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""当前温度 - 会被自动识别的设备属性"""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""目标温度"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def power_on(self) -> bool:
|
||||
"""电源状态"""
|
||||
return self._power_on
|
||||
|
||||
@property
|
||||
def is_cooling(self) -> bool:
|
||||
"""是否正在冷却"""
|
||||
return self._is_cooling
|
||||
|
||||
@property
|
||||
def is_heating(self) -> bool:
|
||||
"""是否正在加热"""
|
||||
return self._is_heating
|
||||
|
||||
def set_temperature(self, temperature: float):
|
||||
"""设置目标温度 - 需要在注册表添加的设备动作"""
|
||||
if not self._power_on:
|
||||
self._status = "Error: Power Off"
|
||||
return False
|
||||
|
||||
# 将传入温度转换为 float,并限制在允许范围内
|
||||
temperature = float(temperature)
|
||||
self._target_temperature = temperature
|
||||
|
||||
# 立即更新状态
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
elif diff < 0:
|
||||
self._status = "Cooling"
|
||||
self._is_cooling = True
|
||||
self._is_heating = False
|
||||
else:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._is_cooling = False
|
||||
|
||||
# 启动温度控制
|
||||
self._start_temperature_control()
|
||||
return True
|
||||
|
||||
def power_on_off(self, power_state: str):
|
||||
"""开关机控制"""
|
||||
if power_state == "on":
|
||||
self._power_on = True
|
||||
# 不在这里直接设置状态和加热/制冷标志
|
||||
self._start_temperature_control()
|
||||
else:
|
||||
self._power_on = False
|
||||
self._status = "Power Off"
|
||||
self._stop_temperature_control()
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
|
||||
def _start_temperature_control(self):
|
||||
"""启动温度控制线程"""
|
||||
if self._power_on: # 移除 not self._running 检查
|
||||
self._running = True
|
||||
if self._temperature_thread is None or not self._temperature_thread.is_alive():
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
def _stop_temperature_control(self):
|
||||
"""停止温度控制"""
|
||||
self._running = False
|
||||
if self._temperature_thread:
|
||||
self._temperature_thread.join(timeout=1.0)
|
||||
|
||||
def _temperature_control_loop(self):
|
||||
"""温度控制循环 - 模拟真实冷却器的温度变化"""
|
||||
while self._running and self._power_on:
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 0.1: # 将判断范围从0.5改小到0.1
|
||||
self._status = "At Target Temperature"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
elif temp_diff < 0: # 需要冷却
|
||||
self._status = "Cooling"
|
||||
self._is_cooling = True
|
||||
self._is_heating = False
|
||||
# 模拟冷却过程,每秒降低0.5度
|
||||
self._current_temperature -= 0.5
|
||||
else: # 需要加热
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._is_cooling = False
|
||||
# 模拟加热过程,每秒升高0.3度
|
||||
self._current_temperature += 0.3
|
||||
|
||||
# 限制温度范围
|
||||
self._current_temperature = max(-20.0, min(80.0, self._current_temperature))
|
||||
|
||||
time.sleep(1.0) # 每秒更新一次
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_temperature_control()
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取完整状态信息"""
|
||||
return {
|
||||
"current_temperature": self._current_temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"status": self._status,
|
||||
"power_on": self._power_on,
|
||||
"is_cooling": self._is_cooling,
|
||||
"is_heating": self._is_heating
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
chiller = MockChiller()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动冷却器测试...")
|
||||
chiller.power_on_off("on")
|
||||
print(f"初始状态: {chiller.get_status_info()}")
|
||||
|
||||
# 设置目标温度为5度
|
||||
chiller.set_temperature(5.0)
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}")
|
||||
|
||||
chiller.emergency_stop()
|
||||
print("测试完成")
|
||||
0
unilabos/devices/Mock/MockChiller/__init__.py
Normal file
0
unilabos/devices/Mock/MockChiller/__init__.py
Normal file
167
unilabos/devices/Mock/MockFilter/MockFilter.py
Normal file
167
unilabos/devices/Mock/MockFilter/MockFilter.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockFilter:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
self._status: str = "Idle"
|
||||
self._is_filtering: bool = False
|
||||
self._filter_efficiency: float = 95.0 # 过滤效率百分比
|
||||
self._flow_rate: float = 0.0 # 流速 L/min
|
||||
self._pressure_drop: float = 0.0 # 压降 Pa
|
||||
self._filter_life: float = 100.0 # 滤芯寿命百分比
|
||||
self._power_on: bool = False
|
||||
|
||||
# 模拟过滤过程的线程
|
||||
self._filter_thread = None
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def is_filtering(self) -> bool:
|
||||
"""是否正在过滤"""
|
||||
return self._is_filtering
|
||||
|
||||
@property
|
||||
def filter_efficiency(self) -> float:
|
||||
"""过滤效率"""
|
||||
return self._filter_efficiency
|
||||
|
||||
@property
|
||||
def flow_rate(self) -> float:
|
||||
"""流速"""
|
||||
return self._flow_rate
|
||||
|
||||
@property
|
||||
def pressure_drop(self) -> float:
|
||||
"""压降"""
|
||||
return self._pressure_drop
|
||||
|
||||
@property
|
||||
def filter_life(self) -> float:
|
||||
"""滤芯寿命"""
|
||||
return self._filter_life
|
||||
|
||||
@property
|
||||
def power_on(self) -> bool:
|
||||
"""电源状态"""
|
||||
return self._power_on
|
||||
|
||||
def start_filtering(self, flow_rate: float = 1.0):
|
||||
"""开始过滤 - 需要在注册表添加的设备动作"""
|
||||
if not self._power_on:
|
||||
self._status = "Error: Power Off"
|
||||
return False
|
||||
|
||||
self._flow_rate = flow_rate
|
||||
self._status = "Starting Filter"
|
||||
self._start_filter_process()
|
||||
return True
|
||||
|
||||
def stop_filtering(self):
|
||||
"""停止过滤"""
|
||||
self._status = "Stopping Filter"
|
||||
self._stop_filter_process()
|
||||
self._flow_rate = 0.0
|
||||
self._is_filtering = False
|
||||
self._status = "Idle"
|
||||
return True
|
||||
|
||||
def power_on_off(self, power_state: str):
|
||||
"""开关机控制"""
|
||||
if power_state == "on":
|
||||
self._power_on = True
|
||||
self._status = "Power On"
|
||||
else:
|
||||
self._power_on = False
|
||||
self._status = "Power Off"
|
||||
self._stop_filter_process()
|
||||
self._is_filtering = False
|
||||
self._flow_rate = 0.0
|
||||
|
||||
def replace_filter(self):
|
||||
"""更换滤芯"""
|
||||
self._filter_life = 100.0
|
||||
self._filter_efficiency = 95.0
|
||||
self._status = "Filter Replaced"
|
||||
return True
|
||||
|
||||
def _start_filter_process(self):
|
||||
"""启动过滤过程线程"""
|
||||
if not self._running and self._power_on:
|
||||
self._running = True
|
||||
self._is_filtering = True
|
||||
self._filter_thread = threading.Thread(target=self._filter_loop)
|
||||
self._filter_thread.daemon = True
|
||||
self._filter_thread.start()
|
||||
|
||||
def _stop_filter_process(self):
|
||||
"""停止过滤过程"""
|
||||
self._running = False
|
||||
if self._filter_thread:
|
||||
self._filter_thread.join(timeout=1.0)
|
||||
|
||||
def _filter_loop(self):
|
||||
"""过滤过程循环 - 模拟真实过滤器的工作过程"""
|
||||
while self._running and self._power_on and self._is_filtering:
|
||||
self._status = "Filtering"
|
||||
|
||||
# 模拟滤芯磨损
|
||||
if self._filter_life > 0:
|
||||
self._filter_life -= 0.1 # 每秒减少0.1%寿命
|
||||
|
||||
# 根据滤芯寿命调整效率和压降
|
||||
life_factor = self._filter_life / 100.0
|
||||
self._filter_efficiency = 95.0 * life_factor + 50.0 * (1 - life_factor)
|
||||
self._pressure_drop = 100.0 + (200.0 * (1 - life_factor)) # 压降随磨损增加
|
||||
|
||||
# 检查滤芯是否需要更换
|
||||
if self._filter_life <= 10.0:
|
||||
self._status = "Filter Needs Replacement"
|
||||
|
||||
time.sleep(1.0) # 每秒更新一次
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_filter_process()
|
||||
self._is_filtering = False
|
||||
self._flow_rate = 0.0
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取完整状态信息"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"is_filtering": self._is_filtering,
|
||||
"filter_efficiency": self._filter_efficiency,
|
||||
"flow_rate": self._flow_rate,
|
||||
"pressure_drop": self._pressure_drop,
|
||||
"filter_life": self._filter_life,
|
||||
"power_on": self._power_on
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
filter_device = MockFilter()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动过滤器测试...")
|
||||
filter_device.power_on_off("on")
|
||||
print(f"初始状态: {filter_device.get_status_info()}")
|
||||
|
||||
# 开始过滤
|
||||
filter_device.start_filtering(2.0)
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 效率={filter_device.filter_efficiency:.1f}%, 寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}")
|
||||
|
||||
filter_device.emergency_stop()
|
||||
print("测试完成")
|
||||
0
unilabos/devices/Mock/MockFilter/__init__.py
Normal file
0
unilabos/devices/Mock/MockFilter/__init__.py
Normal file
199
unilabos/devices/Mock/MockHeater/MockHeater.py
Normal file
199
unilabos/devices/Mock/MockHeater/MockHeater.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockHeater:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
self._current_temperature: float = 25.0 # 室温开始
|
||||
self._target_temperature: float = 25.0
|
||||
self._status: str = "Idle"
|
||||
self._is_heating: bool = False
|
||||
self._power_on: bool = False
|
||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
||||
self._max_temperature: float = 300.0 # 最大加热温度
|
||||
|
||||
# 模拟加热过程的线程
|
||||
self._heating_thread = None
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""当前温度 - 会被自动识别的设备属性"""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""目标温度"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def power_on(self) -> bool:
|
||||
"""电源状态"""
|
||||
return self._power_on
|
||||
|
||||
@property
|
||||
def is_heating(self) -> bool:
|
||||
"""是否正在加热"""
|
||||
return self._is_heating
|
||||
|
||||
@property
|
||||
def heating_power(self) -> float:
|
||||
"""加热功率百分比"""
|
||||
return self._heating_power
|
||||
|
||||
@property
|
||||
def max_temperature(self) -> float:
|
||||
"""最大加热温度"""
|
||||
return self._max_temperature
|
||||
|
||||
def set_temperature(self, temperature: float):
|
||||
"""设置目标温度 - 需要在注册表添加的设备动作"""
|
||||
try:
|
||||
temperature = float(temperature)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid temperature value"
|
||||
return False
|
||||
|
||||
if not self._power_on:
|
||||
self._status = "Error: Power Off"
|
||||
return False
|
||||
|
||||
if temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)"
|
||||
return False
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Setting Temperature"
|
||||
|
||||
# 启动加热控制
|
||||
self._start_heating_control()
|
||||
return True
|
||||
|
||||
def set_heating_power(self, power: float):
|
||||
"""设置加热功率"""
|
||||
try:
|
||||
power = float(power)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid power value"
|
||||
return False
|
||||
|
||||
if not self._power_on:
|
||||
self._status = "Error: Power Off"
|
||||
return False
|
||||
|
||||
self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100%
|
||||
return True
|
||||
|
||||
def power_on_off(self, power_state: str):
|
||||
"""开关机控制,接收字符串命令 "On" 或 "Off" """
|
||||
power_state = power_state.capitalize()
|
||||
if power_state not in ["On", "Off"]:
|
||||
self._status = "Error: Invalid power state"
|
||||
return "Error"
|
||||
self._power_on = True if power_state == "On" else False
|
||||
if self._power_on:
|
||||
self._status = "Power On"
|
||||
self._start_heating_control()
|
||||
else:
|
||||
self._status = "Power Off"
|
||||
self._stop_heating_control()
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
return "Success"
|
||||
|
||||
def _start_heating_control(self):
|
||||
"""启动加热控制线程"""
|
||||
if not self._running and self._power_on:
|
||||
self._running = True
|
||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
||||
self._heating_thread.daemon = True
|
||||
self._heating_thread.start()
|
||||
|
||||
def _stop_heating_control(self):
|
||||
"""停止加热控制"""
|
||||
self._running = False
|
||||
if self._heating_thread:
|
||||
self._heating_thread.join(timeout=1.0)
|
||||
|
||||
def _heating_control_loop(self):
|
||||
"""加热控制循环 - 模拟真实加热器的温度变化"""
|
||||
while self._running and self._power_on:
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 0.5: # 温度接近目标值
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
self._heating_power = 10.0 # 维持温度的最小功率
|
||||
elif temp_diff > 0: # 需要加热
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
# 根据温差调整加热功率
|
||||
if temp_diff > 50:
|
||||
self._heating_power = 100.0
|
||||
elif temp_diff > 20:
|
||||
self._heating_power = 80.0
|
||||
elif temp_diff > 10:
|
||||
self._heating_power = 60.0
|
||||
else:
|
||||
self._heating_power = 40.0
|
||||
|
||||
# 模拟加热过程,加热速度与功率成正比
|
||||
heating_rate = self._heating_power / 100.0 * 2.0 # 最大每秒升温2度
|
||||
self._current_temperature += heating_rate
|
||||
else: # 目标温度低于当前温度,自然冷却
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
# 模拟自然冷却,每秒降低0.2度
|
||||
self._current_temperature -= 0.2
|
||||
|
||||
# 限制温度范围
|
||||
self._current_temperature = max(20.0, min(self._max_temperature, self._current_temperature))
|
||||
|
||||
time.sleep(1.0) # 每秒更新一次
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_heating_control()
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取完整状态信息"""
|
||||
return {
|
||||
"current_temperature": self._current_temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"status": self._status,
|
||||
"power_on": self._power_on,
|
||||
"is_heating": self._is_heating,
|
||||
"heating_power": self._heating_power,
|
||||
"max_temperature": self._max_temperature
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
heater = MockHeater()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动加热器测试...")
|
||||
heater.power_on_off(True)
|
||||
print(f"初始状态: {heater.get_status_info()}")
|
||||
|
||||
# 设置目标温度为80度
|
||||
heater.set_temperature(80.0)
|
||||
|
||||
# 模拟运行15秒
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 当前温度={heater.current_temperature:.1f}°C, 功率={heater.heating_power:.1f}%, 状态={heater.status}")
|
||||
|
||||
heater.emergency_stop()
|
||||
print("测试完成")
|
||||
0
unilabos/devices/Mock/MockHeater/__init__.py
Normal file
0
unilabos/devices/Mock/MockHeater/__init__.py
Normal file
414
unilabos/devices/Mock/MockPump/MockPump.py
Normal file
414
unilabos/devices/Mock/MockPump/MockPump.py
Normal file
@@ -0,0 +1,414 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockPump:
|
||||
"""
|
||||
模拟泵设备类
|
||||
|
||||
这个类模拟了一个实验室泵设备的行为,包括流量控制、压力监测、
|
||||
运行状态管理等功能。所有的控制参数都使用字符串类型以提供更好的
|
||||
可读性和扩展性。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockPump实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
self._power_state: str = "Off" # 电源状态:On, Off
|
||||
self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused
|
||||
|
||||
# 流量相关属性
|
||||
self._flow_rate: float = 0.0 # 当前流速 (mL/min)
|
||||
self._target_flow_rate: float = 0.0 # 目标流速 (mL/min)
|
||||
self._max_flow_rate: float = 100.0 # 最大流速 (mL/min)
|
||||
self._total_volume: float = 0.0 # 累计流量 (mL)
|
||||
|
||||
# 压力相关属性
|
||||
self._pressure: float = 0.0 # 当前压力 (bar)
|
||||
self._max_pressure: float = 10.0 # 最大压力 (bar)
|
||||
|
||||
# 方向控制属性
|
||||
self._direction: str = "Forward" # 泵方向:Forward, Reverse
|
||||
|
||||
# 运行控制线程
|
||||
self._pump_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def power_state(self) -> str:
|
||||
return self._power_state
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def flow_rate(self) -> float:
|
||||
return self._flow_rate
|
||||
|
||||
@property
|
||||
def target_flow_rate(self) -> float:
|
||||
return self._target_flow_rate
|
||||
|
||||
@property
|
||||
def pressure(self) -> float:
|
||||
return self._pressure
|
||||
|
||||
@property
|
||||
def total_volume(self) -> float:
|
||||
return self._total_volume
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def max_flow_rate(self) -> float:
|
||||
return self._max_flow_rate
|
||||
|
||||
@property
|
||||
def max_pressure(self) -> float:
|
||||
return self._max_pressure
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def power_control(self, power_state: str = "On") -> str:
|
||||
"""
|
||||
电源控制方法
|
||||
|
||||
Args:
|
||||
power_state (str): 电源状态,可选值:"On", "Off"
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if power_state not in ["On", "Off"]:
|
||||
self._status = "Error: Invalid power state"
|
||||
return "Error"
|
||||
|
||||
self._power_state = power_state
|
||||
|
||||
if power_state == "On":
|
||||
self._status = "Power On"
|
||||
else:
|
||||
self._status = "Power Off"
|
||||
# 关机时停止所有运行
|
||||
self.stop_pump()
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_flow_rate(self, flow_rate: float) -> str:
|
||||
"""
|
||||
设置目标流速
|
||||
|
||||
Args:
|
||||
flow_rate (float): 目标流速 (mL/min)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
try:
|
||||
flow_rate = float(flow_rate)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid flow rate"
|
||||
return "Error"
|
||||
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
if flow_rate < 0 or flow_rate > self._max_flow_rate:
|
||||
self._status = f"Error: Flow rate out of range (0-{self._max_flow_rate})"
|
||||
return "Error"
|
||||
|
||||
self._target_flow_rate = flow_rate
|
||||
self._status = "Setting Flow Rate"
|
||||
|
||||
# 如果设置了非零流速,自动启动泵
|
||||
if flow_rate > 0:
|
||||
# 自动切换泵状态为 "Running" 以触发操作循环
|
||||
self._pump_state = "Running"
|
||||
self._start_pump_operation()
|
||||
else:
|
||||
self.stop_pump()
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_pump(self) -> str:
|
||||
"""
|
||||
启动泵运行
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
if self._target_flow_rate <= 0:
|
||||
self._status = "Error: No target flow rate set"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Starting Pump"
|
||||
self._start_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def stop_pump(self) -> str:
|
||||
"""
|
||||
停止泵运行
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._pump_state = "Stopped"
|
||||
self._status = "Stopping Pump"
|
||||
self._stop_pump_operation()
|
||||
self._flow_rate = 0.0
|
||||
self._pressure = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
def pause_pump(self) -> str:
|
||||
"""
|
||||
暂停泵运行
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._pump_state != "Running":
|
||||
self._status = "Error: Pump not running"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Paused"
|
||||
self._status = "Pump Paused"
|
||||
self._stop_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def resume_pump(self) -> str:
|
||||
"""
|
||||
恢复泵运行
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._pump_state != "Paused":
|
||||
self._status = "Error: Pump not paused"
|
||||
return "Error"
|
||||
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Resuming Pump"
|
||||
self._start_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_direction(self, direction: str = "Forward") -> str:
|
||||
"""
|
||||
设置泵方向
|
||||
|
||||
Args:
|
||||
direction (str): 泵方向,可选值:"Forward", "Reverse"
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if direction not in ["Forward", "Reverse"]:
|
||||
self._status = "Error: Invalid direction"
|
||||
return "Error"
|
||||
|
||||
# 如果泵正在运行,需要先停止
|
||||
was_running = self._pump_state == "Running"
|
||||
if was_running:
|
||||
self.stop_pump()
|
||||
time.sleep(0.5) # 等待停止完成
|
||||
|
||||
self._direction = direction
|
||||
self._status = f"Direction set to {direction}"
|
||||
|
||||
# 如果之前在运行,重新启动
|
||||
if was_running:
|
||||
self.start_pump()
|
||||
|
||||
return "Success"
|
||||
|
||||
def reset_volume_counter(self) -> str:
|
||||
"""
|
||||
重置累计流量计数器
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._total_volume = 0.0
|
||||
self._status = "Volume counter reset"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_pump_operation()
|
||||
self._flow_rate = 0.0
|
||||
self._pressure = 0.0
|
||||
self._target_flow_rate = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_pump_operation(self):
|
||||
"""
|
||||
启动泵运行线程
|
||||
|
||||
这个方法启动一个后台线程来模拟泵的实际运行过程,
|
||||
包括流速控制、压力变化和累计流量计算。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if not self._running and self._power_state == "On":
|
||||
self._running = True
|
||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
||||
self._pump_thread.daemon = True
|
||||
self._pump_thread.start()
|
||||
|
||||
def _stop_pump_operation(self):
|
||||
"""
|
||||
停止泵运行线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._pump_thread and self._pump_thread.is_alive():
|
||||
self._pump_thread.join(timeout=2.0)
|
||||
|
||||
def _pump_operation_loop(self):
|
||||
"""
|
||||
泵运行主循环
|
||||
|
||||
这个方法在后台线程中运行,模拟真实泵的工作过程:
|
||||
1. 逐步调整流速到目标值
|
||||
2. 根据流速计算压力
|
||||
3. 累计流量统计
|
||||
4. 状态更新
|
||||
"""
|
||||
while self._running and self._power_state == "On" and self._pump_state == "Running":
|
||||
try:
|
||||
# 模拟流速调节过程(逐步接近目标流速)
|
||||
flow_diff = self._target_flow_rate - self._flow_rate
|
||||
|
||||
if abs(flow_diff) < 0.1: # 流速接近目标值
|
||||
self._flow_rate = self._target_flow_rate
|
||||
self._status = "At Target Flow Rate"
|
||||
else:
|
||||
# 模拟流速调节,每秒调整10%的差值
|
||||
adjustment = flow_diff * 0.1
|
||||
self._flow_rate += adjustment
|
||||
self._status = "Adjusting Flow Rate"
|
||||
|
||||
# 确保流速在合理范围内
|
||||
self._flow_rate = max(0.0, min(self._max_flow_rate, self._flow_rate))
|
||||
|
||||
# 模拟压力变化(压力与流速成正比,加上一些随机波动)
|
||||
base_pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure
|
||||
pressure_variation = 0.1 * base_pressure * (time.time() % 1.0 - 0.5) # ±5%波动
|
||||
self._pressure = max(0.0, base_pressure + pressure_variation)
|
||||
|
||||
# 累计流量计算(每秒更新)
|
||||
if self._flow_rate > 0:
|
||||
volume_increment = self._flow_rate / 60.0 # 转换为mL/s
|
||||
if self._direction == "Reverse":
|
||||
volume_increment = -volume_increment
|
||||
self._total_volume += volume_increment
|
||||
|
||||
# 压力保护检查
|
||||
if self._pressure > self._max_pressure * 0.95:
|
||||
self._status = "Warning: High Pressure"
|
||||
|
||||
# 等待1秒后继续下一次循环
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
self._status = f"Error in pump operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束时的清理工作
|
||||
if self._pump_state == "Running":
|
||||
self._status = "Idle"
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"power_state": self._power_state,
|
||||
"pump_state": self._pump_state,
|
||||
"flow_rate": self._flow_rate,
|
||||
"target_flow_rate": self._target_flow_rate,
|
||||
"pressure": self._pressure,
|
||||
"total_volume": self._total_volume,
|
||||
"direction": self._direction,
|
||||
"max_flow_rate": self._max_flow_rate,
|
||||
"max_pressure": self._max_pressure
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
pump = MockPump()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动泵设备测试...")
|
||||
pump.power_control("On")
|
||||
print(f"初始状态: {pump.get_status_info()}")
|
||||
|
||||
# 设置流速并启动
|
||||
pump.set_flow_rate(50.0)
|
||||
pump.start_pump()
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}")
|
||||
|
||||
# 测试方向切换
|
||||
print("切换泵方向...")
|
||||
pump.set_direction("Reverse")
|
||||
|
||||
# 继续运行5秒
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
print(f"反向第{i+1}秒: 累计流量={pump.total_volume:.1f}mL, 方向={pump.direction}")
|
||||
|
||||
pump.emergency_stop()
|
||||
print("测试完成")
|
||||
|
||||
0
unilabos/devices/Mock/MockPump/__init__.py
Normal file
0
unilabos/devices/Mock/MockPump/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user