mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-05 14:05:12 +00:00
Compare commits
2 Commits
6fd95fdb08
...
v0.9.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7db3123547 | ||
|
|
6da7a20a7a |
132
.github/workflows/multi-platform-build.yml
vendored
132
.github/workflows/multi-platform-build.yml
vendored
@@ -1,132 +0,0 @@
|
||||
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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -234,7 +234,3 @@ CATKIN_IGNORE
|
||||
|
||||
*.graphml
|
||||
unilabos/device_mesh/view_robot.rviz
|
||||
|
||||
|
||||
# Certs
|
||||
**/.certs
|
||||
@@ -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.6-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.6-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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.6
|
||||
version: 0.9.5
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
@@ -50,12 +50,12 @@ requirements:
|
||||
- robostack-staging::ros-humble-rosidl-default-generators
|
||||
- robostack-staging::ros-humble-std-msgs
|
||||
- robostack-staging::ros-humble-geometry-msgs
|
||||
- robostack-staging::ros2-distro-mutex=0.5.*
|
||||
- robostack-staging::ros2-distro-mutex=0.6.*
|
||||
run:
|
||||
- robostack-staging::ros-humble-action-msgs
|
||||
- robostack-staging::ros-humble-ros-workspace
|
||||
- robostack-staging::ros-humble-rosidl-default-runtime
|
||||
- robostack-staging::ros-humble-std-msgs
|
||||
- robostack-staging::ros-humble-geometry-msgs
|
||||
# - robostack-staging::ros2-distro-mutex=0.6.*
|
||||
- robostack-staging::ros2-distro-mutex=0.6.*
|
||||
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.6"
|
||||
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.6',
|
||||
version='0.9.5',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
@@ -2,10 +2,4 @@
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
|
||||
```
|
||||
|
||||
使用mock_all.json启动,重新捕获MockContainerForChiller1
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/host_node/create_resource unilabos_msgs/action/_resource_create_from_outer_easy/ResourceCreateFromOuterEasy "{ 'res_id': 'MockContainerForChiller1', 'device_id': 'MockChiller1', 'class_name': 'container', 'parent': 'MockChiller1', 'bind_locations': { 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'liquid_input_slot': [ -1 ], 'liquid_type': [ 'CuCl2' ], 'liquid_volume': [ 100.0 ], 'slot_on_deck': '' }"
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
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(√)
|
||||
@@ -1,882 +0,0 @@
|
||||
{
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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": "virtual_gas_source",
|
||||
"position": {
|
||||
"x": 650,
|
||||
"y": 150,
|
||||
"z": 0
|
||||
},
|
||||
"config": {},
|
||||
"data": {
|
||||
"gas_type": "nitrogen",
|
||||
"max_pressure": 5.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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
{
|
||||
"id": "MockChiller1",
|
||||
"name": "模拟冷却器",
|
||||
"children": [
|
||||
"MockContainerForChiller1"
|
||||
],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "mock_chiller",
|
||||
@@ -27,22 +25,6 @@
|
||||
"purpose": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MockContainerForChiller1",
|
||||
"name": "模拟容器",
|
||||
"type": "container",
|
||||
"parent": "MockChiller1",
|
||||
"position": {
|
||||
"x": 5,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"data": {
|
||||
"liquid_type": "CuCl2",
|
||||
"liquid_volume": "100"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "MockFilter1",
|
||||
"name": "模拟过滤器",
|
||||
|
||||
@@ -4,83 +4,58 @@
|
||||
"id": "AddTestStation",
|
||||
"name": "添加试剂测试工作站",
|
||||
"children": [
|
||||
"transfer_pump",
|
||||
"multiway_valve",
|
||||
"stirrer",
|
||||
"flask_reagent1",
|
||||
"flask_reagent2",
|
||||
"flask_reagent3",
|
||||
"flask_reagent4",
|
||||
"pump_add",
|
||||
"flask_1",
|
||||
"flask_2",
|
||||
"flask_3",
|
||||
"flask_4",
|
||||
"reactor",
|
||||
"flask_waste",
|
||||
"flask_rinsing",
|
||||
"flask_buffer"
|
||||
"stirrer",
|
||||
"flask_air"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 620,
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"protocol_type": ["AddProtocol", "TransferProtocol", "StartStirProtocol", "StopStirProtocol"]
|
||||
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"]
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "transfer_pump",
|
||||
"name": "注射器泵",
|
||||
"id": "pump_add",
|
||||
"name": "pump_add",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "device",
|
||||
"class": "virtual_transfer_pump",
|
||||
"class": "virtual_pump",
|
||||
"position": {
|
||||
"x": 520,
|
||||
"x": 520.6111111111111,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "VIRTUAL",
|
||||
"max_volume": 50.0,
|
||||
"transfer_rate": 5.0
|
||||
"max_volume": 25.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": "搅拌器",
|
||||
"name": "stirrer",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "device",
|
||||
"class": "virtual_stirrer",
|
||||
"position": {
|
||||
"x": 720,
|
||||
"y": 450,
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
@@ -93,115 +68,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_reagent1",
|
||||
"name": "试剂瓶1 (甲醇)",
|
||||
"id": "flask_1",
|
||||
"name": "通用试剂瓶1",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 100,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "甲醇",
|
||||
"volume": 800.0,
|
||||
"concentration": "99.9%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_reagent2",
|
||||
"name": "试剂瓶2 (乙醇)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "乙醇",
|
||||
"volume": 750.0,
|
||||
"concentration": "95%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_reagent3",
|
||||
"name": "试剂瓶3 (丙酮)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 260,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "丙酮",
|
||||
"volume": 900.0,
|
||||
"concentration": "99.5%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_reagent4",
|
||||
"name": "试剂瓶4 (二氯甲烷)",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 340,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"name": "二氯甲烷",
|
||||
"volume": 850.0,
|
||||
"concentration": "99.8%"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactor",
|
||||
"name": "反应器",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 720,
|
||||
"y": 400,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
@@ -212,165 +87,164 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_waste",
|
||||
"name": "废液瓶",
|
||||
"id": "flask_2",
|
||||
"name": "通用试剂瓶2",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 850,
|
||||
"y": 400,
|
||||
"x": 250,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 3000.0
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_rinsing",
|
||||
"name": "冲洗液瓶",
|
||||
"id": "flask_3",
|
||||
"name": "通用试剂瓶3",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 950,
|
||||
"x": 400,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_4",
|
||||
"name": "通用试剂瓶4",
|
||||
"children": [],
|
||||
"parent": "AddTestStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 550,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactor",
|
||||
"name": "reactor",
|
||||
"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,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 1000.0
|
||||
"max_volume": 2000.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"
|
||||
}
|
||||
]
|
||||
"liquid": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"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": {
|
||||
"multiway_valve": "multiway-valve-port-5",
|
||||
"reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "multiway_valve",
|
||||
"target": "flask_waste",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"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",
|
||||
"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",
|
||||
"reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_add",
|
||||
"target": "flask_air",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_add": "inlet",
|
||||
"flask_air": "top"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,17 +30,14 @@
|
||||
"children": [],
|
||||
"parent": "ReactorX",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 5000.0,
|
||||
"size_x": 200.0,
|
||||
"size_y": 200.0,
|
||||
"size_z": 200.0
|
||||
"max_volume": 5000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
@@ -74,7 +71,7 @@
|
||||
"type": "device",
|
||||
"class": "solenoid_valve.mock",
|
||||
"position": {
|
||||
"x": 780,
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
@@ -92,7 +89,7 @@
|
||||
"type": "device",
|
||||
"class": "vacuum_pump.mock",
|
||||
"position": {
|
||||
"x": 500,
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
@@ -110,7 +107,7 @@
|
||||
"type": "device",
|
||||
"class": "gas_source.mock",
|
||||
"position": {
|
||||
"x": 900,
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ def start_backend(
|
||||
backend: str,
|
||||
devices_config: dict = {},
|
||||
resources_config: list = [],
|
||||
resources_edge_config: list = [],
|
||||
graph=None,
|
||||
controllers_config: dict = {},
|
||||
bridges=[],
|
||||
@@ -32,7 +31,7 @@ def start_backend(
|
||||
|
||||
backend_thread = threading.Thread(
|
||||
target=main if not without_host else slave,
|
||||
args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config),
|
||||
args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config),
|
||||
name="backend_thread",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from copy import deepcopy
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.resources.graphio import tree_to_list, modify_to_backend_format
|
||||
from unilabos.resources.graphio import tree_to_list
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -136,16 +136,15 @@ def main():
|
||||
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"])
|
||||
resource_edge_info = []
|
||||
|
||||
devices_and_resources = None
|
||||
if args_dict["graph"] is not None:
|
||||
import unilabos.resources.graphio as graph_res
|
||||
if args_dict["graph"].endswith(".json"):
|
||||
graph, data = read_node_link_json(args_dict["graph"])
|
||||
else:
|
||||
graph, data = read_graphml(args_dict["graph"])
|
||||
graph_res.physical_setup_graph = graph
|
||||
resource_edge_info = modify_to_backend_format(data["links"])
|
||||
graph_res.physical_setup_graph = (
|
||||
read_node_link_json(args_dict["graph"])
|
||||
if args_dict["graph"].endswith(".json")
|
||||
else read_graphml(args_dict["graph"])
|
||||
)
|
||||
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
||||
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||
args_dict["resources_config"] = list(devices_and_resources.values())
|
||||
@@ -186,7 +185,6 @@ def main():
|
||||
signal.signal(signal.SIGTERM, _exit)
|
||||
mqtt_client.start()
|
||||
args_dict["resources_mesh_config"] = {}
|
||||
args_dict["resources_edge_config"] = resource_edge_info
|
||||
# web visiualize 2D
|
||||
if args_dict["visual"] != "disable":
|
||||
enable_rviz = args_dict["visual"] == "rviz"
|
||||
|
||||
@@ -30,27 +30,7 @@ class HTTPClient:
|
||||
self.auth = MQConfig.lab_id
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
Args:
|
||||
resources: 要添加的资源列表
|
||||
database_process_later: 后台处理资源
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={1 if database_process_later else 0}",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=5,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料关系失败: {response.text}")
|
||||
return response
|
||||
|
||||
def resource_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||
def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
@@ -66,8 +46,6 @@ class HTTPClient:
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=5,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response
|
||||
|
||||
def resource_get(self, id: str, with_children: bool = False) -> Dict[str, Any]:
|
||||
|
||||
@@ -16,6 +16,7 @@ from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.registry.registry import lab_registry
|
||||
from unilabos.app.mq import mqtt_client
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||
from unilabos.utils.log import error
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
@@ -15,116 +15,46 @@ def generate_add_protocol(
|
||||
purpose: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成添加试剂的协议序列
|
||||
|
||||
流程:
|
||||
1. 找到包含目标试剂的试剂瓶
|
||||
2. 配置八通阀门到试剂瓶位置
|
||||
3. 使用注射器泵吸取试剂
|
||||
4. 配置八通阀门到反应器位置
|
||||
5. 使用注射器泵推送试剂到反应器
|
||||
6. 如果需要,启动搅拌
|
||||
生成添加试剂的协议序列 - 严格按照 Add.action
|
||||
"""
|
||||
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 available_flasks:
|
||||
reagent_vessel = available_flasks[0]
|
||||
else:
|
||||
if not available_flasks:
|
||||
raise ValueError("没有找到可用的试剂容器")
|
||||
|
||||
reagent_vessel = available_flasks[0]
|
||||
|
||||
# 4. 获取试剂瓶和反应器对应的阀门位置
|
||||
# 这需要根据实际连接图来确定,这里假设:
|
||||
reagent_valve_position = 1 # 试剂瓶连接到阀门位置1
|
||||
reactor_valve_position = 2 # 反应器连接到阀门位置2
|
||||
# 查找泵设备
|
||||
pump_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_pump']
|
||||
|
||||
# 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
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
# 6. 如果需要搅拌,启动搅拌器
|
||||
# 如果需要搅拌,使用 StartStir 而不是 Stir
|
||||
if stir:
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if G.nodes[node].get('class') == 'virtual_stirrer']
|
||||
@@ -133,156 +63,12 @@ def generate_add_protocol(
|
||||
stirrer_id = stirrer_nodes[0]
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_name": "start_stir", # 使用 start_stir 而不是 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
|
||||
@@ -1,46 +0,0 @@
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class VirtualGasSource:
|
||||
"""Virtual gas source for testing"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||
self.device_id = device_id or "unknown_gas_source"
|
||||
self.config = config or {}
|
||||
self.data = {}
|
||||
self._status = "OPEN"
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual gas source"""
|
||||
self.data.update({
|
||||
"status": self._status
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual gas source"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def get_status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def set_status(self, string):
|
||||
self._status = string
|
||||
time.sleep(5)
|
||||
|
||||
def open(self):
|
||||
self._status = "OPEN"
|
||||
|
||||
def close(self):
|
||||
self._status = "CLOSED"
|
||||
|
||||
def is_open(self):
|
||||
return self._status
|
||||
|
||||
def is_closed(self):
|
||||
return not self._status
|
||||
@@ -1,221 +0,0 @@
|
||||
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()}")
|
||||
@@ -1,172 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class VirtualRotavap:
|
||||
"""Virtual rotary evaporator device for EvaporateProtocol testing"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[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")
|
||||
|
||||
# 设置默认值
|
||||
self.device_id = device_id or "unknown_rotavap"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualRotavap.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualRotavap {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_temp = self.config.get("max_temp") or kwargs.get("max_temp", 180.0)
|
||||
self._max_rotation_speed = self.config.get("max_rotation_speed") or kwargs.get("max_rotation_speed", 280.0)
|
||||
|
||||
# 处理其他kwargs参数,但跳过已知的配置参数
|
||||
skip_keys = {"port", "max_temp", "max_rotation_speed"}
|
||||
for key, value in kwargs.items():
|
||||
if key not in skip_keys and not hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual rotary evaporator"""
|
||||
print(f"=== VirtualRotavap {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}")
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Idle",
|
||||
"rotavap_state": "Ready",
|
||||
"current_temp": 25.0,
|
||||
"target_temp": 25.0,
|
||||
"max_temp": self._max_temp,
|
||||
"rotation_speed": 0.0,
|
||||
"max_rotation_speed": self._max_rotation_speed,
|
||||
"vacuum_pressure": 1.0, # atmospheric pressure
|
||||
"evaporated_volume": 0.0,
|
||||
"progress": 0.0,
|
||||
"message": "",
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual rotary evaporator"""
|
||||
self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}")
|
||||
return True
|
||||
|
||||
async def evaporate(
|
||||
self, vessel: str, pressure: float = 0.5, temp: float = 60.0, time: float = 300.0, stir_speed: float = 100.0
|
||||
) -> bool:
|
||||
"""Execute evaporate action - matches Evaporate action"""
|
||||
self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure}, temp={temp}, time={time}")
|
||||
|
||||
# 验证参数
|
||||
if temp > self._max_temp:
|
||||
self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}")
|
||||
self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}"
|
||||
return False
|
||||
|
||||
if stir_speed > self._max_rotation_speed:
|
||||
self.logger.error(f"Rotation speed {stir_speed} exceeds maximum {self._max_rotation_speed}")
|
||||
self.data["message"] = f"旋转速度 {stir_speed} 超过最大值 {self._max_rotation_speed}"
|
||||
return False
|
||||
|
||||
if pressure < 0.01 or pressure > 1.0:
|
||||
self.logger.error(f"Pressure {pressure} bar is out of valid range (0.01-1.0)")
|
||||
self.data["message"] = f"真空度 {pressure} bar 超出有效范围 (0.01-1.0)"
|
||||
return False
|
||||
|
||||
# 开始蒸发
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Running",
|
||||
"rotavap_state": "Evaporating",
|
||||
"target_temp": temp,
|
||||
"current_temp": temp,
|
||||
"rotation_speed": stir_speed,
|
||||
"vacuum_pressure": pressure,
|
||||
"vessel": vessel,
|
||||
"target_time": time,
|
||||
"progress": 0.0,
|
||||
"message": f"正在蒸发: {vessel}",
|
||||
}
|
||||
)
|
||||
|
||||
# 模拟蒸发过程
|
||||
simulation_time = min(time / 60.0, 10.0) # 最多模拟10秒
|
||||
for progress in range(0, 101, 10):
|
||||
await asyncio.sleep(simulation_time / 10)
|
||||
self.data["progress"] = progress
|
||||
self.data["evaporated_volume"] = progress * 0.5 # 假设最多蒸发50mL
|
||||
|
||||
# 蒸发完成
|
||||
evaporated_vol = 50.0 # 假设蒸发了50mL
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Idle",
|
||||
"rotavap_state": "Ready",
|
||||
"current_temp": 25.0,
|
||||
"target_temp": 25.0,
|
||||
"rotation_speed": 0.0,
|
||||
"vacuum_pressure": 1.0,
|
||||
"evaporated_volume": evaporated_vol,
|
||||
"progress": 100.0,
|
||||
"message": f"蒸发完成: {evaporated_vol}mL",
|
||||
}
|
||||
)
|
||||
|
||||
self.logger.info(f"Evaporation completed: {evaporated_vol}mL from {vessel}")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@property
|
||||
def rotavap_state(self) -> str:
|
||||
return self.data.get("rotavap_state", "Unknown")
|
||||
|
||||
@property
|
||||
def current_temp(self) -> float:
|
||||
return self.data.get("current_temp", 25.0)
|
||||
|
||||
@property
|
||||
def target_temp(self) -> float:
|
||||
return self.data.get("target_temp", 25.0)
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
return self.data.get("max_temp", self._max_temp)
|
||||
|
||||
@property
|
||||
def rotation_speed(self) -> float:
|
||||
return self.data.get("rotation_speed", 0.0)
|
||||
|
||||
@property
|
||||
def max_rotation_speed(self) -> float:
|
||||
return self.data.get("max_rotation_speed", self._max_rotation_speed)
|
||||
|
||||
@property
|
||||
def vacuum_pressure(self) -> float:
|
||||
return self.data.get("vacuum_pressure", 1.0)
|
||||
|
||||
@property
|
||||
def evaporated_volume(self) -> float:
|
||||
return self.data.get("evaporated_volume", 0.0)
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self.data.get("progress", 0.0)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.data.get("message", "")
|
||||
@@ -1,184 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class VirtualSeparator:
|
||||
"""Virtual separator device for SeparateProtocol testing"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[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")
|
||||
|
||||
# 设置默认值
|
||||
self.device_id = device_id or "unknown_separator"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualSeparator.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualSeparator {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._volume = self.config.get("volume") or kwargs.get("volume", 250.0)
|
||||
self._has_phases = self.config.get("has_phases") or kwargs.get("has_phases", True)
|
||||
|
||||
# 处理其他kwargs参数,但跳过已知的配置参数
|
||||
skip_keys = {"port", "volume", "has_phases"}
|
||||
for key, value in kwargs.items():
|
||||
if key not in skip_keys and not hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual separator"""
|
||||
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual separator {self.device_id}")
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Ready",
|
||||
"separator_state": "Ready",
|
||||
"volume": self._volume,
|
||||
"has_phases": self._has_phases,
|
||||
"phase_separation": False,
|
||||
"stir_speed": 0.0,
|
||||
"settling_time": 0.0,
|
||||
"progress": 0.0,
|
||||
"message": "",
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual separator"""
|
||||
self.logger.info(f"Cleaning up virtual separator {self.device_id}")
|
||||
return True
|
||||
|
||||
async def separate(
|
||||
self,
|
||||
purpose: str,
|
||||
product_phase: str,
|
||||
from_vessel: str,
|
||||
separation_vessel: str,
|
||||
to_vessel: str,
|
||||
waste_phase_to_vessel: str = "",
|
||||
solvent: str = "",
|
||||
solvent_volume: float = 50.0,
|
||||
through: str = "",
|
||||
repeats: int = 1,
|
||||
stir_time: float = 30.0,
|
||||
stir_speed: float = 300.0,
|
||||
settling_time: float = 300.0,
|
||||
) -> bool:
|
||||
"""Execute separate action - matches Separate action"""
|
||||
self.logger.info(f"Separate: purpose={purpose}, product_phase={product_phase}, from_vessel={from_vessel}")
|
||||
|
||||
# 验证参数
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
self.logger.error(f"Invalid product_phase {product_phase}, must be 'top' or 'bottom'")
|
||||
self.data["message"] = f"产物相位 {product_phase} 无效,必须是 'top' 或 'bottom'"
|
||||
return False
|
||||
|
||||
if purpose not in ["wash", "extract"]:
|
||||
self.logger.error(f"Invalid purpose {purpose}, must be 'wash' or 'extract'")
|
||||
self.data["message"] = f"分离目的 {purpose} 无效,必须是 'wash' 或 'extract'"
|
||||
return False
|
||||
|
||||
# 开始分离
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Running",
|
||||
"separator_state": "Separating",
|
||||
"purpose": purpose,
|
||||
"product_phase": product_phase,
|
||||
"from_vessel": from_vessel,
|
||||
"separation_vessel": separation_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"waste_phase_to_vessel": waste_phase_to_vessel,
|
||||
"solvent": solvent,
|
||||
"solvent_volume": solvent_volume,
|
||||
"repeats": repeats,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time,
|
||||
"phase_separation": True,
|
||||
"progress": 0.0,
|
||||
"message": f"正在分离: {from_vessel} -> {to_vessel}",
|
||||
}
|
||||
)
|
||||
|
||||
# 模拟分离过程
|
||||
total_time = (stir_time + settling_time) * repeats
|
||||
simulation_time = min(total_time / 60.0, 15.0) # 最多模拟15秒
|
||||
|
||||
for repeat in range(repeats):
|
||||
# 搅拌阶段
|
||||
for progress in range(0, 51, 10):
|
||||
await asyncio.sleep(simulation_time / (repeats * 10))
|
||||
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||
self.data["progress"] = overall_progress
|
||||
self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)"
|
||||
|
||||
# 静置分相阶段
|
||||
for progress in range(50, 101, 10):
|
||||
await asyncio.sleep(simulation_time / (repeats * 10))
|
||||
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||
self.data["progress"] = overall_progress
|
||||
self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)"
|
||||
|
||||
# 分离完成
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Ready",
|
||||
"separator_state": "Ready",
|
||||
"phase_separation": False,
|
||||
"stir_speed": 0.0,
|
||||
"progress": 100.0,
|
||||
"message": f"分离完成: {repeats}次分离操作",
|
||||
}
|
||||
)
|
||||
|
||||
self.logger.info(f"Separation completed: {repeats} cycles from {from_vessel} to {to_vessel}")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@property
|
||||
def separator_state(self) -> str:
|
||||
return self.data.get("separator_state", "Unknown")
|
||||
|
||||
@property
|
||||
def volume(self) -> float:
|
||||
return self.data.get("volume", self._volume)
|
||||
|
||||
@property
|
||||
def has_phases(self) -> bool:
|
||||
return self.data.get("has_phases", self._has_phases)
|
||||
|
||||
@property
|
||||
def phase_separation(self) -> bool:
|
||||
return self.data.get("phase_separation", False)
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self.data.get("stir_speed", 0.0)
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self.data.get("settling_time", 0.0)
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self.data.get("progress", 0.0)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.data.get("message", "")
|
||||
@@ -1,151 +0,0 @@
|
||||
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,290 +1,149 @@
|
||||
import asyncio
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Union, Optional
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class VirtualPumpMode(Enum):
|
||||
Normal = 0
|
||||
AccuratePos = 1
|
||||
AccuratePosVel = 2
|
||||
|
||||
|
||||
class VirtualPump:
|
||||
"""虚拟泵类 - 模拟泵的基本功能,无需实际硬件"""
|
||||
class VirtualTransferPump:
|
||||
"""Virtual pump device specifically for Transfer protocol"""
|
||||
|
||||
def __init__(self, device_id: str = None, max_volume: float = 25.0, mode: VirtualPumpMode = VirtualPumpMode.Normal, transfer_rate=0):
|
||||
self.device_id = device_id or "virtual_pump"
|
||||
self.max_volume = max_volume
|
||||
self._transfer_rate = transfer_rate
|
||||
self.mode = mode
|
||||
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')
|
||||
|
||||
# 状态变量
|
||||
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"VirtualPump.{self.device_id}")
|
||||
# 设置默认值
|
||||
self.device_id = device_id or "unknown_transfer_pump"
|
||||
self.config = config or {}
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""初始化虚拟泵"""
|
||||
self.logger.info(f"Initializing virtual pump {self.device_id}")
|
||||
self._status = "Idle"
|
||||
self._position = 0.0
|
||||
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
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"
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""清理虚拟泵"""
|
||||
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
|
||||
self._status = "Idle"
|
||||
"""Cleanup virtual transfer pump"""
|
||||
self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}")
|
||||
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._status
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
"""当前柱塞位置 (ml)"""
|
||||
return self._position
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@property
|
||||
def current_volume(self) -> float:
|
||||
"""当前注射器中的体积 (ml)"""
|
||||
return self._current_volume
|
||||
return self.data.get("current_volume", 0.0)
|
||||
|
||||
@property
|
||||
def max_velocity(self) -> float:
|
||||
return self._max_velocity
|
||||
def max_volume(self) -> float:
|
||||
return self.data.get("max_volume", self._max_volume)
|
||||
|
||||
@property
|
||||
def transfer_rate(self) -> float:
|
||||
return 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")
|
||||
return self.data.get("transfer_rate", self._transfer_rate)
|
||||
|
||||
def get_status(self) -> str:
|
||||
"""获取泵状态"""
|
||||
return self._status
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self.data.get("from_vessel", "")
|
||||
|
||||
async def _simulate_operation(self, duration: float):
|
||||
"""模拟操作延时"""
|
||||
self._status = "Busy"
|
||||
await asyncio.sleep(duration)
|
||||
self._status = "Idle"
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self.data.get("to_vessel", "")
|
||||
|
||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||
"""计算操作持续时间"""
|
||||
if velocity is None:
|
||||
velocity = self._max_velocity
|
||||
return abs(volume) / velocity
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self.data.get("progress", 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 transferred_volume(self) -> float:
|
||||
return self.data.get("transferred_volume", 0.0)
|
||||
|
||||
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())
|
||||
@property
|
||||
def current_status(self) -> str:
|
||||
return self.data.get("current_status", "Ready")
|
||||
@@ -1,47 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class VirtualVacuumPump:
|
||||
"""Virtual vacuum pump for testing"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||
self.device_id = device_id or "unknown_vacuum_pump"
|
||||
self.config = config or {}
|
||||
self.data = {}
|
||||
self._status = "OPEN"
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual vacuum pump"""
|
||||
self.data.update({
|
||||
"status": self._status
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual vacuum pump"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def get_status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def set_status(self, string):
|
||||
self._status = string
|
||||
time.sleep(5)
|
||||
|
||||
def open(self):
|
||||
self._status = "OPEN"
|
||||
|
||||
def close(self):
|
||||
self._status = "CLOSED"
|
||||
|
||||
def is_open(self):
|
||||
return self._status
|
||||
|
||||
def is_closed(self):
|
||||
return not self._status
|
||||
@@ -89,7 +89,7 @@ mock_filter:
|
||||
target_volume: Float64
|
||||
action_value_mappings:
|
||||
filter:
|
||||
type: Filter
|
||||
type: ProtocolFilter
|
||||
goal:
|
||||
vessel: vessel
|
||||
filtrate_vessel: filtrate_vessel
|
||||
@@ -737,7 +737,7 @@ mock_stirrer_new:
|
||||
max_stir_speed: Float64
|
||||
action_value_mappings:
|
||||
start_stir:
|
||||
type: StartStir
|
||||
type: ProtocolStartStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
stir_speed: stir_speed
|
||||
@@ -760,7 +760,7 @@ mock_stirrer_new:
|
||||
result:
|
||||
success: success
|
||||
stop_stir:
|
||||
type: StopStir
|
||||
type: ProtocolStopStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
|
||||
@@ -48,14 +48,14 @@ solenoid_valve.mock:
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: 0
|
||||
label: 0
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
- handler_key: 1
|
||||
label: 1
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -23,12 +23,20 @@ vacuum_pump.mock:
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -64,8 +72,16 @@ gas_source.mock:
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
input:
|
||||
- handler_key: fluid-input
|
||||
label: Fluid Input
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
output:
|
||||
- handler_key: fluid-output
|
||||
label: Fluid Output
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
|
||||
@@ -1,60 +1,3 @@
|
||||
# 虚拟设备清单及连接特性
|
||||
|
||||
# 1. virtual_pump - 虚拟泵
|
||||
# 描述:具有多通道阀门特性的泵,根据valve_position可连接多个容器
|
||||
# 连接特性:1个输入口 + 1个输出口(当前配置,实际应该有多个输出口)
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 2. virtual_stirrer - 虚拟搅拌器
|
||||
# 描述:机械连接设备,提供搅拌功能
|
||||
# 连接特性:1个双向连接点(undirected)
|
||||
# 数据类型: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个双向连接点(undirected)
|
||||
# 数据类型:mechanical(机械/物理接触连接)
|
||||
|
||||
# 7. virtual_transfer_pump - 虚拟转移泵(注射器式)
|
||||
# 描述:注射器式转移泵,通过同一个口吸入和排出液体
|
||||
# 连接特性:1个双向连接点(undirected)
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
# 8. virtual_column - 虚拟色谱柱
|
||||
# 描述:分离纯化设备,用于样品纯化
|
||||
# 连接特性:1个输入口 + 1个输出口
|
||||
# 数据类型:resource(资源/样品连接)
|
||||
|
||||
# 9. virtual_rotavap - 虚拟旋转蒸发仪
|
||||
# 描述:旋转蒸发仪用于溶剂蒸发和浓缩,具有加热、旋转和真空功能
|
||||
# 连接特性:1个输入口(样品),1个输出口(浓缩物),1个冷凝器出口(回收溶剂)
|
||||
# 数据类型:resource(资源/样品连接)
|
||||
|
||||
# 10. virtual_separator - 虚拟分液器
|
||||
# 描述:分液器用于两相液体的分离,可进行萃取和洗涤操作
|
||||
# 连接特性:1个输入口(混合液),2个输出口(上相和下相)
|
||||
# 数据类型:fluid(流体连接)
|
||||
|
||||
virtual_pump:
|
||||
description: Virtual Pump for PumpTransferProtocol Testing
|
||||
class:
|
||||
@@ -63,7 +6,7 @@ virtual_pump:
|
||||
status_types:
|
||||
status: String
|
||||
position: Float64
|
||||
valve_position: Int32 # 修复:使用 Int32 而不是 String
|
||||
valve_position: Int32 # 修复:使用 Int32 而不是 String
|
||||
max_volume: Float64
|
||||
current_volume: Float64
|
||||
action_value_mappings:
|
||||
@@ -87,20 +30,11 @@ virtual_pump:
|
||||
set_valve_position:
|
||||
type: FloatSingleInput
|
||||
goal:
|
||||
float_in: valve_position
|
||||
Int32: Int32
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟泵节点配置 - 具有多通道阀门特性,根据valve_position可连接多个容器
|
||||
handles:
|
||||
- handler_key: pump-inlet
|
||||
label: Pump Inlet
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
description: "泵的进液口,连接源容器"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -131,7 +65,7 @@ virtual_stirrer:
|
||||
result:
|
||||
success: success
|
||||
start_stir:
|
||||
type: StartStir
|
||||
type: ProtocolStartStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
stir_speed: stir_speed
|
||||
@@ -141,23 +75,13 @@ virtual_stirrer:
|
||||
result:
|
||||
success: success
|
||||
stop_stir:
|
||||
type: StopStir
|
||||
type: ProtocolStopStir
|
||||
goal:
|
||||
vessel: vessel
|
||||
feedback:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟搅拌器节点配置 - 机械连接设备,单一双向连接点
|
||||
handles:
|
||||
- handler_key: stirrer-vessel
|
||||
label: Vessel Connection
|
||||
data_type: mechanical
|
||||
side: SOUTH
|
||||
io_type: undirected
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "搅拌器的机械连接口,直接与反应容器连接提供搅拌功能"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -172,10 +96,10 @@ virtual_stirrer:
|
||||
default: 1000.0
|
||||
additionalProperties: false
|
||||
|
||||
virtual_multiway_valve:
|
||||
description: Virtual 8-Way Valve for flow direction control
|
||||
virtual_valve:
|
||||
description: Virtual Valve for AddProtocol Testing
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve
|
||||
module: unilabos.devices.virtual.virtual_valve:VirtualValve
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
@@ -191,79 +115,18 @@ virtual_multiway_valve:
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
# 八通阀门节点配置 - 1个输入口,8个输出口,可切换流向
|
||||
handles:
|
||||
- handler_key: multiway-valve-inlet
|
||||
label: Valve Inlet
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
description: "八通阀门进液口,接收来源流体"
|
||||
- handler_key: multiway-valve-port-1
|
||||
label: 1
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_1
|
||||
description: "八通阀门端口1,position=1时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-2
|
||||
label: 2
|
||||
data_type: fluid
|
||||
side: EAST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_2
|
||||
description: "八通阀门端口2,position=2时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-3
|
||||
label: 3
|
||||
data_type: fluid
|
||||
side: EAST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_3
|
||||
description: "八通阀门端口3,position=3时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-4
|
||||
label: 4
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_4
|
||||
description: "八通阀门端口4,position=4时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-5
|
||||
label: 5
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_5
|
||||
description: "八通阀门端口5,position=5时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-7
|
||||
label: 7
|
||||
data_type: fluid
|
||||
side: WEST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_7
|
||||
description: "八通阀门端口7,position=7时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-6
|
||||
label: 6
|
||||
data_type: fluid
|
||||
side: WEST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_6
|
||||
description: "八通阀门端口6,position=6时流体从此口流出"
|
||||
- handler_key: multiway-valve-port-8
|
||||
label: 8
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_port_8
|
||||
description: "八通阀门端口8,position=8时流体从此口流出"
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -272,68 +135,9 @@ virtual_multiway_valve:
|
||||
default: "VIRTUAL"
|
||||
positions:
|
||||
type: integer
|
||||
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:
|
||||
- handler_key: solenoid-valve-port-in
|
||||
label: in
|
||||
data_type: fluid
|
||||
io_type: undirected
|
||||
data_source: handle
|
||||
data_key: fluid_port
|
||||
description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断"
|
||||
- handler_key: solenoid-valve-port-out
|
||||
label: out
|
||||
data_type: fluid
|
||||
io_type: undirected
|
||||
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
|
||||
default: 6
|
||||
additionalProperties: false
|
||||
|
||||
virtual_centrifuge:
|
||||
description: Virtual Centrifuge for CentrifugeProtocol Testing
|
||||
class:
|
||||
@@ -352,7 +156,7 @@ virtual_centrifuge:
|
||||
time_remaining: Float64
|
||||
action_value_mappings:
|
||||
centrifuge:
|
||||
type: Centrifuge
|
||||
type: ProtocolCentrifuge
|
||||
goal:
|
||||
vessel: vessel
|
||||
speed: speed
|
||||
@@ -366,15 +170,6 @@ virtual_centrifuge:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟离心机节点配置 - 单个样品处理设备,输入输出都是同一个样品容器
|
||||
handles:
|
||||
- handler_key: centrifuge-sample
|
||||
label: Sample Input/Output
|
||||
data_type: transport
|
||||
io_type: undirected
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "需要离心的样品容器"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -410,7 +205,7 @@ virtual_filter:
|
||||
message: String
|
||||
action_value_mappings:
|
||||
filter_sample:
|
||||
type: Filter
|
||||
type: ProtocolFilter
|
||||
goal:
|
||||
vessel: vessel
|
||||
filtrate_vessel: filtrate_vessel
|
||||
@@ -427,32 +222,6 @@ virtual_filter:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟过滤器节点配置 - 分离设备,1个输入(原始样品),2个输出(滤液和滤渣)
|
||||
handles:
|
||||
- handler_key: filter-in
|
||||
label: Input
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "需要过滤的原始样品容器"
|
||||
- handler_key: filter-filtrate-out
|
||||
label: Output
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: filtrate_vessel
|
||||
description: "过滤后的滤液容器"
|
||||
- handler_key: filter-residue-out
|
||||
label: Residue
|
||||
data_type: resource
|
||||
side: WEST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: residue_vessel
|
||||
description: "过滤后的滤渣(固体残留物)"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -506,16 +275,6 @@ virtual_heatchill:
|
||||
status: status
|
||||
result:
|
||||
success: success
|
||||
# 虚拟加热/冷却器节点配置 - 温控设备,单一双向连接点用于放置容器
|
||||
handles:
|
||||
- handler_key: heatchill-vessel
|
||||
label: Connection
|
||||
data_type: mechanical
|
||||
side: NORTH
|
||||
io_type: undirected
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "加热/冷却器的物理连接口,容器直接放置在设备上进行温度控制"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -527,25 +286,30 @@ virtual_heatchill:
|
||||
default: 200.0
|
||||
min_temp:
|
||||
type: number
|
||||
default: -80
|
||||
default: -80.0
|
||||
max_stir_speed:
|
||||
type: number
|
||||
default: 1000.0
|
||||
additionalProperties: false
|
||||
|
||||
virtual_transfer_pump:
|
||||
description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style)
|
||||
description: Virtual Transfer Pump for TransferProtocol Testing
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_transferpump:VirtualPump
|
||||
module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
current_volume: Float64
|
||||
max_volume: Float64
|
||||
transfer_rate: Float64
|
||||
from_vessel: String
|
||||
to_vessel: String
|
||||
progress: Float64
|
||||
transferred_volume: Float64
|
||||
current_status: String
|
||||
action_value_mappings:
|
||||
transfer:
|
||||
type: Transfer
|
||||
type: ProtocolTransfer
|
||||
goal:
|
||||
from_vessel: from_vessel
|
||||
to_vessel: to_vessel
|
||||
@@ -564,16 +328,6 @@ virtual_transfer_pump:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 注射器式转移泵节点配置 - 只有一个双向连接口,可吸入和排出液体
|
||||
handles:
|
||||
undirected:
|
||||
- handler_key: syringe-port
|
||||
label: Syringe Port
|
||||
data_type: fluid
|
||||
io_type: undirected
|
||||
data_source: handle
|
||||
data_key: fluid_port
|
||||
description: "注射器式转移泵的唯一连接口,通过阀门切换实现吸入和排出"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -605,7 +359,7 @@ virtual_column:
|
||||
current_status: String
|
||||
action_value_mappings:
|
||||
run_column:
|
||||
type: RunColumn
|
||||
type: ProtocolRunColumn
|
||||
goal:
|
||||
from_vessel: from_vessel
|
||||
to_vessel: to_vessel
|
||||
@@ -616,24 +370,6 @@ virtual_column:
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟色谱柱节点配置 - 分离纯化设备,1个样品输入口,1个纯化产物输出口
|
||||
handles:
|
||||
- handler_key: column-sample-inlet
|
||||
label: Sample Input
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: from_vessel
|
||||
description: "需要纯化的样品输入口"
|
||||
- handler_key: column-product-outlet
|
||||
label: Purified Product
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: to_vessel
|
||||
description: "经过色谱柱纯化的产物输出口"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
@@ -649,238 +385,4 @@ virtual_column:
|
||||
column_diameter:
|
||||
type: number
|
||||
default: 2.0
|
||||
additionalProperties: false
|
||||
|
||||
virtual_rotavap:
|
||||
description: Virtual Rotary Evaporator for EvaporateProtocol Testing
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
rotavap_state: String
|
||||
current_temp: Float64
|
||||
target_temp: Float64
|
||||
max_temp: Float64
|
||||
rotation_speed: Float64
|
||||
max_rotation_speed: Float64
|
||||
vacuum_pressure: Float64
|
||||
evaporated_volume: Float64
|
||||
progress: Float64
|
||||
message: String
|
||||
action_value_mappings:
|
||||
evaporate:
|
||||
type: Evaporate
|
||||
goal:
|
||||
vessel: vessel
|
||||
pressure: pressure
|
||||
temp: temp
|
||||
time: time
|
||||
stir_speed: stir_speed
|
||||
feedback:
|
||||
progress: progress
|
||||
current_temp: current_temp
|
||||
evaporated_volume: evaporated_volume
|
||||
current_status: status
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟旋转蒸发仪节点配置 - 蒸发浓缩设备,1个输入口(样品),2个输出口(浓缩物和冷凝液)
|
||||
handles:
|
||||
- handler_key: rotavap-sample-inlet
|
||||
label: Sample Input
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: vessel
|
||||
description: "需要蒸发的样品输入口"
|
||||
- handler_key: rotavap-concentrate-outlet
|
||||
label: Concentrate
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: concentrate_vessel
|
||||
description: "蒸发浓缩后的产物输出口"
|
||||
- handler_key: rotavap-distillate-outlet
|
||||
label: Distillate
|
||||
data_type: fluid
|
||||
side: WEST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: distillate_vessel
|
||||
description: "冷凝回收的溶剂输出口"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
default: "VIRTUAL"
|
||||
max_temp:
|
||||
type: number
|
||||
default: 180.0
|
||||
max_rotation_speed:
|
||||
type: number
|
||||
default: 280.0
|
||||
additionalProperties: false
|
||||
|
||||
virtual_separator:
|
||||
description: Virtual Separator for SeparateProtocol Testing
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_separator:VirtualSeparator
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
separator_state: String
|
||||
volume: Float64
|
||||
has_phases: Bool
|
||||
phase_separation: Bool
|
||||
stir_speed: Float64
|
||||
settling_time: Float64
|
||||
progress: Float64
|
||||
message: String
|
||||
action_value_mappings:
|
||||
separate:
|
||||
type: Separate
|
||||
goal:
|
||||
purpose: purpose
|
||||
product_phase: product_phase
|
||||
from_vessel: from_vessel
|
||||
separation_vessel: separation_vessel
|
||||
to_vessel: to_vessel
|
||||
waste_phase_to_vessel: waste_phase_to_vessel
|
||||
solvent: solvent
|
||||
solvent_volume: solvent_volume
|
||||
through: through
|
||||
repeats: repeats
|
||||
stir_time: stir_time
|
||||
stir_speed: stir_speed
|
||||
settling_time: settling_time
|
||||
feedback:
|
||||
progress: progress
|
||||
current_status: status
|
||||
result:
|
||||
success: success
|
||||
message: message
|
||||
# 虚拟分液器节点配置 - 分离设备,1个输入口(混合液),2个输出口(上相和下相)
|
||||
handles:
|
||||
- handler_key: separator-inlet
|
||||
label: Mixed Input
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: from_vessel
|
||||
description: "需要分离的混合液体输入口"
|
||||
- handler_key: separator-top-outlet
|
||||
label: Top Phase
|
||||
data_type: fluid
|
||||
side: EAST
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: top_outlet
|
||||
description: "上相(轻相)液体输出口"
|
||||
- handler_key: separator-bottom-outlet
|
||||
label: Bottom Phase
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: bottom_outlet
|
||||
description: "下相(重相)液体输出口"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
default: "VIRTUAL"
|
||||
volume:
|
||||
type: number
|
||||
default: 250.0
|
||||
has_phases:
|
||||
type: boolean
|
||||
default: true
|
||||
additionalProperties: false
|
||||
|
||||
virtual_vacuum_pump:
|
||||
description: Virtual vacuum pump
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
data_type: fluid
|
||||
io_type: target
|
||||
data_source: handle
|
||||
data_key: fluid_in
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "VIRTUAL"
|
||||
required:
|
||||
- port
|
||||
|
||||
virtual_gas_source:
|
||||
description: Virtual gas source
|
||||
class:
|
||||
module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource
|
||||
type: python
|
||||
status_types:
|
||||
status: String
|
||||
action_value_mappings:
|
||||
open:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
close:
|
||||
type: EmptyIn
|
||||
goal: {}
|
||||
feedback: {}
|
||||
result: {}
|
||||
set_status:
|
||||
type: StrSingleInput
|
||||
goal:
|
||||
string: string
|
||||
feedback: {}
|
||||
result: {}
|
||||
handles:
|
||||
- handler_key: out
|
||||
label: out
|
||||
data_type: fluid
|
||||
io_type: source
|
||||
data_source: executor
|
||||
data_key: fluid_out
|
||||
init_param_schema:
|
||||
type: object
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
description: "通信端口"
|
||||
default: "VIRTUAL"
|
||||
required:
|
||||
- port
|
||||
additionalProperties: false
|
||||
@@ -1,14 +0,0 @@
|
||||
container:
|
||||
description: regular organic container
|
||||
class:
|
||||
module: unilabos.resources.container:RegularContainer
|
||||
type: unilabos
|
||||
handles:
|
||||
- handler_key: top
|
||||
label: top
|
||||
data_type: fluid
|
||||
side: NORTH
|
||||
- handler_key: bottom
|
||||
label: bottom
|
||||
data_type: fluid
|
||||
side: SOUTH
|
||||
@@ -1,67 +0,0 @@
|
||||
import json
|
||||
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
||||
|
||||
|
||||
class RegularContainer(object):
|
||||
# 第一个参数必须是id传入
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, id: str):
|
||||
self.id = id
|
||||
self.ulr_resource = Resource()
|
||||
self._data = None
|
||||
|
||||
@property
|
||||
def ulr_resource_data(self):
|
||||
if self._data is None:
|
||||
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
|
||||
return self._data
|
||||
|
||||
@ulr_resource_data.setter
|
||||
def ulr_resource_data(self, value: dict):
|
||||
self._data = value
|
||||
self.ulr_resource.data = json.dumps(self._data)
|
||||
|
||||
@property
|
||||
def liquid_type(self):
|
||||
return self.ulr_resource_data.get("liquid_type", None)
|
||||
|
||||
@liquid_type.setter
|
||||
def liquid_type(self, value: str):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_type"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_type", None)
|
||||
|
||||
@property
|
||||
def liquid_volume(self):
|
||||
return self.ulr_resource_data.get("liquid_volume", None)
|
||||
|
||||
@liquid_volume.setter
|
||||
def liquid_volume(self, value: float):
|
||||
if value is not None:
|
||||
self.ulr_resource_data["liquid_volume"] = value
|
||||
else:
|
||||
self.ulr_resource_data.pop("liquid_volume", None)
|
||||
|
||||
def get_ulr_resource(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
|
||||
return self.ulr_resource
|
||||
|
||||
def get_ulr_resource_as_dict(self) -> Resource:
|
||||
"""
|
||||
获取UlrResource对象
|
||||
:return: UlrResource对象
|
||||
"""
|
||||
to_dict = convert_from_ros_msg(self.get_ulr_resource())
|
||||
to_dict["type"] = "container"
|
||||
return to_dict
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}"
|
||||
@@ -1,13 +1,9 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
from typing import Union, Any
|
||||
from typing import Union
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg_with_mapping, convert_to_ros_msg
|
||||
|
||||
try:
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
@@ -84,8 +80,6 @@ def canonicalize_links_ports(data: dict) -> dict:
|
||||
# 第一遍处理:将字符串类型的port转换为字典格式
|
||||
for link in data.get("links", []):
|
||||
port = link.get("port")
|
||||
if link["type"] == "physical":
|
||||
link["type"] = "fluid"
|
||||
if isinstance(port, int):
|
||||
port = str(port)
|
||||
if isinstance(port, str):
|
||||
@@ -159,28 +153,7 @@ def read_node_link_json(json_file):
|
||||
|
||||
physical_setup_graph = nx.node_link_graph(data, multigraph=False) # edges="links" 3.6 warning
|
||||
handle_communications(physical_setup_graph)
|
||||
return physical_setup_graph, data
|
||||
|
||||
|
||||
def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
for edge in data:
|
||||
port = edge.pop("port", {})
|
||||
source = edge["source"]
|
||||
target = edge["target"]
|
||||
if source in port:
|
||||
edge["sourceHandle"] = port[source]
|
||||
elif "source_port" in edge:
|
||||
edge["sourceHandle"] = edge.pop("source_port")
|
||||
if target in port:
|
||||
edge["targetHandle"] = port[target]
|
||||
elif "target_port" in edge:
|
||||
edge["targetHandle"] = edge.pop("target_port")
|
||||
if "id" not in edge:
|
||||
edge["id"] = f"link_generated_{source}_{target}"
|
||||
for key in ["source_port", "target_port"]:
|
||||
if key in edge:
|
||||
edge.pop(key)
|
||||
return data
|
||||
return physical_setup_graph
|
||||
|
||||
|
||||
def read_graphml(graphml_file):
|
||||
@@ -205,7 +178,7 @@ def read_graphml(graphml_file):
|
||||
|
||||
physical_setup_graph = nx.node_link_graph(data, edges="links", multigraph=False) # edges="links" 3.6 warning
|
||||
handle_communications(physical_setup_graph)
|
||||
return physical_setup_graph, data
|
||||
return physical_setup_graph
|
||||
|
||||
|
||||
def dict_from_graph(graph: nx.Graph) -> dict:
|
||||
@@ -493,10 +466,6 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
elif resource_class_config["type"] == "unilabos":
|
||||
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
||||
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
|
||||
r = [res_instance.get_ulr_resource_as_dict()]
|
||||
elif isinstance(RESOURCE, dict):
|
||||
r = [RESOURCE.copy()]
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ def exit() -> None:
|
||||
def main(
|
||||
devices_config: Dict[str, Any] = {},
|
||||
resources_config: list=[],
|
||||
resources_edge_config: list=[],
|
||||
graph: Optional[Dict[str, Any]] = None,
|
||||
controllers_config: Dict[str, Any] = {},
|
||||
bridges: List[Any] = [],
|
||||
@@ -63,7 +62,6 @@ def main(
|
||||
"host_node",
|
||||
devices_config,
|
||||
resources_config,
|
||||
resources_edge_config,
|
||||
graph,
|
||||
controllers_config,
|
||||
bridges,
|
||||
@@ -99,7 +97,6 @@ def main(
|
||||
def slave(
|
||||
devices_config: Dict[str, Any] = {},
|
||||
resources_config=[],
|
||||
resources_edge_config=[],
|
||||
graph: Optional[Dict[str, Any]] = None,
|
||||
controllers_config: Dict[str, Any] = {},
|
||||
bridges: List[Any] = [],
|
||||
|
||||
@@ -100,7 +100,7 @@ _action_mapping: Dict[Type, Dict[str, Any]] = {
|
||||
|
||||
# 添加Protocol action类型到映射
|
||||
for py_msgtype in imsg.__all__:
|
||||
if py_msgtype not in _action_mapping and (py_msgtype.endswith("Protocol") or py_msgtype.startswith("Protocol")):
|
||||
if py_msgtype not in _action_mapping and py_msgtype.endswith("Protocol"):
|
||||
try:
|
||||
protocol_class = msg_converter_manager.get_class(f"unilabos.messages.{py_msgtype}")
|
||||
action_name = py_msgtype.replace("Protocol", "")
|
||||
@@ -117,7 +117,6 @@ for py_msgtype in imsg.__all__:
|
||||
"result": {k: k for k in action_type.Result().get_fields_and_field_types().keys()},
|
||||
}
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logger.debug(f"Failed to load Protocol class: {py_msgtype}")
|
||||
|
||||
# Python到ROS消息转换器
|
||||
|
||||
@@ -19,7 +19,6 @@ from rclpy.service import Service
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.resources.graphio import (
|
||||
convert_resources_to_type,
|
||||
convert_resources_from_type,
|
||||
@@ -345,7 +344,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
|
||||
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
|
||||
slot = other_calling_param.pop("slot", "-1")
|
||||
resource = None
|
||||
if slot != "-1": # slot为负数的时候采用assign方法
|
||||
other_calling_param["slot"] = slot
|
||||
# 本地拿到这个物料,可能需要先做初始化?
|
||||
@@ -364,28 +362,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if initialize_full:
|
||||
resources = initialize_resources([resources])
|
||||
request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1:
|
||||
container_instance = request.resources[0]
|
||||
container_query_dict: dict = resources
|
||||
found_resources = self.resource_tracker.figure_resource({"id": container_query_dict["name"]}, try_mode=True)
|
||||
if not len(found_resources):
|
||||
self.resource_tracker.add_resource(container_instance)
|
||||
logger.info(f"添加物料{container_query_dict['name']}到资源跟踪器")
|
||||
else:
|
||||
assert len(found_resources) == 1, f"找到多个同名物料: {container_query_dict['name']}, 请检查物料系统"
|
||||
resource = found_resources[0]
|
||||
if isinstance(resource, Resource):
|
||||
regular_container = RegularContainer(resource.id)
|
||||
regular_container.ulr_resource = resource
|
||||
regular_container.ulr_resource_data.update(json.loads(container_instance.data))
|
||||
logger.info(f"更新物料{container_query_dict['name']}的数据{resource.data} ULR")
|
||||
elif isinstance(resource, dict):
|
||||
if "data" not in resource:
|
||||
resource["data"] = {}
|
||||
resource["data"].update(json.loads(container_instance.data))
|
||||
logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict")
|
||||
else:
|
||||
logger.info(f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}")
|
||||
response = rclient.call(request)
|
||||
# 应该先add_resource了
|
||||
res.response = "OK"
|
||||
@@ -409,8 +385,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
res.response = serialize_result_info(traceback.format_exc(), False, {})
|
||||
return res
|
||||
# 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中
|
||||
if bind_parent_id != self.node_name:
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) # 拿到父节点,进行具体assign等操作
|
||||
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
|
||||
# request.resources = [convert_to_ros_msg(Resource, resources)]
|
||||
|
||||
try:
|
||||
@@ -460,7 +435,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
"bind_parent_id": bind_parent_id,
|
||||
}
|
||||
)
|
||||
future = action_client.send_goal_async(goal)
|
||||
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
|
||||
|
||||
def done_cb(*args):
|
||||
self.lab_logger().info(f"向meshmanager发送新增resource完成")
|
||||
@@ -926,9 +901,9 @@ class ROS2DeviceNode:
|
||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||
|
||||
if self._driver_class is ROS2ProtocolNode:
|
||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children)
|
||||
else:
|
||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||
self._driver_creator = DeviceClassCreator(driver_class)
|
||||
|
||||
if driver_is_ros:
|
||||
driver_params["device_id"] = device_id
|
||||
|
||||
@@ -58,7 +58,6 @@ class HostNode(BaseROS2DeviceNode):
|
||||
device_id: str,
|
||||
devices_config: Dict[str, Any],
|
||||
resources_config: list,
|
||||
resources_edge_config: list[dict],
|
||||
physical_setup_graph: Optional[Dict[str, Any]] = None,
|
||||
controllers_config: Optional[Dict[str, Any]] = None,
|
||||
bridges: Optional[List[Any]] = None,
|
||||
@@ -97,7 +96,6 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.server_latest_timestamp = 0.0 #
|
||||
self.devices_config = devices_config
|
||||
self.resources_config = resources_config
|
||||
self.resources_edge_config = resources_edge_config
|
||||
self.physical_setup_graph = physical_setup_graph
|
||||
if controllers_config is None:
|
||||
controllers_config = {}
|
||||
@@ -193,36 +191,24 @@ class HostNode(BaseROS2DeviceNode):
|
||||
)
|
||||
resource_with_parent_name = []
|
||||
resource_ids_to_instance = {i["id"]: i for i in resources_config}
|
||||
resource_name_to_with_parent_name = {}
|
||||
for res in resources_config:
|
||||
if res.get("parent") and res.get("type") == "device" and res.get("class"):
|
||||
parent_id = res.get("parent")
|
||||
parent_res = resource_ids_to_instance[parent_id]
|
||||
if parent_res.get("type") == "device" and parent_res.get("class"):
|
||||
resource_with_parent_name.append(copy.deepcopy(res))
|
||||
resource_name_to_with_parent_name[resource_with_parent_name[-1]["id"]] = f"{parent_res['id']}/{res['id']}"
|
||||
resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
|
||||
continue
|
||||
resource_with_parent_name.append(copy.deepcopy(res))
|
||||
for edge in self.resources_edge_config:
|
||||
edge["source"] = resource_name_to_with_parent_name.get(edge.get("source"), edge.get("source"))
|
||||
edge["target"] = resource_name_to_with_parent_name.get(edge.get("target"), edge.get("target"))
|
||||
try:
|
||||
for bridge in self.bridges:
|
||||
if hasattr(bridge, "resource_add"):
|
||||
from unilabos.app.web.client import HTTPClient
|
||||
client: HTTPClient = bridge
|
||||
resource_start_time = time.time()
|
||||
resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False)
|
||||
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name), True)
|
||||
resource_end_time = time.time()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
|
||||
)
|
||||
resource_add_res = client.resource_edge_add(self.resources_edge_config, False)
|
||||
resource_edge_end_time = time.time()
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
|
||||
)
|
||||
except Exception as ex:
|
||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
@@ -397,24 +383,18 @@ class HostNode(BaseROS2DeviceNode):
|
||||
liquid_volume: list[int],
|
||||
slot_on_deck: str,
|
||||
):
|
||||
res_creation_input = {
|
||||
"name": res_id,
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
"z": bind_locations.z,
|
||||
},
|
||||
}
|
||||
if len(liquid_input_slot) and liquid_input_slot[0] == -1: # 目前container只逐个创建
|
||||
res_creation_input.update({
|
||||
"data": {
|
||||
"liquid_type": liquid_type[0] if liquid_type else None,
|
||||
"liquid_volume": liquid_volume[0] if liquid_volume else None,
|
||||
}
|
||||
})
|
||||
init_new_res = initialize_resource(res_creation_input) # flatten的格式
|
||||
init_new_res = initialize_resource(
|
||||
{
|
||||
"name": res_id,
|
||||
"class": class_name,
|
||||
"parent": parent,
|
||||
"position": {
|
||||
"x": bind_locations.x,
|
||||
"y": bind_locations.y,
|
||||
"z": bind_locations.z,
|
||||
},
|
||||
}
|
||||
) # flatten的格式
|
||||
resources = init_new_res # initialize_resource已经返回list[dict]
|
||||
device_ids = [device_id]
|
||||
bind_parent_id = [parent]
|
||||
@@ -771,10 +751,8 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.lab_logger().info(f"[Host Node-Resource] Add request received: {len(resources)} resources")
|
||||
|
||||
success = False
|
||||
if len(self.bridges) > 0: # 边的提交待定
|
||||
from unilabos.app.web.client import HTTPClient
|
||||
client: HTTPClient = self.bridges[-1]
|
||||
r = client.resource_add(add_schema(resources), False)
|
||||
if len(self.bridges) > 0:
|
||||
r = self.bridges[-1].resource_add(add_schema(resources))
|
||||
success = bool(r)
|
||||
|
||||
response.success = success
|
||||
|
||||
@@ -25,7 +25,7 @@ class DeviceNodeResourceTracker(object):
|
||||
def clear_resource(self):
|
||||
self.resources = []
|
||||
|
||||
def figure_resource(self, query_resource, try_mode=False):
|
||||
def figure_resource(self, query_resource):
|
||||
if isinstance(query_resource, list):
|
||||
return [self.figure_resource(r) for r in query_resource]
|
||||
res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
|
||||
@@ -45,14 +45,10 @@ class DeviceNodeResourceTracker(object):
|
||||
res_list.extend(
|
||||
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
|
||||
)
|
||||
if not try_mode:
|
||||
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
|
||||
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
||||
else:
|
||||
return [i[1] for i in res_list]
|
||||
# 后续加入其他对比方式
|
||||
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
|
||||
self.resource2parent_resource[id(query_resource)] = res_list[0][0]
|
||||
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
||||
# 后续加入其他对比方式
|
||||
return res_list[0][1]
|
||||
|
||||
def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value, parent_res=None) -> List[Tuple[Any, Any]]:
|
||||
@@ -61,12 +57,8 @@ class DeviceNodeResourceTracker(object):
|
||||
children = getattr(resource, "children", [])
|
||||
for child in children:
|
||||
res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource))
|
||||
if target_resource_cls_type == type(resource):
|
||||
if target_resource_cls_type == dict:
|
||||
if identifier_key in resource:
|
||||
if resource[identifier_key] == compare_value:
|
||||
res_list.append((parent_res, resource))
|
||||
elif hasattr(resource, identifier_key):
|
||||
if target_resource_cls_type == type(resource) or target_resource_cls_type == dict:
|
||||
if hasattr(resource, identifier_key):
|
||||
if getattr(resource, identifier_key) == compare_value:
|
||||
res_list.append((parent_res, resource))
|
||||
return res_list
|
||||
|
||||
@@ -33,7 +33,7 @@ class DeviceClassCreator(Generic[T]):
|
||||
这个类提供了从任意类创建实例的通用方法。
|
||||
"""
|
||||
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
||||
def __init__(self, cls: Type[T]):
|
||||
"""
|
||||
初始化设备类创建器
|
||||
|
||||
@@ -42,18 +42,6 @@ class DeviceClassCreator(Generic[T]):
|
||||
"""
|
||||
self.device_cls = cls
|
||||
self.device_instance: Optional[T] = None
|
||||
self.children = children
|
||||
self.resource_tracker = resource_tracker
|
||||
|
||||
def attach_resource(self):
|
||||
"""
|
||||
附加资源到设备类实例
|
||||
"""
|
||||
if self.device_instance is not None:
|
||||
for c in self.children.values():
|
||||
if c["type"] == "container":
|
||||
self.resource_tracker.add_resource(c)
|
||||
|
||||
|
||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||
"""
|
||||
@@ -72,7 +60,6 @@ class DeviceClassCreator(Generic[T]):
|
||||
}
|
||||
)
|
||||
self.post_create()
|
||||
self.attach_resource()
|
||||
return self.device_instance
|
||||
|
||||
def get_instance(self) -> Optional[T]:
|
||||
@@ -103,15 +90,14 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
|
||||
cls: PyLabRobot设备类
|
||||
children: 子资源字典,用于资源替换
|
||||
"""
|
||||
super().__init__(cls, children, resource_tracker)
|
||||
super().__init__(cls)
|
||||
self.children = children
|
||||
self.resource_tracker = resource_tracker
|
||||
# 检查类是否具有deserialize方法
|
||||
self.has_deserialize = hasattr(cls, "deserialize") and callable(getattr(cls, "deserialize"))
|
||||
if not self.has_deserialize:
|
||||
logger.warning(f"类 {cls.__name__} 没有deserialize方法,将使用标准构造函数")
|
||||
|
||||
def attach_resource(self):
|
||||
pass # 只能增加实例化物料,原来默认物料仅为字典查询
|
||||
|
||||
def _process_resource_mapping(self, resource, source_type):
|
||||
if source_type == dict:
|
||||
from pylabrobot.resources.resource import Resource
|
||||
@@ -274,7 +260,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
||||
"""
|
||||
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
||||
def __init__(self, cls: Type[T], children: Dict[str, Any]):
|
||||
"""
|
||||
初始化ProtocolNode设备类创建器
|
||||
|
||||
@@ -282,7 +268,8 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
cls: ProtocolNode设备类
|
||||
children: 子资源字典,用于资源替换
|
||||
"""
|
||||
super().__init__(cls, children, resource_tracker)
|
||||
super().__init__(cls)
|
||||
self.children = children
|
||||
|
||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||
"""
|
||||
@@ -295,7 +282,8 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
|
||||
ProtocolNode设备类实例
|
||||
"""
|
||||
try:
|
||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||
|
||||
# 创建实例
|
||||
data["children"] = self.children
|
||||
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
|
||||
self.post_create()
|
||||
|
||||
@@ -29,23 +29,23 @@ set(action_files
|
||||
"action/HeatChillStart.action"
|
||||
"action/HeatChillStop.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/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/LiquidHandlerProtocolCreation.action"
|
||||
"action/LiquidHandlerAspirate.action"
|
||||
|
||||
Reference in New Issue
Block a user