mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-09 00:15:10 +00:00
Compare commits
25 Commits
v0.9.0
...
fe492bbca9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe492bbca9 | ||
|
|
8c96be4229 | ||
|
|
f947658bbd | ||
|
|
6d56466897 | ||
|
|
eac2c27e99 | ||
|
|
f2f9b45aa6 | ||
|
|
0e2fdb5695 | ||
|
|
ca15173717 | ||
|
|
94e79418b9 | ||
|
|
02b4750637 | ||
|
|
4e636b91b4 | ||
|
|
3af1964328 | ||
|
|
f661049823 | ||
|
|
a79c26b9a4 | ||
|
|
8cf51ee8d3 | ||
|
|
8c81dab7e1 | ||
|
|
ff76bb1a76 | ||
|
|
6f69df440c | ||
|
|
b420d1fa8e | ||
|
|
767e0fcdee | ||
|
|
84944396e9 | ||
|
|
bfcb214b53 | ||
|
|
ec4e6c6cfd | ||
|
|
53b6457a88 | ||
|
|
133dbf77bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
||||
.vscode
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
service
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
69
README.md
69
README.md
@@ -4,83 +4,86 @@
|
||||
|
||||
# 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 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
Uni-Lab Operating System is a platform for laboratory automation, designed to connect and control various experimental equipment, enabling automation and standardization of experimental workflows.
|
||||
|
||||
## 核心特点
|
||||
## Key Features
|
||||
|
||||
- 多设备集成管理
|
||||
- 自动化实验流程
|
||||
- 云端连接能力
|
||||
- 灵活的配置系统
|
||||
- 支持多种实验协议
|
||||
- Multi-device integration management
|
||||
- Automated experimental workflows
|
||||
- Cloud connectivity capabilities
|
||||
- Flexible configuration system
|
||||
- Support for multiple experimental protocols
|
||||
|
||||
## 文档
|
||||
## Documentation
|
||||
|
||||
详细文档可在以下位置找到:
|
||||
Detailed documentation can be found at:
|
||||
|
||||
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
|
||||
## 快速开始
|
||||
## Quick Start
|
||||
|
||||
1. 配置Conda环境
|
||||
1. Configure Conda Environment
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||
|
||||
```bash
|
||||
# 创建新环境
|
||||
# Create new environment
|
||||
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 环境名
|
||||
# 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
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2
|
||||
# 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
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
cd plr_repo
|
||||
pip install .[opentrons]
|
||||
```
|
||||
|
||||
2. 安装 Uni-Lab-OS:
|
||||
2. Install Uni-Lab-OS:
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 安装 Uni-Lab-OS
|
||||
# Install Uni-Lab-OS
|
||||
pip install .
|
||||
```
|
||||
|
||||
3. 启动 Uni-Lab 系统:
|
||||
3. Start Uni-Lab System:
|
||||
|
||||
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
|
||||
## 消息格式
|
||||
## Message Format
|
||||
|
||||
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
||||
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) page.
|
||||
|
||||
## 许可证
|
||||
## License
|
||||
|
||||
此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
|
||||
This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 项目统计
|
||||
## Project Statistics
|
||||
|
||||
### Stars 趋势
|
||||
### Stars Trend
|
||||
|
||||
<a href="https://star-history.com/#dptech-corp/Uni-Lab-OS&Date">
|
||||
<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)
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.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.9.0"
|
||||
version: "0.9.1"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.0',
|
||||
version='0.9.1',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
使用plr_test.json启动,将Well加入Plate中
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/host_node/add_resource_from_outer unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
||||
ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
||||
```
|
||||
296
test/experiments/mock_devices/mock_all.json
Normal file
296
test/experiments/mock_devices/mock_all.json
Normal file
@@ -0,0 +1,296 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockChiller1",
|
||||
"name": "模拟冷却器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_chiller",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"is_cooling": false,
|
||||
"is_heating": false,
|
||||
"vessel": "",
|
||||
"purpose": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockFilter1",
|
||||
"name": "模拟过滤器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_filter",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"is_filtering": false,
|
||||
"flow_rate": 0.0,
|
||||
"filter_life": 100.0,
|
||||
"vessel": "",
|
||||
"filtrate_vessel": "",
|
||||
"filtered_volume": 0.0,
|
||||
"target_volume": 0.0,
|
||||
"progress": 0.0,
|
||||
"stir": false,
|
||||
"stir_speed": 0.0,
|
||||
"temperature": 25.0,
|
||||
"continue_heatchill": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockHeater1",
|
||||
"name": "模拟加热器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_heater",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"is_heating": false,
|
||||
"heating_power": 0.0,
|
||||
"max_temperature": 300.0,
|
||||
"vessel": "Unknown",
|
||||
"purpose": "Unknown",
|
||||
"stir": false,
|
||||
"stir_speed": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"current_device": "MockPump1",
|
||||
"pump_state": "Stopped",
|
||||
"flow_rate": 0.0,
|
||||
"target_flow_rate": 0.0,
|
||||
"pressure": 0.0,
|
||||
"total_volume": 0.0,
|
||||
"max_flow_rate": 100.0,
|
||||
"max_pressure": 10.0,
|
||||
"from_vessel": "",
|
||||
"to_vessel": "",
|
||||
"transfer_volume": 0.0,
|
||||
"amount": "",
|
||||
"transfer_time": 0.0,
|
||||
"is_viscous": false,
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"is_solid": false,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockRotavap1",
|
||||
"name": "模拟旋转蒸发器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_rotavap",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockSeparator1",
|
||||
"name": "模拟分离器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_separator",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"settling_time": 0.0,
|
||||
"valve_state": "Closed",
|
||||
"shake_time": 0.0,
|
||||
"shake_status": "Not Shaking",
|
||||
"current_device": "MockSeparator1",
|
||||
"purpose": "",
|
||||
"product_phase": "",
|
||||
"from_vessel": "",
|
||||
"separation_vessel": "",
|
||||
"to_vessel": "",
|
||||
"waste_phase_to_vessel": "",
|
||||
"solvent": "",
|
||||
"solvent_volume": 0.0,
|
||||
"through": "",
|
||||
"repeats": 1,
|
||||
"stir_time": 0.0,
|
||||
"stir_speed": 0.0,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockSolenoidValve1",
|
||||
"name": "模拟电磁阀",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_solenoid_valve",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"valve_status": "Closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockStirrer1NEW",
|
||||
"name": "模拟搅拌器(new)",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_stirrer_new",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"vessel": "",
|
||||
"purpose": "",
|
||||
"stir_speed": 0.0,
|
||||
"target_stir_speed": 0.0,
|
||||
"stir_state": "Stopped",
|
||||
"stir_time": 0.0,
|
||||
"settling_time": 0.0,
|
||||
"progress": 0.0,
|
||||
"max_stir_speed": 2000.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockStirrer1",
|
||||
"name": "模拟搅拌器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_stirrer",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockVacuum1",
|
||||
"name": "模拟真空泵",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_vacuum",
|
||||
"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": []
|
||||
}
|
||||
30
test/experiments/mock_devices/mock_chiller.json
Normal file
30
test/experiments/mock_devices/mock_chiller.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockChiller1",
|
||||
"name": "模拟冷却器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_chiller",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"is_cooling": false,
|
||||
"is_heating": false,
|
||||
"vessel": "",
|
||||
"purpose": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
36
test/experiments/mock_devices/mock_filter.json
Normal file
36
test/experiments/mock_devices/mock_filter.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockFilter1",
|
||||
"name": "模拟过滤器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_filter",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"is_filtering": false,
|
||||
"flow_rate": 0.0,
|
||||
"filter_life": 100.0,
|
||||
"vessel": "",
|
||||
"filtrate_vessel": "",
|
||||
"filtered_volume": 0.0,
|
||||
"target_volume": 0.0,
|
||||
"progress": 0.0,
|
||||
"stir": false,
|
||||
"stir_speed": 0.0,
|
||||
"temperature": 25.0,
|
||||
"continue_heatchill": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
33
test/experiments/mock_devices/mock_heater.json
Normal file
33
test/experiments/mock_devices/mock_heater.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockHeater1",
|
||||
"name": "模拟加热器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_heater",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"current_temperature": 25.0,
|
||||
"target_temperature": 25.0,
|
||||
"status": "Idle",
|
||||
"is_heating": false,
|
||||
"heating_power": 0.0,
|
||||
"max_temperature": 300.0,
|
||||
"vessel": "Unknown",
|
||||
"purpose": "Unknown",
|
||||
"stir": false,
|
||||
"stir_speed": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
44
test/experiments/mock_devices/mock_pump.json
Normal file
44
test/experiments/mock_devices/mock_pump.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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",
|
||||
"current_device": "MockPump1",
|
||||
"pump_state": "Stopped",
|
||||
"flow_rate": 0.0,
|
||||
"target_flow_rate": 0.0,
|
||||
"pressure": 0.0,
|
||||
"total_volume": 0.0,
|
||||
"max_flow_rate": 100.0,
|
||||
"max_pressure": 10.0,
|
||||
"from_vessel": "",
|
||||
"to_vessel": "",
|
||||
"transfer_volume": 0.0,
|
||||
"amount": "",
|
||||
"transfer_time": 0.0,
|
||||
"is_viscous": false,
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"is_solid": false,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
33
test/experiments/mock_devices/mock_rotavap.json
Normal file
33
test/experiments/mock_devices/mock_rotavap.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockRotavap1",
|
||||
"name": "模拟旋转蒸发器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_rotavap",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"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": []
|
||||
}
|
||||
43
test/experiments/mock_devices/mock_separator.json
Normal file
43
test/experiments/mock_devices/mock_separator.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockSeparator1",
|
||||
"name": "模拟分离器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_separator",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"settling_time": 0.0,
|
||||
"valve_state": "Closed",
|
||||
"shake_time": 0.0,
|
||||
"shake_status": "Not Shaking",
|
||||
"current_device": "MockSeparator1",
|
||||
"purpose": "",
|
||||
"product_phase": "",
|
||||
"from_vessel": "",
|
||||
"separation_vessel": "",
|
||||
"to_vessel": "",
|
||||
"waste_phase_to_vessel": "",
|
||||
"solvent": "",
|
||||
"solvent_volume": 0.0,
|
||||
"through": "",
|
||||
"repeats": 1,
|
||||
"stir_time": 0.0,
|
||||
"stir_speed": 0.0,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
25
test/experiments/mock_devices/mock_solenoid_valve.json
Normal file
25
test/experiments/mock_devices/mock_solenoid_valve.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockSolenoidValve1",
|
||||
"name": "模拟电磁阀",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_solenoid_valve",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"valve_status": "Closed"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
33
test/experiments/mock_devices/mock_stirrer.json
Normal file
33
test/experiments/mock_devices/mock_stirrer.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockStirrer1",
|
||||
"name": "模拟搅拌器",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_stirrer",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"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": []
|
||||
}
|
||||
33
test/experiments/mock_devices/mock_stirrer_new.json
Normal file
33
test/experiments/mock_devices/mock_stirrer_new.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockStirrer1COPY",
|
||||
"name": "模拟搅拌器(Copy)",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_stirrer_new",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "MOCK"
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"vessel": "",
|
||||
"purpose": "",
|
||||
"stir_speed": 0.0,
|
||||
"target_stir_speed": 0.0,
|
||||
"stir_state": "Stopped",
|
||||
"stir_time": 0.0,
|
||||
"settling_time": 0.0,
|
||||
"progress": 0.0,
|
||||
"max_stir_speed": 2000.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
31
test/experiments/mock_devices/mock_vacuum.json
Normal file
31
test/experiments/mock_devices/mock_vacuum.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "MockVacuum1",
|
||||
"name": "模拟真空泵",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_vacuum",
|
||||
"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": []
|
||||
}
|
||||
@@ -56,6 +56,8 @@ 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
|
||||
|
||||
@@ -56,6 +56,8 @@ 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
|
||||
|
||||
@@ -58,6 +58,8 @@ 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
|
||||
|
||||
@@ -56,6 +56,8 @@ 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
|
||||
|
||||
@@ -31,6 +31,6 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||
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)
|
||||
# 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)
|
||||
|
||||
@@ -18,7 +18,6 @@ if unilabos_dir not in sys.path:
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.device_mesh.resource_visalization import ResourceVisualization
|
||||
|
||||
|
||||
def parse_args():
|
||||
@@ -188,11 +187,12 @@ def main():
|
||||
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"]
|
||||
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
|
||||
))
|
||||
server_thread.start()
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
@@ -201,10 +201,10 @@ def main():
|
||||
time.sleep(1)
|
||||
else:
|
||||
start_backend(**args_dict)
|
||||
start_server(open_browser=not args_dict["disable_browser"])
|
||||
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||
else:
|
||||
start_backend(**args_dict)
|
||||
start_server(open_browser=not args_dict["disable_browser"])
|
||||
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -12,7 +12,7 @@ import tempfile
|
||||
import os
|
||||
|
||||
from unilabos.config.config import MQConfig
|
||||
from unilabos.app.controler import devices, job_add
|
||||
from unilabos.app.controler import job_add
|
||||
from unilabos.app.model import JobAddReq
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
@@ -26,6 +26,7 @@ class MQTTClient:
|
||||
def __init__(self):
|
||||
self.mqtt_disable = not MQConfig.lab_id
|
||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||
self._setup_callbacks()
|
||||
|
||||
@@ -42,20 +43,14 @@ class MQTTClient:
|
||||
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) -> None:
|
||||
logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
# 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:
|
||||
@@ -65,6 +60,14 @@ class MQTTClient:
|
||||
job_req = JobAddReq.model_validate(payload_json)
|
||||
data = job_add(job_req)
|
||||
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}")
|
||||
@@ -181,6 +184,28 @@ class MQTTClient:
|
||||
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()
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ def get_host_node_info() -> Dict[str, Any]:
|
||||
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
|
||||
|
||||
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
0
unilabos/devices/laiyu_add_solid/__init__.py
Normal file
@@ -14,7 +14,7 @@ from pylabrobot.resources import (
|
||||
Well
|
||||
)
|
||||
|
||||
class DPLiquidHandler(LiquidHandler):
|
||||
class LiquidHandlerAbstract(LiquidHandler):
|
||||
"""Extended LiquidHandler with additional operations."""
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
0
unilabos/devices/mock/__init__.py
Normal file
0
unilabos/devices/mock/__init__.py
Normal file
177
unilabos/devices/mock/mock_chiller.py
Normal file
177
unilabos/devices/mock/mock_chiller.py
Normal file
@@ -0,0 +1,177 @@
|
||||
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._vessel = "Unknown"
|
||||
self._purpose = "Unknown"
|
||||
|
||||
# 模拟温度变化的线程
|
||||
self._temperature_thread = None
|
||||
self._running = True
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
@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 is_cooling(self) -> bool:
|
||||
"""是否正在冷却"""
|
||||
return self._is_cooling
|
||||
|
||||
@property
|
||||
def is_heating(self) -> bool:
|
||||
"""是否正在加热"""
|
||||
return self._is_heating
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
"""当前操作的容器名称"""
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
"""当前操作目的"""
|
||||
return self._purpose
|
||||
|
||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str):
|
||||
"""设置目标温度并记录容器和目的"""
|
||||
self._vessel = str(vessel)
|
||||
self._purpose = str(purpose)
|
||||
self._target_temperature = float(temp)
|
||||
|
||||
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 heat_chill_stop(self, vessel: str):
|
||||
"""停止加热/制冷"""
|
||||
if vessel != self._vessel:
|
||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
||||
|
||||
# 停止温度控制线程,锁定当前温度
|
||||
self._stop_temperature_control()
|
||||
|
||||
# 更新状态
|
||||
self._status = "Stopped"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
|
||||
# 重新启动线程但保持温度
|
||||
self._running = True
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def _start_temperature_control(self):
|
||||
"""启动温度控制线程"""
|
||||
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:
|
||||
# 如果状态是 Stopped,不改变温度
|
||||
if self._status == "Stopped":
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 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
|
||||
self._current_temperature -= 0.5
|
||||
else:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._is_cooling = False
|
||||
self._current_temperature += 0.3
|
||||
|
||||
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,
|
||||
"is_cooling": self._is_cooling,
|
||||
"is_heating": self._is_heating,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
chiller = MockChiller()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动冷却器测试...")
|
||||
print(f"初始状态: {chiller.get_status_info()}")
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}")
|
||||
|
||||
chiller.emergency_stop()
|
||||
print("测试完成")
|
||||
235
unilabos/devices/mock/mock_filter.py
Normal file
235
unilabos/devices/mock/mock_filter.py
Normal file
@@ -0,0 +1,235 @@
|
||||
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._flow_rate: float = 1.0 # 流速(L/min)
|
||||
self._pressure_drop: float = 0.0 # 压降(Pa)
|
||||
self._filter_life: float = 100.0 # 滤芯寿命(%)
|
||||
|
||||
# 过滤操作参数
|
||||
self._vessel: str = "" # 源容器
|
||||
self._filtrate_vessel: str = "" # 目标容器
|
||||
self._stir: bool = False # 是否搅拌
|
||||
self._stir_speed: float = 0.0 # 搅拌速度
|
||||
self._temperature: float = 25.0 # 温度(℃)
|
||||
self._continue_heatchill: bool = False # 是否继续加热/制冷
|
||||
self._target_volume: float = 0.0 # 目标过滤体积(L)
|
||||
self._filtered_volume: float = 0.0 # 已过滤体积(L)
|
||||
self._progress: float = 0.0 # 过滤进度(%)
|
||||
|
||||
# 线程控制
|
||||
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 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
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def filtrate_vessel(self) -> str:
|
||||
return self._filtrate_vessel
|
||||
|
||||
@property
|
||||
def filtered_volume(self) -> float:
|
||||
return self._filtered_volume
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self._progress
|
||||
|
||||
@property
|
||||
def stir(self) -> bool:
|
||||
return self._stir
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def continue_heatchill(self) -> bool:
|
||||
return self._continue_heatchill
|
||||
|
||||
@property
|
||||
def target_volume(self) -> float:
|
||||
return self._target_volume
|
||||
|
||||
def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict:
|
||||
"""新的过滤操作"""
|
||||
# 停止任何正在进行的过滤
|
||||
if self._is_filtering:
|
||||
self.stop_filtering()
|
||||
# 验证参数
|
||||
if volume <= 0:
|
||||
return {"success": False, "message": "Target volume must be greater than 0"}
|
||||
# 设置新的过滤参数
|
||||
self._vessel = vessel
|
||||
self._filtrate_vessel = filtrate_vessel
|
||||
self._stir = stir
|
||||
self._stir_speed = stir_speed
|
||||
self._temperature = temp
|
||||
self._continue_heatchill = continue_heatchill
|
||||
self._target_volume = volume
|
||||
# 重置过滤状态
|
||||
self._filtered_volume = 0.0
|
||||
self._progress = 0.0
|
||||
self._status = "Starting Filter"
|
||||
# 启动过滤过程
|
||||
self._flow_rate = 1.0 # 设置默认流速
|
||||
self._start_filter_process()
|
||||
|
||||
return {"success": True, "message": "Filter started"}
|
||||
|
||||
def stop_filtering(self):
|
||||
"""停止过滤"""
|
||||
self._status = "Stopping Filter"
|
||||
self._stop_filter_process()
|
||||
self._flow_rate = 0.0
|
||||
self._is_filtering = False
|
||||
self._status = "Stopped"
|
||||
return True
|
||||
|
||||
def replace_filter(self):
|
||||
"""更换滤芯"""
|
||||
self._filter_life = 100.0
|
||||
self._status = "Filter Replaced"
|
||||
return True
|
||||
|
||||
def _start_filter_process(self):
|
||||
"""启动过滤过程线程"""
|
||||
if not self._running:
|
||||
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):
|
||||
"""过滤进程主循环"""
|
||||
update_interval = 1.0 # 更新间隔(秒)
|
||||
|
||||
while self._running and self._is_filtering:
|
||||
try:
|
||||
self._status = "Filtering"
|
||||
|
||||
# 计算这一秒过滤的体积 (L/min -> L/s)
|
||||
volume_increment = (self._flow_rate / 60.0) * update_interval
|
||||
|
||||
# 更新已过滤体积
|
||||
self._filtered_volume += volume_increment
|
||||
|
||||
# 更新进度 (避免除零错误)
|
||||
if self._target_volume > 0:
|
||||
self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0)
|
||||
|
||||
# 更新滤芯寿命 (每过滤1L减少0.5%寿命)
|
||||
self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5))
|
||||
|
||||
# 更新压降 (根据滤芯寿命和流速动态计算)
|
||||
life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子
|
||||
flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速)
|
||||
base_pressure = 100.0 # 基础压降
|
||||
# 压降随滤芯寿命降低而增加,随流速增加而增加
|
||||
self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor
|
||||
|
||||
# 检查是否完成目标体积
|
||||
if self._target_volume > 0 and self._filtered_volume >= self._target_volume:
|
||||
self._status = "Completed"
|
||||
self._progress = 100.0
|
||||
self.stop_filtering()
|
||||
break
|
||||
|
||||
# 检查滤芯寿命
|
||||
if self._filter_life <= 10.0:
|
||||
self._status = "Filter Needs Replacement"
|
||||
|
||||
time.sleep(update_interval)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in filter loop: {e}")
|
||||
self.emergency_stop()
|
||||
break
|
||||
|
||||
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,
|
||||
"flow_rate": self._flow_rate,
|
||||
"pressure_drop": self._pressure_drop,
|
||||
"filter_life": self._filter_life,
|
||||
"vessel": self._vessel,
|
||||
"filtrate_vessel": self._filtrate_vessel,
|
||||
"filtered_volume": self._filtered_volume,
|
||||
"target_volume": self._target_volume,
|
||||
"progress": self._progress,
|
||||
"temperature": self._temperature,
|
||||
"stir": self._stir,
|
||||
"stir_speed": self._stir_speed
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
filter_device = MockFilter()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动过滤器测试...")
|
||||
print(f"初始状态: {filter_device.get_status_info()}")
|
||||
|
||||
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: "
|
||||
f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}"
|
||||
)
|
||||
|
||||
filter_device.emergency_stop()
|
||||
print("测试完成")
|
||||
247
unilabos/devices/mock/mock_heater.py
Normal file
247
unilabos/devices/mock/mock_heater.py
Normal file
@@ -0,0 +1,247 @@
|
||||
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._heating_power: float = 0.0 # 加热功率百分比 0-100
|
||||
self._max_temperature: float = 300.0 # 最大加热温度
|
||||
|
||||
# 新增加的属性
|
||||
self._vessel: str = "Unknown"
|
||||
self._purpose: str = "Unknown"
|
||||
self._stir: bool = False
|
||||
self._stir_speed: float = 0.0
|
||||
|
||||
# 模拟加热过程的线程
|
||||
self._heating_thread = None
|
||||
self._running = True
|
||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
||||
self._heating_thread.daemon = True
|
||||
self._heating_thread.start()
|
||||
|
||||
@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 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
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
"""当前操作的容器名称"""
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
"""操作目的"""
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def stir(self) -> bool:
|
||||
"""是否搅拌"""
|
||||
return self._stir
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
"""搅拌速度"""
|
||||
return self._stir_speed
|
||||
|
||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict:
|
||||
"""开始加热/制冷过程"""
|
||||
self._vessel = str(vessel)
|
||||
self._purpose = str(purpose)
|
||||
self._target_temperature = float(temp)
|
||||
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
elif diff > 0:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def heat_chill_stop(self, vessel: str) -> dict:
|
||||
"""停止加热/制冷"""
|
||||
if vessel != self._vessel:
|
||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
||||
|
||||
self._status = "Stopped"
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def heat_chill(self, vessel: str, temp: float, time: float,
|
||||
stir: bool = False, stir_speed: float = 0.0,
|
||||
purpose: str = "Unknown") -> dict:
|
||||
"""完整的加热/制冷控制"""
|
||||
self._vessel = str(vessel)
|
||||
self._target_temperature = float(temp)
|
||||
self._purpose = str(purpose)
|
||||
self._stir = stir
|
||||
self._stir_speed = stir_speed
|
||||
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
elif diff > 0:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def set_temperature(self, temperature: float):
|
||||
"""设置目标温度 - 需要在注册表添加的设备动作"""
|
||||
try:
|
||||
temperature = float(temperature)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid temperature value"
|
||||
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
|
||||
|
||||
self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100%
|
||||
return True
|
||||
|
||||
def _start_heating_control(self):
|
||||
"""启动加热控制线程"""
|
||||
if not self._running:
|
||||
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:
|
||||
# 如果状态是 Stopped,不改变温度
|
||||
if self._status == "Stopped":
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 0.1:
|
||||
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
|
||||
self._heating_power = min(100.0, abs(temp_diff) * 2)
|
||||
self._current_temperature += 0.5
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
self._current_temperature -= 0.2
|
||||
|
||||
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,
|
||||
"is_heating": self._is_heating,
|
||||
"heating_power": self._heating_power,
|
||||
"max_temperature": self._max_temperature,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
"stir": self._stir,
|
||||
"stir_speed": self._stir_speed
|
||||
}
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
heater = MockHeater()
|
||||
|
||||
print("启动加热器测试...")
|
||||
print(f"初始状态: {heater.get_status_info()}")
|
||||
|
||||
# 设置目标温度为80度
|
||||
heater.set_temperature(80.0)
|
||||
|
||||
# 模拟运行15秒
|
||||
try:
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
status = heater.get_status_info()
|
||||
print(
|
||||
f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | "
|
||||
f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}",
|
||||
end=""
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
heater.emergency_stop()
|
||||
print("\n测试被手动停止")
|
||||
|
||||
print("\n测试完成")
|
||||
360
unilabos/devices/mock/mock_pump.py
Normal file
360
unilabos/devices/mock/mock_pump.py
Normal file
@@ -0,0 +1,360 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockPump:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._current_device = "MockPump1" # 设备标识符
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
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._pump_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 新增 PumpTransfer 相关属性
|
||||
self._from_vessel: str = ""
|
||||
self._to_vessel: str = ""
|
||||
self._transfer_volume: float = 0.0
|
||||
self._amount: str = ""
|
||||
self._transfer_time: float = 0.0
|
||||
self._is_viscous: bool = False
|
||||
self._rinsing_solvent: str = ""
|
||||
self._rinsing_volume: float = 0.0
|
||||
self._rinsing_repeats: int = 0
|
||||
self._is_solid: bool = False
|
||||
|
||||
# 时间追踪
|
||||
self._start_time: datetime = None
|
||||
self._time_spent: timedelta = timedelta()
|
||||
self._time_remaining: timedelta = timedelta()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def current_device(self) -> str:
|
||||
"""当前设备标识符"""
|
||||
return self._current_device
|
||||
|
||||
@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 max_flow_rate(self) -> float:
|
||||
return self._max_flow_rate
|
||||
|
||||
@property
|
||||
def max_pressure(self) -> float:
|
||||
return self._max_pressure
|
||||
|
||||
# 添加新的属性访问器
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self._from_vessel
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self._to_vessel
|
||||
|
||||
@property
|
||||
def transfer_volume(self) -> float:
|
||||
return self._transfer_volume
|
||||
|
||||
@property
|
||||
def amount(self) -> str:
|
||||
return self._amount
|
||||
|
||||
@property
|
||||
def transfer_time(self) -> float:
|
||||
return self._transfer_time
|
||||
|
||||
@property
|
||||
def is_viscous(self) -> bool:
|
||||
return self._is_viscous
|
||||
|
||||
@property
|
||||
def rinsing_solvent(self) -> str:
|
||||
return self._rinsing_solvent
|
||||
|
||||
@property
|
||||
def rinsing_volume(self) -> float:
|
||||
return self._rinsing_volume
|
||||
|
||||
@property
|
||||
def rinsing_repeats(self) -> int:
|
||||
return self._rinsing_repeats
|
||||
|
||||
@property
|
||||
def is_solid(self) -> bool:
|
||||
return self._is_solid
|
||||
|
||||
# 修改这两个属性装饰器
|
||||
@property
|
||||
def time_spent(self) -> float:
|
||||
"""已用时间(秒)"""
|
||||
if isinstance(self._time_spent, timedelta):
|
||||
return self._time_spent.total_seconds()
|
||||
return float(self._time_spent)
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> float:
|
||||
"""剩余时间(秒)"""
|
||||
if isinstance(self._time_remaining, timedelta):
|
||||
return self._time_remaining.total_seconds()
|
||||
return float(self._time_remaining)
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
||||
amount: str = "", time: float = 0.0, viscous: bool = False,
|
||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
||||
rinsing_repeats: int = 0, solid: bool = False) -> dict:
|
||||
"""Execute pump transfer operation"""
|
||||
# Stop any existing operation first
|
||||
self._stop_pump_operation()
|
||||
|
||||
# Set transfer parameters
|
||||
self._from_vessel = from_vessel
|
||||
self._to_vessel = to_vessel
|
||||
self._transfer_volume = float(volume)
|
||||
self._amount = amount
|
||||
self._transfer_time = float(time)
|
||||
self._is_viscous = viscous
|
||||
self._rinsing_solvent = rinsing_solvent
|
||||
self._rinsing_volume = float(rinsing_volume)
|
||||
self._rinsing_repeats = int(rinsing_repeats)
|
||||
self._is_solid = solid
|
||||
|
||||
# Calculate flow rate
|
||||
if self._transfer_time > 0 and self._transfer_volume > 0:
|
||||
self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0
|
||||
else:
|
||||
self._target_flow_rate = 10.0 if not self._is_viscous else 5.0
|
||||
|
||||
# Reset timers and counters
|
||||
self._start_time = datetime.now()
|
||||
self._time_spent = timedelta()
|
||||
self._time_remaining = timedelta(seconds=self._transfer_time)
|
||||
self._total_volume = 0.0
|
||||
self._flow_rate = 0.0
|
||||
|
||||
# Start pump operation
|
||||
self._pump_state = "Running"
|
||||
self._status = "Starting Transfer"
|
||||
self._running = True
|
||||
|
||||
# Start pump operation thread
|
||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
||||
self._pump_thread.daemon = True
|
||||
self._pump_thread.start()
|
||||
|
||||
# Wait briefly to ensure thread starts
|
||||
time.sleep(0.1)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": self._status,
|
||||
"current_device": self._current_device,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": float(self._transfer_time)
|
||||
}
|
||||
|
||||
def pause_pump(self) -> str:
|
||||
|
||||
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:
|
||||
|
||||
if self._pump_state != "Paused":
|
||||
self._status = "Error: Pump not paused"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Resuming Pump"
|
||||
self._start_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def reset_volume_counter(self) -> str:
|
||||
self._total_volume = 0.0
|
||||
self._status = "Volume counter reset"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
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:
|
||||
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):
|
||||
"""泵运行主循环"""
|
||||
print("Pump operation loop started") # Debug print
|
||||
|
||||
while self._running and self._pump_state == "Running":
|
||||
try:
|
||||
# Calculate flow rate adjustment
|
||||
flow_diff = self._target_flow_rate - self._flow_rate
|
||||
|
||||
# Adjust flow rate more aggressively (50% of difference)
|
||||
adjustment = flow_diff * 0.5
|
||||
self._flow_rate += adjustment
|
||||
|
||||
# Ensure flow rate is within bounds
|
||||
self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate))
|
||||
|
||||
# Update status based on flow rate
|
||||
if abs(flow_diff) < 0.1:
|
||||
self._status = "Running at Target Flow Rate"
|
||||
else:
|
||||
self._status = "Adjusting Flow Rate"
|
||||
|
||||
# Calculate volume increment
|
||||
volume_increment = (self._flow_rate / 60.0) # mL/s
|
||||
self._total_volume += volume_increment
|
||||
|
||||
# Update time tracking
|
||||
self._time_spent = datetime.now() - self._start_time
|
||||
if self._transfer_time > 0:
|
||||
remaining = self._transfer_time - self._time_spent.total_seconds()
|
||||
self._time_remaining = timedelta(seconds=max(0, remaining))
|
||||
|
||||
# Check completion
|
||||
if self._total_volume >= self._transfer_volume:
|
||||
self._status = "Transfer Completed"
|
||||
self._pump_state = "Stopped"
|
||||
self._running = False
|
||||
break
|
||||
|
||||
# Update pressure
|
||||
self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure
|
||||
|
||||
print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in pump operation: {str(e)}")
|
||||
self._status = "Error in pump operation"
|
||||
self._pump_state = "Stopped"
|
||||
self._running = False
|
||||
break
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"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,
|
||||
"max_flow_rate": self._max_flow_rate,
|
||||
"max_pressure": self._max_pressure,
|
||||
"current_device": self._current_device,
|
||||
"from_vessel": self._from_vessel,
|
||||
"to_vessel": self._to_vessel,
|
||||
"transfer_volume": self._transfer_volume,
|
||||
"amount": self._amount,
|
||||
"transfer_time": self._transfer_time,
|
||||
"is_viscous": self._is_viscous,
|
||||
"rinsing_solvent": self._rinsing_solvent,
|
||||
"rinsing_volume": self._rinsing_volume,
|
||||
"rinsing_repeats": self._rinsing_repeats,
|
||||
"is_solid": self._is_solid,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
pump = MockPump()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动泵设备测试...")
|
||||
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.emergency_stop()
|
||||
print("测试完成")
|
||||
390
unilabos/devices/mock/mock_rotavap.py
Normal file
390
unilabos/devices/mock/mock_rotavap.py
Normal file
@@ -0,0 +1,390 @@
|
||||
import time
|
||||
import threading
|
||||
import json
|
||||
|
||||
|
||||
class MockRotavap:
|
||||
"""
|
||||
模拟旋转蒸发器设备类
|
||||
|
||||
这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、
|
||||
真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockRotavap实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
|
||||
# 旋转相关属性
|
||||
self._rotate_state: str = "Stopped" # 旋转状态:Running, Stopped
|
||||
self._rotate_time: float = 0.0 # 旋转剩余时间 (秒)
|
||||
self._rotate_speed: float = 0.0 # 旋转速度 (rpm)
|
||||
self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm)
|
||||
|
||||
# 真空泵相关属性
|
||||
self._pump_state: str = "Stopped" # 泵状态:Running, Stopped
|
||||
self._pump_time: float = 0.0 # 泵剩余时间 (秒)
|
||||
self._vacuum_level: float = 0.0 # 真空度 (mbar)
|
||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
||||
|
||||
# 温度相关属性
|
||||
self._temperature: float = 25.0 # 水浴温度 (°C)
|
||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
||||
self._max_temperature: float = 180.0 # 最大温度 (°C)
|
||||
|
||||
# 运行控制线程
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 操作成功标志
|
||||
self.success: str = "True" # 使用字符串而不是布尔值
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def rotate_state(self) -> str:
|
||||
return self._rotate_state
|
||||
|
||||
@property
|
||||
def rotate_time(self) -> float:
|
||||
return self._rotate_time
|
||||
|
||||
@property
|
||||
def rotate_speed(self) -> float:
|
||||
return self._rotate_speed
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def pump_time(self) -> float:
|
||||
return self._pump_time
|
||||
|
||||
@property
|
||||
def vacuum_level(self) -> float:
|
||||
return self._vacuum_level
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
return self._target_temperature
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def set_timer(self, command: str) -> str:
|
||||
"""
|
||||
设置定时器 - 兼容现有RotavapOne接口
|
||||
|
||||
Args:
|
||||
command (str): JSON格式的命令字符串,包含rotate_time和pump_time
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
try:
|
||||
timer = json.loads(command)
|
||||
rotate_time = timer.get("rotate_time", 0)
|
||||
pump_time = timer.get("pump_time", 0)
|
||||
|
||||
self.success = "False"
|
||||
self._rotate_time = float(rotate_time)
|
||||
self._pump_time = float(pump_time)
|
||||
self.success = "True"
|
||||
|
||||
self._status = "Timer Set"
|
||||
return "Success"
|
||||
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
self._status = f"Error: Invalid command format - {str(e)}"
|
||||
self.success = "False"
|
||||
return "Error"
|
||||
|
||||
def set_rotate_time(self, time_seconds: float) -> str:
|
||||
"""
|
||||
设置旋转时间
|
||||
|
||||
Args:
|
||||
time_seconds (float): 旋转时间 (秒)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
self.success = "False"
|
||||
self._rotate_time = max(0.0, float(time_seconds))
|
||||
self.success = "True"
|
||||
self._status = "Rotate time set"
|
||||
return "Success"
|
||||
|
||||
def set_pump_time(self, time_seconds: float) -> str:
|
||||
"""
|
||||
设置泵时间
|
||||
|
||||
Args:
|
||||
time_seconds (float): 泵时间 (秒)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
self.success = "False"
|
||||
self._pump_time = max(0.0, float(time_seconds))
|
||||
self.success = "True"
|
||||
self._status = "Pump time set"
|
||||
return "Success"
|
||||
|
||||
def set_rotate_speed(self, speed: float) -> str:
|
||||
"""
|
||||
设置旋转速度
|
||||
|
||||
Args:
|
||||
speed (float): 旋转速度 (rpm)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if speed < 0 or speed > self._max_rotate_speed:
|
||||
self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})"
|
||||
return "Error"
|
||||
|
||||
self._rotate_speed = speed
|
||||
self._status = "Rotate speed set"
|
||||
return "Success"
|
||||
|
||||
def set_temperature(self, temperature: float) -> str:
|
||||
"""
|
||||
设置水浴温度
|
||||
|
||||
Args:
|
||||
temperature (float): 目标温度 (°C)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if temperature < 0 or temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
||||
return "Error"
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Temperature set"
|
||||
|
||||
# 启动操作线程以开始温度控制
|
||||
self._start_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_rotation(self) -> str:
|
||||
"""
|
||||
启动旋转
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if self._rotate_time <= 0:
|
||||
self._status = "Error: No rotate time set"
|
||||
return "Error"
|
||||
|
||||
self._rotate_state = "Running"
|
||||
self._status = "Rotation started"
|
||||
return "Success"
|
||||
|
||||
def start_pump(self) -> str:
|
||||
"""
|
||||
启动真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if self._pump_time <= 0:
|
||||
self._status = "Error: No pump time set"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Pump started"
|
||||
return "Success"
|
||||
|
||||
def stop_all_operations(self) -> str:
|
||||
"""
|
||||
停止所有操作
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._rotate_state = "Stopped"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_operation()
|
||||
self._rotate_time = 0.0
|
||||
self._pump_time = 0.0
|
||||
self._vacuum_level = 0.0
|
||||
self._status = "All operations stopped"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self.stop_all_operations()
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_operation(self):
|
||||
"""
|
||||
启动操作线程
|
||||
|
||||
这个方法启动一个后台线程来模拟旋蒸的实际运行过程。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
def _stop_operation(self):
|
||||
"""
|
||||
停止操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=2.0)
|
||||
|
||||
def _operation_loop(self):
|
||||
"""
|
||||
操作主循环
|
||||
|
||||
这个方法在后台线程中运行,模拟真实旋蒸的工作过程:
|
||||
1. 时间倒计时
|
||||
2. 温度控制
|
||||
3. 真空度控制
|
||||
4. 状态更新
|
||||
"""
|
||||
while self._running:
|
||||
try:
|
||||
# 处理旋转时间倒计时
|
||||
if self._rotate_time > 0:
|
||||
self._rotate_state = "Running"
|
||||
self._rotate_time = max(0.0, self._rotate_time - 1.0)
|
||||
else:
|
||||
self._rotate_state = "Stopped"
|
||||
|
||||
# 处理泵时间倒计时
|
||||
if self._pump_time > 0:
|
||||
self._pump_state = "Running"
|
||||
self._pump_time = max(0.0, self._pump_time - 1.0)
|
||||
# 模拟真空度变化
|
||||
if self._vacuum_level > self._target_vacuum:
|
||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0)
|
||||
else:
|
||||
self._pump_state = "Stopped"
|
||||
# 真空度逐渐回升
|
||||
self._vacuum_level = min(1013.25, self._vacuum_level + 2.0)
|
||||
|
||||
# 模拟温度控制
|
||||
temp_diff = self._target_temperature - self._temperature
|
||||
if abs(temp_diff) > 0.5:
|
||||
if temp_diff > 0:
|
||||
self._temperature += min(1.0, temp_diff * 0.1)
|
||||
else:
|
||||
self._temperature += max(-1.0, temp_diff * 0.1)
|
||||
|
||||
# 更新整体状态
|
||||
if self._rotate_state == "Running" or self._pump_state == "Running":
|
||||
self._status = "Operating"
|
||||
elif self._rotate_time > 0 or self._pump_time > 0:
|
||||
self._status = "Ready"
|
||||
else:
|
||||
self._status = "Idle"
|
||||
|
||||
# 等待1秒后继续下一次循环
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
self._status = f"Error in operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束时的清理工作
|
||||
self._status = "Idle"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"rotate_state": self._rotate_state,
|
||||
"rotate_time": self._rotate_time,
|
||||
"rotate_speed": self._rotate_speed,
|
||||
"pump_state": self._pump_state,
|
||||
"pump_time": self._pump_time,
|
||||
"vacuum_level": self._vacuum_level,
|
||||
"temperature": self._temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"success": self.success,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
rotavap = MockRotavap()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动旋转蒸发器测试...")
|
||||
print(f"初始状态: {rotavap.get_status_info()}")
|
||||
|
||||
# 设置定时器
|
||||
timer_command = '{"rotate_time": 300, "pump_time": 600}'
|
||||
rotavap.set_timer(timer_command)
|
||||
|
||||
# 设置温度和转速
|
||||
rotavap.set_temperature(60.0)
|
||||
rotavap.set_rotate_speed(120.0)
|
||||
|
||||
# 启动操作
|
||||
rotavap.start_rotation()
|
||||
rotavap.start_pump()
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, "
|
||||
f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar"
|
||||
)
|
||||
|
||||
rotavap.emergency_stop()
|
||||
print("测试完成")
|
||||
399
unilabos/devices/mock/mock_separator.py
Normal file
399
unilabos/devices/mock/mock_separator.py
Normal file
@@ -0,0 +1,399 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockSeparator:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 基本状态属性
|
||||
self._status: str = "Idle" # 当前总体状态
|
||||
self._valve_state: str = "Closed" # 阀门状态:Open 或 Closed
|
||||
self._settling_time: float = 0.0 # 静置时间(秒)
|
||||
|
||||
# 搅拌相关属性
|
||||
self._shake_time: float = 0.0 # 剩余摇摆时间(秒)
|
||||
self._shake_status: str = "Not Shaking" # 摇摆状态
|
||||
|
||||
# 用于后台模拟 shake 动作
|
||||
self._operation_thread = None
|
||||
self._thread_lock = threading.Lock()
|
||||
self._running = False
|
||||
|
||||
# Separate action 相关属性
|
||||
self._current_device: str = "MockSeparator1"
|
||||
self._purpose: str = "" # wash or extract
|
||||
self._product_phase: str = "" # top or bottom
|
||||
self._from_vessel: str = ""
|
||||
self._separation_vessel: str = ""
|
||||
self._to_vessel: str = ""
|
||||
self._waste_phase_to_vessel: str = ""
|
||||
self._solvent: str = ""
|
||||
self._solvent_volume: float = 0.0
|
||||
self._through: str = ""
|
||||
self._repeats: int = 1
|
||||
self._stir_time: float = 0.0
|
||||
self._stir_speed: float = 0.0
|
||||
self._time_spent = timedelta()
|
||||
self._time_remaining = timedelta()
|
||||
self._start_time = datetime.now() # 添加这一行
|
||||
|
||||
@property
|
||||
def current_device(self) -> str:
|
||||
return self._current_device
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def valve_state(self) -> str:
|
||||
return self._valve_state
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def shake_time(self) -> float:
|
||||
with self._thread_lock:
|
||||
return self._shake_time
|
||||
|
||||
@property
|
||||
def shake_status(self) -> str:
|
||||
with self._thread_lock:
|
||||
return self._shake_status
|
||||
|
||||
@property
|
||||
def product_phase(self) -> str:
|
||||
return self._product_phase
|
||||
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self._from_vessel
|
||||
|
||||
@property
|
||||
def separation_vessel(self) -> str:
|
||||
return self._separation_vessel
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self._to_vessel
|
||||
|
||||
@property
|
||||
def waste_phase_to_vessel(self) -> str:
|
||||
return self._waste_phase_to_vessel
|
||||
|
||||
@property
|
||||
def solvent(self) -> str:
|
||||
return self._solvent
|
||||
|
||||
@property
|
||||
def solvent_volume(self) -> float:
|
||||
return self._solvent_volume
|
||||
|
||||
@property
|
||||
def through(self) -> str:
|
||||
return self._through
|
||||
|
||||
@property
|
||||
def repeats(self) -> int:
|
||||
return self._repeats
|
||||
|
||||
@property
|
||||
def stir_time(self) -> float:
|
||||
return self._stir_time
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def time_spent(self) -> float:
|
||||
if self._running:
|
||||
self._time_spent = datetime.now() - self._start_time
|
||||
return self._time_spent.total_seconds()
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> float:
|
||||
if self._running:
|
||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
||||
remain = max(0, total_time - elapsed)
|
||||
self._time_remaining = timedelta(seconds=remain)
|
||||
return self._time_remaining.total_seconds()
|
||||
|
||||
def separate(self, purpose: str, product_phase: str, from_vessel: str,
|
||||
separation_vessel: str, to_vessel: str, waste_phase_to_vessel: str = "",
|
||||
solvent: str = "", solvent_volume: float = 0.0, through: str = "",
|
||||
repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0,
|
||||
settling_time: float = 60.0) -> dict:
|
||||
"""
|
||||
执行分离操作
|
||||
"""
|
||||
with self._thread_lock:
|
||||
# 检查是否已经在运行
|
||||
if self._running:
|
||||
return {
|
||||
"success": False,
|
||||
"status": "Error: Operation already in progress"
|
||||
}
|
||||
# 必填参数验证
|
||||
if not all([from_vessel, separation_vessel, to_vessel]):
|
||||
self._status = "Error: Missing required vessel parameters"
|
||||
return {"success": False}
|
||||
# 验证参数
|
||||
if purpose not in ["wash", "extract"]:
|
||||
self._status = "Error: Invalid purpose"
|
||||
return {"success": False}
|
||||
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
self._status = "Error: Invalid product phase"
|
||||
return {"success": False}
|
||||
# 数值参数验证
|
||||
try:
|
||||
solvent_volume = float(solvent_volume)
|
||||
repeats = int(repeats)
|
||||
stir_time = float(stir_time)
|
||||
stir_speed = float(stir_speed)
|
||||
settling_time = float(settling_time)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid numeric parameters"
|
||||
return {"success": False}
|
||||
|
||||
# 设置参数
|
||||
self._purpose = purpose
|
||||
self._product_phase = product_phase
|
||||
self._from_vessel = from_vessel
|
||||
self._separation_vessel = separation_vessel
|
||||
self._to_vessel = to_vessel
|
||||
self._waste_phase_to_vessel = waste_phase_to_vessel
|
||||
self._solvent = solvent
|
||||
self._solvent_volume = float(solvent_volume)
|
||||
self._through = through
|
||||
self._repeats = int(repeats)
|
||||
self._stir_time = float(stir_time)
|
||||
self._stir_speed = float(stir_speed)
|
||||
self._settling_time = float(settling_time)
|
||||
|
||||
# 重置计时器
|
||||
self._start_time = datetime.now()
|
||||
self._time_spent = timedelta()
|
||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
||||
self._time_remaining = timedelta(seconds=total_time)
|
||||
|
||||
# 启动分离操作
|
||||
self._status = "Starting Separation"
|
||||
self._running = True
|
||||
|
||||
# 在锁内创建和启动线程
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
# 等待确认操作已经开始
|
||||
time.sleep(0.1) # 短暂等待确保操作线程已启动
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": self._status,
|
||||
"current_device": self._current_device,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
def shake(self, shake_time: float) -> str:
|
||||
"""
|
||||
模拟 shake(搅拌)操作:
|
||||
- 进入 "Shaking" 状态,倒计时 shake_time 秒
|
||||
- shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒
|
||||
- 最后恢复为 Idle
|
||||
"""
|
||||
try:
|
||||
shake_time = float(shake_time)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid shake time"
|
||||
return "Error"
|
||||
|
||||
with self._thread_lock:
|
||||
self._status = "Shaking"
|
||||
self._settling_time = 0.0
|
||||
self._shake_time = shake_time
|
||||
self._shake_status = "Shaking"
|
||||
|
||||
def _run_shake():
|
||||
remaining = shake_time
|
||||
while remaining > 0:
|
||||
time.sleep(1)
|
||||
remaining -= 1
|
||||
with self._thread_lock:
|
||||
self._shake_time = remaining
|
||||
with self._thread_lock:
|
||||
self._status = "Settling"
|
||||
self._settling_time = 60.0 # 固定静置时间为60秒
|
||||
self._shake_status = "Settling"
|
||||
while True:
|
||||
with self._thread_lock:
|
||||
if self._settling_time <= 0:
|
||||
self._status = "Idle"
|
||||
self._shake_status = "Idle"
|
||||
break
|
||||
time.sleep(1)
|
||||
with self._thread_lock:
|
||||
self._settling_time = max(0.0, self._settling_time - 1)
|
||||
|
||||
self._operation_thread = threading.Thread(target=_run_shake)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
return "Success"
|
||||
|
||||
def set_valve(self, command: str) -> str:
|
||||
"""
|
||||
阀门控制命令:传入 "open" 或 "close"
|
||||
"""
|
||||
|
||||
command = command.lower()
|
||||
if command == "open":
|
||||
self._valve_state = "Open"
|
||||
self._status = "Valve Opened"
|
||||
elif command == "close":
|
||||
self._valve_state = "Closed"
|
||||
self._status = "Valve Closed"
|
||||
else:
|
||||
self._status = "Error: Invalid valve command"
|
||||
return "Error"
|
||||
return "Success"
|
||||
|
||||
def _operation_loop(self):
|
||||
"""分离操作主循环"""
|
||||
try:
|
||||
current_repeat = 1
|
||||
|
||||
# 立即更新状态,确保不会停留在Starting Separation
|
||||
with self._thread_lock:
|
||||
self._status = f"Separation Cycle {current_repeat}/{self._repeats}"
|
||||
|
||||
while self._running and current_repeat <= self._repeats:
|
||||
# 第一步:搅拌
|
||||
if self._stir_time > 0:
|
||||
with self._thread_lock:
|
||||
self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})"
|
||||
remaining_stir = self._stir_time
|
||||
while remaining_stir > 0 and self._running:
|
||||
time.sleep(1)
|
||||
remaining_stir -= 1
|
||||
|
||||
# 第二步:静置
|
||||
if self._settling_time > 0:
|
||||
with self._thread_lock:
|
||||
self._status = f"Settling (Repeat {current_repeat}/{self._repeats})"
|
||||
remaining_settle = self._settling_time
|
||||
while remaining_settle > 0 and self._running:
|
||||
time.sleep(1)
|
||||
remaining_settle -= 1
|
||||
|
||||
# 第三步:打开阀门排出
|
||||
with self._thread_lock:
|
||||
self._valve_state = "Open"
|
||||
self._status = f"Draining (Repeat {current_repeat}/{self._repeats})"
|
||||
|
||||
# 模拟排出时间(5秒)
|
||||
time.sleep(10)
|
||||
|
||||
# 关闭阀门
|
||||
with self._thread_lock:
|
||||
self._valve_state = "Closed"
|
||||
|
||||
# 检查是否继续下一次重复
|
||||
if current_repeat < self._repeats:
|
||||
current_repeat += 1
|
||||
else:
|
||||
with self._thread_lock:
|
||||
self._status = "Separation Complete"
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
with self._thread_lock:
|
||||
self._status = f"Error in separation: {str(e)}"
|
||||
finally:
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
self._valve_state = "Closed"
|
||||
if self._status == "Starting Separation":
|
||||
self._status = "Error: Operation failed to start"
|
||||
elif self._status != "Separation Complete":
|
||||
self._status = "Stopped"
|
||||
|
||||
def stop_operations(self) -> str:
|
||||
"""停止任何正在执行的操作"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=1.0)
|
||||
self._operation_thread = None
|
||||
self._settling_time = 0.0
|
||||
self._status = "Idle"
|
||||
self._shake_status = "Idle"
|
||||
self._shake_time = 0.0
|
||||
self._time_remaining = timedelta()
|
||||
return "Success"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取当前设备状态信息"""
|
||||
with self._thread_lock:
|
||||
current_time = datetime.now()
|
||||
if self._start_time:
|
||||
self._time_spent = current_time - self._start_time
|
||||
|
||||
return {
|
||||
"status": self._status,
|
||||
"valve_state": self._valve_state,
|
||||
"settling_time": self._settling_time,
|
||||
"shake_time": self._shake_time,
|
||||
"shake_status": self._shake_status,
|
||||
"current_device": self._current_device,
|
||||
"purpose": self._purpose,
|
||||
"product_phase": self._product_phase,
|
||||
"from_vessel": self._from_vessel,
|
||||
"separation_vessel": self._separation_vessel,
|
||||
"to_vessel": self._to_vessel,
|
||||
"waste_phase_to_vessel": self._waste_phase_to_vessel,
|
||||
"solvent": self._solvent,
|
||||
"solvent_volume": self._solvent_volume,
|
||||
"through": self._through,
|
||||
"repeats": self._repeats,
|
||||
"stir_time": self._stir_time,
|
||||
"stir_speed": self._stir_speed,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
|
||||
# 主函数用于测试
|
||||
if __name__ == "__main__":
|
||||
separator = MockSeparator()
|
||||
|
||||
print("启动简单版分离器测试...")
|
||||
print("初始状态:", separator.get_status_info())
|
||||
|
||||
# 触发 shake 操作,模拟 10 秒的搅拌
|
||||
print("执行 shake 操作...")
|
||||
print(separator.shake(10.0))
|
||||
|
||||
# 循环显示状态变化
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
info = separator.get_status_info()
|
||||
print(
|
||||
f"第{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, "
|
||||
f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, "
|
||||
f"shake_status={info['shake_status']}"
|
||||
)
|
||||
|
||||
# 模拟打开阀门
|
||||
print("打开阀门...", separator.set_valve("open"))
|
||||
print("最终状态:", separator.get_status_info())
|
||||
89
unilabos/devices/mock/mock_solenoid_valve.py
Normal file
89
unilabos/devices/mock/mock_solenoid_valve.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import time
|
||||
|
||||
|
||||
class MockSolenoidValve:
|
||||
"""
|
||||
模拟电磁阀设备类 - 简化版本
|
||||
|
||||
这个类提供了电磁阀的基本功能:开启、关闭和状态查询
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockSolenoidValve实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
self._status: str = "Idle"
|
||||
self._valve_status: str = "Closed" # 阀门位置:Open, Closed
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def valve_status(self) -> str:
|
||||
"""阀门状态"""
|
||||
return self._valve_status
|
||||
|
||||
def set_valve_status(self, status: str) -> str:
|
||||
"""
|
||||
设置阀门位置
|
||||
|
||||
Args:
|
||||
position (str): 阀门位置,可选值:"Open", "Closed"
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if status not in ["Open", "Closed"]:
|
||||
self._status = "Error: Invalid position"
|
||||
return "Error"
|
||||
|
||||
self._status = "Moving"
|
||||
time.sleep(1) # 模拟阀门动作时间
|
||||
|
||||
self._valve_status = status
|
||||
self._status = "Idle"
|
||||
return "Success"
|
||||
|
||||
def open_valve(self) -> str:
|
||||
"""打开阀门"""
|
||||
return self.set_valve_status("Open")
|
||||
|
||||
def close_valve(self) -> str:
|
||||
"""关闭阀门"""
|
||||
return self.set_valve_status("Closed")
|
||||
|
||||
def get_valve_status(self) -> str:
|
||||
"""获取阀门位置"""
|
||||
return self._valve_status
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""检查阀门是否打开"""
|
||||
return self._valve_status == "Open"
|
||||
|
||||
def is_closed(self) -> bool:
|
||||
"""检查阀门是否关闭"""
|
||||
return self._valve_status == "Closed"
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
valve = MockSolenoidValve()
|
||||
|
||||
print("启动电磁阀测试...")
|
||||
print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
# 测试开启阀门
|
||||
valve.open_valve()
|
||||
print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
# 测试关闭阀门
|
||||
valve.close_valve()
|
||||
print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
print("测试完成")
|
||||
307
unilabos/devices/mock/mock_stirrer.py
Normal file
307
unilabos/devices/mock/mock_stirrer.py
Normal file
@@ -0,0 +1,307 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockStirrer:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
|
||||
# 搅拌相关属性
|
||||
self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm)
|
||||
self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm)
|
||||
self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm)
|
||||
self._stir_state: str = "Stopped" # 搅拌状态:Running, Stopped
|
||||
|
||||
# 温度相关属性
|
||||
self._temperature: float = 25.0 # 当前温度 (°C)
|
||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
||||
self._max_temperature: float = 300.0 # 最大温度 (°C)
|
||||
self._heating_state: str = "Off" # 加热状态:On, Off
|
||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
||||
|
||||
# 运行控制线程
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def target_stir_speed(self) -> float:
|
||||
return self._target_stir_speed
|
||||
|
||||
@property
|
||||
def stir_state(self) -> str:
|
||||
return self._stir_state
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
"""
|
||||
当前温度
|
||||
|
||||
Returns:
|
||||
float: 当前温度 (°C)
|
||||
"""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""
|
||||
目标温度
|
||||
|
||||
Returns:
|
||||
float: 目标温度 (°C)
|
||||
"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def heating_state(self) -> str:
|
||||
return self._heating_state
|
||||
|
||||
@property
|
||||
def heating_power(self) -> float:
|
||||
return self._heating_power
|
||||
|
||||
@property
|
||||
def max_stir_speed(self) -> float:
|
||||
return self._max_stir_speed
|
||||
|
||||
@property
|
||||
def max_temperature(self) -> float:
|
||||
return self._max_temperature
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def set_stir_speed(self, speed: float) -> str:
|
||||
|
||||
speed = float(speed) # 确保传入的速度是浮点数
|
||||
|
||||
if speed < 0 or speed > self._max_stir_speed:
|
||||
self._status = f"Error: Speed out of range (0-{self._max_stir_speed})"
|
||||
return "Error"
|
||||
|
||||
self._target_stir_speed = speed
|
||||
self._status = "Setting Stir Speed"
|
||||
|
||||
# 如果设置了非零速度,启动搅拌
|
||||
if speed > 0:
|
||||
self._stir_state = "Running"
|
||||
else:
|
||||
self._stir_state = "Stopped"
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_temperature(self, temperature: float) -> str:
|
||||
temperature = float(temperature) # 确保传入的温度是浮点数
|
||||
|
||||
if temperature < 0 or temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
||||
return "Error"
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Setting Temperature"
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_stirring(self) -> str:
|
||||
|
||||
if self._target_stir_speed <= 0:
|
||||
self._status = "Error: No target speed set"
|
||||
return "Error"
|
||||
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring Started"
|
||||
return "Success"
|
||||
|
||||
def stop_stirring(self) -> str:
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._status = "Stirring Stopped"
|
||||
return "Success"
|
||||
|
||||
def heating_control(self, heating_state: str = "On") -> str:
|
||||
|
||||
if heating_state not in ["On", "Off"]:
|
||||
self._status = "Error: Invalid heating state"
|
||||
return "Error"
|
||||
|
||||
self._heating_state = heating_state
|
||||
|
||||
if heating_state == "On":
|
||||
self._status = "Heating On"
|
||||
else:
|
||||
self._status = "Heating Off"
|
||||
self._heating_power = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
def stop_all_operations(self) -> str:
|
||||
self._stir_state = "Stopped"
|
||||
self._heating_state = "Off"
|
||||
self._stop_operation()
|
||||
self._stir_speed = 0.0
|
||||
self._target_stir_speed = 0.0
|
||||
self._heating_power = 0.0
|
||||
self._status = "All operations stopped"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self.stop_all_operations()
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_operation(self):
|
||||
with self._thread_lock:
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
def _stop_operation(self):
|
||||
"""
|
||||
停止操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=2.0)
|
||||
|
||||
def _operation_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
# 处理搅拌速度控制
|
||||
if self._stir_state == "Running":
|
||||
speed_diff = self._target_stir_speed - self._stir_speed
|
||||
|
||||
if abs(speed_diff) < 1.0: # 速度接近目标值
|
||||
self._stir_speed = self._target_stir_speed
|
||||
if self._stir_speed > 0:
|
||||
self._status = "Stirring at Target Speed"
|
||||
else:
|
||||
# 模拟速度调节,每秒调整10%的差值
|
||||
adjustment = speed_diff * 0.1
|
||||
self._stir_speed += adjustment
|
||||
self._status = "Adjusting Stir Speed"
|
||||
|
||||
# 确保速度在合理范围内
|
||||
self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed))
|
||||
else:
|
||||
# 搅拌停止时,速度逐渐降为0
|
||||
if self._stir_speed > 0:
|
||||
self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm
|
||||
|
||||
# 处理温度控制
|
||||
if self._heating_state == "On":
|
||||
temp_diff = self._target_temperature - self._temperature
|
||||
|
||||
if abs(temp_diff) < 0.5: # 温度接近目标值
|
||||
self._heating_power = 20.0 # 维持温度的最小功率
|
||||
elif temp_diff > 0: # 需要加热
|
||||
# 根据温差调整加热功率
|
||||
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 * 1.5 # 最大每秒升温1.5度
|
||||
self._temperature += heating_rate
|
||||
else: # 目标温度低于当前温度
|
||||
self._heating_power = 0.0
|
||||
# 自然冷却
|
||||
self._temperature -= 0.1
|
||||
else:
|
||||
self._heating_power = 0.0
|
||||
# 自然冷却到室温
|
||||
if self._temperature > 25.0:
|
||||
self._temperature -= 0.2
|
||||
|
||||
# 限制温度范围
|
||||
self._temperature = max(20.0, min(self._max_temperature, self._temperature))
|
||||
|
||||
# 更新整体状态
|
||||
if self._stir_state == "Running" and self._heating_state == "On":
|
||||
self._status = "Stirring and Heating"
|
||||
elif self._stir_state == "Running":
|
||||
self._status = "Stirring Only"
|
||||
elif self._heating_state == "On":
|
||||
self._status = "Heating Only"
|
||||
else:
|
||||
self._status = "Idle"
|
||||
|
||||
# 等待1秒后继续下一次循环
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
self._status = f"Error in operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束时的清理工作
|
||||
self._status = "Idle"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
return {
|
||||
"status": self._status,
|
||||
"stir_speed": self._stir_speed,
|
||||
"target_stir_speed": self._target_stir_speed,
|
||||
"stir_state": self._stir_state,
|
||||
"temperature": self._temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"heating_state": self._heating_state,
|
||||
"heating_power": self._heating_power,
|
||||
"max_stir_speed": self._max_stir_speed,
|
||||
"max_temperature": self._max_temperature,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
stirrer = MockStirrer()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动搅拌器测试...")
|
||||
print(f"初始状态: {stirrer.get_status_info()}")
|
||||
|
||||
# 设置搅拌速度和温度
|
||||
stirrer.set_stir_speed(800.0)
|
||||
stirrer.set_temperature(60.0)
|
||||
stirrer.heating_control("On")
|
||||
|
||||
# 模拟运行15秒
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, "
|
||||
f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}"
|
||||
)
|
||||
|
||||
stirrer.emergency_stop()
|
||||
print("测试完成")
|
||||
229
unilabos/devices/mock/mock_stirrer_new.py
Normal file
229
unilabos/devices/mock/mock_stirrer_new.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockStirrer_new:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 基本状态属性
|
||||
self._status: str = "Idle"
|
||||
self._vessel: str = ""
|
||||
self._purpose: str = ""
|
||||
|
||||
# 搅拌相关属性
|
||||
self._stir_speed: float = 0.0
|
||||
self._target_stir_speed: float = 0.0
|
||||
self._max_stir_speed: float = 2000.0
|
||||
self._stir_state: str = "Stopped"
|
||||
|
||||
# 计时相关
|
||||
self._stir_time: float = 0.0
|
||||
self._settling_time: float = 0.0
|
||||
self._start_time = datetime.now()
|
||||
self._time_remaining = timedelta()
|
||||
|
||||
# 运行控制
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 创建操作线程
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def target_stir_speed(self) -> float:
|
||||
return self._target_stir_speed
|
||||
|
||||
@property
|
||||
def stir_state(self) -> str:
|
||||
return self._stir_state
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def stir_time(self) -> float:
|
||||
return self._stir_time
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@property
|
||||
def max_stir_speed(self) -> float:
|
||||
return self._max_stir_speed
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
"""返回当前操作的进度(0-100)"""
|
||||
if not self._running:
|
||||
return 0.0
|
||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
||||
total_time = self._stir_time + self._settling_time
|
||||
if total_time <= 0:
|
||||
return 100.0
|
||||
return min(100.0, (elapsed / total_time) * 100)
|
||||
|
||||
# ==================== Action Server 方法 ====================
|
||||
def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict:
|
||||
"""
|
||||
StartStir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if self._running:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Operation already in progress"
|
||||
}
|
||||
|
||||
try:
|
||||
# 重置所有参数
|
||||
self._vessel = vessel
|
||||
self._purpose = purpose
|
||||
self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间
|
||||
self._settling_time = 0.0
|
||||
self._start_time = datetime.now() # 重置开始时间
|
||||
|
||||
if stir_speed > 0:
|
||||
self._target_stir_speed = min(stir_speed, self._max_stir_speed)
|
||||
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring Started"
|
||||
self._running = True
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Stirring started successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
|
||||
def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict:
|
||||
"""
|
||||
Stir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
try:
|
||||
# 如果已经在运行,先停止当前操作
|
||||
if self._running:
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
time.sleep(0.1) # 给一个短暂的停止时间
|
||||
|
||||
|
||||
# 重置所有参数
|
||||
self._stir_time = float(stir_time)
|
||||
self._settling_time = float(settling_time)
|
||||
self._target_stir_speed = min(float(stir_speed), self._max_stir_speed)
|
||||
self._start_time = datetime.now() # 重置开始时间
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring"
|
||||
self._running = True
|
||||
|
||||
return {"success": True}
|
||||
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid parameters"
|
||||
return {"success": False}
|
||||
|
||||
def stop_stir(self, vessel: str) -> dict:
|
||||
"""
|
||||
StopStir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if vessel != self._vessel:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Vessel mismatch"
|
||||
}
|
||||
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._status = "Stirring Stopped"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Stirring stopped successfully"
|
||||
}
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _operation_loop(self):
|
||||
"""操作主循环"""
|
||||
while True:
|
||||
try:
|
||||
current_time = datetime.now()
|
||||
|
||||
with self._thread_lock: # 添加锁保护
|
||||
if self._stir_state == "Running":
|
||||
# 实际搅拌逻辑
|
||||
speed_diff = self._target_stir_speed - self._stir_speed
|
||||
if abs(speed_diff) > 0.1:
|
||||
adjustment = speed_diff * 0.1
|
||||
self._stir_speed += adjustment
|
||||
else:
|
||||
self._stir_speed = self._target_stir_speed
|
||||
|
||||
# 更新进度
|
||||
if self._running:
|
||||
if self._stir_time > 0: # 定时搅拌模式
|
||||
elapsed = (current_time - self._start_time).total_seconds()
|
||||
if elapsed >= self._stir_time + self._settling_time:
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._stir_speed = 0.0
|
||||
self._status = "Stirring Complete"
|
||||
elif elapsed >= self._stir_time:
|
||||
self._status = "Settling"
|
||||
else: # 连续搅拌模式
|
||||
self._status = "Stirring"
|
||||
else:
|
||||
# 停止状态下慢慢降低速度
|
||||
if self._stir_speed > 0:
|
||||
self._stir_speed = max(0, self._stir_speed - 20.0)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in operation loop: {str(e)}") # 添加错误输出
|
||||
self._status = f"Error: {str(e)}"
|
||||
time.sleep(1.0) # 错误发生时等待较长时间
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取设备状态信息"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
"stir_speed": self._stir_speed,
|
||||
"target_stir_speed": self._target_stir_speed,
|
||||
"stir_state": self._stir_state,
|
||||
"stir_time": self._stir_time, # 添加
|
||||
"settling_time": self._settling_time, # 添加
|
||||
"progress": self.progress,
|
||||
"max_stir_speed": self._max_stir_speed
|
||||
}
|
||||
410
unilabos/devices/mock/mock_vacuum.py
Normal file
410
unilabos/devices/mock/mock_vacuum.py
Normal file
@@ -0,0 +1,410 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockVacuum:
|
||||
"""
|
||||
模拟真空泵设备类
|
||||
|
||||
这个类模拟了一个实验室真空泵的行为,包括真空度控制、
|
||||
压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockVacuum实例
|
||||
|
||||
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._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始
|
||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
||||
self._min_vacuum: float = 1.0 # 最小真空度 (mbar)
|
||||
self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压
|
||||
|
||||
# 泵性能相关属性
|
||||
self._pump_speed: float = 0.0 # 泵速 (L/s)
|
||||
self._max_pump_speed: float = 100.0 # 最大泵速 (L/s)
|
||||
self._pump_efficiency: float = 95.0 # 泵效率百分比
|
||||
|
||||
# 运行控制线程
|
||||
self._vacuum_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""
|
||||
设备状态 - 会被自动识别的设备属性
|
||||
|
||||
Returns:
|
||||
str: 当前设备状态 (Idle, Running, Error, Stopped)
|
||||
"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def power_state(self) -> str:
|
||||
"""
|
||||
电源状态
|
||||
|
||||
Returns:
|
||||
str: 电源状态 (On, Off)
|
||||
"""
|
||||
return self._power_state
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
"""
|
||||
泵运行状态
|
||||
|
||||
Returns:
|
||||
str: 泵状态 (Running, Stopped, Paused)
|
||||
"""
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def vacuum_level(self) -> float:
|
||||
"""
|
||||
当前真空度
|
||||
|
||||
Returns:
|
||||
float: 当前真空度 (mbar)
|
||||
"""
|
||||
return self._vacuum_level
|
||||
|
||||
@property
|
||||
def target_vacuum(self) -> float:
|
||||
"""
|
||||
目标真空度
|
||||
|
||||
Returns:
|
||||
float: 目标真空度 (mbar)
|
||||
"""
|
||||
return self._target_vacuum
|
||||
|
||||
@property
|
||||
def pump_speed(self) -> float:
|
||||
"""
|
||||
泵速
|
||||
|
||||
Returns:
|
||||
float: 泵速 (L/s)
|
||||
"""
|
||||
return self._pump_speed
|
||||
|
||||
@property
|
||||
def pump_efficiency(self) -> float:
|
||||
"""
|
||||
泵效率
|
||||
|
||||
Returns:
|
||||
float: 泵效率百分比
|
||||
"""
|
||||
return self._pump_efficiency
|
||||
|
||||
@property
|
||||
def max_pump_speed(self) -> float:
|
||||
"""
|
||||
最大泵速
|
||||
|
||||
Returns:
|
||||
float: 最大泵速 (L/s)
|
||||
"""
|
||||
return self._max_pump_speed
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为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"
|
||||
self._start_vacuum_operation()
|
||||
else:
|
||||
self._status = "Power Off"
|
||||
self.stop_vacuum()
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_vacuum_level(self, vacuum_level: float) -> str:
|
||||
"""
|
||||
设置目标真空度
|
||||
|
||||
Args:
|
||||
vacuum_level (float): 目标真空度 (mbar)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
try:
|
||||
vacuum_level = float(vacuum_level)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid vacuum level"
|
||||
return "Error"
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum:
|
||||
self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})"
|
||||
return "Error"
|
||||
|
||||
self._target_vacuum = vacuum_level
|
||||
self._status = "Setting Vacuum Level"
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_vacuum(self) -> str:
|
||||
"""
|
||||
启动真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Starting Vacuum Pump"
|
||||
self._start_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def stop_vacuum(self) -> str:
|
||||
"""
|
||||
停止真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._pump_state = "Stopped"
|
||||
self._status = "Stopping Vacuum Pump"
|
||||
self._stop_vacuum_operation()
|
||||
self._pump_speed = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
def pause_vacuum(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 = "Vacuum Pump Paused"
|
||||
self._stop_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def resume_vacuum(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 Vacuum Pump"
|
||||
self._start_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def vent_to_atmosphere(self) -> str:
|
||||
"""
|
||||
通大气 - 将真空度恢复到大气压
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._target_vacuum = self._max_vacuum # 设置为大气压
|
||||
self._status = "Venting to Atmosphere"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_vacuum_operation()
|
||||
self._pump_speed = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_vacuum_operation(self):
|
||||
"""
|
||||
启动真空操作线程
|
||||
|
||||
这个方法启动一个后台线程来模拟真空泵的实际运行过程。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if not self._running and self._power_state == "On":
|
||||
self._running = True
|
||||
self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop)
|
||||
self._vacuum_thread.daemon = True
|
||||
self._vacuum_thread.start()
|
||||
|
||||
def _stop_vacuum_operation(self):
|
||||
"""
|
||||
停止真空操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._vacuum_thread and self._vacuum_thread.is_alive():
|
||||
self._vacuum_thread.join(timeout=2.0)
|
||||
|
||||
def _vacuum_operation_loop(self):
|
||||
"""
|
||||
真空操作主循环
|
||||
|
||||
这个方法在后台线程中运行,模拟真空泵的工作过程:
|
||||
1. 检查电源状态和运行状态
|
||||
2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度
|
||||
3. 否则等待
|
||||
"""
|
||||
while self._running and self._power_state == "On":
|
||||
try:
|
||||
with self._thread_lock:
|
||||
# 只有泵状态为 Running 时才进行更新
|
||||
if self._pump_state == "Running":
|
||||
vacuum_diff = self._vacuum_level - self._target_vacuum
|
||||
|
||||
if abs(vacuum_diff) < 1.0: # 真空度接近目标值
|
||||
self._status = "At Target Vacuum"
|
||||
self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速
|
||||
elif vacuum_diff > 0: # 需要抽真空(降低压力)
|
||||
self._status = "Pumping Down"
|
||||
if vacuum_diff > 500:
|
||||
self._pump_speed = self._max_pump_speed
|
||||
elif vacuum_diff > 100:
|
||||
self._pump_speed = self._max_pump_speed * 0.8
|
||||
elif vacuum_diff > 50:
|
||||
self._pump_speed = self._max_pump_speed * 0.6
|
||||
else:
|
||||
self._pump_speed = self._max_pump_speed * 0.4
|
||||
|
||||
# 根据泵速和效率计算真空降幅
|
||||
pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0
|
||||
vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar
|
||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction)
|
||||
else: # 目标真空度高于当前值,需要通气
|
||||
self._status = "Venting"
|
||||
self._pump_speed = 0.0
|
||||
self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0)
|
||||
|
||||
# 限制真空度范围
|
||||
self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level))
|
||||
else:
|
||||
# 当泵状态不是 Running 时,可保持原状态
|
||||
self._status = "Vacuum Pump Not Running"
|
||||
# 释放锁后等待1秒钟
|
||||
time.sleep(1.0)
|
||||
except Exception as e:
|
||||
with self._thread_lock:
|
||||
self._status = f"Error in vacuum operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束后的清理工作
|
||||
if self._pump_state == "Running":
|
||||
self._status = "Idle"
|
||||
# 停止泵后,真空度逐渐回升到大气压
|
||||
while self._vacuum_level < self._max_vacuum * 0.9:
|
||||
with self._thread_lock:
|
||||
self._vacuum_level += 2.0
|
||||
time.sleep(0.1)
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"power_state": self._power_state,
|
||||
"pump_state": self._pump_state,
|
||||
"vacuum_level": self._vacuum_level,
|
||||
"target_vacuum": self._target_vacuum,
|
||||
"pump_speed": self._pump_speed,
|
||||
"pump_efficiency": self._pump_efficiency,
|
||||
"max_pump_speed": self._max_pump_speed,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
vacuum = MockVacuum()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动真空泵测试...")
|
||||
vacuum.power_control("On")
|
||||
print(f"初始状态: {vacuum.get_status_info()}")
|
||||
|
||||
# 设置目标真空度并启动
|
||||
vacuum.set_vacuum_level(10.0) # 设置为10mbar
|
||||
vacuum.start_vacuum()
|
||||
|
||||
# 模拟运行15秒
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}"
|
||||
)
|
||||
# 测试通大气
|
||||
print("测试通大气...")
|
||||
vacuum.vent_to_atmosphere()
|
||||
|
||||
# 继续运行5秒观察通大气过程
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}")
|
||||
|
||||
vacuum.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,11 +1,96 @@
|
||||
liquid_handler:
|
||||
description: Liquid handler device controlled by pylabrobot
|
||||
class:
|
||||
module: pylabrobot.liquid_handling:LiquidHandler
|
||||
module: unilabos.devices.liquid_handling.liquid_handler_abstract:LiquidHandlerAbstract
|
||||
type: python
|
||||
status_types:
|
||||
name: String
|
||||
action_value_mappings:
|
||||
remove:
|
||||
type: LiquidHandlerRemove
|
||||
goal:
|
||||
vols: vols
|
||||
sources: sources
|
||||
waste_liquid: waste_liquid
|
||||
use_channels: use_channels
|
||||
flow_rates: flow_rates
|
||||
offsets: offsets
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
delays: delays
|
||||
is_96_well: is_96_well
|
||||
top: top
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
add_liquid:
|
||||
type: LiquidHandlerAdd
|
||||
goal:
|
||||
asp_vols: asp_vols
|
||||
dis_vols: dis_vols
|
||||
reagent_sources: reagent_sources
|
||||
targets: targets
|
||||
use_channels: use_channels
|
||||
flow_rates: flow_rates
|
||||
offsets: offsets
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
is_96_well: is_96_well
|
||||
mix_time: mix_time
|
||||
mix_vol: mix_vol
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
transfer_liquid:
|
||||
type: LiquidHandlerTransfer
|
||||
goal:
|
||||
asp_vols: asp_vols
|
||||
dis_vols: dis_vols
|
||||
sources: sources
|
||||
targets: targets
|
||||
tip_racks: tip_racks
|
||||
use_channels: use_channels
|
||||
asp_flow_rates: asp_flow_rates
|
||||
dis_flow_rates: dis_flow_rates
|
||||
offsets: offsets
|
||||
touch_tip: touch_tip
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
is_96_well: is_96_well
|
||||
mix_stage: mix_stage
|
||||
mix_times: mix_times
|
||||
mix_vol: mix_vol
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
delays: delays
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
mix:
|
||||
type: LiquidHandlerMix
|
||||
goal:
|
||||
targets: targets
|
||||
mix_time: mix_time
|
||||
mix_vol: mix_vol
|
||||
height_to_bottom: height_to_bottom
|
||||
offsets: offsets
|
||||
mix_rate: mix_rate
|
||||
none_keys: none_keys
|
||||
feedback: { }
|
||||
result: { }
|
||||
move_to:
|
||||
type: LiquidHandlerMoveTo
|
||||
goal:
|
||||
well: well
|
||||
dis_to_top: dis_to_top
|
||||
channel: channel
|
||||
feedback: { }
|
||||
result: { }
|
||||
aspirate:
|
||||
type: LiquidHandlerAspirate
|
||||
goal:
|
||||
@@ -170,127 +255,6 @@ liquid_handler:
|
||||
- name
|
||||
additionalProperties: false
|
||||
|
||||
dp_liquid_handler:
|
||||
description: 通用液体处理
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.action_definition:DPLiquidHandler
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
remove_liquid:
|
||||
type: DPLiquidHandlerRemoveLiquid
|
||||
goal:
|
||||
vols: vols
|
||||
sources: sources
|
||||
waste_liquid: waste_liquid
|
||||
use_channels: use_channels
|
||||
flow_rates: flow_rates
|
||||
offsets: offsets
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
delays: delays
|
||||
is_96_well: is_96_well
|
||||
top: top
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
add_liquid:
|
||||
type: DPLiquidHandlerAddLiquid
|
||||
goal:
|
||||
asp_vols: asp_vols
|
||||
dis_vols: dis_vols
|
||||
reagent_sources: reagent_sources
|
||||
targets: targets
|
||||
use_channels: use_channels
|
||||
flow_rates: flow_rates
|
||||
offsets: offsets
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
is_96_well: is_96_well
|
||||
mix_time: mix_time
|
||||
mix_vol: mix_vol
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
transfer_liquid:
|
||||
type: DPLiquidHandlerTransferLiquid
|
||||
goal:
|
||||
asp_vols: asp_vols
|
||||
dis_vols: dis_vols
|
||||
sources: sources
|
||||
targets: targets
|
||||
tip_racks: tip_racks
|
||||
use_channels: use_channels
|
||||
asp_flow_rates: asp_flow_rates
|
||||
dis_flow_rates: dis_flow_rates
|
||||
offsets: offsets
|
||||
touch_tip: touch_tip
|
||||
liquid_height: liquid_height
|
||||
blow_out_air_volume: blow_out_air_volume
|
||||
spread: spread
|
||||
is_96_well: is_96_well
|
||||
mix_stage: mix_stage
|
||||
mix_times: mix_times
|
||||
mix_vol: mix_vol
|
||||
mix_rate: mix_rate
|
||||
mix_liquid_height: mix_liquid_height
|
||||
delays: delays
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
custom_delay:
|
||||
type: DPLiquidHandlerCustomDelay
|
||||
goal:
|
||||
seconds: seconds
|
||||
msg: msg
|
||||
feedback: {}
|
||||
result: {}
|
||||
touch_tip:
|
||||
type: DPLiquidHandlerTouchTip
|
||||
goal:
|
||||
targets: targets
|
||||
feedback: {}
|
||||
result: {}
|
||||
mix:
|
||||
type: DPLiquidHandlerMix
|
||||
goal:
|
||||
targets: targets
|
||||
mix_time: mix_time
|
||||
mix_vol: mix_vol
|
||||
height_to_bottom: height_to_bottom
|
||||
offsets: offsets
|
||||
mix_rate: mix_rate
|
||||
none_keys: none_keys
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_tiprack:
|
||||
type: DPLiquidHandlerSetTiprack
|
||||
goal:
|
||||
tip_racks: tip_racks
|
||||
feedback: {}
|
||||
result: {}
|
||||
move_to:
|
||||
type: DPLiquidHandlerMoveTo
|
||||
goal:
|
||||
well: well
|
||||
dis_to_top: dis_to_top
|
||||
channel: channel
|
||||
feedback: {}
|
||||
result: {}
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 物料名
|
||||
required:
|
||||
- name
|
||||
|
||||
liquid_handler.revvity:
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.revvity:Revvity
|
||||
|
||||
892
unilabos/registry/devices/mock_devices.yaml
Normal file
892
unilabos/registry/devices/mock_devices.yaml
Normal file
@@ -0,0 +1,892 @@
|
||||
mock_chiller:
|
||||
description: Mock Chiller Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_chiller:MockChiller
|
||||
type: python
|
||||
status_types:
|
||||
current_temperature: Float64
|
||||
target_temperature: Float64
|
||||
status: String
|
||||
is_cooling: Bool
|
||||
is_heating: Bool
|
||||
vessel: String # 新增
|
||||
purpose: String # 新增
|
||||
action_value_mappings:
|
||||
emergency_stop:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
vessel: vessel
|
||||
temp: temp
|
||||
purpose: purpose
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
status: status
|
||||
heat_chill_stop:
|
||||
type: HeatChillStop
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
status: status
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
description: Current temperature of the chiller in °C
|
||||
target_temperature:
|
||||
type: number
|
||||
description: Target temperature setting in °C
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the device
|
||||
is_cooling:
|
||||
type: boolean
|
||||
description: Whether the device is actively cooling
|
||||
is_heating:
|
||||
type: boolean
|
||||
description: Whether the device is actively heating
|
||||
vessel: # 新增
|
||||
type: string
|
||||
description: Current vessel being processed
|
||||
purpose: # 新增
|
||||
type: string
|
||||
description: Purpose of the current operation
|
||||
required:
|
||||
- current_temperature
|
||||
- target_temperature
|
||||
- status
|
||||
- vessel
|
||||
- purpose
|
||||
additionalProperties: false
|
||||
mock_filter:
|
||||
description: Mock Filter Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_filter:MockFilter
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
is_filtering: Bool
|
||||
flow_rate: Float64
|
||||
pressure_drop: Float64
|
||||
filter_life: Float64
|
||||
vessel: String
|
||||
filtrate_vessel: String
|
||||
filtered_volume: Float64
|
||||
progress: Float64
|
||||
stir: Bool
|
||||
stir_speed: Float64
|
||||
temperature: Float64
|
||||
continue_heatchill: Bool
|
||||
target_volume: Float64
|
||||
action_value_mappings:
|
||||
filter:
|
||||
type: Filter
|
||||
goal:
|
||||
vessel: vessel
|
||||
filtrate_vessel: filtrate_vessel
|
||||
stir: stir
|
||||
stir_speed: stir_speed
|
||||
temp: temp
|
||||
continue_heatchill: continue_heatchill
|
||||
volume: volume
|
||||
feedback:
|
||||
progress: progress
|
||||
current_temp: current_temp
|
||||
filtered_volume: filtered_volume
|
||||
current_status: current_status
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
stop_filtering:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
replace_filter:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the filter
|
||||
is_filtering:
|
||||
type: boolean
|
||||
description: Whether the filter is actively filtering
|
||||
flow_rate:
|
||||
type: number
|
||||
description: Current flow rate in L/min
|
||||
pressure_drop:
|
||||
type: number
|
||||
description: Pressure drop across the filter in Pa
|
||||
filter_life:
|
||||
type: number
|
||||
description: Remaining filter life percentage
|
||||
power_on:
|
||||
type: boolean
|
||||
description: Power state of the device
|
||||
required:
|
||||
- status
|
||||
- is_filtering
|
||||
- flow_rate
|
||||
- filter_life
|
||||
- vessel
|
||||
- filtrate_vessel
|
||||
- filtered_volume
|
||||
- progress
|
||||
additionalProperties: false
|
||||
mock_heater:
|
||||
description: Mock Heater Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_heater:MockHeater
|
||||
type: python
|
||||
status_types:
|
||||
current_temperature: Float64
|
||||
target_temperature: Float64
|
||||
status: String
|
||||
is_heating: Bool
|
||||
heating_power: Float64
|
||||
max_temperature: Float64
|
||||
vessel: String
|
||||
purpose: String
|
||||
stir: Bool
|
||||
stir_speed: Float64
|
||||
action_value_mappings:
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
vessel: vessel
|
||||
temp: temp
|
||||
purpose: purpose
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
heat_chill_stop:
|
||||
type: HeatChillStop
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
heat_chill:
|
||||
type: HeatChill
|
||||
goal:
|
||||
vessel: vessel
|
||||
temp: temp
|
||||
time: time
|
||||
stir: stir
|
||||
stir_speed: stir_speed
|
||||
purpose: purpose
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
emergency_stop:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
description: Current temperature of the heater in °C
|
||||
target_temperature:
|
||||
type: number
|
||||
description: Target temperature setting in °C
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the device
|
||||
is_heating:
|
||||
type: boolean
|
||||
description: Whether the device is actively heating
|
||||
heating_power:
|
||||
type: number
|
||||
description: Current heating power percentage
|
||||
max_temperature:
|
||||
type: number
|
||||
description: Maximum temperature limit
|
||||
vessel:
|
||||
type: string
|
||||
description: Current vessel being heated
|
||||
purpose:
|
||||
type: string
|
||||
description: Purpose of the heating operation
|
||||
stir:
|
||||
type: boolean
|
||||
description: Whether stirring is enabled
|
||||
stir_speed:
|
||||
type: number
|
||||
description: Current stirring speed
|
||||
required:
|
||||
- current_temperature
|
||||
- target_temperature
|
||||
- status
|
||||
- vessel
|
||||
- purpose
|
||||
additionalProperties: false
|
||||
mock_pump:
|
||||
description: Mock Pump Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_pump:MockPump
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
pump_state: String
|
||||
flow_rate: Float64
|
||||
target_flow_rate: Float64
|
||||
pressure: Float64
|
||||
total_volume: Float64
|
||||
max_flow_rate: Float64
|
||||
max_pressure: Float64
|
||||
from_vessel: String
|
||||
to_vessel: String
|
||||
transfer_volume: Float64
|
||||
amount: String
|
||||
transfer_time: Float64
|
||||
is_viscous: Bool
|
||||
rinsing_solvent: String
|
||||
rinsing_volume: Float64
|
||||
rinsing_repeats: Int32
|
||||
is_solid: Bool
|
||||
time_spent: Float64
|
||||
time_remaining: Float64
|
||||
current_device: String
|
||||
action_value_mappings:
|
||||
pump_transfer:
|
||||
type: PumpTransfer
|
||||
goal:
|
||||
from_vessel: from_vessel
|
||||
to_vessel: to_vessel
|
||||
volume: volume
|
||||
amount: amount
|
||||
time: time
|
||||
viscous: viscous
|
||||
rinsing_solvent: rinsing_solvent
|
||||
rinsing_volume: rinsing_volume
|
||||
rinsing_repeats: rinsing_repeats
|
||||
solid: solid
|
||||
feedback:
|
||||
status: status
|
||||
current_device: current_device
|
||||
time_spent: time_spent
|
||||
time_remaining: time_remaining
|
||||
result:
|
||||
success: success
|
||||
pause_pump:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
resume_pump:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
reset_volume_counter:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the pump
|
||||
pump_state:
|
||||
type: string
|
||||
description: Pump operation state (Running/Stopped/Paused)
|
||||
flow_rate:
|
||||
type: number
|
||||
description: Current flow rate in mL/min
|
||||
target_flow_rate:
|
||||
type: number
|
||||
description: Target flow rate in mL/min
|
||||
pressure:
|
||||
type: number
|
||||
description: Current pressure in bar
|
||||
total_volume:
|
||||
type: number
|
||||
description: Total accumulated volume in mL
|
||||
max_flow_rate:
|
||||
type: number
|
||||
description: Maximum flow rate in mL/min
|
||||
max_pressure:
|
||||
type: number
|
||||
description: Maximum pressure in bar
|
||||
from_vessel:
|
||||
type: string
|
||||
description: Source vessel for transfer
|
||||
to_vessel:
|
||||
type: string
|
||||
description: Target vessel for transfer
|
||||
transfer_volume:
|
||||
type: number
|
||||
description: Volume to transfer in mL
|
||||
amount:
|
||||
type: string
|
||||
description: Amount description
|
||||
transfer_time:
|
||||
type: number
|
||||
description: Transfer time in seconds
|
||||
is_viscous:
|
||||
type: boolean
|
||||
description: Whether the liquid is viscous
|
||||
rinsing_solvent:
|
||||
type: string
|
||||
description: Solvent used for rinsing
|
||||
rinsing_volume:
|
||||
type: number
|
||||
description: Volume used for rinsing
|
||||
rinsing_repeats:
|
||||
type: integer
|
||||
description: Number of rinsing cycles
|
||||
is_solid:
|
||||
type: boolean
|
||||
description: Whether transferring solid material
|
||||
current_device:
|
||||
type: string
|
||||
description: Current device identifier
|
||||
required:
|
||||
- status
|
||||
- pump_state
|
||||
- flow_rate
|
||||
- from_vessel
|
||||
- to_vessel
|
||||
additionalProperties: false
|
||||
mock_rotavap:
|
||||
description: Mock Rotavap Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_rotavap:MockRotavap
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
rotate_state: String
|
||||
rotate_time: Float64
|
||||
rotate_speed: Float64
|
||||
pump_state: String
|
||||
pump_time: Float64
|
||||
vacuum_level: Float64
|
||||
temperature: Float64
|
||||
target_temperature: Float64
|
||||
success: String
|
||||
action_value_mappings:
|
||||
set_timer:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: command
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_rotate_time:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: time_seconds
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_pump_time:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: time_seconds
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_rotate_speed:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_temperature:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: temperature
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
start_rotation:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
start_pump:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the rotavap
|
||||
rotate_state:
|
||||
type: string
|
||||
description: Rotation state (Running/Stopped)
|
||||
rotate_time:
|
||||
type: number
|
||||
description: Remaining rotation time in seconds
|
||||
rotate_speed:
|
||||
type: number
|
||||
description: Rotation speed in rpm
|
||||
pump_state:
|
||||
type: string
|
||||
description: Pump state (Running/Stopped)
|
||||
pump_time:
|
||||
type: number
|
||||
description: Remaining pump time in seconds
|
||||
vacuum_level:
|
||||
type: number
|
||||
description: Current vacuum level in mbar
|
||||
temperature:
|
||||
type: number
|
||||
description: Current water bath temperature
|
||||
target_temperature:
|
||||
type: number
|
||||
description: Target water bath temperature
|
||||
success:
|
||||
type: string
|
||||
description: Operation success status
|
||||
required:
|
||||
- status
|
||||
- rotate_time
|
||||
- pump_time
|
||||
- temperature
|
||||
additionalProperties: false
|
||||
mock_separator:
|
||||
description: Simplified Mock Separator Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_separator:MockSeparator
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
settling_time: Float64
|
||||
valve_state: String
|
||||
shake_time: Float64
|
||||
shake_status: String
|
||||
current_device: String
|
||||
purpose: String
|
||||
product_phase: String
|
||||
from_vessel: String
|
||||
separation_vessel: String
|
||||
to_vessel: String
|
||||
waste_phase_to_vessel: String
|
||||
solvent: String
|
||||
solvent_volume: Float64
|
||||
through: String
|
||||
repeats: Int32
|
||||
stir_time: Float64
|
||||
stir_speed: Float64
|
||||
time_spent: Float64
|
||||
time_remaining: Float64
|
||||
action_value_mappings:
|
||||
separate:
|
||||
type: Separate
|
||||
goal:
|
||||
purpose: purpose
|
||||
product_phase: product_phase
|
||||
from_vessel: from_vessel
|
||||
separation_vessel: separation_vessel
|
||||
to_vessel: to_vessel
|
||||
waste_phase_to_vessel: waste_phase_to_vessel
|
||||
solvent: solvent
|
||||
solvent_volume: solvent_volume
|
||||
through: through
|
||||
repeats: repeats
|
||||
stir_time: stir_time
|
||||
stir_speed: stir_speed
|
||||
settling_time: settling_time
|
||||
feedback:
|
||||
status: status
|
||||
current_device: current_device
|
||||
time_spent: time_spent
|
||||
time_remaining: time_remaining
|
||||
result:
|
||||
success: success
|
||||
shake:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: shake_time
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
stop_operations:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_valve:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: command
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the separator
|
||||
settling_time:
|
||||
type: number
|
||||
description: Settling time in seconds
|
||||
valve_state:
|
||||
type: string
|
||||
description: Valve state (Open/Closed)
|
||||
shake_time:
|
||||
type: number
|
||||
description: Remaining shake time in seconds
|
||||
shake_status:
|
||||
type: string
|
||||
description: Current shake state
|
||||
purpose:
|
||||
type: string
|
||||
description: Separation purpose (wash/extract)
|
||||
product_phase:
|
||||
type: string
|
||||
description: Product phase (top/bottom)
|
||||
from_vessel:
|
||||
type: string
|
||||
description: Source vessel
|
||||
separation_vessel:
|
||||
type: string
|
||||
description: Vessel for separation
|
||||
to_vessel:
|
||||
type: string
|
||||
description: Target vessel
|
||||
required:
|
||||
- status
|
||||
- valve_state
|
||||
- shake_status
|
||||
- current_device
|
||||
additionalProperties: false
|
||||
mock_solenoid_valve:
|
||||
description: Mock Solenoid Valve Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_solenoid_valve:MockSolenoidValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_status: String
|
||||
action_value_mappings:
|
||||
set_valve_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: status
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
open_valve:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
close_valve:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the valve
|
||||
valve_status:
|
||||
type: string
|
||||
description: Valve status (Open/Closed)
|
||||
required:
|
||||
- status
|
||||
- valve_status
|
||||
additionalProperties: false
|
||||
mock_stirrer:
|
||||
description: Mock Stirrer Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_stirrer:MockStirrer
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
stir_speed: Float64
|
||||
target_stir_speed: Float64
|
||||
stir_state: String
|
||||
temperature: Float64
|
||||
target_temperature: Float64
|
||||
heating_state: String
|
||||
heating_power: Float64
|
||||
max_stir_speed: Float64
|
||||
max_temperature: Float64
|
||||
action_value_mappings:
|
||||
set_stir_speed:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_temperature:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: temperature
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
start_stirring:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
stop_stirring:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
heating_control:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: heating_state
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the stirrer
|
||||
stir_speed:
|
||||
type: number
|
||||
description: Current stirring speed in rpm
|
||||
target_stir_speed:
|
||||
type: number
|
||||
description: Target stirring speed in rpm
|
||||
stir_state:
|
||||
type: string
|
||||
description: Stirring state (Running/Stopped)
|
||||
temperature:
|
||||
type: number
|
||||
description: Current temperature in °C
|
||||
target_temperature:
|
||||
type: number
|
||||
description: Target temperature in °C
|
||||
heating_state:
|
||||
type: string
|
||||
description: Heating state (On/Off)
|
||||
heating_power:
|
||||
type: number
|
||||
description: Current heating power percentage
|
||||
max_stir_speed:
|
||||
type: number
|
||||
description: Maximum stirring speed in rpm
|
||||
max_temperature:
|
||||
type: number
|
||||
description: Maximum temperature in °C
|
||||
required:
|
||||
- status
|
||||
- stir_speed
|
||||
- temperature
|
||||
- power_state
|
||||
additionalProperties: false
|
||||
mock_stirrer_new:
|
||||
description: Mock Stirrer Device (Copy Version)
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_stirrer_new:MockStirrer_new
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
vessel: String
|
||||
purpose: String
|
||||
stir_speed: Float64
|
||||
target_stir_speed: Float64
|
||||
stir_state: String
|
||||
stir_time: Float64
|
||||
settling_time: Float64
|
||||
progress: Float64
|
||||
max_stir_speed: Float64
|
||||
action_value_mappings:
|
||||
start_stir:
|
||||
type: StartStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
stir_speed: stir_speed
|
||||
purpose: purpose
|
||||
feedback:
|
||||
progress: progress
|
||||
current_speed: stir_speed
|
||||
current_status: status
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
stir:
|
||||
type: Stir
|
||||
goal:
|
||||
stir_time: stir_time
|
||||
stir_speed: stir_speed
|
||||
settling_time: settling_time
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
stop_stir:
|
||||
type: StopStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
progress: progress
|
||||
current_status: status
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
vessel:
|
||||
type: string
|
||||
purpose:
|
||||
type: string
|
||||
stir_speed:
|
||||
type: number
|
||||
target_stir_speed:
|
||||
type: number
|
||||
stir_state:
|
||||
type: string
|
||||
stir_time:
|
||||
type: number
|
||||
settling_time:
|
||||
type: number
|
||||
progress:
|
||||
type: number
|
||||
max_stir_speed:
|
||||
type: number
|
||||
required:
|
||||
- status
|
||||
- stir_speed
|
||||
- stir_state
|
||||
- vessel
|
||||
additionalProperties: false
|
||||
mock_vacuum:
|
||||
description: Mock Vacuum Pump Device
|
||||
class:
|
||||
module: unilabos.devices.mock.mock_vacuum:MockVacuum
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
power_state: String
|
||||
pump_state: String
|
||||
vacuum_level: Float64
|
||||
target_vacuum: Float64
|
||||
pump_speed: Float64
|
||||
pump_efficiency: Float64
|
||||
max_pump_speed: Float64
|
||||
action_value_mappings:
|
||||
power_control:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: power_state
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_vacuum_level:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: vacuum_level
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
start_vacuum:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
stop_vacuum:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
pause_vacuum:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
resume_vacuum:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
vent_to_atmosphere:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the vacuum pump
|
||||
power_state:
|
||||
type: string
|
||||
description: Power state (On/Off)
|
||||
pump_state:
|
||||
type: string
|
||||
description: Pump operation state (Running/Stopped/Paused)
|
||||
vacuum_level:
|
||||
type: number
|
||||
description: Current vacuum level in mbar
|
||||
target_vacuum:
|
||||
type: number
|
||||
description: Target vacuum level in mbar
|
||||
pump_speed:
|
||||
type: number
|
||||
description: Current pump speed in L/s
|
||||
pump_efficiency:
|
||||
type: number
|
||||
description: Pump efficiency percentage
|
||||
max_pump_speed:
|
||||
type: number
|
||||
description: Maximum pump speed in L/s
|
||||
required:
|
||||
- status
|
||||
- power_state
|
||||
- pump_state
|
||||
- vacuum_level
|
||||
additionalProperties: false
|
||||
@@ -12,7 +12,7 @@ separator.homemade:
|
||||
goal:
|
||||
stir_time: stir_time,
|
||||
stir_speed: stir_speed
|
||||
settling_time": settling_time
|
||||
settling_time: settling_time
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
|
||||
@@ -62,4 +62,4 @@ tempsensor:
|
||||
command: command
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
success: success
|
||||
|
||||
@@ -3,6 +3,25 @@ vacuum_pump.mock:
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
|
||||
gas_source.mock:
|
||||
description: Mock gas source
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -7,10 +6,9 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.utils import logger
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.decorator import singleton
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
DEFAULT_PATHS = [Path(__file__).absolute().parent]
|
||||
|
||||
@@ -21,43 +19,16 @@ class Registry:
|
||||
self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值
|
||||
if registry_paths:
|
||||
self.registry_paths.extend(registry_paths)
|
||||
action_type = self._replace_type_with_class(
|
||||
"ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer"
|
||||
self.ResourceCreateFromOuter = self._replace_type_with_class(
|
||||
"ResourceCreateFromOuter", "host_node", f"动作 create_resource_detailed"
|
||||
)
|
||||
schema = ros_action_to_json_schema(action_type)
|
||||
self.device_type_registry = {
|
||||
"host_node": {
|
||||
"description": "UniLabOS主机节点",
|
||||
"class": {
|
||||
"module": "unilabos.ros.nodes.presets.host_node",
|
||||
"type": "python",
|
||||
"status_types": {},
|
||||
"action_value_mappings": {
|
||||
"add_resource_from_outer": {
|
||||
"type": msg_converter_manager.search_class("ResourceCreateFromOuter"),
|
||||
"goal": {
|
||||
"resources": "resources",
|
||||
"device_ids": "device_ids",
|
||||
"bind_parent_ids": "bind_parent_ids",
|
||||
"bind_locations": "bind_locations",
|
||||
"other_calling_params": "other_calling_params",
|
||||
},
|
||||
"feedback": {},
|
||||
"result": {
|
||||
"success": "success"
|
||||
},
|
||||
"schema": schema
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
"type": "object"
|
||||
},
|
||||
"file_path": "/"
|
||||
}
|
||||
}
|
||||
self.ResourceCreateFromOuterEasy = self._replace_type_with_class(
|
||||
"ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource"
|
||||
)
|
||||
self.EmptyIn = self._replace_type_with_class(
|
||||
"EmptyIn", "host_node", f""
|
||||
)
|
||||
self.device_type_registry = {}
|
||||
self.resource_type_registry = {}
|
||||
self._setup_called = False # 跟踪setup是否已调用
|
||||
# 其他状态变量
|
||||
@@ -69,9 +40,70 @@ class Registry:
|
||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||
return
|
||||
|
||||
# 标记setup已被调用
|
||||
self._setup_called = True
|
||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||
|
||||
self.device_type_registry.update(
|
||||
{
|
||||
"host_node": {
|
||||
"description": "UniLabOS主机节点",
|
||||
"class": {
|
||||
"module": "unilabos.ros.nodes.presets.host_node",
|
||||
"type": "python",
|
||||
"status_types": {},
|
||||
"action_value_mappings": {
|
||||
"create_resource_detailed": {
|
||||
"type": self.ResourceCreateFromOuter,
|
||||
"goal": {
|
||||
"resources": "resources",
|
||||
"device_ids": "device_ids",
|
||||
"bind_parent_ids": "bind_parent_ids",
|
||||
"bind_locations": "bind_locations",
|
||||
"other_calling_params": "other_calling_params",
|
||||
},
|
||||
"feedback": {},
|
||||
"result": {"success": "success"},
|
||||
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter),
|
||||
"goal_default": yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
|
||||
),
|
||||
},
|
||||
"create_resource": {
|
||||
"type": self.ResourceCreateFromOuterEasy,
|
||||
"goal": {
|
||||
"res_id": "res_id",
|
||||
"class_name": "class_name",
|
||||
"parent": "parent",
|
||||
"device_id": "device_id",
|
||||
"bind_locations": "bind_locations",
|
||||
"liquid_input_slot": "liquid_input_slot[]",
|
||||
"liquid_type": "liquid_type[]",
|
||||
"liquid_volume": "liquid_volume[]",
|
||||
"slot_on_deck": "slot_on_deck",
|
||||
},
|
||||
"feedback": {},
|
||||
"result": {"success": "success"},
|
||||
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy),
|
||||
"goal_default": yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
|
||||
),
|
||||
},
|
||||
"test_latency": {
|
||||
"type": self.EmptyIn,
|
||||
"goal": {},
|
||||
"feedback": {},
|
||||
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
|
||||
"schema": ros_action_to_json_schema(self.EmptyIn),
|
||||
"goal_default": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
"icon": "icon_device.webp",
|
||||
"registry_type": "device",
|
||||
"schema": {"properties": {}, "additionalProperties": False, "type": "object"},
|
||||
"file_path": "/",
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
||||
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
||||
for i, path in enumerate(self.registry_paths):
|
||||
@@ -81,6 +113,8 @@ class Registry:
|
||||
self.load_device_types(path)
|
||||
self.load_resource_types(path)
|
||||
logger.info("[UniLab Registry] 注册表设置完成")
|
||||
# 标记setup已被调用
|
||||
self._setup_called = True
|
||||
|
||||
def load_resource_types(self, path: os.PathLike):
|
||||
abs_path = Path(path).absolute()
|
||||
@@ -96,6 +130,9 @@ class Registry:
|
||||
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
if "description" not in resource_info:
|
||||
resource_info["description"] = ""
|
||||
if "icon" not in resource_info:
|
||||
resource_info["icon"] = ""
|
||||
resource_info["registry_type"] = "resource"
|
||||
self.resource_type_registry.update(data)
|
||||
logger.debug(
|
||||
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
||||
@@ -145,6 +182,7 @@ class Registry:
|
||||
)
|
||||
current_device_number = len(self.device_type_registry) + 1
|
||||
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
|
||||
|
||||
for i, file in enumerate(files):
|
||||
data = yaml.safe_load(open(file, encoding="utf-8"))
|
||||
if data:
|
||||
@@ -154,6 +192,9 @@ class Registry:
|
||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
if "description" not in device_config:
|
||||
device_config["description"] = ""
|
||||
if "icon" not in device_config:
|
||||
device_config["icon"] = ""
|
||||
device_config["registry_type"] = "device"
|
||||
if "class" in device_config:
|
||||
# 处理状态类型
|
||||
if "status_types" in device_config["class"]:
|
||||
@@ -169,8 +210,15 @@ class Registry:
|
||||
action_config["type"] = self._replace_type_with_class(
|
||||
action_config["type"], device_id, f"动作 {action_name}"
|
||||
)
|
||||
action_config["goal_default"] = yaml.safe_load(io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal)))
|
||||
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
||||
if action_config["type"] is not None:
|
||||
action_config["goal_default"] = yaml.safe_load(
|
||||
io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal))
|
||||
)
|
||||
action_config["schema"] = ros_action_to_json_schema(action_config["type"])
|
||||
else:
|
||||
logger.warning(
|
||||
f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换"
|
||||
)
|
||||
|
||||
self.device_type_registry.update(data)
|
||||
|
||||
@@ -188,13 +236,17 @@ class Registry:
|
||||
def obtain_registry_device_info(self):
|
||||
devices = []
|
||||
for device_id, device_info in self.device_type_registry.items():
|
||||
msg = {
|
||||
"id": device_id,
|
||||
**device_info
|
||||
}
|
||||
msg = {"id": device_id, **device_info}
|
||||
devices.append(msg)
|
||||
return devices
|
||||
|
||||
def obtain_registry_resource_info(self):
|
||||
resources = []
|
||||
for resource_id, resource_info in self.resource_type_registry.items():
|
||||
msg = {"id": resource_id, **resource_info}
|
||||
resources.append(msg)
|
||||
return resources
|
||||
|
||||
|
||||
# 全局单例实例
|
||||
lab_registry = Registry()
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
agilent_1_reservoir_290ml:
|
||||
description: Agilent 1 reservoir 290ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:agilent_1_reservoir_290ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:agilent_1_reservoir_290ml
|
||||
type: pylabrobot
|
||||
|
||||
axygen_1_reservoir_90ml:
|
||||
description: Axygen 1 reservoir 90ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:axygen_1_reservoir_90ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:axygen_1_reservoir_90ml
|
||||
type: pylabrobot
|
||||
|
||||
nest_12_reservoir_15ml:
|
||||
description: Nest 12 reservoir 15ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:nest_12_reservoir_15ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:nest_12_reservoir_15ml
|
||||
type: pylabrobot
|
||||
|
||||
nest_1_reservoir_195ml:
|
||||
description: Nest 1 reservoir 195ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_195ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_195ml
|
||||
type: pylabrobot
|
||||
|
||||
nest_1_reservoir_290ml:
|
||||
description: Nest 1 reservoir 290ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_290ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_290ml
|
||||
type: pylabrobot
|
||||
|
||||
usascientific_12_reservoir_22ml:
|
||||
description: USAScientific 12 reservoir 22ml
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.reserviors:usascientific_12_reservoir_22ml
|
||||
module: pylabrobot.resources.opentrons.reservoirs:usascientific_12_reservoir_22ml
|
||||
type: pylabrobot
|
||||
|
||||
@@ -189,6 +189,7 @@ def dict_from_graph(graph: nx.Graph) -> dict:
|
||||
def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
# 将节点转换为字典,以便通过 ID 快速查找
|
||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||
id_list = [node["id"] for node in nodes_list]
|
||||
|
||||
# 初始化每个节点的 children 为包含节点字典的列表
|
||||
for node in nodes_list:
|
||||
@@ -196,7 +197,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
|
||||
# 找到根节点并返回
|
||||
root_nodes = [
|
||||
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan]
|
||||
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or node.get("parent", node.get("parent_name")) not in id_list
|
||||
]
|
||||
|
||||
# 如果存在多个根节点,返回所有根节点
|
||||
@@ -421,7 +422,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
|
||||
return r
|
||||
|
||||
|
||||
def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]:
|
||||
def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
"""Initializes a resource based on its configuration.
|
||||
|
||||
If the config is detailed, then do nothing;
|
||||
@@ -433,6 +434,7 @@ def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
from unilabos.registry.registry import lab_registry
|
||||
resource_class_config = resource_config.get("class", None)
|
||||
if resource_class_config is None:
|
||||
return [resource_config]
|
||||
@@ -476,11 +478,8 @@ def initialize_resources(resources_config) -> list[dict]:
|
||||
None
|
||||
"""
|
||||
|
||||
from unilabos.registry.registry import lab_registry
|
||||
resources = []
|
||||
for resource_config in resources_config:
|
||||
if resource_config["parent"] == "tip_rack" or resource_config["parent"] == "plate_well":
|
||||
continue
|
||||
resources.extend(initialize_resource(resource_config, lab_registry))
|
||||
resources.extend(initialize_resource(resource_config))
|
||||
|
||||
return resources
|
||||
|
||||
@@ -348,10 +348,16 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
|
||||
if isinstance(td, NamespacedType):
|
||||
target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}")
|
||||
setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value])
|
||||
elif isinstance(td, UnboundedString):
|
||||
setattr(ros_msg, key, value)
|
||||
else:
|
||||
logger.warning(f"Not Supported type: {td}")
|
||||
setattr(ros_msg, key, []) # FIXME
|
||||
elif "array.array" in str(type(attr)):
|
||||
setattr(ros_msg, key, value)
|
||||
if attr.typecode == "f":
|
||||
setattr(ros_msg, key, [float(i) for i in value])
|
||||
else:
|
||||
setattr(ros_msg, key, value)
|
||||
else:
|
||||
nested_ros_msg = convert_to_ros_msg(type(attr)(), value)
|
||||
setattr(ros_msg, key, nested_ros_msg)
|
||||
@@ -574,6 +580,7 @@ basic_type_map = {
|
||||
'int64': {'type': 'integer'},
|
||||
'uint64': {'type': 'integer', 'minimum': 0},
|
||||
'double': {'type': 'number'},
|
||||
'float': {'type': 'number'},
|
||||
'float32': {'type': 'number'},
|
||||
'float64': {'type': 'number'},
|
||||
'string': {'type': 'string'},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@@ -19,7 +21,7 @@ from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \
|
||||
initialize_resources
|
||||
initialize_resources, list_to_nested_dict, dict_to_tree, resource_plr_to_ulab, tree_to_list
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
@@ -311,7 +313,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 物料传输到对应的node节点
|
||||
rclient = self.create_client(ResourceAdd, "/resources/add")
|
||||
rclient.wait_for_service()
|
||||
rclient2 = self.create_client(ResourceAdd, "/resources/add")
|
||||
rclient2.wait_for_service()
|
||||
request = ResourceAdd.Request()
|
||||
request2 = ResourceAdd.Request()
|
||||
command_json = json.loads(req.command)
|
||||
namespace = command_json["namespace"]
|
||||
bind_parent_id = command_json["bind_parent_id"]
|
||||
@@ -320,11 +325,23 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
other_calling_param = command_json["other_calling_param"]
|
||||
resources = command_json["resource"]
|
||||
initialize_full = other_calling_param.pop("initialize_full", False)
|
||||
# 用来增加液体
|
||||
ADD_LIQUID_TYPE = other_calling_param.pop("ADD_LIQUID_TYPE", [])
|
||||
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
||||
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
||||
slot = other_calling_param.pop("slot", -1)
|
||||
if slot >= 0: # slot为负数的时候采用assign方法
|
||||
other_calling_param["slot"] = slot
|
||||
# 本地拿到这个物料,可能需要先做初始化?
|
||||
if isinstance(resources, list):
|
||||
if initialize_full:
|
||||
if len(resources) == 1 and isinstance(resources[0], list) and not initialize_full: # 取消,不存在的情况
|
||||
# 预先initialize过,以整组的形式传入
|
||||
request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]]
|
||||
elif initialize_full:
|
||||
resources = initialize_resources(resources)
|
||||
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
||||
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
||||
else:
|
||||
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
|
||||
else:
|
||||
if initialize_full:
|
||||
resources = initialize_resources([resources])
|
||||
@@ -334,20 +351,31 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
res.response = "OK"
|
||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||
request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
|
||||
try:
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
from pylabrobot.resources.deck import Deck
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources import OTDeck
|
||||
from pylabrobot.resources import Plate
|
||||
contain_model = not isinstance(resource, Deck)
|
||||
if isinstance(resource, ResourcePLR):
|
||||
# resources.list()
|
||||
plr_instance = resource_ulab_to_plr(resources, contain_model)
|
||||
resources_tree = dict_to_tree(copy.deepcopy({r["id"]: r for r in resources}))
|
||||
plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model)
|
||||
if isinstance(plr_instance, Plate):
|
||||
empty_liquid_info_in = [(None, 0)] * plr_instance.num_items
|
||||
for liquid_type, liquid_volume, liquid_input_slot in zip(ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT):
|
||||
empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume)
|
||||
plr_instance.set_well_liquids(empty_liquid_info_in)
|
||||
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
|
||||
resource.assign_child_at_slot(plr_instance, **other_calling_param)
|
||||
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param)
|
||||
else:
|
||||
_discard_slot = other_calling_param.pop("slot", -1)
|
||||
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param)
|
||||
request2.resources = [convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])]
|
||||
rclient2.call(request2)
|
||||
# 发送给ResourceMeshManager
|
||||
action_client = ActionClient(
|
||||
self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group
|
||||
@@ -404,6 +432,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# 加入全局注册表
|
||||
registered_devices[self.device_id] = device_info
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
if not BasicConfig.is_host_mode:
|
||||
sclient = self.create_client(SerialCommand, "/node_info_update")
|
||||
# 启动线程执行发送任务
|
||||
@@ -413,6 +442,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_send_slave_node_info"
|
||||
).start()
|
||||
else:
|
||||
host_node = HostNode.get_instance(0)
|
||||
if host_node is not None:
|
||||
host_node.device_machine_names[self.device_id] = "本地"
|
||||
|
||||
def send_slave_node_info(self, sclient):
|
||||
sclient.wait_for_service()
|
||||
@@ -481,6 +514,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
self.lab_logger().debug(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||
|
||||
def get_real_function(self, instance, attr_name):
|
||||
if hasattr(instance.__class__, attr_name):
|
||||
obj = getattr(instance.__class__, attr_name)
|
||||
if isinstance(obj, property):
|
||||
return lambda *args, **kwargs: obj.fset(instance, *args, **kwargs), get_type_hints(obj.fset)
|
||||
obj = getattr(instance, attr_name)
|
||||
return obj, get_type_hints(obj)
|
||||
else:
|
||||
obj = getattr(instance, attr_name)
|
||||
return obj, get_type_hints(obj)
|
||||
|
||||
def _create_execute_callback(self, action_name, action_value_mapping):
|
||||
"""创建动作执行回调函数"""
|
||||
|
||||
@@ -495,22 +539,21 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
for i, action in enumerate(self._action_value_mappings["sequence"]):
|
||||
if i == 0:
|
||||
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
||||
getattr(self.driver_instance, action)(**kwargs)
|
||||
self.get_real_function(self.driver_instance, action)[0](**kwargs)
|
||||
else:
|
||||
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
||||
getattr(self.driver_instance, action)()
|
||||
self.get_real_function(self.driver_instance, action)[0]()
|
||||
|
||||
action_paramtypes = get_type_hints(
|
||||
getattr(self.driver_instance, self._action_value_mappings["sequence"][0])
|
||||
)
|
||||
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
|
||||
)[1]
|
||||
else:
|
||||
ACTION = getattr(self.driver_instance, action_name)
|
||||
action_paramtypes = get_type_hints(ACTION)
|
||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||
|
||||
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||
self.lab_logger().debug(f"接收到原始目标: {action_kwargs}")
|
||||
# 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过
|
||||
if action_name != "add_resource_from_outer":
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}")
|
||||
@@ -609,7 +652,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
del future
|
||||
|
||||
# 向Host更新物料当前状态
|
||||
if action_name != "add_resource_from_outer":
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
continue
|
||||
@@ -748,7 +791,7 @@ class ROS2DeviceNode:
|
||||
self.resource_tracker = DeviceNodeResourceTracker()
|
||||
|
||||
# use_pylabrobot_creator 使用 cls的包路径检测
|
||||
use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler"
|
||||
use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "LiquidHandlerAbstract"
|
||||
|
||||
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
||||
# 创建设备类实例
|
||||
|
||||
@@ -12,11 +12,18 @@ from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
|
||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||
from rclpy.service import Service
|
||||
from unilabos_msgs.msg import Resource # type: ignore
|
||||
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
|
||||
SerialCommand # type: ignore
|
||||
from unilabos_msgs.srv import (
|
||||
ResourceAdd,
|
||||
ResourceGet,
|
||||
ResourceDelete,
|
||||
ResourceUpdate,
|
||||
ResourceList,
|
||||
SerialCommand,
|
||||
) # type: ignore
|
||||
from unique_identifier_msgs.msg import UUID
|
||||
|
||||
from unilabos.registry.registry import lab_registry
|
||||
from unilabos.resources.graphio import initialize_resource
|
||||
from unilabos.resources.registry import add_schema
|
||||
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
@@ -86,6 +93,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.__class__._instance = self
|
||||
|
||||
# 初始化配置
|
||||
self.server_latest_timestamp = 0.0 #
|
||||
self.devices_config = devices_config
|
||||
self.resources_config = resources_config
|
||||
self.physical_setup_graph = physical_setup_graph
|
||||
@@ -99,9 +107,32 @@ class HostNode(BaseROS2DeviceNode):
|
||||
# 创建设备、动作客户端和目标存储
|
||||
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
|
||||
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
|
||||
self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射
|
||||
self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例
|
||||
self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
||||
self.device_machine_names: Dict[str, str] = {
|
||||
device_id: "本地",
|
||||
} # 存储设备ID到机器名称的映射
|
||||
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好
|
||||
"/devices/host_node/create_resource": ActionClient(
|
||||
self,
|
||||
lab_registry.ResourceCreateFromOuterEasy,
|
||||
"/devices/host_node/create_resource",
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
"/devices/host_node/create_resource_detailed": ActionClient(
|
||||
self,
|
||||
lab_registry.ResourceCreateFromOuter,
|
||||
"/devices/host_node/create_resource_detailed",
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
"/devices/host_node/test_latency": ActionClient(
|
||||
self,
|
||||
lab_registry.EmptyIn,
|
||||
"/devices/host_node/test_latency",
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
} # 用来存储多个ActionClient实例
|
||||
self._action_value_mappings: Dict[str, Dict] = (
|
||||
{}
|
||||
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
||||
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
||||
@@ -115,8 +146,11 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
|
||||
|
||||
from unilabos.app.mq import mqtt_client
|
||||
for device_config in lab_registry.obtain_registry_device_info():
|
||||
mqtt_client.publish_registry(device_config["id"], device_config)
|
||||
|
||||
for device_info in lab_registry.obtain_registry_device_info():
|
||||
mqtt_client.publish_registry(device_info["id"], device_info)
|
||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||
mqtt_client.publish_registry(resource_info["id"], resource_info)
|
||||
|
||||
# 首次发现网络中的设备
|
||||
self._discover_devices()
|
||||
@@ -141,12 +175,36 @@ class HostNode(BaseROS2DeviceNode):
|
||||
].items():
|
||||
controller_config["update_rate"] = update_rate
|
||||
self.initialize_controller(controller_id, controller_config)
|
||||
|
||||
resources_config.insert(
|
||||
0,
|
||||
{
|
||||
"id": "host_node",
|
||||
"name": "host_node",
|
||||
"parent": None,
|
||||
"type": "device",
|
||||
"class": "host_node",
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": [],
|
||||
},
|
||||
)
|
||||
resource_with_parent_name = []
|
||||
resource_ids_to_instance = {i["id"]: i for i in resources_config}
|
||||
for res in resources_config:
|
||||
if res.get("parent") and res.get("type") == "device" and res.get("class"):
|
||||
parent_id = res.get("parent")
|
||||
parent_res = resource_ids_to_instance[parent_id]
|
||||
if parent_res.get("type") == "device" and parent_res.get("class"):
|
||||
resource_with_parent_name.append(copy.deepcopy(res))
|
||||
resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
|
||||
continue
|
||||
resource_with_parent_name.append(copy.deepcopy(res))
|
||||
try:
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "resource_add"):
|
||||
self.lab_logger().info("[Host Node-Resource] Adding resources to bridge.")
|
||||
bridge.resource_add(add_schema(resources_config))
|
||||
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name))
|
||||
except Exception as ex:
|
||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
@@ -156,6 +214,10 @@ class HostNode(BaseROS2DeviceNode):
|
||||
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
||||
)
|
||||
|
||||
# 添加ping-pong相关属性
|
||||
self._ping_responses = {} # 存储ping响应
|
||||
self._ping_lock = threading.Lock()
|
||||
|
||||
self.lab_logger().info("[Host Node] Host node initialized.")
|
||||
HostNode._ready_event.set()
|
||||
|
||||
@@ -191,7 +253,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
# 如果是新设备,记录并创建ActionClient
|
||||
if edge_device_id not in self.devices_names:
|
||||
self.lab_logger().info(f"[Host Node] Discovered new device: {device_key}")
|
||||
self.lab_logger().info(f"[Host Node] Discovered new device: {edge_device_id}")
|
||||
self.devices_names[edge_device_id] = namespace
|
||||
self._create_action_clients_for_device(device_id, namespace)
|
||||
self._online_devices.add(device_key)
|
||||
@@ -200,7 +262,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
target=self._send_re_register,
|
||||
args=(sclient,),
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||
).start()
|
||||
elif device_key not in self._online_devices:
|
||||
# 设备重新上线
|
||||
@@ -211,7 +273,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
target=self._send_re_register,
|
||||
args=(sclient,),
|
||||
daemon=True,
|
||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}"
|
||||
name=f"ROSDevice{self.device_id}_query_host_name_{namespace}",
|
||||
).start()
|
||||
|
||||
# 检测离线设备
|
||||
@@ -255,7 +317,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self, action_type, action_id, callback_group=self.callback_group
|
||||
)
|
||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
|
||||
action_name = action_id[len(namespace) + 1:]
|
||||
action_name = action_id[len(namespace) + 1 :]
|
||||
edge_device_id = namespace[9:]
|
||||
# from unilabos.app.mq import mqtt_client
|
||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||
@@ -268,30 +330,84 @@ class HostNode(BaseROS2DeviceNode):
|
||||
except Exception as e:
|
||||
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
|
||||
|
||||
def add_resource_from_outer(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]):
|
||||
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params):
|
||||
def create_resource_detailed(
|
||||
self,
|
||||
resources: list["Resource"],
|
||||
device_ids: list[str],
|
||||
bind_parent_ids: list[str],
|
||||
bind_locations: list[Point],
|
||||
other_calling_params: list[str],
|
||||
):
|
||||
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(
|
||||
resources, device_ids, bind_parent_ids, bind_locations, other_calling_params
|
||||
):
|
||||
# 这里要求device_id传入必须是edge_device_id
|
||||
namespace = "/devices/" + device_id
|
||||
srv_address = f"/srv{namespace}/append_resource"
|
||||
sclient = self.create_client(SerialCommand, srv_address)
|
||||
sclient.wait_for_service()
|
||||
request = SerialCommand.Request()
|
||||
request.command = json.dumps({
|
||||
"resource": resource,
|
||||
"namespace": namespace,
|
||||
"edge_device_id": device_id,
|
||||
"bind_parent_id": bind_parent_id,
|
||||
"bind_location": {
|
||||
"x": bind_location.x,
|
||||
"y": bind_location.y,
|
||||
"z": bind_location.z,
|
||||
request.command = json.dumps(
|
||||
{
|
||||
"resource": resource, # 单个/单组 可为 list[list[Resource]]
|
||||
"namespace": namespace,
|
||||
"edge_device_id": device_id,
|
||||
"bind_parent_id": bind_parent_id,
|
||||
"bind_location": {
|
||||
"x": bind_location.x,
|
||||
"y": bind_location.y,
|
||||
"z": bind_location.z,
|
||||
},
|
||||
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
||||
},
|
||||
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
|
||||
}, ensure_ascii=False)
|
||||
ensure_ascii=False,
|
||||
)
|
||||
response = sclient.call(request)
|
||||
pass
|
||||
pass
|
||||
|
||||
def create_resource(
|
||||
self,
|
||||
device_id: str,
|
||||
res_id: str,
|
||||
class_name: str,
|
||||
parent: str,
|
||||
bind_locations: Point,
|
||||
liquid_input_slot: list[int],
|
||||
liquid_type: list[str],
|
||||
liquid_volume: list[int],
|
||||
slot_on_deck: int,
|
||||
):
|
||||
init_new_res = initialize_resource(
|
||||
{
|
||||
"name": res_id,
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
"z": bind_locations.z,
|
||||
},
|
||||
}
|
||||
) # flatten的格式
|
||||
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||
device_ids = [device_id]
|
||||
bind_parent_id = [parent]
|
||||
bind_location = [bind_locations]
|
||||
other_calling_param = [
|
||||
json.dumps(
|
||||
{
|
||||
"ADD_LIQUID_TYPE": liquid_type,
|
||||
"LIQUID_VOLUME": liquid_volume,
|
||||
"LIQUID_INPUT_SLOT": liquid_input_slot,
|
||||
"initialize_full": False,
|
||||
"slot": slot_on_deck,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
return self.create_resource_detailed(resources, device_ids, bind_parent_id, bind_location, other_calling_param)
|
||||
|
||||
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
根据配置初始化设备,
|
||||
@@ -319,7 +435,9 @@ class HostNode(BaseROS2DeviceNode):
|
||||
if action_id not in self._action_clients:
|
||||
action_type = action_value_mapping["type"]
|
||||
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
|
||||
self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的
|
||||
self.lab_logger().debug(
|
||||
f"[Host Node] Created ActionClient (Local): {action_id}"
|
||||
) # 子设备再创建用的是Discover发现的
|
||||
# from unilabos.app.mq import mqtt_client
|
||||
# info_with_schema = ros_action_to_json_schema(action_type)
|
||||
# mqtt_client.publish_actions(action_name, {
|
||||
@@ -419,7 +537,12 @@ class HostNode(BaseROS2DeviceNode):
|
||||
)
|
||||
|
||||
def send_goal(
|
||||
self, device_id: str, action_name: str, action_kwargs: Dict[str, Any], goal_uuid: Optional[str] = None
|
||||
self,
|
||||
device_id: str,
|
||||
action_name: str,
|
||||
action_kwargs: Dict[str, Any],
|
||||
goal_uuid: Optional[str] = None,
|
||||
server_info: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
向设备发送目标请求
|
||||
@@ -431,6 +554,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
goal_uuid: 目标UUID,如果为None则自动生成
|
||||
"""
|
||||
action_id = f"/devices/{device_id}/{action_name}"
|
||||
if action_name == "test_latency" and server_info is not None:
|
||||
self.server_latest_timestamp = server_info.get("send_timestamp", 0.0)
|
||||
if action_id not in self._action_clients:
|
||||
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
|
||||
return
|
||||
@@ -725,3 +850,148 @@ class HostNode(BaseROS2DeviceNode):
|
||||
# 这里可以实现返回资源列表的逻辑
|
||||
self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}")
|
||||
return response
|
||||
|
||||
def test_latency(self):
|
||||
"""
|
||||
测试网络延迟的action实现
|
||||
通过5次ping-pong机制校对时间误差并计算实际延迟
|
||||
"""
|
||||
import time
|
||||
import uuid as uuid_module
|
||||
|
||||
self.lab_logger().info("=" * 60)
|
||||
self.lab_logger().info("开始网络延迟测试...")
|
||||
|
||||
# 记录任务开始执行的时间
|
||||
task_start_time = time.time()
|
||||
|
||||
# 进行5次ping-pong测试
|
||||
ping_results = []
|
||||
|
||||
for i in range(5):
|
||||
self.lab_logger().info(f"第{i+1}/5次ping-pong测试...")
|
||||
|
||||
# 生成唯一的ping ID
|
||||
ping_id = str(uuid_module.uuid4())
|
||||
|
||||
# 记录发送时间
|
||||
send_timestamp = time.time()
|
||||
|
||||
# 发送ping
|
||||
from unilabos.app.mq import mqtt_client
|
||||
|
||||
mqtt_client.send_ping(ping_id, send_timestamp)
|
||||
|
||||
# 等待pong响应
|
||||
timeout = 10.0
|
||||
start_wait_time = time.time()
|
||||
|
||||
while time.time() - start_wait_time < timeout:
|
||||
with self._ping_lock:
|
||||
if ping_id in self._ping_responses:
|
||||
pong_data = self._ping_responses.pop(ping_id)
|
||||
break
|
||||
time.sleep(0.001)
|
||||
else:
|
||||
self.lab_logger().error(f"❌ 第{i+1}次测试超时")
|
||||
continue
|
||||
|
||||
# 计算本次测试结果
|
||||
receive_timestamp = time.time()
|
||||
client_timestamp = pong_data["client_timestamp"]
|
||||
server_timestamp = pong_data["server_timestamp"]
|
||||
|
||||
# 往返时间
|
||||
rtt_ms = (receive_timestamp - send_timestamp) * 1000
|
||||
|
||||
# 客户端与服务端时间差(客户端时间 - 服务端时间)
|
||||
# 假设网络延迟对称,取中间点的服务端时间
|
||||
mid_point_time = send_timestamp + (receive_timestamp - send_timestamp) / 2
|
||||
time_diff_ms = (mid_point_time - server_timestamp) * 1000
|
||||
|
||||
ping_results.append({"rtt_ms": rtt_ms, "time_diff_ms": time_diff_ms})
|
||||
|
||||
self.lab_logger().info(f"✅ 第{i+1}次: 往返时间={rtt_ms:.2f}ms, 时间差={time_diff_ms:.2f}ms")
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
if not ping_results:
|
||||
self.lab_logger().error("❌ 所有ping-pong测试都失败了")
|
||||
return {"status": "all_timeout"}
|
||||
|
||||
# 统计分析
|
||||
rtts = [r["rtt_ms"] for r in ping_results]
|
||||
time_diffs = [r["time_diff_ms"] for r in ping_results]
|
||||
|
||||
avg_rtt_ms = sum(rtts) / len(rtts)
|
||||
avg_time_diff_ms = sum(time_diffs) / len(time_diffs)
|
||||
max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs)))
|
||||
|
||||
self.lab_logger().info("-" * 50)
|
||||
self.lab_logger().info("[测试统计]")
|
||||
self.lab_logger().info(f"有效测试次数: {len(ping_results)}/5")
|
||||
self.lab_logger().info(f"平均往返时间: {avg_rtt_ms:.2f}ms")
|
||||
self.lab_logger().info(f"平均时间差: {avg_time_diff_ms:.2f}ms")
|
||||
self.lab_logger().info(f"时间差范围: {min(time_diffs):.2f}ms ~ {max(time_diffs):.2f}ms")
|
||||
self.lab_logger().info(f"最大时间误差: ±{max_time_diff_error_ms:.2f}ms")
|
||||
|
||||
# 计算任务执行延迟
|
||||
if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0:
|
||||
self.lab_logger().info("-" * 50)
|
||||
self.lab_logger().info("[任务执行延迟分析]")
|
||||
self.lab_logger().info(f"服务端任务下发时间: {self.server_latest_timestamp:.6f}")
|
||||
self.lab_logger().info(f"客户端任务开始时间: {task_start_time:.6f}")
|
||||
|
||||
# 原始时间差(不考虑时间同步误差)
|
||||
raw_delay_ms = (task_start_time - self.server_latest_timestamp) * 1000
|
||||
|
||||
# 考虑时间同步误差后的延迟(用平均时间差校正)
|
||||
corrected_delay_ms = raw_delay_ms - avg_time_diff_ms
|
||||
|
||||
self.lab_logger().info(f"📊 原始时间差: {raw_delay_ms:.2f}ms")
|
||||
self.lab_logger().info(f"🔧 时间同步校正: {avg_time_diff_ms:.2f}ms")
|
||||
self.lab_logger().info(f"⏰ 实际任务延迟: {corrected_delay_ms:.2f}ms")
|
||||
self.lab_logger().info(f"📏 误差范围: ±{max_time_diff_error_ms:.2f}ms")
|
||||
|
||||
# 给出延迟范围
|
||||
min_delay = corrected_delay_ms - max_time_diff_error_ms
|
||||
max_delay = corrected_delay_ms + max_time_diff_error_ms
|
||||
self.lab_logger().info(f"📋 延迟范围: {min_delay:.2f}ms ~ {max_delay:.2f}ms")
|
||||
|
||||
else:
|
||||
self.lab_logger().warning("⚠️ 无法获取服务端任务下发时间,跳过任务延迟分析")
|
||||
corrected_delay_ms = -1
|
||||
|
||||
self.lab_logger().info("=" * 60)
|
||||
|
||||
return {
|
||||
"avg_rtt_ms": avg_rtt_ms,
|
||||
"avg_time_diff_ms": avg_time_diff_ms,
|
||||
"max_time_error_ms": max_time_diff_error_ms,
|
||||
"task_delay_ms": corrected_delay_ms if corrected_delay_ms > 0 else -1,
|
||||
"raw_delay_ms": (
|
||||
raw_delay_ms if hasattr(self, "server_latest_timestamp") and self.server_latest_timestamp > 0 else -1
|
||||
),
|
||||
"test_count": len(ping_results),
|
||||
"status": "success",
|
||||
}
|
||||
|
||||
def handle_pong_response(self, pong_data: dict):
|
||||
"""
|
||||
处理pong响应
|
||||
"""
|
||||
ping_id = pong_data.get("ping_id")
|
||||
if ping_id:
|
||||
with self._ping_lock:
|
||||
self._ping_responses[ping_id] = pong_data
|
||||
|
||||
# 详细信息合并为一条日志
|
||||
client_timestamp = pong_data.get("client_timestamp", 0)
|
||||
server_timestamp = pong_data.get("server_timestamp", 0)
|
||||
current_time = time.time()
|
||||
|
||||
self.lab_logger().debug(
|
||||
f"📨 Pong | ID:{ping_id[:8]}.. | C→S→C: {client_timestamp:.3f}→{server_timestamp:.3f}→{current_time:.3f}"
|
||||
)
|
||||
else:
|
||||
self.lab_logger().warning("⚠️ 收到无效的Pong响应(缺少ping_id)")
|
||||
|
||||
@@ -13,6 +13,7 @@ endif()
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
add_compile_options(-include cstdint)
|
||||
endif()
|
||||
|
||||
# find dependencies
|
||||
@@ -27,7 +28,17 @@ set(action_files
|
||||
"action/HeatChill.action"
|
||||
"action/HeatChillStart.action"
|
||||
"action/HeatChillStop.action"
|
||||
|
||||
"action/Filter.action"
|
||||
"action/Add.action"
|
||||
"action/Centrifuge.action"
|
||||
"action/Crystallize.action"
|
||||
"action/Dry.action"
|
||||
"action/Purge.action"
|
||||
"action/StartPurge.action"
|
||||
"action/StartStir.action"
|
||||
"action/StopPurge.action"
|
||||
"action/StopStir.action"
|
||||
"action/Transfer.action"
|
||||
"action/LiquidHandlerAspirate.action"
|
||||
"action/LiquidHandlerDiscardTips.action"
|
||||
"action/LiquidHandlerDispense.action"
|
||||
@@ -43,14 +54,10 @@ set(action_files
|
||||
"action/LiquidHandlerStamp.action"
|
||||
"action/LiquidHandlerTransfer.action"
|
||||
|
||||
"action/DPLiquidHandlerAddLiquid.action"
|
||||
"action/DPLiquidHandlerCustomDelay.action"
|
||||
"action/DPLiquidHandlerMix.action"
|
||||
"action/DPLiquidHandlerMoveTo.action"
|
||||
"action/DPLiquidHandlerRemoveLiquid.action"
|
||||
"action/DPLiquidHandlerSetTiprack.action"
|
||||
"action/DPLiquidHandlerTouchTip.action"
|
||||
"action/DPLiquidHandlerTransferLiquid.action"
|
||||
"action/LiquidHandlerAdd.action"
|
||||
"action/LiquidHandlerMix.action"
|
||||
"action/LiquidHandlerMoveTo.action"
|
||||
"action/LiquidHandlerRemove.action"
|
||||
|
||||
"action/EmptyIn.action"
|
||||
"action/FloatSingleInput.action"
|
||||
@@ -59,9 +66,10 @@ set(action_files
|
||||
"action/Point3DSeparateInput.action"
|
||||
|
||||
"action/ResourceCreateFromOuter.action"
|
||||
"action/ResourceCreateFromOuterEasy.action"
|
||||
|
||||
"action/SolidDispenseAddPowderTube.action"
|
||||
|
||||
|
||||
"action/PumpTransfer.action"
|
||||
"action/Clean.action"
|
||||
"action/Separate.action"
|
||||
|
||||
19
unilabos_msgs/action/Add.action
Normal file
19
unilabos_msgs/action/Add.action
Normal file
@@ -0,0 +1,19 @@
|
||||
# Goal - 添加试剂的目标参数
|
||||
string vessel # 目标容器
|
||||
string reagent # 试剂名称
|
||||
float64 volume # 体积 (可选)
|
||||
float64 mass # 质量 (可选)
|
||||
string amount # 数量描述 (可选)
|
||||
float64 time # 添加时间 (可选)
|
||||
bool stir # 是否搅拌
|
||||
float64 stir_speed # 搅拌速度 (可选)
|
||||
bool viscous # 是否为粘性液体
|
||||
string purpose # 添加目的 (可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
string current_status # 当前状态描述
|
||||
15
unilabos_msgs/action/Centrifuge.action
Normal file
15
unilabos_msgs/action/Centrifuge.action
Normal file
@@ -0,0 +1,15 @@
|
||||
# Goal - 离心操作的目标参数
|
||||
string vessel # 离心容器
|
||||
float64 speed # 离心速度 (rpm)
|
||||
float64 time # 离心时间 (秒)
|
||||
float64 temp # 温度 (可选,摄氏度)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_speed # 当前转速
|
||||
float64 current_temp # 当前温度
|
||||
string current_status # 当前状态描述
|
||||
13
unilabos_msgs/action/Crystallize.action
Normal file
13
unilabos_msgs/action/Crystallize.action
Normal file
@@ -0,0 +1,13 @@
|
||||
# Goal - 结晶操作的目标参数
|
||||
string vessel # 结晶容器
|
||||
float64 ramp_time # 升温/降温时间 (可选,秒)
|
||||
float64 ramp_temp # 目标温度 (可选,摄氏度)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_temp # 当前温度
|
||||
string current_status # 当前状态描述
|
||||
@@ -1,6 +0,0 @@
|
||||
float64 seconds
|
||||
string msg
|
||||
---
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
@@ -1,5 +0,0 @@
|
||||
Resource[] tip_racks
|
||||
---
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
@@ -1,5 +0,0 @@
|
||||
Resource[] targets
|
||||
---
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
@@ -1,25 +0,0 @@
|
||||
float64[] asp_vols
|
||||
float64[] dis_vols
|
||||
Resource[] sources
|
||||
Resource[] targets
|
||||
Resource[] tip_racks
|
||||
int32[] use_channels
|
||||
float64[] asp_flow_rates
|
||||
float64[] dis_flow_rates
|
||||
geometry_msgs/Point[] offsets
|
||||
bool touch_tip
|
||||
float64[] liquid_height
|
||||
float64[] blow_out_air_volume
|
||||
string spread
|
||||
bool is_96_well
|
||||
string mix_stage
|
||||
int32[] mix_times
|
||||
int32 mix_vol
|
||||
int32 mix_rate
|
||||
float64 mix_liquid_height
|
||||
int32[] delays
|
||||
string[] none_keys
|
||||
---
|
||||
bool success
|
||||
---
|
||||
# 反馈
|
||||
16
unilabos_msgs/action/Dry.action
Normal file
16
unilabos_msgs/action/Dry.action
Normal file
@@ -0,0 +1,16 @@
|
||||
# Goal - 干燥操作的目标参数
|
||||
string vessel # 干燥容器
|
||||
float64 time # 干燥时间 (可选,秒)
|
||||
float64 pressure # 压力 (可选,Pa)
|
||||
float64 temp # 温度 (可选,摄氏度)
|
||||
bool continue_heatchill # 是否继续加热冷却
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_temp # 当前温度
|
||||
float64 current_pressure # 当前压力
|
||||
string current_status # 当前状态描述
|
||||
18
unilabos_msgs/action/Filter.action
Normal file
18
unilabos_msgs/action/Filter.action
Normal file
@@ -0,0 +1,18 @@
|
||||
# Goal - 过滤操作的目标参数
|
||||
string vessel # 过滤容器
|
||||
string filtrate_vessel # 滤液容器 (可选)
|
||||
bool stir # 是否搅拌
|
||||
float64 stir_speed # 搅拌速度 (可选)
|
||||
float64 temp # 温度 (可选,摄氏度)
|
||||
bool continue_heatchill # 是否继续加热冷却
|
||||
float64 volume # 过滤体积 (可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_temp # 当前温度
|
||||
float64 filtered_volume # 已过滤体积
|
||||
string current_status # 当前状态描述
|
||||
@@ -5,7 +5,7 @@ float64[] flow_rates
|
||||
geometry_msgs/Point[] offsets
|
||||
float64[] liquid_height
|
||||
float64[] blow_out_air_volume
|
||||
string spread="wide"
|
||||
string spread
|
||||
---
|
||||
bool success
|
||||
---
|
||||
@@ -5,7 +5,7 @@ int32[] use_channels
|
||||
float64[] flow_rates
|
||||
geometry_msgs/Point[] offsets
|
||||
int32[] blow_out_air_volume
|
||||
string spread="wide"
|
||||
string spread
|
||||
---
|
||||
# 结果字段
|
||||
bool success
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
# Bio
|
||||
Resource source
|
||||
float64[] asp_vols
|
||||
float64[] dis_vols
|
||||
Resource[] sources
|
||||
Resource[] targets
|
||||
float64 source_vol
|
||||
float64[] ratios
|
||||
float64[] target_vols
|
||||
float64 aspiration_flow_rate
|
||||
float64[] dispense_flow_rates
|
||||
Resource[] tip_racks
|
||||
int32[] use_channels
|
||||
float64[] asp_flow_rates
|
||||
float64[] dis_flow_rates
|
||||
geometry_msgs/Point[] offsets
|
||||
bool touch_tip
|
||||
float64[] liquid_height
|
||||
float64[] blow_out_air_volume
|
||||
string spread
|
||||
bool is_96_well
|
||||
string mix_stage
|
||||
int32[] mix_times
|
||||
int32 mix_vol
|
||||
int32 mix_rate
|
||||
float64 mix_liquid_height
|
||||
int32[] delays
|
||||
string[] none_keys
|
||||
---
|
||||
bool success
|
||||
---
|
||||
---
|
||||
# 反馈
|
||||
16
unilabos_msgs/action/Purge.action
Normal file
16
unilabos_msgs/action/Purge.action
Normal file
@@ -0,0 +1,16 @@
|
||||
# Goal - 清洗/吹扫操作的目标参数
|
||||
string vessel # 清洗容器
|
||||
string gas # 清洗气体 (可选)
|
||||
float64 time # 清洗时间 (可选,秒)
|
||||
float64 pressure # 压力 (可选,Pa)
|
||||
float64 flow_rate # 流速 (可选,mL/min)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_pressure # 当前压力
|
||||
float64 current_flow_rate # 当前流速
|
||||
string current_status # 当前状态描述
|
||||
12
unilabos_msgs/action/ResourceCreateFromOuterEasy.action
Normal file
12
unilabos_msgs/action/ResourceCreateFromOuterEasy.action
Normal file
@@ -0,0 +1,12 @@
|
||||
string res_id
|
||||
string device_id
|
||||
string class_name
|
||||
string parent
|
||||
geometry_msgs/Point bind_locations
|
||||
int32[] liquid_input_slot
|
||||
string[] liquid_type
|
||||
float32[] liquid_volume
|
||||
int32 slot_on_deck
|
||||
---
|
||||
bool success
|
||||
---
|
||||
15
unilabos_msgs/action/StartPurge.action
Normal file
15
unilabos_msgs/action/StartPurge.action
Normal file
@@ -0,0 +1,15 @@
|
||||
# Goal - 启动清洗/吹扫操作的目标参数
|
||||
string vessel # 清洗容器
|
||||
string gas # 清洗气体 (可选)
|
||||
float64 pressure # 压力 (可选,Pa)
|
||||
float64 flow_rate # 流速 (可选,mL/min)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_pressure # 当前压力
|
||||
float64 current_flow_rate # 当前流速
|
||||
string current_status # 当前状态描述
|
||||
13
unilabos_msgs/action/StartStir.action
Normal file
13
unilabos_msgs/action/StartStir.action
Normal file
@@ -0,0 +1,13 @@
|
||||
# Goal - 启动搅拌操作的目标参数
|
||||
string vessel # 搅拌容器
|
||||
float64 stir_speed # 搅拌速度 (可选,rpm)
|
||||
string purpose # 搅拌目的 (可选)
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 current_speed # 当前搅拌速度
|
||||
string current_status # 当前状态描述
|
||||
10
unilabos_msgs/action/StopPurge.action
Normal file
10
unilabos_msgs/action/StopPurge.action
Normal file
@@ -0,0 +1,10 @@
|
||||
# Goal - 停止清洗/吹扫操作的目标参数
|
||||
string vessel # 清洗容器
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
string current_status # 当前状态描述
|
||||
10
unilabos_msgs/action/StopStir.action
Normal file
10
unilabos_msgs/action/StopStir.action
Normal file
@@ -0,0 +1,10 @@
|
||||
# Goal - 停止搅拌操作的目标参数
|
||||
string vessel # 搅拌容器
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
string current_status # 当前状态描述
|
||||
19
unilabos_msgs/action/Transfer.action
Normal file
19
unilabos_msgs/action/Transfer.action
Normal file
@@ -0,0 +1,19 @@
|
||||
string from_vessel # 源容器
|
||||
string to_vessel # 目标容器
|
||||
float64 volume # 转移体积 (可选)
|
||||
string amount # 数量描述 (可选)
|
||||
float64 time # 转移时间 (可选,秒)
|
||||
bool viscous # 是否为粘性液体
|
||||
string rinsing_solvent # 冲洗溶剂 (可选)
|
||||
float64 rinsing_volume # 冲洗体积 (可选)
|
||||
int32 rinsing_repeats # 冲洗重复次数
|
||||
bool solid # 是否涉及固体
|
||||
---
|
||||
# Result - 操作结果
|
||||
bool success # 操作是否成功
|
||||
string message # 结果消息
|
||||
---
|
||||
# Feedback - 实时反馈
|
||||
float64 progress # 进度百分比 (0-100)
|
||||
float64 transferred_volume # 已转移体积
|
||||
string current_status # 当前状态描述
|
||||
Reference in New Issue
Block a user