mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-08 07:55:12 +00:00
Compare commits
176 Commits
workflow_u
...
7b04f3fa50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b04f3fa50 | ||
|
|
ff6998501e | ||
|
|
b875f86bbb | ||
|
|
d058de3702 | ||
|
|
6385065ba3 | ||
|
|
3b32dcf066 | ||
|
|
7c714721db | ||
|
|
5478ba3237 | ||
|
|
49f1aa9c28 | ||
|
|
d5d516f0ef | ||
|
|
4471fed4b8 | ||
|
|
30d143e1a5 | ||
|
|
75ea45f21e | ||
|
|
66af337d6c | ||
|
|
ae3c65c1d3 | ||
|
|
11e4f053f1 | ||
|
|
96f37b3b0d | ||
|
|
d7d0a27976 | ||
|
|
34151f5cb2 | ||
|
|
369a21b904 | ||
|
|
90169981c1 | ||
|
|
d297abfd19 | ||
|
|
9c515a252a | ||
|
|
ea5e7a5ce2 | ||
|
|
2e9a0a4677 | ||
|
|
4c7aa8a89a | ||
|
|
d8a0c5e715 | ||
|
|
133ffaac17 | ||
|
|
729a0fcf0c | ||
|
|
6ae77e0408 | ||
|
|
bab4b1d67a | ||
|
|
12c17ec26e | ||
|
|
6577fe12eb | ||
|
|
f1fee5fad9 | ||
|
|
9b3377aedb | ||
|
|
526327727d | ||
|
|
aaa86314e3 | ||
|
|
6a14104e6b | ||
|
|
ab0c4b708b | ||
|
|
c0b7f2decd | ||
|
|
b6c9530c61 | ||
|
|
8698821c52 | ||
|
|
3f53f88390 | ||
|
|
e840516ba4 | ||
|
|
146d8c5296 | ||
|
|
6573c9e02e | ||
|
|
c7b9c6a825 | ||
|
|
48c43d3303 | ||
|
|
55be5e8188 | ||
|
|
1b9f3c666d | ||
|
|
097114d38c | ||
|
|
5bec899479 | ||
|
|
5e86112ebf | ||
|
|
24ecb13b79 | ||
|
|
2573d34713 | ||
|
|
106d71e1db | ||
|
|
3c2a4a64ac | ||
|
|
1e00a66a65 | ||
|
|
46da42deef | ||
|
|
101c1bc3cc | ||
|
|
a62112ae26 | ||
|
|
dd5a7cab75 | ||
|
|
39de3ac58e | ||
|
|
b99969278c | ||
|
|
b957ad2f71 | ||
|
|
e1a7c3a103 | ||
|
|
e63c15997c | ||
|
|
c5a495f409 | ||
|
|
5b240cb0ea | ||
|
|
147b8f47c0 | ||
|
|
6d2489af5f | ||
|
|
807dcdd226 | ||
|
|
8a29bc5597 | ||
|
|
6f6c70ee57 | ||
|
|
478a85951c | ||
|
|
0f2555c90c | ||
|
|
d2dda6ee03 | ||
|
|
208540b307 | ||
|
|
cb7c56a1d9 | ||
|
|
ea2e9c3e3a | ||
|
|
0452a68180 | ||
|
|
90a0f3db9b | ||
|
|
055d120ba8 | ||
|
|
a948f09f60 | ||
|
|
c2c2c2f020 | ||
|
|
4decd9a174 | ||
|
|
83c765f0ab | ||
|
|
3600b6f934 | ||
|
|
f0576e5666 | ||
|
|
8e1dbb56b1 | ||
|
|
013c25f3aa | ||
|
|
3d71c8bc78 | ||
|
|
42f0994147 | ||
|
|
4223f9b72c | ||
|
|
bec58e1301 | ||
|
|
6f9773157c | ||
|
|
da50e435c1 | ||
|
|
34e03bbd6e | ||
|
|
ad5168c3eb | ||
|
|
2dde5b6aae | ||
|
|
45a73e2f6d | ||
|
|
fbff27a52d | ||
|
|
1b190ee62f | ||
|
|
83abf877b5 | ||
|
|
f3637d4043 | ||
|
|
c12c2a876c | ||
|
|
6cdd8c18e8 | ||
|
|
3d60cb36b8 | ||
|
|
5df304bc64 | ||
|
|
6d5ada06de | ||
|
|
aad23596b6 | ||
|
|
b43f2321cd | ||
|
|
8617b1284f | ||
|
|
cd1e9a9f7d | ||
|
|
3d607db49a | ||
|
|
3dc62e3e99 | ||
|
|
d199fda9a5 | ||
|
|
ed2858a610 | ||
|
|
de28c50d8b | ||
|
|
e373220ce3 | ||
|
|
b6a3f17e9b | ||
|
|
49a9f05c51 | ||
|
|
32e370a562 | ||
|
|
852d10d751 | ||
|
|
b47f67d129 | ||
|
|
194985222e | ||
|
|
948f590b47 | ||
|
|
164417e1cf | ||
|
|
1a107cfd18 | ||
|
|
65d0cbe28a | ||
|
|
3c98c77cab | ||
|
|
d6b8104824 | ||
|
|
1223e05dcc | ||
|
|
a52133b7d0 | ||
|
|
80380d1f4b | ||
|
|
5668310401 | ||
|
|
78239ab1a3 | ||
|
|
fa5db06347 | ||
|
|
2b428080e7 | ||
|
|
9eb271f64e | ||
|
|
752442cb37 | ||
|
|
9d2bfec1dd | ||
|
|
5212d2d8eb | ||
|
|
44c191fe90 | ||
|
|
7a51b2adc1 | ||
|
|
2d034f728a | ||
|
|
8ab108c489 | ||
|
|
4dbb6649b4 | ||
|
|
dc197bffe8 | ||
|
|
49bb11b2a3 | ||
|
|
d407423aaa | ||
|
|
111c3f42e4 | ||
|
|
2990e70c25 | ||
|
|
0d24606d46 | ||
|
|
2baa232b86 | ||
|
|
b7a16cdfc8 | ||
|
|
8921bcd9fb | ||
|
|
5038219fe6 | ||
|
|
0d2f1be37a | ||
|
|
6b649bfdec | ||
|
|
ba6a43c594 | ||
|
|
ea6f25d1ce | ||
|
|
e5749a8058 | ||
|
|
09fc17429e | ||
|
|
bdf97be256 | ||
|
|
dbd1557095 | ||
|
|
ff8b75bf1f | ||
|
|
bed9720de3 | ||
|
|
1e01eae896 | ||
|
|
6155ec2798 | ||
|
|
279c5ed519 | ||
|
|
5b4f580a6f | ||
|
|
e971424220 | ||
|
|
82881f5882 | ||
|
|
bb1cac0dbd | ||
|
|
275e3a36f7 |
132
.github/workflows/multi-platform-build.yml
vendored
Normal file
132
.github/workflows/multi-platform-build.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: Multi-Platform Conda Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platforms:
|
||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||
required: false
|
||||
default: 'osx-arm64'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux-64
|
||||
env_file: unilabos-linux-64.yaml
|
||||
- os: macos-13 # Intel
|
||||
platform: osx-64
|
||||
env_file: unilabos-osx-64.yaml
|
||||
- os: macos-latest # ARM64
|
||||
platform: osx-arm64
|
||||
env_file: unilabos-osx-arm64.yaml
|
||||
- os: windows-latest
|
||||
platform: win-64
|
||||
env_file: unilabos-win64.yaml
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -l {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if platform should be built
|
||||
id: should_build
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Miniconda
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: "latest"
|
||||
channels: conda-forge,robostack-staging,defaults
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
auto-activate-base: false
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install boa and build tools
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge boa conda-build
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(boa|conda-build)"
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
|
||||
- name: Build conda package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
|
||||
boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
else
|
||||
boa build -m ./recipes/conda_build_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
fi
|
||||
|
||||
- name: List built packages
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Built packages in conda-bld:"
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" | head -10
|
||||
ls -la $CONDA_PREFIX/conda-bld/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found"
|
||||
ls -la $CONDA_PREFIX/conda-bld/noarch/ || echo "noarch directory not found"
|
||||
echo "CONDA_PREFIX: $CONDA_PREFIX"
|
||||
echo "Full path would be: $CONDA_PREFIX/conda-bld/**/*.tar.bz2"
|
||||
|
||||
- name: Prepare artifacts for upload
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/conda-packages
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} ${{ runner.temp }}/conda-packages/ \;
|
||||
echo "Copied files to temp directory:"
|
||||
ls -la ${{ runner.temp }}/conda-packages/
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conda-package-${{ matrix.platform }}
|
||||
path: ${{ runner.temp }}/conda-packages
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
- name: Create release assets (on tags)
|
||||
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} release-assets/ \;
|
||||
|
||||
- name: Upload to release
|
||||
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: release-assets/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -1,3 +1,5 @@
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/web *.html
|
||||
recursive-include unilabos/app/web *.css
|
||||
recursive-include unilabos/device_mesh/devices *
|
||||
recursive-include unilabos/device_mesh/resources *
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||
|
||||
# Currently, you need to install the `unilabos_msgs` package
|
||||
# You can download the system-specific package from the Release page
|
||||
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
|
||||
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
|
||||
conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
|
||||
7
recipes/macos_sdk_config.yaml
Normal file
7
recipes/macos_sdk_config.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
CONDA_BUILD_SYSROOT:
|
||||
- /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
|
||||
MACOSX_DEPLOYMENT_TARGET:
|
||||
- "11.0"
|
||||
CONDA_SUBDIR:
|
||||
- osx-arm64
|
||||
# boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.4
|
||||
version: 0.9.5
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.4"
|
||||
version: "0.9.5"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.4',
|
||||
version='0.9.5',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
32
test/experiments/comprehensive_protocol/checklist.md
Normal file
32
test/experiments/comprehensive_protocol/checklist.md
Normal file
@@ -0,0 +1,32 @@
|
||||
1. 用到的仪器
|
||||
virtual_multiway_valve() 八通阀门
|
||||
virtual_transfer_pump() 转移泵
|
||||
virtual_centrifuge() 离心机
|
||||
virtual_rotavap() 旋蒸仪
|
||||
virtual_heatchill() 加热器
|
||||
virtual_stirrer() 搅拌器
|
||||
virtual_solenoid_valve() 电磁阀
|
||||
vacuum_pump() vacuum_pump.mock 真空泵
|
||||
gas_source() 气源
|
||||
virtual_filter() 过滤器
|
||||
virtual_column(√) 层析柱
|
||||
separator() homemade_grbl_conductivity 分液漏斗
|
||||
2. 用到的protocol
|
||||
AddProtocol()
|
||||
TransferProtocol() 应该用pump_protocol.py删掉transfer
|
||||
StartStirProtocol()
|
||||
StopStirProtocol()
|
||||
StirProtocol()
|
||||
RunColumnProtocol()
|
||||
CentrifugeProtocol()
|
||||
FilterProtocol()
|
||||
CleanVesselProtocol()
|
||||
DissolveProtocol()
|
||||
FilterThroughProtocol()
|
||||
WashSolidProtocol()
|
||||
SeparateProtocol(√)
|
||||
EvaporateProtocol(√)
|
||||
HeatChillProtocol()
|
||||
HeatChillStartProtocol()
|
||||
HeatChillStopProtocol()
|
||||
EvacuateAndRefillProtocol(√)
|
||||
@@ -0,0 +1,887 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "ComprehensiveProtocolStation",
|
||||
"name": "综合协议测试工作站",
|
||||
"children": [
|
||||
"multiway_valve_1",
|
||||
"multiway_valve_2",
|
||||
"transfer_pump_1",
|
||||
"transfer_pump_2",
|
||||
"reagent_bottle_1",
|
||||
"reagent_bottle_2",
|
||||
"reagent_bottle_3",
|
||||
"reagent_bottle_4",
|
||||
"reagent_bottle_5",
|
||||
"centrifuge_1",
|
||||
"rotavap_1",
|
||||
"main_reactor",
|
||||
"heater_1",
|
||||
"stirrer_1",
|
||||
"stirrer_2",
|
||||
"waste_bottle_1",
|
||||
"waste_bottle_2",
|
||||
"solenoid_valve_1",
|
||||
"solenoid_valve_2",
|
||||
"vacuum_pump_1",
|
||||
"gas_source_1",
|
||||
"filter_1",
|
||||
"column_1",
|
||||
"separator_1",
|
||||
"collection_bottle_1",
|
||||
"collection_bottle_2",
|
||||
"collection_bottle_3"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"protocol_type": [
|
||||
"AddProtocol",
|
||||
"TransferProtocol",
|
||||
"StartStirProtocol",
|
||||
"StopStirProtocol",
|
||||
"StirProtocol",
|
||||
"RunColumnProtocol",
|
||||
"CentrifugeProtocol",
|
||||
"FilterProtocol",
|
||||
"CleanVesselProtocol",
|
||||
"DissolveProtocol",
|
||||
"FilterThroughProtocol",
|
||||
"WashSolidProtocol",
|
||||
"SeparateProtocol",
|
||||
"EvaporateProtocol",
|
||||
"HeatChillProtocol",
|
||||
"HeatChillStartProtocol",
|
||||
"HeatChillStopProtocol",
|
||||
"EvacuateAndRefillProtocol"
|
||||
]
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "multiway_valve_1",
|
||||
"name": "八通阀门1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_multiway_valve",
|
||||
"position": {
|
||||
"x": 400,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"positions": 8,
|
||||
"current_position": 1
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Ready",
|
||||
"current_position": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "multiway_valve_2",
|
||||
"name": "八通阀门2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_multiway_valve",
|
||||
"position": {
|
||||
"x": 800,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"positions": 8,
|
||||
"current_position": 1
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Ready",
|
||||
"current_position": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "transfer_pump_1",
|
||||
"name": "转移泵1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_transfer_pump",
|
||||
"position": {
|
||||
"x": 350,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 25.0,
|
||||
"transfer_rate": 10.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "transfer_pump_2",
|
||||
"name": "转移泵2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_transfer_pump",
|
||||
"position": {
|
||||
"x": 850,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 25.0,
|
||||
"transfer_rate": 10.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reagent_bottle_1",
|
||||
"name": "试剂瓶1-DMF",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 200,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "DMF"
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "DMF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reagent_bottle_2",
|
||||
"name": "试剂瓶2-乙酸乙酯",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 250,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "ethyl_acetate"
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "ethyl_acetate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reagent_bottle_3",
|
||||
"name": "试剂瓶3-己烷",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 300,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "hexane"
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "hexane"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reagent_bottle_4",
|
||||
"name": "试剂瓶4-甲醇",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 900,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "methanol"
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "methanol"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reagent_bottle_5",
|
||||
"name": "试剂瓶5-水",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 950,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "water"
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "water"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "centrifuge_1",
|
||||
"name": "离心机",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_centrifuge",
|
||||
"position": {
|
||||
"x": 200,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_speed": 15000.0,
|
||||
"max_temp": 40.0,
|
||||
"min_temp": 4.0
|
||||
},
|
||||
"data": {
|
||||
"current_speed": 0.0,
|
||||
"status": "Idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rotavap_1",
|
||||
"name": "旋转蒸发仪",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_rotavap",
|
||||
"position": {
|
||||
"x": 300,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_temp": 180.0,
|
||||
"max_rotation_speed": 280.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_temp": 25.0,
|
||||
"rotation_speed": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "main_reactor",
|
||||
"name": "主反应器",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 400,
|
||||
"y": 450,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"max_temp": 200.0,
|
||||
"min_temp": -20.0,
|
||||
"has_stirrer": true,
|
||||
"has_heater": true
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0,
|
||||
"current_temp": 25.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "heater_1",
|
||||
"name": "加热器",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_heatchill",
|
||||
"position": {
|
||||
"x": 450,
|
||||
"y": 450,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_temp": 200.0,
|
||||
"min_temp": -20.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_temp": 25.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "stirrer_1",
|
||||
"name": "搅拌器1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_stirrer",
|
||||
"position": {
|
||||
"x": 350,
|
||||
"y": 450,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_speed": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_speed": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "stirrer_2",
|
||||
"name": "搅拌器2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_stirrer",
|
||||
"position": {
|
||||
"x": 351,
|
||||
"y": 451,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_speed": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_speed": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "waste_bottle_1",
|
||||
"name": "废液瓶1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 500,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "waste_bottle_2",
|
||||
"name": "废液瓶2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1100,
|
||||
"y": 500,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solenoid_valve_1",
|
||||
"name": "电磁阀1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solenoid_valve",
|
||||
"position": {
|
||||
"x": 700,
|
||||
"y": 200,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"voltage": 12.0,
|
||||
"response_time": 0.1
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Closed",
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solenoid_valve_2",
|
||||
"name": "电磁阀2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solenoid_valve",
|
||||
"position": {
|
||||
"x": 700,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"voltage": 12.0,
|
||||
"response_time": 0.1
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Closed",
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vacuum_pump_1",
|
||||
"name": "真空泵",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_vacuum_pump",
|
||||
"position": {
|
||||
"x": 650,
|
||||
"y": 200,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_vacuum": 0.1,
|
||||
"pump_rate": 50.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Off",
|
||||
"current_vacuum": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gas_source_1",
|
||||
"name": "气源",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "gas_source.mock",
|
||||
"position": {
|
||||
"x": 650,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"gas_type": "nitrogen",
|
||||
"max_pressure": 5.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Off",
|
||||
"current_pressure": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "filter_1",
|
||||
"name": "过滤器",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_filter",
|
||||
"position": {
|
||||
"x": 900,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"filter_type": "membrane",
|
||||
"max_pressure": 5.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Ready",
|
||||
"pressure": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "column_1",
|
||||
"name": "洗脱柱",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_column",
|
||||
"position": {
|
||||
"x": 950,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"column_type": "silica_gel",
|
||||
"length": 30.0,
|
||||
"diameter": 2.5
|
||||
},
|
||||
"data": {
|
||||
"status": "Ready",
|
||||
"loaded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "separator_1",
|
||||
"name": "分液器",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "device",
|
||||
"class": "virtual_separator",
|
||||
"position": {
|
||||
"x": 1000,
|
||||
"y": 450,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0,
|
||||
"has_phases": true
|
||||
},
|
||||
"data": {
|
||||
"status": "Ready",
|
||||
"phase_separation": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "collection_bottle_1",
|
||||
"name": "接收瓶1",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 900,
|
||||
"y": 500,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "collection_bottle_2",
|
||||
"name": "接收瓶2",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 950,
|
||||
"y": 500,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "collection_bottle_3",
|
||||
"name": "接收瓶3",
|
||||
"children": [],
|
||||
"parent": "ComprehensiveProtocolStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1050,
|
||||
"y": 500,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "link_valve1_pump1",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "transfer_pump_1",
|
||||
"source_port": "port_0",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_reagent1",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "reagent_bottle_1",
|
||||
"source_port": "port_1",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_reagent2",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "reagent_bottle_2",
|
||||
"source_port": "port_2",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_reagent3",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "reagent_bottle_3",
|
||||
"source_port": "port_3",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_centrifuge",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "centrifuge_1",
|
||||
"source_port": "port_4",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_rotavap",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "rotavap_1",
|
||||
"source_port": "port_5",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_reactor",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "main_reactor",
|
||||
"source_port": "port_6",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_waste1",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "waste_bottle_1",
|
||||
"source_port": "port_7",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve1_valve2",
|
||||
"source": "multiway_valve_1",
|
||||
"target": "multiway_valve_2",
|
||||
"source_port": "port_8",
|
||||
"target_port": "port_1",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "port_8",
|
||||
"multiway_valve_2": "port_1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_pump2",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "transfer_pump_2",
|
||||
"source_port": "port_0",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_solenoid1",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "solenoid_valve_1",
|
||||
"source_port": "port_2",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solenoid1_vacuum",
|
||||
"source": "solenoid_valve_1",
|
||||
"target": "vacuum_pump_1",
|
||||
"source_port": "outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_solenoid2",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "solenoid_valve_2",
|
||||
"source_port": "port_3",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solenoid2_gas",
|
||||
"source": "solenoid_valve_2",
|
||||
"target": "gas_source_1",
|
||||
"source_port": "outlet",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_filter",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "filter_1",
|
||||
"source_port": "port_4",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_filter_collection1",
|
||||
"source": "filter_1",
|
||||
"target": "collection_bottle_1",
|
||||
"source_port": "filtrate_outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_column",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "column_1",
|
||||
"source_port": "port_5",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_column_collection2",
|
||||
"source": "column_1",
|
||||
"target": "collection_bottle_2",
|
||||
"source_port": "outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_separator",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "separator_1",
|
||||
"source_port": "port_6",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_separator_collection3",
|
||||
"source": "separator_1",
|
||||
"target": "collection_bottle_3",
|
||||
"source_port": "top_outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_separator_stirrer_2",
|
||||
"source": "separator_1",
|
||||
"target": "stirrer_2",
|
||||
"source_port": "top_outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_separator_waste2",
|
||||
"source": "separator_1",
|
||||
"target": "waste_bottle_2",
|
||||
"source_port": "bottom_outlet",
|
||||
"target_port": "inlet",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_reagent4",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "reagent_bottle_4",
|
||||
"source_port": "port_7",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve2_reagent5",
|
||||
"source": "multiway_valve_2",
|
||||
"target": "reagent_bottle_5",
|
||||
"source_port": "port_8",
|
||||
"target_port": "outlet",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "port_8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mech_stirrer_reactor",
|
||||
"source": "stirrer_1",
|
||||
"target": "main_reactor",
|
||||
"type": "fluid"
|
||||
},
|
||||
{
|
||||
"id": "thermal_heater_reactor",
|
||||
"source": "heater_1",
|
||||
"target": "main_reactor",
|
||||
"type": "fluid"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,58 +4,83 @@
|
||||
"id": "AddTestStation",
|
||||
"name": "添加试剂测试工作站",
|
||||
"children": [
|
||||
"pump_add",
|
||||
"flask_1",
|
||||
"flask_2",
|
||||
"flask_3",
|
||||
"flask_4",
|
||||
"reactor",
|
||||
"transfer_pump",
|
||||
"multiway_valve",
|
||||
"stirrer",
|
||||
"flask_air"
|
||||
"flask_reagent1",
|
||||
"flask_reagent2",
|
||||
"flask_reagent3",
|
||||
"flask_reagent4",
|
||||
"reactor",
|
||||
"flask_waste",
|
||||
"flask_rinsing",
|
||||
"flask_buffer"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"x": 620,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"]
|
||||
"protocol_type": ["AddProtocol", "TransferProtocol", "StartStirProtocol", "StopStirProtocol"]
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "pump_add",
|
||||
"name": "pump_add",
|
||||
"id": "transfer_pump",
|
||||
"name": "注射器泵",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "device",
|
||||
"class": "virtual_pump",
|
||||
"class": "virtual_transfer_pump",
|
||||
"position": {
|
||||
"x": 520.6111111111111,
|
||||
"x": 520,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "VIRTUAL",
|
||||
"max_volume": 25.0
|
||||
"max_volume": 50.0,
|
||||
"transfer_rate": 5.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "multiway_valve",
|
||||
"name": "八通阀门",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "device",
|
||||
"class": "virtual_multiway_valve",
|
||||
"position": {
|
||||
"x": 420,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "VIRTUAL",
|
||||
"positions": 8
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"current_position": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "stirrer",
|
||||
"name": "stirrer",
|
||||
"name": "搅拌器",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "device",
|
||||
"class": "virtual_stirrer",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"x": 720,
|
||||
"y": 450,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
@@ -68,110 +93,115 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_1",
|
||||
"name": "通用试剂瓶1",
|
||||
"id": "flask_reagent1",
|
||||
"name": "试剂瓶1 (甲醇)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 100,
|
||||
"y": 428,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
"liquid": [
|
||||
{
|
||||
"name": "甲醇",
|
||||
"volume": 800.0,
|
||||
"concentration": "99.9%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_2",
|
||||
"name": "通用试剂瓶2",
|
||||
"id": "flask_reagent2",
|
||||
"name": "试剂瓶2 (乙醇)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 250,
|
||||
"y": 428,
|
||||
"x": 180,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
"liquid": [
|
||||
{
|
||||
"name": "乙醇",
|
||||
"volume": 750.0,
|
||||
"concentration": "95%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_3",
|
||||
"name": "通用试剂瓶3",
|
||||
"id": "flask_reagent3",
|
||||
"name": "试剂瓶3 (丙酮)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 400,
|
||||
"y": 428,
|
||||
"x": 260,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
"liquid": [
|
||||
{
|
||||
"name": "丙酮",
|
||||
"volume": 900.0,
|
||||
"concentration": "99.5%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_4",
|
||||
"name": "通用试剂瓶4",
|
||||
"id": "flask_reagent4",
|
||||
"name": "试剂瓶4 (二氯甲烷)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 550,
|
||||
"y": 428,
|
||||
"x": 340,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
"liquid": [
|
||||
{
|
||||
"name": "二氯甲烷",
|
||||
"volume": 850.0,
|
||||
"concentration": "99.8%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactor",
|
||||
"name": "reactor",
|
||||
"name": "反应器",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 5000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_air",
|
||||
"name": "flask_air",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 800,
|
||||
"y": 300,
|
||||
"x": 720,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
@@ -180,70 +210,166 @@
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_waste",
|
||||
"name": "废液瓶",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 850,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 3000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_rinsing",
|
||||
"name": "冲洗液瓶",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 950,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "去离子水",
|
||||
"volume": 800.0,
|
||||
"concentration": "纯净"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_buffer",
|
||||
"name": "缓冲液瓶",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 950,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "磷酸盐缓冲液",
|
||||
"volume": 700.0,
|
||||
"concentration": "0.1M, pH 7.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "stirrer",
|
||||
"source": "transfer_pump",
|
||||
"target": "multiway_valve",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"transfer_pump": "syringe-port",
|
||||
"multiway_valve": "multiway-valve-inlet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_reagent1",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-1",
|
||||
"flask_reagent1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_reagent2",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-2",
|
||||
"flask_reagent2": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_reagent3",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-3",
|
||||
"flask_reagent3": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_reagent4",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-4",
|
||||
"flask_reagent4": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "reactor",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"stirrer": "top",
|
||||
"reactor": "bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_1",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "outlet",
|
||||
"flask_1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_2",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "inlet",
|
||||
"flask_2": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_3",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "inlet",
|
||||
"flask_3": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_4",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "inlet",
|
||||
"flask_4": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "reactor",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "outlet",
|
||||
"multiway_valve": "multiway-valve-port-5",
|
||||
"reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_air",
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_waste",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "inlet",
|
||||
"flask_air": "top"
|
||||
"multiway_valve": "multiway-valve-port-6",
|
||||
"flask_waste": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_rinsing",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-7",
|
||||
"flask_rinsing": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_buffer",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"multiway_valve": "multiway-valve-port-8",
|
||||
"flask_buffer": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "stirrer",
|
||||
"target": "reactor",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"stirrer": "stirrer-vessel",
|
||||
"reactor": "bottom"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,46 +15,116 @@ def generate_add_protocol(
|
||||
purpose: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成添加试剂的协议序列 - 严格按照 Add.action
|
||||
生成添加试剂的协议序列
|
||||
|
||||
流程:
|
||||
1. 找到包含目标试剂的试剂瓶
|
||||
2. 配置八通阀门到试剂瓶位置
|
||||
3. 使用注射器泵吸取试剂
|
||||
4. 配置八通阀门到反应器位置
|
||||
5. 使用注射器泵推送试剂到反应器
|
||||
6. 如果需要,启动搅拌
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
|
||||
# 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 {vessel} 不存在")
|
||||
|
||||
# 如果指定了体积,执行液体转移
|
||||
if volume > 0:
|
||||
# 查找可用的试剂瓶
|
||||
# 1. 查找注射器泵 (transfer pump)
|
||||
transfer_pump_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_transfer_pump']
|
||||
|
||||
if not transfer_pump_nodes:
|
||||
raise ValueError("没有找到可用的注射器泵 (virtual_transfer_pump)")
|
||||
|
||||
transfer_pump_id = transfer_pump_nodes[0]
|
||||
|
||||
# 2. 查找八通阀门
|
||||
multiway_valve_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_multiway_valve']
|
||||
|
||||
if not multiway_valve_nodes:
|
||||
raise ValueError("没有找到可用的八通阀门 (virtual_multiway_valve)")
|
||||
|
||||
valve_id = multiway_valve_nodes[0]
|
||||
|
||||
# 3. 查找包含指定试剂的试剂瓶
|
||||
reagent_vessel = None
|
||||
available_flasks = [node for node in G.nodes()
|
||||
if node.startswith('flask_')
|
||||
and G.nodes[node].get('type') == 'container']
|
||||
|
||||
if not available_flasks:
|
||||
# 简化:使用第一个可用的试剂瓶,实际应该根据试剂名称匹配
|
||||
if available_flasks:
|
||||
reagent_vessel = available_flasks[0]
|
||||
else:
|
||||
raise ValueError("没有找到可用的试剂容器")
|
||||
|
||||
reagent_vessel = available_flasks[0]
|
||||
|
||||
# 查找泵设备
|
||||
pump_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_pump']
|
||||
# 4. 获取试剂瓶和反应器对应的阀门位置
|
||||
# 这需要根据实际连接图来确定,这里假设:
|
||||
reagent_valve_position = 1 # 试剂瓶连接到阀门位置1
|
||||
reactor_valve_position = 2 # 反应器连接到阀门位置2
|
||||
|
||||
if pump_nodes:
|
||||
pump_id = pump_nodes[0]
|
||||
action_sequence.append({
|
||||
"device_id": pump_id,
|
||||
"action_name": "transfer",
|
||||
"action_kwargs": {
|
||||
"from_vessel": reagent_vessel,
|
||||
"to_vessel": vessel,
|
||||
"volume": volume,
|
||||
"amount": amount,
|
||||
"time": time,
|
||||
"viscous": viscous,
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"solid": False
|
||||
}
|
||||
})
|
||||
# 5. 执行添加操作序列
|
||||
|
||||
# 5.1 设置阀门到试剂瓶位置
|
||||
action_sequence.append({
|
||||
"device_id": valve_id,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": reagent_valve_position
|
||||
}
|
||||
})
|
||||
|
||||
# 5.2 使用注射器泵从试剂瓶吸取液体
|
||||
action_sequence.append({
|
||||
"device_id": transfer_pump_id,
|
||||
"action_name": "transfer",
|
||||
"action_kwargs": {
|
||||
"from_vessel": reagent_vessel,
|
||||
"to_vessel": transfer_pump_id, # 吸入到注射器
|
||||
"volume": volume,
|
||||
"amount": amount,
|
||||
"time": time / 2, # 吸取时间为总时间的一半
|
||||
"viscous": viscous,
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"solid": False
|
||||
}
|
||||
})
|
||||
|
||||
# 5.3 设置阀门到反应器位置
|
||||
action_sequence.append({
|
||||
"device_id": valve_id,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": reactor_valve_position
|
||||
}
|
||||
})
|
||||
|
||||
# 5.4 使用注射器泵将液体推送到反应器
|
||||
action_sequence.append({
|
||||
"device_id": transfer_pump_id,
|
||||
"action_name": "transfer",
|
||||
"action_kwargs": {
|
||||
"from_vessel": transfer_pump_id, # 从注射器推出
|
||||
"to_vessel": vessel,
|
||||
"volume": volume,
|
||||
"amount": amount,
|
||||
"time": time / 2, # 推送时间为总时间的一半
|
||||
"viscous": viscous,
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"solid": False
|
||||
}
|
||||
})
|
||||
|
||||
# 如果需要搅拌,使用 StartStir 而不是 Stir
|
||||
# 6. 如果需要搅拌,启动搅拌器
|
||||
if stir:
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_stirrer']
|
||||
@@ -63,12 +133,156 @@ def generate_add_protocol(
|
||||
stirrer_id = stirrer_nodes[0]
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir", # 使用 start_stir 而不是 stir
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"添加 {reagent} 后搅拌"
|
||||
"purpose": f"添加 {reagent} 后搅拌混合"
|
||||
}
|
||||
})
|
||||
else:
|
||||
print("警告:需要搅拌但未找到搅拌设备")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
def find_valve_position_for_vessel(G: nx.DiGraph, valve_id: str, vessel_id: str) -> int:
|
||||
"""
|
||||
根据连接图找到容器对应的阀门位置
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
valve_id: 阀门设备ID
|
||||
vessel_id: 容器ID
|
||||
|
||||
Returns:
|
||||
int: 阀门位置编号 (1-8)
|
||||
"""
|
||||
# 查找阀门到容器的连接
|
||||
edges = G.edges(data=True)
|
||||
|
||||
for source, target, data in edges:
|
||||
if source == valve_id and target == vessel_id:
|
||||
# 从连接数据中提取端口信息
|
||||
port_info = data.get('port', {})
|
||||
valve_port = port_info.get(valve_id, '')
|
||||
|
||||
# 解析端口名称获取位置编号
|
||||
if valve_port.startswith('multiway-valve-port-'):
|
||||
position = valve_port.split('-')[-1]
|
||||
return int(position)
|
||||
|
||||
# 默认返回位置1
|
||||
return 1
|
||||
|
||||
|
||||
def generate_add_with_autodiscovery(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
reagent: str,
|
||||
volume: float,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
智能添加协议生成器 - 自动发现设备连接关系
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 查找必需的设备
|
||||
devices = {
|
||||
'transfer_pump': None,
|
||||
'multiway_valve': None,
|
||||
'stirrer': None
|
||||
}
|
||||
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class')
|
||||
if node_class == 'virtual_transfer_pump':
|
||||
devices['transfer_pump'] = node
|
||||
elif node_class == 'virtual_multiway_valve':
|
||||
devices['multiway_valve'] = node
|
||||
elif node_class == 'virtual_stirrer':
|
||||
devices['stirrer'] = node
|
||||
|
||||
# 验证必需设备
|
||||
if not devices['transfer_pump']:
|
||||
raise ValueError("缺少注射器泵设备")
|
||||
if not devices['multiway_valve']:
|
||||
raise ValueError("缺少八通阀门设备")
|
||||
|
||||
# 查找试剂容器
|
||||
reagent_vessels = [node for node in G.nodes()
|
||||
if node.startswith('flask_')
|
||||
and G.nodes[node].get('type') == 'container']
|
||||
|
||||
if not reagent_vessels:
|
||||
raise ValueError("没有找到试剂容器")
|
||||
|
||||
# 执行添加流程
|
||||
reagent_vessel = reagent_vessels[0]
|
||||
reagent_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], reagent_vessel)
|
||||
reactor_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], vessel)
|
||||
|
||||
# 生成操作序列
|
||||
action_sequence.extend([
|
||||
# 切换到试剂瓶
|
||||
{
|
||||
"device_id": devices['multiway_valve'],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {"position": reagent_pos}
|
||||
},
|
||||
# 吸取试剂
|
||||
{
|
||||
"device_id": devices['transfer_pump'],
|
||||
"action_name": "transfer",
|
||||
"action_kwargs": {
|
||||
"from_vessel": reagent_vessel,
|
||||
"to_vessel": devices['transfer_pump'],
|
||||
"volume": volume,
|
||||
"amount": kwargs.get('amount', ''),
|
||||
"time": kwargs.get('time', 10.0) / 2,
|
||||
"viscous": kwargs.get('viscous', False),
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"solid": False
|
||||
}
|
||||
},
|
||||
# 切换到反应器
|
||||
{
|
||||
"device_id": devices['multiway_valve'],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {"position": reactor_pos}
|
||||
},
|
||||
# 推送到反应器
|
||||
{
|
||||
"device_id": devices['transfer_pump'],
|
||||
"action_name": "transfer",
|
||||
"action_kwargs": {
|
||||
"from_vessel": devices['transfer_pump'],
|
||||
"to_vessel": vessel,
|
||||
"volume": volume,
|
||||
"amount": kwargs.get('amount', ''),
|
||||
"time": kwargs.get('time', 10.0) / 2,
|
||||
"viscous": kwargs.get('viscous', False),
|
||||
"rinsing_solvent": "",
|
||||
"rinsing_volume": 0.0,
|
||||
"rinsing_repeats": 0,
|
||||
"solid": False
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
# 如果需要搅拌
|
||||
if kwargs.get('stir', False) and devices['stirrer']:
|
||||
action_sequence.append({
|
||||
"device_id": devices['stirrer'],
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"stir_speed": kwargs.get('stir_speed', 300.0),
|
||||
"purpose": f"添加 {reagent} 后混合"
|
||||
}
|
||||
})
|
||||
|
||||
return action_sequence
|
||||
@@ -2,17 +2,42 @@ import numpy as np
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def is_integrated_pump(node_name):
|
||||
return "pump" in node_name and "valve" in node_name
|
||||
|
||||
|
||||
def find_connected_pump(G, valve_node):
|
||||
for neighbor in G.neighbors(valve_node):
|
||||
if "pump" in G.nodes[neighbor]["class"]:
|
||||
return neighbor
|
||||
raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
|
||||
|
||||
|
||||
def build_pump_valve_maps(G, pump_backbone):
|
||||
pumps_from_node = {}
|
||||
valve_from_node = {}
|
||||
for node in pump_backbone:
|
||||
if is_integrated_pump(node):
|
||||
pumps_from_node[node] = node
|
||||
valve_from_node[node] = node
|
||||
else:
|
||||
pump_node = find_connected_pump(G, node)
|
||||
pumps_from_node[node] = pump_node
|
||||
valve_from_node[node] = node
|
||||
return pumps_from_node, valve_from_node
|
||||
|
||||
|
||||
def generate_pump_protocol(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
flowrate: float = 0.5,
|
||||
transfer_flowrate: float = 0,
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
flowrate: float = 0.5,
|
||||
transfer_flowrate: float = 0,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
生成泵操作的动作序列。
|
||||
|
||||
|
||||
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
|
||||
:param from_vessel: 容器A
|
||||
:param to_vessel: 容器B
|
||||
@@ -21,194 +46,137 @@ def generate_pump_protocol(
|
||||
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
:return: 泵操作的动作序列
|
||||
"""
|
||||
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
pump_action_sequence = []
|
||||
|
||||
# 检查节点是否存在
|
||||
if from_vessel not in G.nodes:
|
||||
print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.")
|
||||
return []
|
||||
|
||||
if to_vessel not in G.nodes:
|
||||
print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.")
|
||||
return []
|
||||
|
||||
# 检查是否存在路径
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
except nx.NetworkXNoPath:
|
||||
print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.")
|
||||
return []
|
||||
except nx.NodeNotFound as e:
|
||||
print(f"Warning: Node not found: {e}. Skipping.")
|
||||
return []
|
||||
|
||||
print(f"Shortest path: {shortest_path}")
|
||||
nodes = G.nodes(data=True)
|
||||
# 从from_vessel到to_vessel的最短路径
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
print(shortest_path)
|
||||
|
||||
pump_backbone = shortest_path
|
||||
if not from_vessel.startswith("pump"):
|
||||
pump_backbone = pump_backbone[1:]
|
||||
if not to_vessel.startswith("pump"):
|
||||
pump_backbone = pump_backbone[:-1]
|
||||
|
||||
print(f"Pump backbone: {pump_backbone}")
|
||||
|
||||
# 修复:检查pump_backbone是否为空
|
||||
if not pump_backbone:
|
||||
print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.")
|
||||
return []
|
||||
|
||||
|
||||
if transfer_flowrate == 0:
|
||||
transfer_flowrate = flowrate
|
||||
|
||||
# 修复:正确访问节点数据
|
||||
pump_max_volumes = []
|
||||
for pump in pump_backbone:
|
||||
# 直接使用 G.nodes[pump] 来访问节点数据
|
||||
pump_data = G.nodes[pump] if pump in G.nodes else {}
|
||||
# 尝试多种可能的键名,并提供默认值
|
||||
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
|
||||
if max_vol is None:
|
||||
# 如果是设备节点,尝试从config中获取
|
||||
config = pump_data.get('config', {})
|
||||
max_vol = config.get('max_volume', 25.0)
|
||||
pump_max_volumes.append(float(max_vol))
|
||||
|
||||
if pump_max_volumes:
|
||||
min_transfer_volume = min(pump_max_volumes)
|
||||
else:
|
||||
min_transfer_volume = 25.0 # 默认值
|
||||
|
||||
|
||||
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||
|
||||
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
|
||||
repeats = int(np.ceil(volume / min_transfer_volume))
|
||||
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
|
||||
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
|
||||
|
||||
|
||||
volume_left = volume
|
||||
|
||||
|
||||
# 生成泵操作的动作序列
|
||||
for i in range(repeats):
|
||||
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
|
||||
if not from_vessel.startswith("pump") and pump_backbone:
|
||||
# 修复:添加边缘数据检查
|
||||
edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
|
||||
if edge_data and "port" in edge_data:
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": pump_backbone[0],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_data["port"][pump_backbone[0]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pump_backbone[0],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
if not from_vessel.startswith("pump"):
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": valve_from_node[pump_backbone[0]],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}")
|
||||
|
||||
# 修复:检查pump_backbone长度,避免多泵操作时出错
|
||||
if len(pump_backbone) > 1:
|
||||
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
|
||||
# 相邻两泵同时切换阀门至连通位置
|
||||
edge_AB = G.get_edge_data(pumpA, pumpB)
|
||||
edge_BA = G.get_edge_data(pumpB, pumpA)
|
||||
|
||||
if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA:
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumpA,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_AB["port"][pumpA]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumpB,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_BA["port"][pumpB],
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[pump_backbone[0]],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
])
|
||||
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumpA,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumpB,
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
|
||||
# 相邻两泵同时切换阀门至连通位置
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": valve_from_node[nodeA],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pumpA} and {pumpB}")
|
||||
|
||||
if not to_vessel.startswith("pump") and pump_backbone:
|
||||
},
|
||||
{
|
||||
"device_id": valve_from_node[nodeB],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
|
||||
}
|
||||
}
|
||||
])
|
||||
# 相邻两泵液体转移:泵A排出液体,泵B吸入液体
|
||||
pump_action_sequence.append([
|
||||
{
|
||||
"device_id": pumps_from_node[nodeA],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[nodeB],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": float(min(volume_left, min_transfer_volume)),
|
||||
"max_velocity": transfer_flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
|
||||
if not to_vessel.startswith("pump"):
|
||||
# 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B
|
||||
edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
|
||||
if edge_data and "port" in edge_data:
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": pump_backbone[-1],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": edge_data["port"][pump_backbone[-1]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": pump_backbone[-1],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": flowrate
|
||||
}
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
"device_id": valve_from_node[pump_backbone[-1]],
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
else:
|
||||
print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}")
|
||||
|
||||
},
|
||||
{
|
||||
"device_id": pumps_from_node[pump_backbone[-1]],
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": flowrate
|
||||
}
|
||||
}
|
||||
])
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
|
||||
|
||||
volume_left -= min_transfer_volume
|
||||
return pump_action_sequence
|
||||
|
||||
|
||||
# Pump protocol compilation
|
||||
def generate_pump_protocol_with_rinsing(
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
amount: str = "",
|
||||
time: float = 0,
|
||||
viscous: bool = False,
|
||||
rinsing_solvent: str = "air",
|
||||
rinsing_volume: float = 5.0,
|
||||
rinsing_repeats: int = 2,
|
||||
solid: bool = False,
|
||||
flowrate: float = 2.5,
|
||||
transfer_flowrate: float = 0.5,
|
||||
G: nx.DiGraph,
|
||||
from_vessel: str,
|
||||
to_vessel: str,
|
||||
volume: float,
|
||||
amount: str = "",
|
||||
time: float = 0,
|
||||
viscous: bool = False,
|
||||
rinsing_solvent: str = "air",
|
||||
rinsing_volume: float = 5.0,
|
||||
rinsing_repeats: int = 2,
|
||||
solid: bool = False,
|
||||
flowrate: float = 2.5,
|
||||
transfer_flowrate: float = 0.5,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
|
||||
|
||||
|
||||
Args:
|
||||
G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
|
||||
from_vessel (str): The name of the vessel to transfer from.
|
||||
@@ -223,96 +191,64 @@ def generate_pump_protocol_with_rinsing(
|
||||
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
|
||||
flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速
|
||||
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
|
||||
|
||||
|
||||
Returns:
|
||||
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
|
||||
|
||||
|
||||
Raises:
|
||||
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
|
||||
|
||||
|
||||
Examples:
|
||||
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
|
||||
"""
|
||||
# 修复:使用实际存在的节点名称
|
||||
air_vessel = "flask_air" # 这个在你的配置中存在
|
||||
|
||||
# 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
|
||||
waste_vessel = None
|
||||
available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel]
|
||||
if available_vessels:
|
||||
# 使用第一个可用的容器作为废料容器
|
||||
waste_vessel = available_vessels[0]
|
||||
print(f"Using {waste_vessel} as waste vessel")
|
||||
else:
|
||||
waste_vessel = "flask_1" # 备用选择
|
||||
|
||||
# 修复:添加路径检查
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
pump_backbone = shortest_path[1: -1]
|
||||
except (nx.NetworkXNoPath, nx.NodeNotFound) as e:
|
||||
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
|
||||
return []
|
||||
|
||||
# 修复:正确访问节点数据
|
||||
pump_max_volumes = []
|
||||
for pump in pump_backbone:
|
||||
# 直接使用 G.nodes[pump] 来访问节点数据
|
||||
pump_data = G.nodes[pump] if pump in G.nodes else {}
|
||||
# 尝试多种可能的键名,并提供默认值
|
||||
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
|
||||
if max_vol is None:
|
||||
# 如果是设备节点,尝试从config中获取
|
||||
config = pump_data.get('config', {})
|
||||
max_vol = config.get('max_volume', 25.0)
|
||||
pump_max_volumes.append(float(max_vol))
|
||||
|
||||
if pump_max_volumes:
|
||||
min_transfer_volume = float(min(pump_max_volumes))
|
||||
else:
|
||||
min_transfer_volume = 25.0 # 默认值
|
||||
|
||||
air_vessel = "flask_air"
|
||||
waste_vessel = f"waste_workup"
|
||||
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
|
||||
pump_backbone = shortest_path[1: -1]
|
||||
nodes = G.nodes(data=True)
|
||||
|
||||
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
|
||||
|
||||
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
|
||||
if time != 0:
|
||||
flowrate = transfer_flowrate = volume / time
|
||||
|
||||
|
||||
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
|
||||
|
||||
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
|
||||
if rinsing_solvent != "air" and pump_backbone:
|
||||
if rinsing_solvent != "air" and rinsing_solvent != "":
|
||||
if "," in rinsing_solvent:
|
||||
rinsing_solvents = rinsing_solvent.split(",")
|
||||
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
|
||||
assert len(
|
||||
rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
|
||||
else:
|
||||
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
|
||||
|
||||
|
||||
for rinsing_solvent in rinsing_solvents:
|
||||
solvent_vessel = f"flask_{rinsing_solvent}"
|
||||
|
||||
# 检查溶剂容器是否存在
|
||||
if solvent_vessel not in G.nodes:
|
||||
print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.")
|
||||
continue
|
||||
|
||||
# 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行
|
||||
if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes:
|
||||
# 清洗泵
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
|
||||
transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate,
|
||||
transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate,
|
||||
transfer_flowrate)
|
||||
)
|
||||
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
|
||||
if rinsing_solvent == rinsing_solvents[0]:
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
|
||||
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
|
||||
)
|
||||
|
||||
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
|
||||
if rinsing_solvent == rinsing_solvents[0]:
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
|
||||
# 最后的空气清洗 - 只有当节点存在时才执行
|
||||
if air_vessel in G.nodes:
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
|
||||
generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
|
||||
if rinsing_solvent != "":
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
pump_action_sequence.extend(
|
||||
generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
|
||||
|
||||
return pump_action_sequence
|
||||
# End Protocols
|
||||
|
||||
221
unilabos/devices/virtual/virtual_multiway_valve.py
Normal file
221
unilabos/devices/virtual/virtual_multiway_valve.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import time
|
||||
from typing import Union, Dict, Optional
|
||||
|
||||
|
||||
class VirtualMultiwayValve:
|
||||
"""
|
||||
虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备
|
||||
"""
|
||||
def __init__(self, port: str = "VIRTUAL", positions: int = 8):
|
||||
self.port = port
|
||||
self.max_positions = positions # 1-8号位
|
||||
self.total_positions = positions + 1 # 0-8号位,共9个位置
|
||||
|
||||
# 状态属性
|
||||
self._status = "Idle"
|
||||
self._valve_state = "Ready"
|
||||
self._current_position = 0 # 默认在0号位(transfer pump位置)
|
||||
self._target_position = 0
|
||||
|
||||
# 位置映射说明
|
||||
self.position_map = {
|
||||
0: "transfer_pump", # 0号位连接转移泵
|
||||
1: "port_1", # 1号位
|
||||
2: "port_2", # 2号位
|
||||
3: "port_3", # 3号位
|
||||
4: "port_4", # 4号位
|
||||
5: "port_5", # 5号位
|
||||
6: "port_6", # 6号位
|
||||
7: "port_7", # 7号位
|
||||
8: "port_8" # 8号位
|
||||
}
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def valve_state(self) -> str:
|
||||
return self._valve_state
|
||||
|
||||
@property
|
||||
def current_position(self) -> int:
|
||||
return self._current_position
|
||||
|
||||
@property
|
||||
def target_position(self) -> int:
|
||||
return self._target_position
|
||||
|
||||
def get_current_position(self) -> int:
|
||||
"""获取当前阀门位置"""
|
||||
return self._current_position
|
||||
|
||||
def get_current_port(self) -> str:
|
||||
"""获取当前连接的端口名称"""
|
||||
return self.position_map.get(self._current_position, "unknown")
|
||||
|
||||
def set_position(self, command: Union[int, str]):
|
||||
"""
|
||||
设置阀门位置 - 支持0-8位置
|
||||
|
||||
Args:
|
||||
command: 目标位置 (0-8) 或位置字符串
|
||||
0: transfer pump位置
|
||||
1-8: 其他设备位置
|
||||
"""
|
||||
try:
|
||||
# 如果是字符串形式的位置,先转换为数字
|
||||
if isinstance(command, str):
|
||||
pos = int(command)
|
||||
else:
|
||||
pos = int(command)
|
||||
|
||||
if pos < 0 or pos > self.max_positions:
|
||||
raise ValueError(f"Position must be between 0 and {self.max_positions}")
|
||||
|
||||
self._status = "Busy"
|
||||
self._valve_state = "Moving"
|
||||
self._target_position = pos
|
||||
|
||||
# 模拟阀门切换时间
|
||||
switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒
|
||||
time.sleep(switch_time)
|
||||
|
||||
self._current_position = pos
|
||||
self._status = "Idle"
|
||||
self._valve_state = "Ready"
|
||||
|
||||
current_port = self.get_current_port()
|
||||
return f"Position set to {pos} ({current_port})"
|
||||
|
||||
except ValueError as e:
|
||||
self._status = "Error"
|
||||
self._valve_state = "Error"
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def set_to_pump_position(self):
|
||||
"""切换到transfer pump位置(0号位)"""
|
||||
return self.set_position(0)
|
||||
|
||||
def set_to_port(self, port_number: int):
|
||||
"""
|
||||
切换到指定端口位置
|
||||
|
||||
Args:
|
||||
port_number: 端口号 (1-8)
|
||||
"""
|
||||
if port_number < 1 or port_number > self.max_positions:
|
||||
raise ValueError(f"Port number must be between 1 and {self.max_positions}")
|
||||
return self.set_position(port_number)
|
||||
|
||||
def open(self):
|
||||
"""打开阀门 - 设置到transfer pump位置(0号位)"""
|
||||
return self.set_to_pump_position()
|
||||
|
||||
def close(self):
|
||||
"""关闭阀门 - 对于多通阀门,设置到一个"关闭"状态"""
|
||||
self._status = "Busy"
|
||||
self._valve_state = "Closing"
|
||||
time.sleep(0.5)
|
||||
|
||||
# 可以选择保持当前位置或设置特殊关闭状态
|
||||
self._status = "Idle"
|
||||
self._valve_state = "Closed"
|
||||
|
||||
return f"Valve closed at position {self._current_position}"
|
||||
|
||||
def get_valve_position(self) -> int:
|
||||
"""获取阀门位置 - 兼容性方法"""
|
||||
return self._current_position
|
||||
|
||||
def is_at_position(self, position: int) -> bool:
|
||||
"""检查是否在指定位置"""
|
||||
return self._current_position == position
|
||||
|
||||
def is_at_pump_position(self) -> bool:
|
||||
"""检查是否在transfer pump位置"""
|
||||
return self._current_position == 0
|
||||
|
||||
def is_at_port(self, port_number: int) -> bool:
|
||||
"""检查是否在指定端口位置"""
|
||||
return self._current_position == port_number
|
||||
|
||||
def get_available_positions(self) -> list:
|
||||
"""获取可用位置列表"""
|
||||
return list(range(0, self.max_positions + 1))
|
||||
|
||||
def get_available_ports(self) -> Dict[int, str]:
|
||||
"""获取可用端口映射"""
|
||||
return self.position_map.copy()
|
||||
|
||||
def reset(self):
|
||||
"""重置阀门到transfer pump位置(0号位)"""
|
||||
return self.set_position(0)
|
||||
|
||||
def switch_between_pump_and_port(self, port_number: int):
|
||||
"""
|
||||
在transfer pump位置和指定端口之间切换
|
||||
|
||||
Args:
|
||||
port_number: 目标端口号 (1-8)
|
||||
"""
|
||||
if self._current_position == 0:
|
||||
# 当前在pump位置,切换到指定端口
|
||||
return self.set_to_port(port_number)
|
||||
else:
|
||||
# 当前在某个端口,切换到pump位置
|
||||
return self.set_to_pump_position()
|
||||
|
||||
def get_flow_path(self) -> str:
|
||||
"""获取当前流路路径描述"""
|
||||
current_port = self.get_current_port()
|
||||
if self._current_position == 0:
|
||||
return f"Transfer pump connected (position {self._current_position})"
|
||||
else:
|
||||
return f"Port {self._current_position} connected ({current_port})"
|
||||
|
||||
def get_info(self) -> dict:
|
||||
"""获取阀门详细信息"""
|
||||
return {
|
||||
"port": self.port,
|
||||
"max_positions": self.max_positions,
|
||||
"total_positions": self.total_positions,
|
||||
"current_position": self._current_position,
|
||||
"current_port": self.get_current_port(),
|
||||
"target_position": self._target_position,
|
||||
"status": self._status,
|
||||
"valve_state": self._valve_state,
|
||||
"flow_path": self.get_flow_path(),
|
||||
"position_map": self.position_map
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})"
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
valve = VirtualMultiwayValve()
|
||||
|
||||
print("=== 虚拟九通阀门测试 ===")
|
||||
print(f"初始状态: {valve}")
|
||||
print(f"当前流路: {valve.get_flow_path()}")
|
||||
|
||||
# 切换到试剂瓶1(1号位)
|
||||
print(f"\n切换到1号位: {valve.set_position(1)}")
|
||||
print(f"当前状态: {valve}")
|
||||
|
||||
# 切换到transfer pump位置(0号位)
|
||||
print(f"\n切换到pump位置: {valve.set_to_pump_position()}")
|
||||
print(f"当前状态: {valve}")
|
||||
|
||||
# 切换到试剂瓶2(2号位)
|
||||
print(f"\n切换到2号位: {valve.set_to_port(2)}")
|
||||
print(f"当前状态: {valve}")
|
||||
|
||||
# 显示所有可用位置
|
||||
print(f"\n可用位置: {valve.get_available_positions()}")
|
||||
print(f"端口映射: {valve.get_available_ports()}")
|
||||
|
||||
# 获取详细信息
|
||||
print(f"\n详细信息: {valve.get_info()}")
|
||||
151
unilabos/devices/virtual/virtual_solenoid_valve.py
Normal file
151
unilabos/devices/virtual/virtual_solenoid_valve.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import time
|
||||
from typing import Union
|
||||
|
||||
|
||||
class VirtualSolenoidValve:
|
||||
"""
|
||||
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
|
||||
"""
|
||||
def __init__(self, port: str = "VIRTUAL", voltage: float = 12.0, response_time: float = 0.1):
|
||||
self.port = port
|
||||
self.voltage = voltage
|
||||
self.response_time = response_time
|
||||
|
||||
# 状态属性
|
||||
self._status = "Idle"
|
||||
self._valve_state = "Closed" # "Open" or "Closed"
|
||||
self._is_open = False
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def valve_state(self) -> str:
|
||||
return self._valve_state
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
return self._is_open
|
||||
|
||||
def get_valve_position(self) -> str:
|
||||
"""获取阀门位置状态"""
|
||||
return "OPEN" if self._is_open else "CLOSED"
|
||||
|
||||
def set_valve_position(self, position: Union[str, bool]):
|
||||
"""
|
||||
设置阀门位置
|
||||
|
||||
Args:
|
||||
position: "OPEN"/"CLOSED" 或 True/False
|
||||
"""
|
||||
self._status = "Busy"
|
||||
|
||||
# 模拟阀门响应时间
|
||||
time.sleep(self.response_time)
|
||||
|
||||
if isinstance(position, str):
|
||||
target_open = position.upper() == "OPEN"
|
||||
elif isinstance(position, bool):
|
||||
target_open = position
|
||||
else:
|
||||
self._status = "Error"
|
||||
return "Error: Invalid position"
|
||||
|
||||
self._is_open = target_open
|
||||
self._valve_state = "Open" if target_open else "Closed"
|
||||
self._status = "Idle"
|
||||
|
||||
return f"Valve {'opened' if target_open else 'closed'}"
|
||||
|
||||
def open(self):
|
||||
"""打开电磁阀"""
|
||||
self._status = "Busy"
|
||||
time.sleep(self.response_time)
|
||||
|
||||
self._is_open = True
|
||||
self._valve_state = "Open"
|
||||
self._status = "Idle"
|
||||
|
||||
return "Valve opened"
|
||||
|
||||
def close(self):
|
||||
"""关闭电磁阀"""
|
||||
self._status = "Busy"
|
||||
time.sleep(self.response_time)
|
||||
|
||||
self._is_open = False
|
||||
self._valve_state = "Closed"
|
||||
self._status = "Idle"
|
||||
|
||||
return "Valve closed"
|
||||
|
||||
def set_state(self, command: Union[bool, str]):
|
||||
"""
|
||||
设置阀门状态 - 兼容 SendCmd 类型
|
||||
|
||||
Args:
|
||||
command: True/False 或 "open"/"close"
|
||||
"""
|
||||
if isinstance(command, bool):
|
||||
return self.open() if command else self.close()
|
||||
elif isinstance(command, str):
|
||||
if command.lower() in ["open", "on", "true", "1"]:
|
||||
return self.open()
|
||||
elif command.lower() in ["close", "closed", "off", "false", "0"]:
|
||||
return self.close()
|
||||
else:
|
||||
self._status = "Error"
|
||||
return "Error: Invalid command"
|
||||
else:
|
||||
self._status = "Error"
|
||||
return "Error: Invalid command type"
|
||||
|
||||
def toggle(self):
|
||||
"""切换阀门状态"""
|
||||
if self._is_open:
|
||||
return self.close()
|
||||
else:
|
||||
return self.open()
|
||||
|
||||
def is_closed(self) -> bool:
|
||||
"""检查阀门是否关闭"""
|
||||
return not self._is_open
|
||||
|
||||
def get_state(self) -> dict:
|
||||
"""获取阀门完整状态"""
|
||||
return {
|
||||
"port": self.port,
|
||||
"voltage": self.voltage,
|
||||
"response_time": self.response_time,
|
||||
"is_open": self._is_open,
|
||||
"valve_state": self._valve_state,
|
||||
"status": self._status,
|
||||
"position": self.get_valve_position()
|
||||
}
|
||||
|
||||
def reset(self):
|
||||
"""重置阀门到关闭状态"""
|
||||
return self.close()
|
||||
|
||||
def test_cycle(self, cycles: int = 3, delay: float = 1.0):
|
||||
"""
|
||||
测试阀门开关循环
|
||||
|
||||
Args:
|
||||
cycles: 循环次数
|
||||
delay: 每次开关间隔时间(秒)
|
||||
"""
|
||||
results = []
|
||||
for i in range(cycles):
|
||||
# 打开
|
||||
result_open = self.open()
|
||||
results.append(f"Cycle {i+1} - Open: {result_open}")
|
||||
time.sleep(delay)
|
||||
|
||||
# 关闭
|
||||
result_close = self.close()
|
||||
results.append(f"Cycle {i+1} - Close: {result_close}")
|
||||
time.sleep(delay)
|
||||
|
||||
return results
|
||||
@@ -1,149 +1,285 @@
|
||||
import asyncio
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Union, Optional
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class VirtualTransferPump:
|
||||
"""Virtual pump device specifically for Transfer protocol"""
|
||||
|
||||
class VirtualPumpMode(Enum):
|
||||
Normal = 0
|
||||
AccuratePos = 1
|
||||
AccuratePosVel = 2
|
||||
|
||||
|
||||
class VirtualPump:
|
||||
"""虚拟泵类 - 模拟泵的基本功能,无需实际硬件"""
|
||||
|
||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||
# 处理可能的不同调用方式
|
||||
if device_id is None and 'id' in kwargs:
|
||||
device_id = kwargs.pop('id')
|
||||
if config is None and 'config' in kwargs:
|
||||
config = kwargs.pop('config')
|
||||
def __init__(self, device_id: str = None, max_volume: float = 25.0, mode: VirtualPumpMode = VirtualPumpMode.Normal):
|
||||
self.device_id = device_id or "virtual_pump"
|
||||
self.max_volume = max_volume
|
||||
self.mode = mode
|
||||
|
||||
# 设置默认值
|
||||
self.device_id = device_id or "unknown_transfer_pump"
|
||||
self.config = config or {}
|
||||
# 状态变量
|
||||
self._status = "Idle"
|
||||
self._position = 0.0 # 当前柱塞位置 (ml)
|
||||
self._max_velocity = 5.0 # 默认最大速度 (ml/s)
|
||||
self._current_volume = 0.0 # 当前注射器中的体积
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
||||
self.data = {}
|
||||
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualTransferPump {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
|
||||
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 5.0)
|
||||
self._current_volume = 0.0
|
||||
self.is_running = False
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual transfer pump"""
|
||||
print(f"=== VirtualTransferPump {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual transfer pump {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_volume": 0.0,
|
||||
"max_volume": self._max_volume,
|
||||
"transfer_rate": self._transfer_rate,
|
||||
"from_vessel": "",
|
||||
"to_vessel": "",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0,
|
||||
"current_status": "Ready"
|
||||
})
|
||||
"""初始化虚拟泵"""
|
||||
self.logger.info(f"Initializing virtual pump {self.device_id}")
|
||||
self._status = "Idle"
|
||||
self._position = 0.0
|
||||
self._current_volume = 0.0
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual transfer pump"""
|
||||
self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}")
|
||||
"""清理虚拟泵"""
|
||||
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
|
||||
self._status = "Idle"
|
||||
return True
|
||||
|
||||
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
||||
amount: str = "", time: float = 0, viscous: bool = False,
|
||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
||||
rinsing_repeats: int = 0, solid: bool = False) -> bool:
|
||||
"""Execute liquid transfer - matches Transfer action"""
|
||||
self.logger.info(f"Transfer: {volume}mL from {from_vessel} to {to_vessel}")
|
||||
|
||||
# 计算转移时间
|
||||
if time > 0:
|
||||
transfer_time = time
|
||||
else:
|
||||
# 如果是粘性液体,降低转移速率
|
||||
rate = self._transfer_rate * 0.5 if viscous else self._transfer_rate
|
||||
transfer_time = volume / rate
|
||||
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"current_status": "Transferring",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0
|
||||
})
|
||||
|
||||
# 模拟转移过程
|
||||
steps = 10
|
||||
step_time = transfer_time / steps
|
||||
step_volume = volume / steps
|
||||
|
||||
for i in range(steps):
|
||||
await asyncio.sleep(step_time)
|
||||
progress = (i + 1) / steps * 100
|
||||
transferred = (i + 1) * step_volume
|
||||
|
||||
self.data.update({
|
||||
"progress": progress,
|
||||
"transferred_volume": transferred,
|
||||
"current_status": f"Transferring {progress:.1f}%"
|
||||
})
|
||||
|
||||
self.logger.info(f"Transfer progress: {progress:.1f}% ({transferred:.1f}/{volume}mL)")
|
||||
|
||||
# 如果需要冲洗
|
||||
if rinsing_solvent and rinsing_volume > 0 and rinsing_repeats > 0:
|
||||
self.data["current_status"] = "Rinsing"
|
||||
for repeat in range(rinsing_repeats):
|
||||
self.logger.info(f"Rinsing cycle {repeat + 1}/{rinsing_repeats} with {rinsing_solvent}")
|
||||
await asyncio.sleep(1) # 模拟冲洗时间
|
||||
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_status": "Transfer completed",
|
||||
"progress": 100.0,
|
||||
"transferred_volume": volume
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
# 添加所有在virtual_device.yaml中定义的状态属性
|
||||
# 基本属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
"""当前柱塞位置 (ml)"""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def current_volume(self) -> float:
|
||||
return self.data.get("current_volume", 0.0)
|
||||
"""当前注射器中的体积 (ml)"""
|
||||
return self._current_volume
|
||||
|
||||
@property
|
||||
def max_volume(self) -> float:
|
||||
return self.data.get("max_volume", self._max_volume)
|
||||
def max_velocity(self) -> float:
|
||||
return self._max_velocity
|
||||
|
||||
@property
|
||||
def transfer_rate(self) -> float:
|
||||
return self.data.get("transfer_rate", self._transfer_rate)
|
||||
def set_max_velocity(self, velocity: float):
|
||||
"""设置最大速度 (ml/s)"""
|
||||
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
|
||||
self.logger.info(f"Set max velocity to {self._max_velocity} ml/s")
|
||||
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self.data.get("from_vessel", "")
|
||||
def get_status(self) -> str:
|
||||
"""获取泵状态"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self.data.get("to_vessel", "")
|
||||
async def _simulate_operation(self, duration: float):
|
||||
"""模拟操作延时"""
|
||||
self._status = "Busy"
|
||||
await asyncio.sleep(duration)
|
||||
self._status = "Idle"
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self.data.get("progress", 0.0)
|
||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||
"""计算操作持续时间"""
|
||||
if velocity is None:
|
||||
velocity = self._max_velocity
|
||||
return abs(volume) / velocity
|
||||
|
||||
@property
|
||||
def transferred_volume(self) -> float:
|
||||
return self.data.get("transferred_volume", 0.0)
|
||||
# 基本泵操作
|
||||
async def set_position(self, position: float, velocity: float = None):
|
||||
"""
|
||||
移动到绝对位置
|
||||
|
||||
Args:
|
||||
position (float): 目标位置 (ml)
|
||||
velocity (float): 移动速度 (ml/s)
|
||||
"""
|
||||
position = max(0, min(self.max_volume, position)) # 限制在有效范围内
|
||||
|
||||
volume_to_move = abs(position - self._position)
|
||||
duration = self._calculate_duration(volume_to_move, velocity)
|
||||
|
||||
self.logger.info(f"Moving to position {position} ml (current: {self._position} ml)")
|
||||
|
||||
# 模拟移动过程
|
||||
await self._simulate_operation(duration)
|
||||
|
||||
self._position = position
|
||||
self._current_volume = position # 假设位置等于体积
|
||||
|
||||
self.logger.info(f"Reached position {self._position} ml")
|
||||
|
||||
@property
|
||||
def current_status(self) -> str:
|
||||
return self.data.get("current_status", "Ready")
|
||||
async def pull_plunger(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
拉取柱塞(吸液)
|
||||
|
||||
Args:
|
||||
volume (float): 要拉取的体积 (ml)
|
||||
velocity (float): 拉取速度 (ml/s)
|
||||
"""
|
||||
new_position = min(self.max_volume, self._position + volume)
|
||||
actual_volume = new_position - self._position
|
||||
|
||||
if actual_volume <= 0:
|
||||
self.logger.warning("Cannot pull - already at maximum volume")
|
||||
return
|
||||
|
||||
duration = self._calculate_duration(actual_volume, velocity)
|
||||
|
||||
self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})")
|
||||
|
||||
await self._simulate_operation(duration)
|
||||
|
||||
self._position = new_position
|
||||
self._current_volume = new_position
|
||||
|
||||
self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml")
|
||||
|
||||
async def push_plunger(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
推出柱塞(排液)
|
||||
|
||||
Args:
|
||||
volume (float): 要推出的体积 (ml)
|
||||
velocity (float): 推出速度 (ml/s)
|
||||
"""
|
||||
new_position = max(0, self._position - volume)
|
||||
actual_volume = self._position - new_position
|
||||
|
||||
if actual_volume <= 0:
|
||||
self.logger.warning("Cannot push - already at minimum volume")
|
||||
return
|
||||
|
||||
duration = self._calculate_duration(actual_volume, velocity)
|
||||
|
||||
self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})")
|
||||
|
||||
await self._simulate_operation(duration)
|
||||
|
||||
self._position = new_position
|
||||
self._current_volume = new_position
|
||||
|
||||
self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml")
|
||||
|
||||
# 便捷操作方法
|
||||
async def aspirate(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
吸液操作
|
||||
|
||||
Args:
|
||||
volume (float): 吸液体积 (ml)
|
||||
velocity (float): 吸液速度 (ml/s)
|
||||
"""
|
||||
await self.pull_plunger(volume, velocity)
|
||||
|
||||
async def dispense(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
排液操作
|
||||
|
||||
Args:
|
||||
volume (float): 排液体积 (ml)
|
||||
velocity (float): 排液速度 (ml/s)
|
||||
"""
|
||||
await self.push_plunger(volume, velocity)
|
||||
|
||||
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
|
||||
"""
|
||||
转移操作(先吸后排)
|
||||
|
||||
Args:
|
||||
volume (float): 转移体积 (ml)
|
||||
aspirate_velocity (float): 吸液速度 (ml/s)
|
||||
dispense_velocity (float): 排液速度 (ml/s)
|
||||
"""
|
||||
# 吸液
|
||||
await self.aspirate(volume, aspirate_velocity)
|
||||
|
||||
# 短暂停顿
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 排液
|
||||
await self.dispense(volume, dispense_velocity)
|
||||
|
||||
async def empty_syringe(self, velocity: float = None):
|
||||
"""清空注射器"""
|
||||
await self.set_position(0, velocity)
|
||||
|
||||
async def fill_syringe(self, velocity: float = None):
|
||||
"""充满注射器"""
|
||||
await self.set_position(self.max_volume, velocity)
|
||||
|
||||
async def stop_operation(self):
|
||||
"""停止当前操作"""
|
||||
self._status = "Idle"
|
||||
self.logger.info("Operation stopped")
|
||||
|
||||
# 状态查询方法
|
||||
def get_position(self) -> float:
|
||||
"""获取当前位置"""
|
||||
return self._position
|
||||
|
||||
def get_current_volume(self) -> float:
|
||||
"""获取当前体积"""
|
||||
return self._current_volume
|
||||
|
||||
def get_remaining_capacity(self) -> float:
|
||||
"""获取剩余容量"""
|
||||
return self.max_volume - self._current_volume
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""检查是否为空"""
|
||||
return self._current_volume <= 0.01 # 允许小量误差
|
||||
|
||||
def is_full(self) -> bool:
|
||||
"""检查是否已满"""
|
||||
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
||||
|
||||
# 调试和状态信息
|
||||
def get_pump_info(self) -> dict:
|
||||
"""获取泵的详细信息"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"status": self._status,
|
||||
"position": self._position,
|
||||
"current_volume": self._current_volume,
|
||||
"max_volume": self.max_volume,
|
||||
"max_velocity": self._max_velocity,
|
||||
"mode": self.mode.name,
|
||||
"is_empty": self.is_empty(),
|
||||
"is_full": self.is_full(),
|
||||
"remaining_capacity": self.get_remaining_capacity()
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"VirtualPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
# 使用示例
|
||||
async def demo():
|
||||
"""虚拟泵使用示例"""
|
||||
pump = VirtualPump("demo_pump", max_volume=50.0)
|
||||
|
||||
await pump.initialize()
|
||||
|
||||
print(f"Initial state: {pump}")
|
||||
|
||||
# 吸液测试
|
||||
await pump.aspirate(10.0, velocity=2.0)
|
||||
print(f"After aspirating 10ml: {pump}")
|
||||
|
||||
# 排液测试
|
||||
await pump.dispense(5.0, velocity=3.0)
|
||||
print(f"After dispensing 5ml: {pump}")
|
||||
|
||||
# 转移测试
|
||||
await pump.transfer(3.0)
|
||||
print(f"After transfer 3ml: {pump}")
|
||||
|
||||
# 清空测试
|
||||
await pump.empty_syringe()
|
||||
print(f"After emptying: {pump}")
|
||||
|
||||
print("\nPump info:", pump.get_pump_info())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo())
|
||||
@@ -1,7 +1,7 @@
|
||||
io_snrd:
|
||||
description: IO Board with 16 IOs
|
||||
class:
|
||||
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
|
||||
type: python
|
||||
hardware_interface:
|
||||
name: modbus_client
|
||||
|
||||
@@ -89,7 +89,7 @@ mock_filter:
|
||||
target_volume: Float64
|
||||
action_value_mappings:
|
||||
filter:
|
||||
type: ProtocolFilter
|
||||
type: Filter
|
||||
goal:
|
||||
vessel: vessel
|
||||
filtrate_vessel: filtrate_vessel
|
||||
@@ -737,7 +737,7 @@ mock_stirrer_new:
|
||||
max_stir_speed: Float64
|
||||
action_value_mappings:
|
||||
start_stir:
|
||||
type: ProtocolStartStir
|
||||
type: StartStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
stir_speed: stir_speed
|
||||
@@ -760,7 +760,7 @@ mock_stirrer_new:
|
||||
result:
|
||||
success: success
|
||||
stop_stir:
|
||||
type: ProtocolStopStir
|
||||
type: StopStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
|
||||
@@ -71,3 +71,13 @@ solenoid_valve:
|
||||
class:
|
||||
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_position: String
|
||||
action_value_mappings:
|
||||
set_valve_position:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: position
|
||||
feedback: {}
|
||||
result: {}
|
||||
@@ -1,3 +1,49 @@
|
||||
# 虚拟设备清单及连接特性
|
||||
|
||||
# 1. virtual_pump - 虚拟泵
|
||||
# 描述:具有多通道阀门特性的泵,根据valve_position可连接多个容器
|
||||
# 连接特性:1个输入口 + 1个输出口(当前配置,实际应该有多个输出口)
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 2. virtual_stirrer - 虚拟搅拌器
|
||||
# 描述:机械连接设备,提供搅拌功能
|
||||
# 连接特性:1个双向连接点(bidirectional)
|
||||
# 数据类型:mechanical(机械连接)
|
||||
|
||||
# 3a. virtual_valve - 虚拟八通阀门
|
||||
# 描述:8通阀门(实际配置为7通),可切换流向
|
||||
# 连接特性:1个口连接注射泵 + 7个输出口
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 3b. virtual_solenoid_valve (电磁阀门)
|
||||
# 描述:简单的开关型电磁阀,只有开启和关闭两个状态
|
||||
# 连接特性:1个输入口 + 1个输出口,控制通断
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 4. virtual_centrifuge - 虚拟离心机
|
||||
# 描述:单个样品处理设备,原地处理样品
|
||||
# 连接特性:1个输入口 + 1个输出口
|
||||
# 数据类型:resource(资源/样品连接)
|
||||
|
||||
# 5. virtual_filter - 虚拟过滤器
|
||||
# 描述:分离设备,将样品分离为滤液和滤渣
|
||||
# 连接特性:1个输入口 + 2个输出口(滤液和滤渣)
|
||||
# 数据类型:resource(资源/样品连接)
|
||||
|
||||
# 6. virtual_heatchill - 虚拟加热/冷却器
|
||||
# 描述:温控设备,容器直接放置在设备上进行温度控制
|
||||
# 连接特性:1个双向连接点(bidirectional)
|
||||
# 数据类型:mechanical(机械/物理接触连接)
|
||||
|
||||
# 7. virtual_transfer_pump - 虚拟转移泵(注射器式)
|
||||
# 描述:注射器式转移泵,通过同一个口吸入和排出液体
|
||||
# 连接特性:1个双向连接点(bidirectional)
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 8. virtual_column - 虚拟色谱柱
|
||||
# 描述:分离纯化设备,用于样品纯化
|
||||
# 连接特性:1个输入口 + 1个输出口
|
||||
# 数据类型:resource(资源/样品连接)
|
||||
virtual_pump:
|
||||
description: Virtual Pump for PumpTransferProtocol Testing
|
||||
class:
|
||||
@@ -30,11 +76,29 @@ virtual_pump:
|
||||
set_valve_position:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
Int32: Int32
|
||||
float_in: valve_position
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟泵节点配置 - 具有多通道阀门特性,根据valve_position可连接多个容器
|
||||
handles:
|
||||
input:
|
||||
- handler_key: pump-inlet
|
||||
label: Pump Inlet
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
description: "泵的进液口,连接源容器"
|
||||
output:
|
||||
- handler_key: pump-outlet-1
|
||||
label: Outlet Port 1
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out_1
|
||||
description: "阀门位置1的出液口"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -65,7 +129,7 @@ virtual_stirrer:
|
||||
result:
|
||||
success: success
|
||||
start_stir:
|
||||
type: ProtocolStartStir
|
||||
type: StartStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
stir_speed: stir_speed
|
||||
@@ -75,13 +139,23 @@ virtual_stirrer:
|
||||
result:
|
||||
success: success
|
||||
stop_stir:
|
||||
type: ProtocolStopStir
|
||||
type: StopStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟搅拌器节点配置 - 机械连接设备,单一双向连接点
|
||||
handles:
|
||||
bidirectional:
|
||||
- handler_key: stirrer-vessel
|
||||
label: Vessel Connection
|
||||
data_type: mechanical
|
||||
io_type: bidirectional
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "搅拌器的机械连接口,直接与反应容器连接提供搅拌功能"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -96,10 +170,10 @@ virtual_stirrer:
|
||||
default: 1000.0
|
||||
additionalProperties: false
|
||||
|
||||
virtual_valve:
|
||||
description: Virtual Valve for AddProtocol Testing
|
||||
virtual_multiway_valve:
|
||||
description: Virtual 8-Way Valve for flow direction control
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_valve:VirtualValve
|
||||
module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
@@ -115,18 +189,73 @@ virtual_valve:
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
# 八通阀门节点配置 - 1个输入口,8个输出口,可切换流向
|
||||
handles:
|
||||
input:
|
||||
- handler_key: multiway-valve-inlet
|
||||
label: Valve Inlet
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
description: "八通阀门进液口,接收来源流体"
|
||||
output:
|
||||
- handler_key: multiway-valve-port-1
|
||||
label: Port 1
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_1
|
||||
description: "八通阀门端口1,position=1时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-2
|
||||
label: Port 2
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_2
|
||||
description: "八通阀门端口2,position=2时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-3
|
||||
label: Port 3
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_3
|
||||
description: "八通阀门端口3,position=3时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-4
|
||||
label: Port 4
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_4
|
||||
description: "八通阀门端口4,position=4时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-5
|
||||
label: Port 5
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_5
|
||||
description: "八通阀门端口5,position=5时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-6
|
||||
label: Port 6
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_6
|
||||
description: "八通阀门端口6,position=6时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-7
|
||||
label: Port 7
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_7
|
||||
description: "八通阀门端口7,position=7时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-8
|
||||
label: Port 8
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_8
|
||||
description: "八通阀门端口8,position=8时流体从此口流出"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -135,9 +264,62 @@ virtual_valve:
|
||||
default: "VIRTUAL"
|
||||
positions:
|
||||
type: integer
|
||||
default: 6
|
||||
default: 8
|
||||
additionalProperties: false
|
||||
virtual_solenoid_valve:
|
||||
description: Virtual Solenoid Valve for simple on/off flow control
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
valve_state: String # "open" or "closed"
|
||||
is_open: Bool
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: "open"
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
close:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: "close"
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
set_state:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: command
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
# 电磁阀门节点配置 - 双向流通的开关型阀门,流动方向由泵决定
|
||||
handles:
|
||||
bidirectional:
|
||||
- handler_key: solenoid-valve-port
|
||||
label: Valve Port
|
||||
data_type: fluid
|
||||
io_type: bidirectional
|
||||
data_source: handle
|
||||
data_key: fluid_port
|
||||
description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
default: "VIRTUAL"
|
||||
voltage:
|
||||
type: number
|
||||
default: 12.0
|
||||
response_time:
|
||||
type: number
|
||||
default: 0.1
|
||||
additionalProperties: false
|
||||
|
||||
virtual_centrifuge:
|
||||
description: Virtual Centrifuge for CentrifugeProtocol Testing
|
||||
class:
|
||||
@@ -156,7 +338,7 @@ virtual_centrifuge:
|
||||
time_remaining: Float64
|
||||
action_value_mappings:
|
||||
centrifuge:
|
||||
type: ProtocolCentrifuge
|
||||
type: Centrifuge
|
||||
goal:
|
||||
vessel: vessel
|
||||
speed: speed
|
||||
@@ -170,6 +352,24 @@ virtual_centrifuge:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟离心机节点配置 - 单个样品处理设备,输入输出都是同一个样品容器
|
||||
handles:
|
||||
input:
|
||||
- handler_key: centrifuge-sample
|
||||
label: Sample Input
|
||||
data_type: resource
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "需要离心的样品容器"
|
||||
output:
|
||||
- handler_key: centrifuge-sample-out
|
||||
label: Centrifuged Sample
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: vessel
|
||||
description: "经过离心处理的样品容器"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -205,7 +405,7 @@ virtual_filter:
|
||||
message: String
|
||||
action_value_mappings:
|
||||
filter_sample:
|
||||
type: ProtocolFilter
|
||||
type: Filter
|
||||
goal:
|
||||
vessel: vessel
|
||||
filtrate_vessel: filtrate_vessel
|
||||
@@ -222,6 +422,31 @@ virtual_filter:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟过滤器节点配置 - 分离设备,1个输入(原始样品),2个输出(滤液和滤渣)
|
||||
handles:
|
||||
input:
|
||||
- handler_key: filter-sample-in
|
||||
label: Sample Input
|
||||
data_type: resource
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "需要过滤的原始样品容器"
|
||||
output:
|
||||
- handler_key: filter-filtrate-out
|
||||
label: Filtrate Output
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: filtrate_vessel
|
||||
description: "过滤后的滤液容器"
|
||||
- handler_key: filter-residue-out
|
||||
label: Filter Residue
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: residue_vessel
|
||||
description: "过滤后的滤渣(固体残留物)"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -275,6 +500,16 @@ virtual_heatchill:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟加热/冷却器节点配置 - 温控设备,单一双向连接点用于放置容器
|
||||
handles:
|
||||
bidirectional:
|
||||
- handler_key: heatchill-vessel
|
||||
label: Vessel Connection
|
||||
data_type: mechanical
|
||||
io_type: bidirectional
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "加热/冷却器的物理连接口,容器直接放置在设备上进行温度控制"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -293,7 +528,7 @@ virtual_heatchill:
|
||||
additionalProperties: false
|
||||
|
||||
virtual_transfer_pump:
|
||||
description: Virtual Transfer Pump for TransferProtocol Testing
|
||||
description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style)
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump
|
||||
type: python
|
||||
@@ -309,7 +544,7 @@ virtual_transfer_pump:
|
||||
current_status: String
|
||||
action_value_mappings:
|
||||
transfer:
|
||||
type: ProtocolTransfer
|
||||
type: Transfer
|
||||
goal:
|
||||
from_vessel: from_vessel
|
||||
to_vessel: to_vessel
|
||||
@@ -328,6 +563,16 @@ virtual_transfer_pump:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 注射器式转移泵节点配置 - 只有一个双向连接口,可吸入和排出液体
|
||||
handles:
|
||||
bidirectional:
|
||||
- handler_key: syringe-port
|
||||
label: Syringe Port
|
||||
data_type: fluid
|
||||
io_type: bidirectional
|
||||
data_source: handle
|
||||
data_key: fluid_port
|
||||
description: "注射器式转移泵的唯一连接口,通过阀门切换实现吸入和排出"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -359,7 +604,7 @@ virtual_column:
|
||||
current_status: String
|
||||
action_value_mappings:
|
||||
run_column:
|
||||
type: ProtocolRunColumn
|
||||
type: RunColumn
|
||||
goal:
|
||||
from_vessel: from_vessel
|
||||
to_vessel: to_vessel
|
||||
@@ -370,6 +615,24 @@ virtual_column:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟色谱柱节点配置 - 分离纯化设备,1个样品输入口,1个纯化产物输出口
|
||||
handles:
|
||||
input:
|
||||
- handler_key: column-sample-inlet
|
||||
label: Sample Input
|
||||
data_type: resource
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: from_vessel
|
||||
description: "需要纯化的样品输入口"
|
||||
output:
|
||||
- handler_key: column-product-outlet
|
||||
label: Purified Product
|
||||
data_type: resource
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: to_vessel
|
||||
description: "经过色谱柱纯化的产物输出口"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -601,10 +601,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
goal = goal_handle.request
|
||||
|
||||
# 从目标消息中提取参数, 并调用对应的方法
|
||||
if "sequence" in self._action_value_mappings:
|
||||
if "sequence" in action_value_mapping:
|
||||
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
|
||||
def ACTION(**kwargs):
|
||||
for i, action in enumerate(self._action_value_mappings["sequence"]):
|
||||
for i, action in enumerate(action_value_mapping["sequence"]):
|
||||
if i == 0:
|
||||
self.lab_logger().info(f"执行序列动作第一步: {action}")
|
||||
self.get_real_function(self.driver_instance, action)[0](**kwargs)
|
||||
@@ -612,9 +612,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
|
||||
self.get_real_function(self.driver_instance, action)[0]()
|
||||
|
||||
action_paramtypes = get_type_hints(
|
||||
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
|
||||
)[1]
|
||||
action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1]
|
||||
else:
|
||||
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
|
||||
|
||||
|
||||
@@ -256,12 +256,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
||||
return write_func(*args, **kwargs)
|
||||
|
||||
if read_method:
|
||||
bound_read = MethodType(_read, device.driver_instance)
|
||||
setattr(device.driver_instance, read_method, bound_read)
|
||||
# bound_read = MethodType(_read, device.driver_instance)
|
||||
setattr(device.driver_instance, read_method, _read)
|
||||
|
||||
if write_method:
|
||||
bound_write = MethodType(_write, device.driver_instance)
|
||||
setattr(device.driver_instance, write_method, bound_write)
|
||||
# bound_write = MethodType(_write, device.driver_instance)
|
||||
setattr(device.driver_instance, write_method, _write)
|
||||
|
||||
|
||||
async def _update_resources(self, goal, protocol_kwargs):
|
||||
|
||||
@@ -29,23 +29,23 @@ set(action_files
|
||||
"action/HeatChillStart.action"
|
||||
"action/HeatChillStop.action"
|
||||
|
||||
"action/ProtocolCleanVessel.action"
|
||||
"action/ProtocolDissolve.action"
|
||||
"action/ProtocolFilterThrough.action"
|
||||
"action/ProtocolRunColumn.action"
|
||||
"action/ProtocolWait.action"
|
||||
"action/ProtocolWashSolid.action"
|
||||
"action/ProtocolFilter.action"
|
||||
|
||||
"action/ProtocolCentrifuge.action"
|
||||
"action/ProtocolCrystallize.action"
|
||||
"action/ProtocolDry.action"
|
||||
"action/ProtocolPurge.action"
|
||||
"action/ProtocolStartPurge.action"
|
||||
"action/ProtocolStartStir.action"
|
||||
"action/ProtocolStopPurge.action"
|
||||
"action/ProtocolStopStir.action"
|
||||
"action/ProtocolTransfer.action"
|
||||
"action/CleanVessel.action"
|
||||
"action/Dissolve.action"
|
||||
"action/FilterThrough.action"
|
||||
"action/RunColumn.action"
|
||||
"action/Wait.action"
|
||||
"action/WashSolid.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/LiquidHandlerProtocolCreation.action"
|
||||
"action/LiquidHandlerAspirate.action"
|
||||
|
||||
Reference in New Issue
Block a user