diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml new file mode 100644 index 00000000..72dafa57 --- /dev/null +++ b/.github/workflows/multi-platform-build.yml @@ -0,0 +1,132 @@ +name: Multi-Platform Conda Build + +on: + push: + branches: [ main, dev ] + tags: [ 'v*' ] + pull_request: + branches: [ main, dev ] + workflow_dispatch: + inputs: + platforms: + description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64' + required: false + default: 'osx-arm64' + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux-64 + env_file: unilabos-linux-64.yaml + - os: macos-13 # Intel + platform: osx-64 + env_file: unilabos-osx-64.yaml + - os: macos-latest # ARM64 + platform: osx-arm64 + env_file: unilabos-osx-arm64.yaml + - os: windows-latest + platform: win-64 + env_file: unilabos-win64.yaml + + runs-on: ${{ matrix.os }} + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if platform should be built + id: should_build + run: | + if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then + echo "should_build=true" >> $GITHUB_OUTPUT + elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then + echo "should_build=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then + echo "should_build=true" >> $GITHUB_OUTPUT + else + echo "should_build=false" >> $GITHUB_OUTPUT + fi + + - name: Setup Miniconda + if: steps.should_build.outputs.should_build == 'true' + uses: conda-incubator/setup-miniconda@v3 + with: + miniconda-version: "latest" + channels: conda-forge,robostack-staging,defaults + channel-priority: strict + activate-environment: build-env + auto-activate-base: false + auto-update-conda: false + show-channel-urls: true + + - name: Install boa and build tools + if: steps.should_build.outputs.should_build == 'true' + run: | + conda install -c conda-forge boa conda-build + + - name: Show environment info + if: steps.should_build.outputs.should_build == 'true' + run: | + conda info + conda list | grep -E "(boa|conda-build)" + echo "Platform: ${{ matrix.platform }}" + echo "OS: ${{ matrix.os }}" + + - name: Build conda package + if: steps.should_build.outputs.should_build == 'true' + run: | + if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then + boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs + else + boa build -m ./recipes/conda_build_config.yaml ./recipes/ros-humble-unilabos-msgs + fi + + - name: List built packages + if: steps.should_build.outputs.should_build == 'true' + run: | + echo "Built packages in conda-bld:" + find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" | head -10 + ls -la $CONDA_PREFIX/conda-bld/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found" + ls -la $CONDA_PREFIX/conda-bld/noarch/ || echo "noarch directory not found" + echo "CONDA_PREFIX: $CONDA_PREFIX" + echo "Full path would be: $CONDA_PREFIX/conda-bld/**/*.tar.bz2" + + - name: Prepare artifacts for upload + if: steps.should_build.outputs.should_build == 'true' + run: | + mkdir -p ${{ runner.temp }}/conda-packages + find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} ${{ runner.temp }}/conda-packages/ \; + echo "Copied files to temp directory:" + ls -la ${{ runner.temp }}/conda-packages/ + + - name: Upload conda package artifacts + if: steps.should_build.outputs.should_build == 'true' + uses: actions/upload-artifact@v4 + with: + name: conda-package-${{ matrix.platform }} + path: ${{ runner.temp }}/conda-packages + if-no-files-found: warn + retention-days: 30 + + - name: Create release assets (on tags) + if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/') + run: | + mkdir -p release-assets + find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} release-assets/ \; + + - name: Upload to release + if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: release-assets/* + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index f915811b..b6ca0d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,7 @@ CATKIN_IGNORE *.graphml unilabos/device_mesh/view_robot.rviz + + +# Certs +**/.certs \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 036215b8..a8d25e98 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ recursive-include unilabos/registry *.yaml recursive-include unilabos/app/web *.html recursive-include unilabos/app/web *.css +recursive-include unilabos/device_mesh/devices * +recursive-include unilabos/device_mesh/resources * diff --git a/README.md b/README.md index 34352760..94010e9f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index 4f3607d7..d4f77f4b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/macos_sdk_config.yaml b/recipes/macos_sdk_config.yaml new file mode 100644 index 00000000..2151611a --- /dev/null +++ b/recipes/macos_sdk_config.yaml @@ -0,0 +1,7 @@ +CONDA_BUILD_SYSROOT: + - /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk +MACOSX_DEPLOYMENT_TARGET: + - "11.0" +CONDA_SUBDIR: + - osx-arm64 +# boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs \ No newline at end of file diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index febca425..e476d1b8 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.4 + version: 0.9.7 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.6.* + - robostack-staging::ros2-distro-mutex=0.5.* 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') }} diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 51ddea1f..2a48b043 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.4" + version: "0.9.7" source: path: ../.. diff --git a/setup.py b/setup.py index 038d820d..8fd9bbc8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.9.4', + version='0.9.7', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], @@ -17,6 +17,7 @@ setup( entry_points={ 'console_scripts': [ "unilab = unilabos.app.main:main", + "unilab-register = unilabos.app.register:main" ], }, ) diff --git a/test/commands/resource_add.md b/test/commands/resource_add.md index d80e1557..84f52326 100644 --- a/test/commands/resource_add.md +++ b/test/commands/resource_add.md @@ -2,4 +2,10 @@ ```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': '' }" ``` \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/add_protocol_test_station.json b/test/experiments/Protocol_Test_Station/add_protocol_test_station.json new file mode 100644 index 00000000..df7f8182 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/add_protocol_test_station.json @@ -0,0 +1,563 @@ +{ + "nodes": [ + { + "id": "AddProtocolTestStation", + "name": "添加协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "stirrer_1", + "stirrer_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_acetone", + "flask_water", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup", + "collection_bottle_1", + "collection_bottle_2" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol", "AddProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 750, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "试剂分配阀", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "反应器分配阀", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 750, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "stirrer_1", + "name": "主反应器搅拌器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "stirrer_2", + "name": "副反应器搅拌器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 900, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER2", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 450, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_air": "top" + } + }, + { + "id": "link_valve2_main_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_secondary_reactor", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer_head", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_stirrer2_secondary_reactor", + "source": "stirrer_2", + "target": "secondary_reactor", + "type": "mechanical", + "port": { + "stirrer_2": "stirrer_head", + "secondary_reactor": "stirrer_port" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json b/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json new file mode 100644 index 00000000..affb6e8b --- /dev/null +++ b/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json @@ -0,0 +1,438 @@ +{ + "nodes": [ + { + "id": "CentrifugeProtocolTestStation", + "name": "离心协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "centrifuge_1", + "reaction_mixture", + "centrifuge_tube", + "collection_bottle_1", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "CentrifugeProtocol", + "PumpTransferProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "样品分配阀", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "centrifuge_1", + "name": "离心机", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_centrifuge", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_CENTRIFUGE1", + "max_speed": 15000.0, + "max_temp": 40.0, + "min_temp": 4.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "cell_suspension", + "liquid_volume": 200.0 + } + ] + } + }, + { + "id": "centrifuge_tube", + "name": "离心管", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 15.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "上清液收集瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_centrifuge_tube", + "source": "multiway_valve_2", + "target": "centrifuge_tube", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "centrifuge_tube": "inlet" + } + }, + { + "id": "link_valve2_collection", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "waste_workup": "inlet" + } + }, + { + "id": "link_centrifuge1_centrifuge_tube", + "source": "centrifuge_1", + "target": "centrifuge_tube", + "type": "transport", + "port": { + "centrifuge_1": "centrifuge", + "centrifuge_tube": "centrifuge_port" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json b/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json new file mode 100644 index 00000000..6d793453 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json @@ -0,0 +1,446 @@ +{ + "nodes": [ + { + "id": "CleanVesselProtocolTestStation", + "name": "容器清洗协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "heatchill_1", + "flask_water", + "flask_acetone", + "flask_ethanol", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "CleanVesselProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主清洗泵", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副清洗泵", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 450, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "容器分配阀", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 450, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "heatchill_1", + "name": "加热清洗器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 10.0, + "max_stir_speed": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 50, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 150, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 250, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 350, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "residue", + "liquid_volume": 50.0 + } + ] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 800, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "organic_residue", + "liquid_volume": 30.0 + } + ] + } + }, + { + "id": "waste_workup", + "name": "清洗废液瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": "container", + "position": { + "x": 700, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_to_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_to_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_to_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "top" + } + }, + { + "id": "link_valve1_to_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_acetone": "top" + } + }, + { + "id": "link_valve1_to_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_ethanol": "top" + } + }, + { + "id": "link_valve1_to_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_air": "top" + } + }, + { + "id": "link_valve1_to_valve2_for_cleaning", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "multiway_valve_2": "8" + } + }, + { + "id": "link_valve2_to_main_reactor_in", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "top" + } + }, + { + "id": "link_valve2_to_secondary_reactor_in", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "top" + } + }, + { + "id": "link_main_reactor_out_to_valve2", + "source": "main_reactor", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "main_reactor": "bottom", + "multiway_valve_2": "6" + } + }, + { + "id": "link_secondary_reactor_out_to_valve2", + "source": "secondary_reactor", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "secondary_reactor": "bottom", + "multiway_valve_2": "7" + } + }, + { + "id": "link_valve2_to_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "waste_workup": "top" + } + }, + { + "id": "link_heatchill1_to_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "main_reactor": "bind" + } + }, + { + "id": "link_heatchill1_to_secondary_reactor", + "source": "heatchill_1", + "target": "secondary_reactor", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "secondary_reactor": "bind" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json b/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json new file mode 100644 index 00000000..61520ee6 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json @@ -0,0 +1,367 @@ +{ + "nodes": [ + { + "id": "DualValvePumpStation", + "name": "双阀门泵站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_air", + "main_reactor", + "waste_workup", + "collection_bottle_1" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "DualValvePumpStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "DualValvePumpStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 700, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "第一个八通阀", + "children": [], + "parent": "DualValvePumpStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 300, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "第二个八通阀", + "children": [], + "parent": "DualValvePumpStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 700, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "DualValvePumpStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve2_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json b/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json new file mode 100644 index 00000000..b4f89c25 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json @@ -0,0 +1,557 @@ +{ + "nodes": [ + { + "id": "EvacuateRefillTestStation", + "name": "抽真空充气测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_air", + "vacuum_pump_1", + "gas_source_nitrogen", + "gas_source_air", + "solenoid_valve_vacuum", + "solenoid_valve_gas", + "main_reactor", + "stirrer_1", + "waste_workup", + "collection_bottle_1" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol", "EvacuateAndRefillProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 700, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "第一个八通阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 300, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "第二个八通阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 700, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "vacuum_pump_1", + "name": "真空泵1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_vacuum_pump", + "position": { + "x": 150, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VACUUM1", + "max_pressure": -0.9 + }, + "data": { + "status": "OFF", + "pressure": 0.0 + } + }, + { + "id": "gas_source_nitrogen", + "name": "氮气源", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_gas_source", + "position": { + "x": 850, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_GAS_N2", + "gas_type": "nitrogen", + "max_pressure": 5.0 + }, + "data": { + "status": "OFF", + "flow_rate": 0.0 + } + }, + { + "id": "gas_source_air", + "name": "空气源", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_gas_source", + "position": { + "x": 950, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_GAS_AIR", + "gas_type": "air", + "max_pressure": 3.0 + }, + "data": { + "status": "OFF", + "flow_rate": 0.0 + } + }, + { + "id": "solenoid_valve_vacuum", + "name": "真空电磁阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_solenoid_valve", + "position": { + "x": 225, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_SOLENOID_VACUUM" + }, + "data": { + "valve_position": "CLOSED" + } + }, + { + "id": "solenoid_valve_gas", + "name": "气源电磁阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_solenoid_valve", + "position": { + "x": 775, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_SOLENOID_GAS" + }, + "data": { + "valve_position": "CLOSED" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "stirrer_1", + "name": "搅拌器1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0 + }, + "data": { + "speed": 0.0, + "status": "OFF" + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_vacuum_solenoid", + "source": "vacuum_pump_1", + "target": "solenoid_valve_vacuum", + "type": "fluid", + "port": { + "vacuum_pump_1": "outlet", + "solenoid_valve_vacuum": "inlet" + } + }, + { + "id": "link_solenoid_vacuum_valve1", + "source": "solenoid_valve_vacuum", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "solenoid_valve_vacuum": "outlet", + "multiway_valve_1": "7" + } + }, + { + "id": "link_gas_solenoid", + "source": "gas_source_nitrogen", + "target": "solenoid_valve_gas", + "type": "fluid", + "port": { + "gas_source_nitrogen": "outlet", + "solenoid_valve_gas": "inlet" + } + }, + { + "id": "link_solenoid_gas_valve2", + "source": "solenoid_valve_gas", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "solenoid_valve_gas": "outlet", + "multiway_valve_2": "8" + } + }, + { + "id": "link_air_source_valve2", + "source": "gas_source_air", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "gas_source_air": "outlet", + "multiway_valve_2": "2" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve2_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_stirrer_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer", + "main_reactor": "stirrer" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json b/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json new file mode 100644 index 00000000..d47bb4d6 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json @@ -0,0 +1,503 @@ +{ + "nodes": [ + { + "id": "EvaporateProtocolTestStation", + "name": "蒸发协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "rotavap_1", + "heatchill_1", + "reaction_mixture", + "rotavap_flask", + "rotavap_condenser", + "flask_distillate", + "flask_ethanol", + "flask_acetone", + "flask_water", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "EvaporateProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "容器分配阀", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "rotavap_1", + "name": "旋转蒸发仪", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_rotavap", + "position": { + "x": 700, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_ROTAVAP1", + "max_temp": 180.0, + "max_rotation_speed": 280.0 + }, + "data": { + "status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "预加热器", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 100, + "y": 550, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 10.0, + "max_stir_speed": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "reaction_mixture", + "liquid_volume": 600.0 + } + ] + } + }, + { + "id": "rotavap_flask", + "name": "旋蒸样品瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "rotavap_condenser", + "name": "旋蒸冷凝器", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 350, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_distillate", + "name": "溶剂回收瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_rotavap_flask", + "source": "multiway_valve_2", + "target": "rotavap_flask", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "rotavap_flask": "inlet" + } + }, + { + "id": "link_valve2_rotavap_condenser", + "source": "multiway_valve_2", + "target": "rotavap_condenser", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "rotavap_condenser": "inlet" + } + }, + { + "id": "link_valve2_distillate", + "source": "multiway_valve_2", + "target": "flask_distillate", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "flask_distillate": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_rotavap1_rotavap_flask", + "source": "rotavap_1", + "target": "rotavap_flask", + "type": "fluid", + "port": { + "rotavap_1": "rotavap-sample", + "rotavap_flask": "rotavap_port" + } + }, + { + "id": "link_heatchill1_reaction_mixture", + "source": "heatchill_1", + "target": "reaction_mixture", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "reaction_mixture": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json b/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json new file mode 100644 index 00000000..9a25b76c --- /dev/null +++ b/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json @@ -0,0 +1,534 @@ +{ + "nodes": [ + { + "id": "FilterProtocolTestStation", + "name": "过滤协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "filter_1", + "heatchill_1", + "reaction_mixture", + "filter_vessel", + "filtrate_vessel", + "collection_bottle_1", + "collection_bottle_2", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "FilterProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "样品分配阀", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_FILTER1", + "max_temp": 100.0, + "max_stir_speed": 1000.0, + "max_volume": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "加热搅拌器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 4.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "cell_suspension", + "liquid_volume": 200.0 + } + ] + } + }, + { + "id": "filter_vessel", + "name": "过滤器容器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "filtrate_vessel", + "name": "滤液收集容器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_filter_vessel", + "source": "multiway_valve_2", + "target": "filter_vessel", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "filter_vessel": "inlet" + } + }, + { + "id": "link_valve2_filtrate_vessel", + "source": "multiway_valve_2", + "target": "filtrate_vessel", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "filtrate_vessel": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "waste_workup": "inlet" + } + }, + { + "id": "link_filter1_filter_vessel", + "source": "filter_1", + "target": "filter_vessel", + "type": "transport", + "port": { + "filter_1": "filter", + "filter_vessel": "filter_port" + } + }, + { + "id": "link_heatchill1_filter_vessel", + "source": "heatchill_1", + "target": "filter_vessel", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "filter_vessel": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json b/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json new file mode 100644 index 00000000..751f3129 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json @@ -0,0 +1,671 @@ +{ + "nodes": [ + { + "id": "HeatChillProtocolTestStation", + "name": "加热冷却协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "stirrer_1", + "stirrer_2", + "heatchill_1", + "heatchill_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_acetone", + "flask_water", + "flask_ethanol", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup", + "collection_bottle_1", + "collection_bottle_2" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "PumpTransferProtocol", + "AddProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol", + "DissolveProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 750, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "试剂分配阀", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "反应器分配阀", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 750, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "stirrer_1", + "name": "主反应器搅拌器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "stirrer_2", + "name": "副反应器搅拌器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 900, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER2", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "heatchill_1", + "name": "主反应器加热冷却器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 550, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_2", + "name": "副反应器加热冷却器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 850, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL2", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 650, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 450, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_air": "top" + } + }, + { + "id": "link_valve2_main_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_secondary_reactor", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer_head", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_stirrer2_secondary_reactor", + "source": "stirrer_2", + "target": "secondary_reactor", + "type": "mechanical", + "port": { + "stirrer_2": "stirrer_head", + "secondary_reactor": "stirrer_port" + } + }, + { + "id": "link_heatchill1_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "thermal", + "port": { + "heatchill_1": "heating_surface", + "main_reactor": "heating_jacket" + } + }, + { + "id": "link_heatchill2_secondary_reactor", + "source": "heatchill_2", + "target": "secondary_reactor", + "type": "thermal", + "port": { + "heatchill_2": "heating_surface", + "secondary_reactor": "heating_jacket" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "flask_ethanol": "outlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json b/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json new file mode 100644 index 00000000..b9acd35a --- /dev/null +++ b/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json @@ -0,0 +1,778 @@ +{ + "nodes": [ + { + "id": "PumpTransferFilterThroughTestStation", + "name": "泵转移+过滤介质测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "reaction_mixture", + "crude_product", + "filter_celite", + "column_silica_gel", + "filter_C18", + "pure_product", + "collection_bottle_1", + "collection_bottle_2", + "collection_bottle_3", + "intermediate_vessel_1", + "intermediate_vessel_2", + "flask_water", + "flask_ethanol", + "flask_methanol", + "flask_ethyl_acetate", + "flask_acetone", + "flask_hexane", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "PumpTransferProtocol", + "FilterThroughProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "样品分配阀", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "organic_reaction_mixture", + "liquid_volume": 250.0 + } + ] + } + }, + { + "id": "crude_product", + "name": "粗产品", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "crude_organic_compound", + "liquid_volume": 150.0 + } + ] + } + }, + { + "id": "filter_celite", + "name": "硅藻土过滤器", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 300.0, + "filter_type": "celite_pad" + }, + "data": { + "liquid": [] + } + }, + { + "id": "column_silica_gel", + "name": "硅胶柱", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 200.0, + "filter_type": "silica_gel_column" + }, + "data": { + "liquid": [] + } + }, + { + "id": "filter_C18", + "name": "C18固相萃取柱", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 100.0, + "filter_type": "C18_cartridge" + }, + "data": { + "liquid": [] + } + }, + { + "id": "pure_product", + "name": "纯产品", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_3", + "name": "收集瓶3", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "intermediate_vessel_1", + "name": "中间容器1", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "intermediate_vessel_2", + "name": "中间容器2", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 500, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_hexane", + "name": "正己烷瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "hexane", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "PumpTransferFilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_hexane", + "source": "multiway_valve_1", + "target": "flask_hexane", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "flask_hexane": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_crude_product", + "source": "multiway_valve_2", + "target": "crude_product", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "crude_product": "inlet" + } + }, + { + "id": "link_valve2_intermediate1", + "source": "multiway_valve_2", + "target": "intermediate_vessel_1", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "intermediate_vessel_1": "inlet" + } + }, + { + "id": "link_valve2_intermediate2", + "source": "multiway_valve_2", + "target": "intermediate_vessel_2", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "intermediate_vessel_2": "inlet" + } + }, + { + "id": "link_valve2_celite", + "source": "multiway_valve_2", + "target": "filter_celite", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "filter_celite": "inlet" + } + }, + { + "id": "link_valve2_silica_gel", + "source": "multiway_valve_2", + "target": "column_silica_gel", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "column_silica_gel": "inlet" + } + }, + { + "id": "link_valve2_C18", + "source": "multiway_valve_2", + "target": "filter_C18", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "filter_C18": "inlet" + } + }, + { + "id": "link_celite_collection1", + "source": "filter_celite", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "filter_celite": "outlet", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_silica_gel_collection2", + "source": "column_silica_gel", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "column_silica_gel": "outlet", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_C18_collection3", + "source": "filter_C18", + "target": "collection_bottle_3", + "type": "fluid", + "port": { + "filter_C18": "outlet", + "collection_bottle_3": "inlet" + } + }, + { + "id": "link_collection1_pure_product", + "source": "collection_bottle_1", + "target": "pure_product", + "type": "fluid", + "port": { + "collection_bottle_1": "outlet", + "pure_product": "inlet" + } + }, + { + "id": "link_collection2_pure_product", + "source": "collection_bottle_2", + "target": "pure_product", + "type": "fluid", + "port": { + "collection_bottle_2": "outlet", + "pure_product": "inlet" + } + }, + { + "id": "link_collection3_pure_product", + "source": "collection_bottle_3", + "target": "pure_product", + "type": "fluid", + "port": { + "collection_bottle_3": "outlet", + "pure_product": "inlet" + } + }, + { + "id": "link_waste_connection", + "source": "pure_product", + "target": "waste_workup", + "type": "fluid", + "port": { + "pure_product": "waste_outlet", + "waste_workup": "inlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json b/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json new file mode 100644 index 00000000..ab23d8da --- /dev/null +++ b/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json @@ -0,0 +1,304 @@ +{ + "nodes": [ + { + "id": "SimpleProtocolStation", + "name": "简单协议工作站", + "children": [ + "transfer_pump_1", + "multiway_valve_1", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "main_reactor", + "waste_workup", + "collection_bottle_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "SimpleProtocolStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 500, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle", + "valve_position": "0" + } + }, + { + "id": "multiway_valve_1", + "name": "八通阀1", + "children": [], + "parent": "SimpleProtocolStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 500, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "SimpleProtocolStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump_valve", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_valve_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve_reactor", + "source": "multiway_valve_1", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve_waste", + "source": "multiway_valve_1", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve_collection", + "source": "multiway_valve_1", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "collection_bottle_1": "inlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json b/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json new file mode 100644 index 00000000..e10d73e6 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json @@ -0,0 +1,432 @@ +{ + "nodes": [ + { + "id": "RunColumnTestStation", + "name": "柱层析测试工作站", + "children": [ + "transfer_pump_1", + "multiway_valve_1", + "column_1", + "flask_sample", + "flask_hexane", + "flask_ethyl_acetate", + "flask_methanol", + "column_vessel", + "collection_flask_1", + "collection_flask_2", + "collection_flask_3", + "waste_flask", + "main_reactor" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["RunColumnProtocol", "PumpTransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "position": 0.0 + } + }, + { + "id": "multiway_valve_1", + "name": "八通阀门", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 300, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "column_1", + "name": "柱层析设备", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_column", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_COLUMN1", + "max_flow_rate": 5.0, + "column_length": 30.0, + "column_diameter": 2.5 + }, + "data": { + "status": "Idle", + "column_state": "Ready" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "crude_mixture", + "volume": 200.0, + "concentration": 70.0 + } + ] + } + }, + { + "id": "flask_hexane", + "name": "正己烷洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "hexane", + "volume": 1500.0, + "concentration": 99.8 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethyl_acetate", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "column_vessel", + "name": "柱容器", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 300.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_1", + "name": "收集瓶1", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_2", + "name": "收集瓶2", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_3", + "name": "收集瓶3", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "反应器", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "reaction_mixture", + "volume": 300.0, + "concentration": 85.0 + } + ] + } + } + ], + "links": [ + { + "id": "link_pump_valve", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_valve_sample", + "source": "multiway_valve_1", + "target": "flask_sample", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_sample": "outlet" + } + }, + { + "id": "link_valve_hexane", + "source": "multiway_valve_1", + "target": "flask_hexane", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_hexane": "outlet" + } + }, + { + "id": "link_valve_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve_column_vessel", + "source": "multiway_valve_1", + "target": "column_vessel", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "column_vessel": "inlet" + } + }, + { + "id": "link_valve_collection1", + "source": "multiway_valve_1", + "target": "collection_flask_1", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "collection_flask_1": "inlet" + } + }, + { + "id": "link_valve_collection2", + "source": "multiway_valve_1", + "target": "collection_flask_2", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "collection_flask_2": "inlet" + } + }, + { + "id": "link_valve_waste", + "source": "multiway_valve_1", + "target": "waste_flask", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "waste_flask": "inlet" + } + }, + { + "id": "link_column_device_vessel", + "source": "column_1", + "target": "column_vessel", + "type": "transport", + "port": { + "column_1": "columnin", + "column_vessel": "column_port" + } + }, + { + "id": "link_column_collection3", + "source": "column_1", + "target": "collection_flask_3", + "type": "transport", + "port": { + "column_1": "columnout", + "collection_flask_3": "column_outlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json b/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json new file mode 100644 index 00000000..1f65cc71 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json @@ -0,0 +1,141 @@ +{ + "nodes": [ + { + "id": "SimpleStirHeatChillTestStation", + "name": "搅拌加热测试站", + "children": [ + "stirrer_1", + "heatchill_1", + "main_reactor", + "secondary_reactor" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "StirProtocol", + "StartStirProtocol", + "StopStirProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "stirrer_1", + "name": "主搅拌器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 400, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "min_speed": 50.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "主加热冷却器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 500, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 500.0 + } + ] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_heatchill1_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "main_reactor": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md new file mode 100644 index 00000000..ed73ec5a --- /dev/null +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -0,0 +1,36 @@ +1. 用到的仪器 + virtual_multiway_valve(√) 八通阀门 + virtual_transfer_pump(√) 转移泵 + virtual_centrifuge() 离心机 + virtual_rotavap() 旋蒸仪 + virtual_heatchill() 加热器 + virtual_stirrer() 搅拌器 + virtual_solenoid_valve() 电磁阀 + virtual_vacuum_pump(√) vacuum_pump.mock 真空泵 + virtual_gas_source(√) 气源 + virtual_filter() 过滤器 + virtual_column(√) 层析柱 + separator() homemade_grbl_conductivity 分液漏斗 +2. 用到的protocol + PumpTransferProtocol: generate_pump_protocol_with_rinsing, (√) + 这个重复了,删掉CleanProtocol: generate_clean_protocol, + SeparateProtocol: generate_separate_protocol, (×) + EvaporateProtocol: generate_evaporate_protocol, (√) + EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, (√) + CentrifugeProtocol: generate_centrifuge_protocol, (√) + AddProtocol: generate_add_protocol, (√) + FilterProtocol: generate_filter_protocol, (√) + HeatChillProtocol: generate_heat_chill_protocol, (√) + HeatChillStartProtocol: generate_heat_chill_start_protocol, (√) + HeatChillStopProtocol: generate_heat_chill_stop_protocol, (√) + StirProtocol: generate_stir_protocol, (√) + StartStirProtocol: generate_start_stir_protocol, (√) + StopStirProtocol: generate_stop_stir_protocol, (√) + 这个重复了,删掉TransferProtocol: generate_transfer_protocol, + CleanVesselProtocol: generate_clean_vessel_protocol, (√) + DissolveProtocol: generate_dissolve_protocol, (√) + FilterThroughProtocol: generate_filter_through_protocol, (√) + RunColumnProtocol: generate_run_column_protocol, (×) + WashSolidProtocol: generate_wash_solid_protocol, (×) + +上下文体积搜索 \ No newline at end of file diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json new file mode 100644 index 00000000..071d9137 --- /dev/null +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -0,0 +1,897 @@ +{ + "nodes": [ + { + "id": "OrganicSynthesisStation", + "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", + "PumpTransferProtocol" + ] + }, + "data": {} + }, + { + "id": "multiway_valve_1", + "name": "八通阀门1", + "children": [], + "parent": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 500, + "y": 400, + "z": 0 + }, + "config": { + "volume": 2000.0 + }, + "data": { + "current_volume": 0.0 + } + }, + { + "id": "waste_bottle_2", + "name": "废液瓶2", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 1100, + "y": 500, + "z": 0 + }, + "config": { + "volume": 2000.0 + }, + "data": { + "current_volume": 0.0 + } + }, + { + "id": "solenoid_valve_1", + "name": "电磁阀1", + "children": [], + "parent": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "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": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "volume": 250.0 + }, + "data": { + "current_volume": 0.0 + } + }, + { + "id": "collection_bottle_2", + "name": "接收瓶2", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 950, + "y": 500, + "z": 0 + }, + "config": { + "volume": 250.0 + }, + "data": { + "current_volume": 0.0 + } + }, + { + "id": "collection_bottle_3", + "name": "接收瓶3", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 1050, + "y": 500, + "z": 0 + }, + "config": { + "volume": 250.0 + }, + "data": { + "current_volume": 0.0 + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_valve1_reagent1", + "source": "multiway_valve_1", + "target": "reagent_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "reagent_bottle_1": "top" + } + }, + { + "id": "link_valve1_reagent2", + "source": "multiway_valve_1", + "target": "reagent_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "reagent_bottle_2": "top" + } + }, + { + "id": "link_valve1_reagent3", + "source": "multiway_valve_1", + "target": "reagent_bottle_3", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "reagent_bottle_3": "top" + } + }, + { + "id": "link_valve1_centrifuge", + "source": "multiway_valve_1", + "target": "centrifuge_1", + "type": "transport", + "port": { + "multiway_valve_1": "4", + "centrifuge_1": "centrifuge" + } + }, + { + "id": "link_valve1_rotavap", + "source": "multiway_valve_1", + "target": "rotavap_1", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "rotavap_1": "sample_in" + } + }, + { + "id": "link_valve1_reactor", + "source": "multiway_valve_1", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "main_reactor": "top" + } + }, + { + "id": "link_valve1_waste1", + "source": "multiway_valve_1", + "target": "waste_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "waste_bottle_1": "top" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve2_solenoid1", + "source": "multiway_valve_2", + "target": "solenoid_valve_1", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "solenoid_valve_1": "in" + } + }, + { + "id": "link_vacuum_solenoid1", + "source": "vacuum_pump_1", + "target": "solenoid_valve_1", + "type": "fluid", + "port": { + "vacuum_pump_1": "vacuumpump", + "solenoid_valve_1": "out" + } + }, + { + "id": "link_valve2_solenoid2", + "source": "multiway_valve_2", + "target": "solenoid_valve_2", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "solenoid_valve_2": "in" + } + }, + { + "id": "link_gas_solenoid2", + "source": "gas_source_1", + "target": "solenoid_valve_2", + "type": "fluid", + "port": { + "gas_source_1": "gassource", + "solenoid_valve_2": "out" + } + }, + { + "id": "link_valve2_filter", + "source": "multiway_valve_2", + "target": "filter_1", + "type": "transport", + "port": { + "multiway_valve_2": "4", + "filter_1": "filter_in" + } + }, + { + "id": "link_valve2_column", + "source": "multiway_valve_2", + "target": "column_1", + "type": "transport", + "port": { + "multiway_valve_2": "5", + "column_1": "columnin" + } + }, + { + "id": "link_column_collection2", + "source": "column_1", + "target": "collection_bottle_2", + "type": "transport", + "port": { + "column_1": "columnout", + "collection_bottle_2": "top" + } + }, + { + "id": "link_valve2_separator", + "source": "multiway_valve_2", + "target": "separator_1", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "separator_1": "separator_in" + } + }, + { + "id": "link_separator_collection3", + "source": "separator_1", + "target": "collection_bottle_3", + "type": "fluid", + "port": { + "separator_1": "bottom_phase_out", + "collection_bottle_3": "top" + } + }, + { + "id": "link_valve2_reagent4", + "source": "multiway_valve_2", + "target": "reagent_bottle_4", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "reagent_bottle_4": "top" + } + }, + { + "id": "link_valve2_reagent5", + "source": "multiway_valve_2", + "target": "reagent_bottle_5", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "reagent_bottle_5": "top" + } + }, + { + "id": "mech_stirrer_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer", + "main_reactor": "bind" + } + }, + { + "id": "thermal_heater_reactor", + "source": "heater_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "heater_1": "heatchill", + "main_reactor": "bind" + } + }, + { + "id": "link_separator_waste2", + "source": "separator_1", + "target": "waste_bottle_2", + "type": "fluid", + "port": { + "separator_1": "top_phase_out", + "waste_bottle_2": "top" + } + }, + { + "id": "mech_stirrer2_separator", + "source": "stirrer_2", + "target": "separator_1", + "type": "mechanical", + "port": { + "stirrer_2": "stirrer", + "separator_1": "bind" + } + }, + { + "id": "link_filter_filtrate_to_collection1", + "source": "filter_1", + "target": "collection_bottle_1", + "type": "transport", + "port": { + "filter_1": "filtrate_out", + "collection_bottle_1": "top" + } + }, + { + "id": "link_filter_retentate_to_waste1", + "source": "filter_1", + "target": "waste_bottle_1", + "type": "transport", + "port": { + "filter_1": "retentate_out", + "waste_bottle_1": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_all.json b/test/experiments/mock_devices/mock_all.json new file mode 100644 index 00000000..10d224f5 --- /dev/null +++ b/test/experiments/mock_devices/mock_all.json @@ -0,0 +1,314 @@ +{ + "nodes": [ + { + "id": "MockChiller1", + "name": "模拟冷却器", + "children": [ + "MockContainerForChiller1" + ], + "parent": null, + "type": "device", + "class": "mock_chiller", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_cooling": false, + "is_heating": false, + "vessel": "", + "purpose": "" + } + }, + { + "id": "MockContainerForChiller1", + "name": "模拟容器", + "type": "container", + "parent": "MockChiller1", + "position": { + "x": 5, + "y": 0, + "z": 0 + }, + "data": { + "liquid_type": "CuCl2", + "liquid_volume": "100" + }, + "children": [] + }, + { + "id": "MockFilter1", + "name": "模拟过滤器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_filter", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "is_filtering": false, + "flow_rate": 0.0, + "filter_life": 100.0, + "vessel": "", + "filtrate_vessel": "", + "filtered_volume": 0.0, + "target_volume": 0.0, + "progress": 0.0, + "stir": false, + "stir_speed": 0.0, + "temperature": 25.0, + "continue_heatchill": false + } + }, + { + "id": "MockHeater1", + "name": "模拟加热器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_heater", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_heating": false, + "heating_power": 0.0, + "max_temperature": 300.0, + "vessel": "Unknown", + "purpose": "Unknown", + "stir": false, + "stir_speed": 0.0 + } + }, + { + "id": "MockPump1", + "name": "模拟泵设备", + "children": [], + "parent": null, + "type": "device", + "class": "mock_pump", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "current_device": "MockPump1", + "pump_state": "Stopped", + "flow_rate": 0.0, + "target_flow_rate": 0.0, + "pressure": 0.0, + "total_volume": 0.0, + "max_flow_rate": 100.0, + "max_pressure": 10.0, + "from_vessel": "", + "to_vessel": "", + "transfer_volume": 0.0, + "amount": "", + "transfer_time": 0.0, + "is_viscous": false, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "is_solid": false, + "time_spent": 0.0, + "time_remaining": 0.0 + } + }, + { + "id": "MockRotavap1", + "name": "模拟旋转蒸发器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_rotavap", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "rotate_state": "Stopped", + "rotate_time": 0.0, + "rotate_speed": 0.0, + "pump_state": "Stopped", + "pump_time": 0.0, + "vacuum_level": 1013.25, + "temperature": 25.0, + "target_temperature": 25.0, + "success": "True" + } + }, + { + "id": "MockSeparator1", + "name": "模拟分离器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_separator", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "settling_time": 0.0, + "valve_state": "Closed", + "shake_time": 0.0, + "shake_status": "Not Shaking", + "current_device": "MockSeparator1", + "purpose": "", + "product_phase": "", + "from_vessel": "", + "separation_vessel": "", + "to_vessel": "", + "waste_phase_to_vessel": "", + "solvent": "", + "solvent_volume": 0.0, + "through": "", + "repeats": 1, + "stir_time": 0.0, + "stir_speed": 0.0, + "time_spent": 0.0, + "time_remaining": 0.0 + } + }, + { + "id": "MockSolenoidValve1", + "name": "模拟电磁阀", + "children": [], + "parent": null, + "type": "device", + "class": "mock_solenoid_valve", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "valve_status": "Closed" + } + }, + { + "id": "MockStirrer1NEW", + "name": "模拟搅拌器(new)", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer_new", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "vessel": "", + "purpose": "", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "stir_time": 0.0, + "settling_time": 0.0, + "progress": 0.0, + "max_stir_speed": 2000.0 + } + }, + { + "id": "MockStirrer1", + "name": "模拟搅拌器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "temperature": 25.0, + "target_temperature": 25.0, + "heating_state": "Off", + "heating_power": 0.0, + "max_stir_speed": 2000.0, + "max_temperature": 300.0 + } + }, + { + "id": "MockVacuum1", + "name": "模拟真空泵", + "children": [], + "parent": null, + "type": "device", + "class": "mock_vacuum", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "power_state": "Off", + "pump_state": "Stopped", + "vacuum_level": 1013.25, + "target_vacuum": 50.0, + "pump_speed": 0.0, + "pump_efficiency": 95.0, + "max_pump_speed": 100.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_chiller.json b/test/experiments/mock_devices/mock_chiller.json new file mode 100644 index 00000000..fc0e2f4c --- /dev/null +++ b/test/experiments/mock_devices/mock_chiller.json @@ -0,0 +1,30 @@ +{ + "nodes": [ + { + "id": "MockChiller1", + "name": "模拟冷却器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_chiller", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_cooling": false, + "is_heating": false, + "vessel": "", + "purpose": "" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_filter.json b/test/experiments/mock_devices/mock_filter.json new file mode 100644 index 00000000..d8d80299 --- /dev/null +++ b/test/experiments/mock_devices/mock_filter.json @@ -0,0 +1,36 @@ +{ + "nodes": [ + { + "id": "MockFilter1", + "name": "模拟过滤器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_filter", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "is_filtering": false, + "flow_rate": 0.0, + "filter_life": 100.0, + "vessel": "", + "filtrate_vessel": "", + "filtered_volume": 0.0, + "target_volume": 0.0, + "progress": 0.0, + "stir": false, + "stir_speed": 0.0, + "temperature": 25.0, + "continue_heatchill": false + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_heater.json b/test/experiments/mock_devices/mock_heater.json new file mode 100644 index 00000000..1aca9688 --- /dev/null +++ b/test/experiments/mock_devices/mock_heater.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockHeater1", + "name": "模拟加热器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_heater", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "current_temperature": 25.0, + "target_temperature": 25.0, + "status": "Idle", + "is_heating": false, + "heating_power": 0.0, + "max_temperature": 300.0, + "vessel": "Unknown", + "purpose": "Unknown", + "stir": false, + "stir_speed": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_pump.json b/test/experiments/mock_devices/mock_pump.json new file mode 100644 index 00000000..d1c13945 --- /dev/null +++ b/test/experiments/mock_devices/mock_pump.json @@ -0,0 +1,44 @@ +{ + "nodes": [ + { + "id": "MockPump1", + "name": "模拟泵设备", + "children": [], + "parent": null, + "type": "device", + "class": "mock_pump", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "current_device": "MockPump1", + "pump_state": "Stopped", + "flow_rate": 0.0, + "target_flow_rate": 0.0, + "pressure": 0.0, + "total_volume": 0.0, + "max_flow_rate": 100.0, + "max_pressure": 10.0, + "from_vessel": "", + "to_vessel": "", + "transfer_volume": 0.0, + "amount": "", + "transfer_time": 0.0, + "is_viscous": false, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "is_solid": false, + "time_spent": 0.0, + "time_remaining": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_rotavap.json b/test/experiments/mock_devices/mock_rotavap.json new file mode 100644 index 00000000..b28cfe2a --- /dev/null +++ b/test/experiments/mock_devices/mock_rotavap.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockRotavap1", + "name": "模拟旋转蒸发器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_rotavap", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "rotate_state": "Stopped", + "rotate_time": 0.0, + "rotate_speed": 0.0, + "pump_state": "Stopped", + "pump_time": 0.0, + "vacuum_level": 1013.25, + "temperature": 25.0, + "target_temperature": 25.0, + "success": "True" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_separator.json b/test/experiments/mock_devices/mock_separator.json new file mode 100644 index 00000000..20f26718 --- /dev/null +++ b/test/experiments/mock_devices/mock_separator.json @@ -0,0 +1,43 @@ +{ + "nodes": [ + { + "id": "MockSeparator1", + "name": "模拟分离器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_separator", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "settling_time": 0.0, + "valve_state": "Closed", + "shake_time": 0.0, + "shake_status": "Not Shaking", + "current_device": "MockSeparator1", + "purpose": "", + "product_phase": "", + "from_vessel": "", + "separation_vessel": "", + "to_vessel": "", + "waste_phase_to_vessel": "", + "solvent": "", + "solvent_volume": 0.0, + "through": "", + "repeats": 1, + "stir_time": 0.0, + "stir_speed": 0.0, + "time_spent": 0.0, + "time_remaining": 0.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_solenoid_valve.json b/test/experiments/mock_devices/mock_solenoid_valve.json new file mode 100644 index 00000000..e4e23cc1 --- /dev/null +++ b/test/experiments/mock_devices/mock_solenoid_valve.json @@ -0,0 +1,25 @@ +{ + "nodes": [ + { + "id": "MockSolenoidValve1", + "name": "模拟电磁阀", + "children": [], + "parent": null, + "type": "device", + "class": "mock_solenoid_valve", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "valve_status": "Closed" + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_stirrer.json b/test/experiments/mock_devices/mock_stirrer.json new file mode 100644 index 00000000..9dfc59c7 --- /dev/null +++ b/test/experiments/mock_devices/mock_stirrer.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockStirrer1", + "name": "模拟搅拌器", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "temperature": 25.0, + "target_temperature": 25.0, + "heating_state": "Off", + "heating_power": 0.0, + "max_stir_speed": 2000.0, + "max_temperature": 300.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_stirrer_new.json b/test/experiments/mock_devices/mock_stirrer_new.json new file mode 100644 index 00000000..837b2fec --- /dev/null +++ b/test/experiments/mock_devices/mock_stirrer_new.json @@ -0,0 +1,33 @@ +{ + "nodes": [ + { + "id": "MockStirrer1COPY", + "name": "模拟搅拌器(Copy)", + "children": [], + "parent": null, + "type": "device", + "class": "mock_stirrer_new", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "vessel": "", + "purpose": "", + "stir_speed": 0.0, + "target_stir_speed": 0.0, + "stir_state": "Stopped", + "stir_time": 0.0, + "settling_time": 0.0, + "progress": 0.0, + "max_stir_speed": 2000.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_devices/mock_vacuum.json b/test/experiments/mock_devices/mock_vacuum.json new file mode 100644 index 00000000..31406aea --- /dev/null +++ b/test/experiments/mock_devices/mock_vacuum.json @@ -0,0 +1,31 @@ +{ + "nodes": [ + { + "id": "MockVacuum1", + "name": "模拟真空泵", + "children": [], + "parent": null, + "type": "device", + "class": "mock_vacuum", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "MOCK" + }, + "data": { + "status": "Idle", + "power_state": "Off", + "pump_state": "Stopped", + "vacuum_level": 1013.25, + "target_vacuum": 50.0, + "pump_speed": 0.0, + "pump_efficiency": 95.0, + "max_pump_speed": 100.0 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/addteststation.json b/test/experiments/mock_protocol/addteststation.json new file mode 100644 index 00000000..beb68ffa --- /dev/null +++ b/test/experiments/mock_protocol/addteststation.json @@ -0,0 +1,376 @@ +{ + "nodes": [ + { + "id": "AddTestStation", + "name": "添加试剂测试工作站", + "children": [ + "transfer_pump", + "multiway_valve", + "stirrer", + "flask_reagent1", + "flask_reagent2", + "flask_reagent3", + "flask_reagent4", + "reactor", + "flask_waste", + "flask_rinsing", + "flask_buffer" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "TransferProtocol", "StartStirProtocol", "StopStirProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump", + "name": "注射器泵", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 5.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "multiway_valve", + "name": "八通阀门", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 420, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "positions": 8 + }, + "data": { + "status": "Idle", + "current_position": 1 + } + }, + { + "id": "stirrer", + "name": "搅拌器", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 720, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_reagent1", + "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, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 400, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_rinsing", + "name": "冲洗液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "去离子水", + "volume": 800.0, + "concentration": "纯净" + } + ] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 400, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "磷酸盐缓冲液", + "volume": 700.0, + "concentration": "0.1M, pH 7.4" + } + ] + } + } + ], + "links": [ + { + "source": "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", + "reactor": "bottom" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/centrifugeteststation.json b/test/experiments/mock_protocol/centrifugeteststation.json new file mode 100644 index 00000000..e6918349 --- /dev/null +++ b/test/experiments/mock_protocol/centrifugeteststation.json @@ -0,0 +1,271 @@ +{ + "nodes": [ + { + "id": "CentrifugeTestStation", + "name": "离心机测试工作站", + "children": [ + "pump_add", + "flask_1", + "flask_2", + "flask_3", + "reactor", + "stirrer", + "centrifuge_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "CentrifugeProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "centrifuge_1", + "name": "离心机", + "children": [], + "parent": "CentrifugeTestStation", + "type": "device", + "class": "virtual_centrifuge", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 15000.0, + "max_temp": 40.0, + "min_temp": 4.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_1", + "name": "样品瓶1", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_2", + "name": "样品瓶2", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_3", + "name": "缓冲液瓶", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CentrifugeTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_1", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_2": "top" + } + }, + { + "source": "pump_add", + "target": "flask_3", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_3": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + }, + { + "source": "centrifuge_1", + "target": "reactor", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "reactor": "vessel" + } + }, + { + "source": "centrifuge_1", + "target": "flask_1", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "flask_1": "vessel" + } + }, + { + "source": "centrifuge_1", + "target": "flask_2", + "type": "logical", + "port": { + "centrifuge_1": "chamber", + "flask_2": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/cleanvesselteststation.json b/test/experiments/mock_protocol/cleanvesselteststation.json new file mode 100644 index 00000000..c5d30868 --- /dev/null +++ b/test/experiments/mock_protocol/cleanvesselteststation.json @@ -0,0 +1,362 @@ +{ + "nodes": [ + { + "id": "CleanVesselTestStation", + "name": "容器清洗测试工作站", + "children": [ + "transfer_pump_cleaner", + "heatchill_1", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_waste", + "reactor", + "flask_buffer", + "flask_sample", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["CleanVesselProtocol", "TransferProtocol", "AddProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_cleaner", + "name": "清洗转移泵", + "children": [], + "parent": "CleanVesselTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "CleanVesselTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1500.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮溶剂瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "acetone", + "volume": 1800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "residue", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "buffer", + "volume": 1000.0, + "concentration": 10.0 + } + ] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CleanVesselTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump_cleaner", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_cleaner": "1", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_cleaner": "2", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_acetone", + "type": "physical", + "port": { + "transfer_pump_cleaner": "3", + "flask_acetone": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_waste", + "type": "physical", + "port": { + "transfer_pump_cleaner": "4", + "flask_waste": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_cleaner": "5", + "reactor": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_buffer", + "type": "physical", + "port": { + "transfer_pump_cleaner": "6", + "flask_buffer": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_cleaner": "7", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_cleaner", + "target": "flask_air", + "type": "physical", + "port": { + "transfer_pump_cleaner": "8", + "flask_air": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/dissolveteststation.json b/test/experiments/mock_protocol/dissolveteststation.json new file mode 100644 index 00000000..8b7ad28f --- /dev/null +++ b/test/experiments/mock_protocol/dissolveteststation.json @@ -0,0 +1,343 @@ +{ + "nodes": [ + { + "id": "DissolveTestStation", + "name": "溶解测试工作站", + "children": [ + "transfer_pump_1", + "heatchill_1", + "stirrer_1", + "flask_water", + "flask_ethanol", + "flask_dmso", + "reactor", + "flask_sample", + "flask_buffer" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["DissolveProtocol", "TransferProtocol", "HeatChillProtocol", "StirProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "stirrer_1", + "name": "搅拌器", + "children": [], + "parent": "DissolveTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 750.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1500.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_dmso", + "name": "DMSO溶剂瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "dmso", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "solid_sample", + "volume": 10.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "DissolveTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "buffer", + "volume": 1000.0, + "concentration": 10.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_dmso", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_dmso": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_buffer", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "flask_buffer": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + }, + { + "source": "stirrer_1", + "target": "reactor", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "reactor": "center" + } + }, + { + "source": "stirrer_1", + "target": "flask_sample", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "flask_sample": "center" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/filterteststation.json b/test/experiments/mock_protocol/filterteststation.json new file mode 100644 index 00000000..a816def8 --- /dev/null +++ b/test/experiments/mock_protocol/filterteststation.json @@ -0,0 +1,270 @@ +{ + "nodes": [ + { + "id": "FilterTestStation", + "name": "过滤器测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_filtrate", + "flask_buffer", + "reactor", + "stirrer", + "filter_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "FilterProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_filtrate", + "name": "滤液瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "FilterTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_filtrate", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_filtrate": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer": "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" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "logical", + "port": { + "filter_1": "input", + "reactor": "vessel" + } + }, + { + "source": "filter_1", + "target": "flask_sample", + "type": "logical", + "port": { + "filter_1": "input", + "flask_sample": "vessel" + } + }, + { + "source": "filter_1", + "target": "flask_filtrate", + "type": "logical", + "port": { + "filter_1": "output", + "flask_filtrate": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/filterthroughteststation.json b/test/experiments/mock_protocol/filterthroughteststation.json new file mode 100644 index 00000000..b250df5d --- /dev/null +++ b/test/experiments/mock_protocol/filterthroughteststation.json @@ -0,0 +1,388 @@ +{ + "nodes": [ + { + "id": "FilterThroughTestStation", + "name": "过滤通过测试工作站", + "children": [ + "transfer_pump_1", + "filter_1", + "flask_ethanol", + "flask_water", + "flask_methanol", + "reactor", + "collection_flask", + "waste_flask", + "flask_sample", + "flask_celite", + "flask_silica" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["FilterThroughProtocol", "TransferProtocol", "FilterProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "FilterThroughTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0, + "target_temp": 25.0, + "max_temp": 100.0, + "stir_speed": 0.0, + "max_stir_speed": 1000.0, + "filtered_volume": 0.0, + "progress": 0.0, + "message": "" + } + }, + { + "id": "flask_ethanol", + "name": "乙醇溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_water", + "name": "水溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1800.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇溶剂瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "crude_product", + "volume": 200.0, + "concentration": 80.0 + } + ] + } + }, + { + "id": "collection_flask", + "name": "收集瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "sample_mixture", + "volume": 100.0, + "concentration": 50.0 + } + ] + } + }, + { + "id": "flask_celite", + "name": "硅藻土容器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "celite", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_silica", + "name": "硅胶容器", + "children": [], + "parent": "FilterThroughTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "silica", + "volume": 30.0, + "concentration": 100.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "collection_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "flask_sample": "top" + } + }, + { + "source": "filter_1", + "target": "collection_flask", + "type": "physical", + "port": { + "filter_1": "filter_element", + "collection_flask": "top" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "physical", + "port": { + "filter_1": "filter_element", + "reactor": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/heatchillteststation.json b/test/experiments/mock_protocol/heatchillteststation.json new file mode 100644 index 00000000..9d243b84 --- /dev/null +++ b/test/experiments/mock_protocol/heatchillteststation.json @@ -0,0 +1,262 @@ +{ + "nodes": [ + { + "id": "HeatChillTestStation", + "name": "加热冷却测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_buffer1", + "flask_buffer2", + "reactor", + "stirrer", + "heatchill_1", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "pump_add", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "stirrer", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "HeatChillTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 800, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer1", + "name": "缓冲液瓶1", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer2", + "name": "缓冲液瓶2", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "HeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer1", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer2": "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" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "logical", + "port": { + "heatchill_1": "heating_element", + "reactor": "vessel" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "logical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "vessel" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/runcolumnteststation.json b/test/experiments/mock_protocol/runcolumnteststation.json new file mode 100644 index 00000000..51741f1c --- /dev/null +++ b/test/experiments/mock_protocol/runcolumnteststation.json @@ -0,0 +1,412 @@ +{ + "nodes": [ + { + "id": "RunColumnTestStation", + "name": "柱层析测试工作站", + "children": [ + "transfer_pump_1", + "column_1", + "flask_sample", + "flask_hexane", + "flask_ethyl_acetate", + "flask_methanol", + "collection_flask_1", + "collection_flask_2", + "collection_flask_3", + "waste_flask", + "reactor" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["RunColumnProtocol", "TransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "column_1", + "name": "柱层析设备", + "children": [], + "parent": "RunColumnTestStation", + "type": "device", + "class": "virtual_column", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_flow_rate": 5.0, + "column_length": 30.0, + "column_diameter": 2.5 + }, + "data": { + "status": "Idle", + "column_state": "Ready", + "current_flow_rate": 0.0, + "max_flow_rate": 5.0, + "column_length": 30.0, + "column_diameter": 2.5, + "processed_volume": 0.0, + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "crude_mixture", + "volume": 200.0, + "concentration": 70.0 + } + ] + } + }, + { + "id": "flask_hexane", + "name": "正己烷洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "hexane", + "volume": 1500.0, + "concentration": 99.8 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethyl_acetate", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇洗脱剂", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "collection_flask_1", + "name": "收集瓶1", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 750, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_2", + "name": "收集瓶2", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_flask_3", + "name": "收集瓶3", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 1050, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 1200, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "RunColumnTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "reaction_mixture", + "volume": 300.0, + "concentration": 85.0 + } + ] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_hexane", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_hexane": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_ethyl_acetate", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_ethyl_acetate": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "column_1", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "column_1": "inlet" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_1", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "collection_flask_1": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_2", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "collection_flask_2": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask_3", + "type": "physical", + "port": { + "transfer_pump_1": "8", + "collection_flask_3": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "9", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "10", + "reactor": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_1", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_1": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_2", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_2": "top" + } + }, + { + "source": "column_1", + "target": "collection_flask_3", + "type": "physical", + "port": { + "column_1": "outlet", + "collection_flask_3": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/stirteststation.json b/test/experiments/mock_protocol/stirteststation.json new file mode 100644 index 00000000..20694d1b --- /dev/null +++ b/test/experiments/mock_protocol/stirteststation.json @@ -0,0 +1,250 @@ +{ + "nodes": [ + { + "id": "StirTestStation", + "name": "搅拌测试工作站", + "children": [ + "pump_add", + "flask_sample", + "flask_buffer1", + "flask_buffer2", + "reactor", + "stirrer", + "flask_waste", + "flask_air" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol"] + }, + "data": {} + }, + { + "id": "pump_add", + "name": "添加泵", + "children": [], + "parent": "StirTestStation", + "type": "device", + "class": "virtual_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 25.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "stirrer", + "name": "搅拌器", + "children": [], + "parent": "StirTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer1", + "name": "缓冲液瓶1", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_buffer2", + "name": "缓冲液瓶2", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 5000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "StirTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "top", + "reactor": "bottom" + } + }, + { + "source": "pump_add", + "target": "flask_sample", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_sample": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer1", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer1": "top" + } + }, + { + "source": "pump_add", + "target": "flask_buffer2", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_buffer2": "top" + } + }, + { + "source": "pump_add", + "target": "reactor", + "type": "physical", + "port": { + "pump_add": "outlet", + "reactor": "top" + } + }, + { + "source": "pump_add", + "target": "flask_waste", + "type": "physical", + "port": { + "pump_add": "outlet", + "flask_waste": "top" + } + }, + { + "source": "pump_add", + "target": "flask_air", + "type": "physical", + "port": { + "pump_add": "inlet", + "flask_air": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/transferteststation.json b/test/experiments/mock_protocol/transferteststation.json new file mode 100644 index 00000000..cbe485b7 --- /dev/null +++ b/test/experiments/mock_protocol/transferteststation.json @@ -0,0 +1,249 @@ +{ + "nodes": [ + { + "id": "TransferTestStation", + "name": "液体转移测试工作站", + "children": [ + "transfer_pump", + "flask_source1", + "flask_source2", + "flask_target1", + "flask_target2", + "reactor", + "flask_waste", + "flask_rinsing" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["TransferProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump", + "name": "转移泵", + "children": [], + "parent": "TransferTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 5.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_source1", + "name": "源容器1", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_source2", + "name": "源容器2", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_target1", + "name": "目标容器1", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_target2", + "name": "目标容器2", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_rinsing", + "name": "冲洗液瓶", + "children": [], + "parent": "TransferTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump", + "target": "flask_source1", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_source1": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_source2", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_source2": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_target1", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_target1": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_target2", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_target2": "top" + } + }, + { + "source": "transfer_pump", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "reactor": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_waste", + "type": "physical", + "port": { + "transfer_pump": "outlet", + "flask_waste": "top" + } + }, + { + "source": "transfer_pump", + "target": "flask_rinsing", + "type": "physical", + "port": { + "transfer_pump": "inlet", + "flask_rinsing": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/washsolidteststation.json b/test/experiments/mock_protocol/washsolidteststation.json new file mode 100644 index 00000000..170c825c --- /dev/null +++ b/test/experiments/mock_protocol/washsolidteststation.json @@ -0,0 +1,494 @@ +{ + "nodes": [ + { + "id": "WashSolidTestStation", + "name": "固体清洗测试工作站", + "children": [ + "transfer_pump_1", + "heatchill_1", + "stirrer_1", + "filter_1", + "flask_ethanol", + "flask_water", + "flask_acetone", + "flask_methanol", + "reactor", + "collection_flask", + "waste_flask", + "flask_sample", + "filtrate_flask" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": ["WashSolidProtocol", "TransferProtocol", "FilterProtocol", "HeatChillProtocol", "StirProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 520.6111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_volume": 50.0, + "transfer_rate": 10.0 + }, + "data": { + "status": "Idle", + "current_volume": 0.0, + "max_volume": 50.0, + "transfer_rate": 10.0, + "from_vessel": "", + "to_vessel": "", + "progress": 0.0, + "transferred_volume": 0.0, + "current_status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "加热冷却器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 650.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 150.0, + "min_temp": -20.0 + }, + "data": { + "status": "Idle", + "current_temp": 25.0, + "target_temp": 25.0, + "vessel": "", + "purpose": "", + "progress": 0.0, + "current_status": "Ready" + } + }, + { + "id": "stirrer_1", + "name": "搅拌器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 750.1111111111111, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "WashSolidTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 850.1111111111111, + "y": 478, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "max_temp": 100.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle", + "filter_state": "Ready", + "current_temp": 25.0, + "target_temp": 25.0, + "max_temp": 100.0, + "stir_speed": 0.0, + "max_stir_speed": 1000.0, + "filtered_volume": 0.0, + "progress": 0.0, + "message": "" + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "ethanol", + "volume": 1500.0, + "concentration": 99.5 + } + ] + } + }, + { + "id": "flask_water", + "name": "水清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "name": "water", + "volume": 1800.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "acetone", + "volume": 800.0, + "concentration": 99.8 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇清洗剂", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "methanol", + "volume": 800.0, + "concentration": 99.9 + } + ] + } + }, + { + "id": "reactor", + "name": "反应器", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 698.1111111111111, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "solid_product", + "volume": 50.0, + "concentration": 100.0 + } + ] + } + }, + { + "id": "collection_flask", + "name": "收集瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_flask", + "name": "废液瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 428, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_sample", + "name": "样品瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1150, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "name": "crude_solid", + "volume": 30.0, + "concentration": 80.0 + } + ] + } + }, + { + "id": "filtrate_flask", + "name": "滤液收集瓶", + "children": [], + "parent": "WashSolidTestStation", + "type": "container", + "class": null, + "position": { + "x": 1000, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1500.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "source": "transfer_pump_1", + "target": "flask_ethanol", + "type": "physical", + "port": { + "transfer_pump_1": "1", + "flask_ethanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_water", + "type": "physical", + "port": { + "transfer_pump_1": "2", + "flask_water": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_acetone", + "type": "physical", + "port": { + "transfer_pump_1": "3", + "flask_acetone": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_methanol", + "type": "physical", + "port": { + "transfer_pump_1": "4", + "flask_methanol": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "reactor", + "type": "physical", + "port": { + "transfer_pump_1": "5", + "reactor": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "collection_flask", + "type": "physical", + "port": { + "transfer_pump_1": "6", + "collection_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "waste_flask", + "type": "physical", + "port": { + "transfer_pump_1": "7", + "waste_flask": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "flask_sample", + "type": "physical", + "port": { + "transfer_pump_1": "8", + "flask_sample": "top" + } + }, + { + "source": "transfer_pump_1", + "target": "filtrate_flask", + "type": "physical", + "port": { + "transfer_pump_1": "9", + "filtrate_flask": "top" + } + }, + { + "source": "heatchill_1", + "target": "reactor", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "reactor": "bottom" + } + }, + { + "source": "heatchill_1", + "target": "flask_sample", + "type": "physical", + "port": { + "heatchill_1": "heating_element", + "flask_sample": "bottom" + } + }, + { + "source": "stirrer_1", + "target": "reactor", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "reactor": "center" + } + }, + { + "source": "stirrer_1", + "target": "flask_sample", + "type": "physical", + "port": { + "stirrer_1": "stir_rod", + "flask_sample": "center" + } + }, + { + "source": "filter_1", + "target": "reactor", + "type": "physical", + "port": { + "filter_1": "filter_element", + "reactor": "top" + } + }, + { + "source": "filter_1", + "target": "flask_sample", + "type": "physical", + "port": { + "filter_1": "filter_element", + "flask_sample": "top" + } + }, + { + "source": "filter_1", + "target": "filtrate_flask", + "type": "physical", + "port": { + "filter_1": "filter_element", + "filtrate_flask": "top" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_reactor.json b/test/experiments/mock_reactor.json index b0994cd0..1c03d315 100644 --- a/test/experiments/mock_reactor.json +++ b/test/experiments/mock_reactor.json @@ -30,14 +30,17 @@ "children": [], "parent": "ReactorX", "type": "container", - "class": null, + "class": "container", "position": { "x": 698.1111111111111, "y": 428, "z": 0 }, "config": { - "max_volume": 5000.0 + "max_volume": 5000.0, + "size_x": 200.0, + "size_y": 200.0, + "size_z": 200.0 }, "data": { "liquid": [ @@ -71,7 +74,7 @@ "type": "device", "class": "solenoid_valve.mock", "position": { - "x": 620.6111111111111, + "x": 780, "y": 171, "z": 0 }, @@ -89,7 +92,7 @@ "type": "device", "class": "vacuum_pump.mock", "position": { - "x": 620.6111111111111, + "x": 500, "y": 171, "z": 0 }, @@ -107,7 +110,7 @@ "type": "device", "class": "gas_source.mock", "position": { - "x": 620.6111111111111, + "x": 900, "y": 171, "z": 0 }, @@ -119,39 +122,39 @@ ], "links": [ { - "source": "reactor", - "target": "vacuum_valve", - "type": "physical", + "source": "vacuum_valve", + "target": "reactor", + "type": "fluid", "port": { "reactor": "top", - "vacuum_valve": "1" + "vacuum_valve": "out" } }, { - "source": "reactor", - "target": "gas_valve", - "type": "physical", + "source": "gas_valve", + "target": "reactor", + "type": "fluid", "port": { "reactor": "top", - "gas_valve": "1" + "gas_valve": "out" } }, { "source": "vacuum_pump", "target": "vacuum_valve", - "type": "physical", + "type": "fluid", "port": { "vacuum_pump": "out", - "vacuum_valve": "0" + "vacuum_valve": "in" } }, { "source": "gas_source", "target": "gas_valve", - "type": "physical", + "type": "fluid", "port": { "gas_source": "out", - "gas_valve": "0" + "gas_valve": "in" } } ] diff --git a/unilabos/app/backend.py b/unilabos/app/backend.py index 5bc4ad39..9f7d4279 100644 --- a/unilabos/app/backend.py +++ b/unilabos/app/backend.py @@ -8,6 +8,7 @@ def start_backend( backend: str, devices_config: dict = {}, resources_config: list = [], + resources_edge_config: list = [], graph=None, controllers_config: dict = {}, bridges=[], @@ -31,7 +32,7 @@ def start_backend( backend_thread = threading.Thread( target=main if not without_host else slave, - args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config), + args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config), name="backend_thread", daemon=True, ) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 0f6b2f42..835c57d6 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -10,7 +10,7 @@ from copy import deepcopy import yaml -from unilabos.resources.graphio import tree_to_list +from unilabos.resources.graphio import tree_to_list, modify_to_backend_format # 首先添加项目根目录到路径 current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -22,6 +22,21 @@ from unilabos.config.config import load_config, BasicConfig, _update_config_from from unilabos.utils.banner_print import print_status, print_unilab_banner +def load_config_from_file(config_path): + if config_path is None: + config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None) + if config_path: + if not os.path.exists(config_path): + print_status(f"配置文件 {config_path} 不存在", "error") + elif not config_path.endswith(".py"): + print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error") + else: + load_config(config_path) + else: + print_status(f"启动 UniLab-OS时,配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning") + load_config(config_path) + + def parse_args(): """解析命令行参数""" parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.") @@ -58,6 +73,11 @@ def parse_args(): action="store_true", help="Slave模式下跳过等待host服务", ) + parser.add_argument( + "--upload_registry", + action="store_true", + help="启动unilab时同时报送注册表信息", + ) parser.add_argument( "--config", type=str, @@ -97,22 +117,12 @@ def main(): # 加载配置文件,优先加载config,然后从env读取 config_path = args_dict.get("config") - if config_path is None: - config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None) - if config_path: - if not os.path.exists(config_path): - print_status(f"配置文件 {config_path} 不存在", "error") - elif not config_path.endswith(".py"): - print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error") - else: - load_config(config_path) - else: - print_status(f"启动 UniLab-OS时,配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning") - load_config(config_path) + load_config_from_file(config_path) # 设置BasicConfig参数 BasicConfig.is_host_mode = not args_dict.get("without_host", False) BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) + BasicConfig.upload_registry = args_dict.get("upload_registry", False) machine_name = os.popen("hostname").read().strip() machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) BasicConfig.machine_name = machine_name @@ -136,15 +146,16 @@ 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 - graph_res.physical_setup_graph = ( - read_node_link_json(args_dict["graph"]) - if args_dict["graph"].endswith(".json") - else read_graphml(args_dict["graph"]) - ) + 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"]) 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()) @@ -185,6 +196,7 @@ 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" diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index fbd57b93..ac744c9d 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -172,13 +172,14 @@ class MQTTClient: jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info} self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2) - def publish_registry(self, device_id: str, device_info: dict): + def publish_registry(self, device_id: str, device_info: dict, print_debug: bool = True): if self.mqtt_disable: return address = f"labs/{MQConfig.lab_id}/registry/" registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder) self.client.publish(address, registry_data, qos=2) - logger.debug(f"Registry data published: address: {address}, {registry_data}") + if print_debug: + logger.debug(f"Registry data published: address: {address}, {registry_data}") def publish_actions(self, action_id: str, action_info: dict): if self.mqtt_disable: diff --git a/unilabos/app/register.py b/unilabos/app/register.py new file mode 100644 index 00000000..76ad5a57 --- /dev/null +++ b/unilabos/app/register.py @@ -0,0 +1,67 @@ +import argparse +import time + +from unilabos.registry.registry import build_registry + +from unilabos.app.main import load_config_from_file +from unilabos.utils.log import logger + + +def register_devices_and_resources(mqtt_client, lab_registry): + """ + 注册设备和资源到 MQTT + """ + logger.info("[UniLab Register] 开始注册设备和资源...") + + # 注册设备信息 + for device_info in lab_registry.obtain_registry_device_info(): + mqtt_client.publish_registry(device_info["id"], device_info, False) + logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}") + + # 注册资源信息 + for resource_info in lab_registry.obtain_registry_resource_info(): + mqtt_client.publish_registry(resource_info["id"], resource_info, False) + logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}") + + time.sleep(10) + + logger.info("[UniLab Register] 设备和资源注册完成.") + + +def main(): + """ + 命令行入口函数 + """ + parser = argparse.ArgumentParser(description="注册设备和资源到 MQTT") + parser.add_argument( + "--registry_path", + type=str, + default=None, + action="append", + help="注册表路径", + ) + parser.add_argument( + "--config", + type=str, + default=None, + help="配置文件路径,支持.py格式的Python配置文件", + ) + args = parser.parse_args() + + # 构建注册表 + build_registry(args.registry_path) + load_config_from_file(args.config) + + from unilabos.app.mq import mqtt_client + + # 连接mqtt + mqtt_client.start() + + from unilabos.registry.registry import lab_registry + + # 注册设备和资源 + register_devices_and_resources(mqtt_client, lab_registry) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 69329489..09b6ede6 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -30,7 +30,27 @@ class HTTPClient: self.auth = MQConfig.lab_id info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}") - def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response: + 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=100, + ) + if response.status_code != 200 and response.status_code != 201: + logger.error(f"添加物料关系失败: {response.status_code}, {response.text}") + return response + + def resource_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response: """ 添加资源 @@ -44,8 +64,10 @@ class HTTPClient: f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}", json=resources, headers={"Authorization": f"lab {self.auth}"}, - timeout=5, + timeout=100, ) + 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]: @@ -63,7 +85,7 @@ class HTTPClient: f"{self.remote_addr}/lab/resource/?edge_format=1", params={"id": id, "with_children": with_children}, headers={"Authorization": f"lab {self.auth}"}, - timeout=5, + timeout=20, ) return response.json() @@ -81,7 +103,7 @@ class HTTPClient: f"{self.remote_addr}/lab/resource/batch_delete/", params={"id": id}, headers={"Authorization": f"lab {self.auth}"}, - timeout=5, + timeout=20, ) return response @@ -99,7 +121,7 @@ class HTTPClient: f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1", json=resources, headers={"Authorization": f"lab {self.auth}"}, - timeout=5, + timeout=100, ) return response diff --git a/unilabos/app/web/pages.py b/unilabos/app/web/pages.py index a08cebb5..51b109b1 100644 --- a/unilabos/app/web/pages.py +++ b/unilabos/app/web/pages.py @@ -16,7 +16,6 @@ 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 diff --git a/unilabos/compile/__init__.py b/unilabos/compile/__init__.py index 820f43f1..98ea8a28 100644 --- a/unilabos/compile/__init__.py +++ b/unilabos/compile/__init__.py @@ -5,15 +5,45 @@ from .separate_protocol import generate_separate_protocol from .evaporate_protocol import generate_evaporate_protocol from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol from .agv_transfer_protocol import generate_agv_transfer_protocol +from .add_protocol import generate_add_protocol +from .centrifuge_protocol import generate_centrifuge_protocol +from .filter_protocol import generate_filter_protocol +from .heatchill_protocol import ( + generate_heat_chill_protocol, + generate_heat_chill_start_protocol, + generate_heat_chill_stop_protocol, + generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议 +) +from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol +from .transfer_protocol import generate_transfer_protocol +from .clean_vessel_protocol import generate_clean_vessel_protocol +from .dissolve_protocol import generate_dissolve_protocol +from .filter_through_protocol import generate_filter_through_protocol +from .run_column_protocol import generate_run_column_protocol +from .wash_solid_protocol import generate_wash_solid_protocol # Define a dictionary of protocol generators. action_protocol_generators = { - PumpTransferProtocol: generate_pump_protocol_with_rinsing, - CleanProtocol: generate_clean_protocol, - SeparateProtocol: generate_separate_protocol, - EvaporateProtocol: generate_evaporate_protocol, - EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, + AddProtocol: generate_add_protocol, AGVTransferProtocol: generate_agv_transfer_protocol, -} -# End Protocols + CentrifugeProtocol: generate_centrifuge_protocol, + CleanProtocol: generate_clean_protocol, + CleanVesselProtocol: generate_clean_vessel_protocol, + DissolveProtocol: generate_dissolve_protocol, + EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, + EvaporateProtocol: generate_evaporate_protocol, + FilterProtocol: generate_filter_protocol, + FilterThroughProtocol: generate_filter_through_protocol, + HeatChillProtocol: generate_heat_chill_protocol, + HeatChillStartProtocol: generate_heat_chill_start_protocol, + HeatChillStopProtocol: generate_heat_chill_stop_protocol, + PumpTransferProtocol: generate_pump_protocol_with_rinsing, + RunColumnProtocol: generate_run_column_protocol, + SeparateProtocol: generate_separate_protocol, + StartStirProtocol: generate_start_stir_protocol, + StirProtocol: generate_stir_protocol, + StopStirProtocol: generate_stop_stir_protocol, + TransferProtocol: generate_transfer_protocol, + WashSolidProtocol: generate_wash_solid_protocol, +} \ No newline at end of file diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py new file mode 100644 index 00000000..144ec96f --- /dev/null +++ b/unilabos/compile/add_protocol.py @@ -0,0 +1,627 @@ +import networkx as nx +from typing import List, Dict, Any +from .pump_protocol import generate_pump_protocol_with_rinsing + + +def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: + """ + 根据试剂名称查找对应的试剂瓶,支持多种匹配模式: + 1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF) + 2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol") + 3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate") + + Args: + G: 网络图 + reagent: 试剂名称 + + Returns: + str: 试剂瓶的vessel ID + + Raises: + ValueError: 如果找不到对应的试剂瓶 + """ + print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...") + + # 第一步:通过容器名称匹配 + possible_names = [ + f"flask_{reagent}", # flask_DMF, flask_ethanol + f"bottle_{reagent}", # bottle_DMF, bottle_ethanol + f"vessel_{reagent}", # vessel_DMF, vessel_ethanol + f"{reagent}_flask", # DMF_flask, ethanol_flask + f"{reagent}_bottle", # DMF_bottle, ethanol_bottle + f"{reagent}", # 直接用试剂名 + f"reagent_{reagent}", # reagent_DMF, reagent_ethanol + f"reagent_bottle_{reagent}", # reagent_bottle_DMF + ] + + # 尝试名称匹配 + for vessel_name in possible_names: + if vessel_name in G.nodes(): + print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}") + return vessel_name + + # 第二步:通过模糊名称匹配(名称中包含试剂名) + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + # 检查节点ID或名称中是否包含试剂名 + node_name = G.nodes[node_id].get('name', '').lower() + if (reagent.lower() in node_id.lower() or + reagent.lower() in node_name): + print(f"ADD_PROTOCOL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})") + return node_id + + # 第三步:通过液体类型匹配 + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + vessel_data = G.nodes[node_id].get('data', {}) + liquids = vessel_data.get('liquid', []) + + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式的液体类型字段 + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + reagent_name = vessel_data.get('reagent_name', '') + config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '') + + # 检查多个可能的字段 + if (liquid_type.lower() == reagent.lower() or + reagent_name.lower() == reagent.lower() or + config_reagent.lower() == reagent.lower()): + print(f"ADD_PROTOCOL: 通过液体类型匹配找到容器: {node_id}") + print(f" - liquid_type: {liquid_type}") + print(f" - reagent_name: {reagent_name}") + print(f" - config.reagent: {config_reagent}") + return node_id + + # 第四步:列出所有可用的容器信息帮助调试 + available_containers = [] + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + vessel_data = G.nodes[node_id].get('data', {}) + config_data = G.nodes[node_id].get('config', {}) + liquids = vessel_data.get('liquid', []) + + container_info = { + 'id': node_id, + 'name': G.nodes[node_id].get('name', ''), + 'liquid_types': [], + 'reagent_name': vessel_data.get('reagent_name', ''), + 'config_reagent': config_data.get('reagent', '') + } + + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type: + container_info['liquid_types'].append(liquid_type) + + available_containers.append(container_info) + + print(f"ADD_PROTOCOL: 可用容器列表:") + for container in available_containers: + print(f" - {container['id']}: {container['name']}") + print(f" 液体类型: {container['liquid_types']}") + print(f" 试剂名称: {container['reagent_name']}") + print(f" 配置试剂: {container['config_reagent']}") + + raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。尝试了名称匹配: {possible_names}") + + +def find_reagent_vessel_by_any_match(G: nx.DiGraph, reagent: str) -> str: + """ + 增强版试剂容器查找,支持各种匹配方式的别名函数 + """ + return find_reagent_vessel(G, reagent) + + +def get_vessel_reagent_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的试剂体积""" + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) + volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) + total_volume += volume + + return total_volume + + +def get_vessel_reagent_types(G: nx.DiGraph, vessel: str) -> List[str]: + """获取容器中所有试剂的类型""" + if vessel not in G.nodes(): + return [] + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + reagent_types = [] + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式的试剂类型字段 + reagent_type = liquid.get('liquid_type') or liquid.get('name', '') + if reagent_type: + reagent_types.append(reagent_type) + + # 同时检查配置中的试剂信息 + config_reagent = G.nodes[vessel].get('config', {}).get('reagent', '') + reagent_name = vessel_data.get('reagent_name', '') + + if config_reagent and config_reagent not in reagent_types: + reagent_types.append(config_reagent) + if reagent_name and reagent_name not in reagent_types: + reagent_types.append(reagent_name) + + return reagent_types + + +def find_vessels_by_reagent(G: nx.DiGraph, reagent: str) -> List[str]: + """ + 根据试剂类型查找所有匹配的容器 + 返回匹配容器的ID列表 + """ + matching_vessels = [] + + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + # 检查容器名称匹配 + node_name = G.nodes[node_id].get('name', '').lower() + if reagent.lower() in node_id.lower() or reagent.lower() in node_name: + matching_vessels.append(node_id) + continue + + # 检查试剂类型匹配 + vessel_data = G.nodes[node_id].get('data', {}) + liquids = vessel_data.get('liquid', []) + config_data = G.nodes[node_id].get('config', {}) + + # 检查 reagent_name 和 config.reagent + reagent_name = vessel_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + if (reagent.lower() == reagent_name or + reagent.lower() == config_reagent): + matching_vessels.append(node_id) + continue + + # 检查液体列表 + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type.lower() == reagent.lower(): + matching_vessels.append(node_id) + break + + return matching_vessels + + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """ + 查找与指定容器相连的搅拌器 + + Args: + G: 网络图 + vessel: 容器ID + + Returns: + str: 搅拌器ID,如果找不到则返回None + """ + # 查找所有搅拌器节点 + stirrer_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + return stirrer + + # 如果没有直接连接,返回第一个可用的搅拌器 + return stirrer_nodes[0] if stirrer_nodes else None + + +def generate_add_protocol( + G: nx.DiGraph, + vessel: str, + reagent: str, + volume: float, + mass: float = 0.0, + amount: str = "", + time: float = 0.0, + stir: bool = False, + stir_speed: float = 300.0, + viscous: bool = False, + purpose: str = "添加试剂" +) -> List[Dict[str, Any]]: + """ + 生成添加试剂的协议序列,支持智能试剂匹配 + + 基于pump_protocol的成熟算法,实现试剂添加功能: + 1. 智能查找试剂瓶(支持名称匹配、液体类型匹配、试剂配置匹配) + 2. **先启动搅拌,再进行转移** - 确保试剂添加更均匀 + 3. 使用pump_protocol实现液体转移 + + Args: + G: 有向图,节点为容器和设备,边为连接关系 + vessel: 目标容器(要添加试剂的容器) + reagent: 试剂名称(用于查找对应的试剂瓶) + volume: 要添加的体积 (mL) + mass: 要添加的质量 (g) - 暂时未使用,预留接口 + amount: 其他数量描述 + time: 添加时间 (s),如果指定则计算流速 + stir: 是否启用搅拌 + stir_speed: 搅拌速度 (RPM) + viscous: 是否为粘稠液体 + purpose: 添加目的描述 + + Returns: + List[Dict[str, Any]]: 动作序列 + + Raises: + ValueError: 当找不到必要的设备或容器时 + """ + action_sequence = [] + + print(f"ADD_PROTOCOL: 开始生成添加试剂协议") + print(f" - 目标容器: {vessel}") + print(f" - 试剂: {reagent}") + print(f" - 体积: {volume} mL") + print(f" - 质量: {mass} g") + print(f" - 搅拌: {stir} (速度: {stir_speed} RPM)") + print(f" - 粘稠: {viscous}") + print(f" - 目的: {purpose}") + + # 1. 验证目标容器存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + + # 2. 智能查找试剂瓶 + try: + reagent_vessel = find_reagent_vessel(G, reagent) + print(f"ADD_PROTOCOL: 找到试剂容器: {reagent_vessel}") + except ValueError as e: + raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") + + # 3. 验证试剂容器中的试剂体积 + available_volume = get_vessel_reagent_volume(G, reagent_vessel) + print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂") + + if available_volume < volume: + print(f"ADD_PROTOCOL: 警告 - 试剂容器中的试剂不足!需要 {volume} mL,可用 {available_volume} mL") + + # 4. 验证是否存在从试剂瓶到目标容器的路径 + try: + path = nx.shortest_path(G, source=reagent_vessel, target=vessel) + print(f"ADD_PROTOCOL: 找到路径 {reagent_vessel} -> {vessel}: {path}") + except nx.NetworkXNoPath: + raise ValueError(f"从试剂瓶 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + + # 5. **先启动搅拌** - 关键改进! + if stir: + try: + stirrer_id = find_connected_stirrer(G, vessel) + + if stirrer_id: + print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌") + + # 先启动搅拌 + stir_action = { + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" + } + } + + action_sequence.append(stir_action) + print(f"ADD_PROTOCOL: 已添加搅拌动作,速度 {stir_speed} RPM") + + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + else: + print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器") + + except Exception as e: + print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}") + + # 6. 如果指定了体积,执行液体转移 + if volume > 0: + # 6.1 计算流速参数 + if time > 0: + # 根据时间计算流速 + transfer_flowrate = volume / time + flowrate = transfer_flowrate + else: + # 使用默认流速 + if viscous: + transfer_flowrate = 0.3 # 粘稠液体用较慢速度 + flowrate = 1.0 + else: + transfer_flowrate = 0.5 # 普通液体默认速度 + flowrate = 2.5 + + print(f"ADD_PROTOCOL: 准备转移 {volume} mL 从 {reagent_vessel} 到 {vessel}") + print(f"ADD_PROTOCOL: 转移流速={transfer_flowrate} mL/s, 注入流速={flowrate} mL/s") + + # 6.2 使用pump_protocol的核心算法实现液体转移 + try: + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + 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, + flowrate=flowrate, + transfer_flowrate=transfer_flowrate + ) + + # 添加pump actions到序列中 + action_sequence.extend(pump_actions) + + except Exception as e: + raise ValueError(f"生成泵协议时出错: {str(e)}") + + print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作") + print(f"ADD_PROTOCOL: 添加试剂协议生成完成") + + return action_sequence + + +def generate_add_protocol_with_cleaning( + G: nx.DiGraph, + vessel: str, + reagent: str, + volume: float, + mass: float = 0.0, + amount: str = "", + time: float = 0.0, + stir: bool = False, + stir_speed: float = 300.0, + viscous: bool = False, + purpose: str = "添加试剂", + cleaning_solvent: str = "air", + cleaning_volume: float = 5.0, + cleaning_repeats: int = 1 +) -> List[Dict[str, Any]]: + """ + 生成带清洗的添加试剂协议,支持智能试剂匹配 + + 与普通添加协议的区别是会在添加后进行管道清洗 + + Args: + G: 有向图 + vessel: 目标容器 + reagent: 试剂名称 + volume: 添加体积 + mass: 添加质量(预留) + amount: 其他数量描述 + time: 添加时间 + stir: 是否搅拌 + stir_speed: 搅拌速度 + viscous: 是否粘稠 + purpose: 添加目的 + cleaning_solvent: 清洗溶剂("air"表示空气清洗) + cleaning_volume: 清洗体积 + cleaning_repeats: 清洗重复次数 + + Returns: + List[Dict[str, Any]]: 动作序列 + """ + action_sequence = [] + + # 1. 智能查找试剂瓶 + reagent_vessel = find_reagent_vessel(G, reagent) + + # 2. **先启动搅拌** + if stir: + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" + } + }) + + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # 3. 计算流速 + if time > 0: + transfer_flowrate = volume / time + flowrate = transfer_flowrate + else: + if viscous: + transfer_flowrate = 0.3 + flowrate = 1.0 + else: + transfer_flowrate = 0.5 + flowrate = 2.5 + + # 4. 使用带清洗的pump_protocol + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=reagent_vessel, + to_vessel=vessel, + volume=volume, + amount=amount, + time=time, + viscous=viscous, + rinsing_solvent=cleaning_solvent, + rinsing_volume=cleaning_volume, + rinsing_repeats=cleaning_repeats, + solid=False, + flowrate=flowrate, + transfer_flowrate=transfer_flowrate + ) + + action_sequence.extend(pump_actions) + + return action_sequence + + +def generate_sequential_add_protocol( + G: nx.DiGraph, + vessel: str, + reagents: List[Dict[str, Any]], + stir_between_additions: bool = True, + final_stir: bool = True, + final_stir_speed: float = 400.0, + final_stir_time: float = 300.0 +) -> List[Dict[str, Any]]: + """ + 生成连续添加多种试剂的协议,支持智能试剂匹配 + + Args: + G: 网络图 + vessel: 目标容器 + reagents: 试剂列表,每个元素包含试剂添加参数 + stir_between_additions: 是否在每次添加之间搅拌 + final_stir: 是否在所有添加完成后进行最终搅拌 + final_stir_speed: 最终搅拌速度 + final_stir_time: 最终搅拌时间 + + Returns: + List[Dict[str, Any]]: 完整的动作序列 + + Example: + reagents = [ + { + "reagent": "DMF", # 会匹配 reagent_bottle_1 (reagent_name: "DMF") + "volume": 10.0, + "viscous": False, + "stir_speed": 300.0 + }, + { + "reagent": "ethyl_acetate", # 会匹配 reagent_bottle_2 (reagent_name: "ethyl_acetate") + "volume": 5.0, + "viscous": False, + "stir_speed": 350.0 + } + ] + """ + action_sequence = [] + + print(f"ADD_PROTOCOL: 开始连续添加 {len(reagents)} 种试剂到容器 {vessel}") + + for i, reagent_params in enumerate(reagents): + reagent_name = reagent_params.get('reagent') + print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_name}") + + # 生成单个试剂的添加协议 + add_actions = generate_add_protocol( + G=G, + vessel=vessel, + reagent=reagent_name, + volume=reagent_params.get('volume', 0.0), + mass=reagent_params.get('mass', 0.0), + amount=reagent_params.get('amount', ''), + time=reagent_params.get('time', 0.0), + stir=stir_between_additions, + stir_speed=reagent_params.get('stir_speed', 300.0), + viscous=reagent_params.get('viscous', False), + purpose=reagent_params.get('purpose', f'添加试剂 {reagent_name} ({i+1}/{len(reagents)})') + ) + + action_sequence.extend(add_actions) + + # 在添加之间加入等待时间 + if i < len(reagents) - 1: # 不是最后一个试剂 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10} # 试剂混合时间 + }) + + # 最终搅拌 + if final_stir: + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + print(f"ADD_PROTOCOL: 添加最终搅拌动作,速度 {final_stir_speed} RPM,时间 {final_stir_time} 秒") + action_sequence.extend([ + { + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": final_stir_time, + "stir_speed": final_stir_speed, + "settling_time": 30.0 + } + } + ]) + + print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 便捷函数:常用添加方案 +def generate_organic_add_protocol( + G: nx.DiGraph, + vessel: str, + organic_reagent: str, + volume: float, + stir_speed: float = 400.0 +) -> List[Dict[str, Any]]: + """有机试剂添加:慢速、搅拌""" + return generate_add_protocol( + G, vessel, organic_reagent, volume, 0.0, "", 0.0, + True, stir_speed, False, f"添加有机试剂 {organic_reagent}" + ) + + +def generate_viscous_add_protocol( + G: nx.DiGraph, + vessel: str, + viscous_reagent: str, + volume: float, + addition_time: float = 120.0 +) -> List[Dict[str, Any]]: + """粘稠试剂添加:慢速、长时间""" + return generate_add_protocol( + G, vessel, viscous_reagent, volume, 0.0, "", addition_time, + True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}" + ) + + +def generate_solvent_add_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float +) -> List[Dict[str, Any]]: + """溶剂添加:快速、无需特殊处理""" + return generate_add_protocol( + G, vessel, solvent, volume, 0.0, "", 0.0, + False, 300.0, False, f"添加溶剂 {solvent}" + ) + + +# 使用示例和测试函数 +def test_add_protocol(): + """测试添加协议的示例""" + print("=== ADD PROTOCOL 智能匹配测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_add_protocol() \ No newline at end of file diff --git a/unilabos/compile/centrifuge_protocol.py b/unilabos/compile/centrifuge_protocol.py new file mode 100644 index 00000000..3f541071 --- /dev/null +++ b/unilabos/compile/centrifuge_protocol.py @@ -0,0 +1,285 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """ + 获取容器中的液体体积 + """ + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict) and 'liquid_volume' in liquid: + total_volume += liquid['liquid_volume'] + + return total_volume + + +def find_centrifuge_device(G: nx.DiGraph) -> str: + """ + 查找离心机设备 + """ + centrifuge_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_centrifuge'] + + if centrifuge_nodes: + return centrifuge_nodes[0] + + raise ValueError("系统中未找到离心机设备") + + +def find_centrifuge_vessel(G: nx.DiGraph) -> str: + """ + 查找离心机专用容器 + """ + possible_names = [ + "centrifuge_tube", + "centrifuge_vessel", + "tube_centrifuge", + "vessel_centrifuge", + "centrifuge", + "tube_15ml", + "tube_50ml" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到离心机容器。尝试了以下名称: {possible_names}") + + +def generate_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + speed: float, + time: float, + temp: float = 25.0 +) -> List[Dict[str, Any]]: + """ + 生成离心操作的协议序列,复用 pump_protocol 的成熟算法 + + 离心流程: + 1. 液体转移:将待离心溶液从源容器转移到离心机容器 + 2. 离心操作:执行离心分离 + 3. 上清液转移:将离心后的上清液转移回原容器或新容器 + 4. 沉淀处理:处理离心沉淀(可选) + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待离心溶液的容器名称 + speed: 离心速度 (rpm) + time: 离心时间 (秒) + temp: 离心温度 (°C),默认25°C + + Returns: + List[Dict[str, Any]]: 离心操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + centrifuge_actions = generate_centrifuge_protocol(G, "reaction_mixture", 5000, 600, 4.0) + """ + action_sequence = [] + + print(f"CENTRIFUGE: 开始生成离心协议") + print(f" - 源容器: {vessel}") + print(f" - 离心速度: {speed} rpm") + print(f" - 离心时间: {time}s ({time/60:.1f}分钟)") + print(f" - 离心温度: {temp}°C") + + # 验证源容器存在 + if vessel not in G.nodes(): + raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, vessel) + print(f"CENTRIFUGE: 源容器 {vessel} 中有 {source_volume} mL 液体") + + # 查找离心机设备 + try: + centrifuge_id = find_centrifuge_device(G) + print(f"CENTRIFUGE: 找到离心机: {centrifuge_id}") + except ValueError as e: + raise ValueError(f"无法找到离心机: {str(e)}") + + # 查找离心机容器 + try: + centrifuge_vessel = find_centrifuge_vessel(G) + print(f"CENTRIFUGE: 找到离心机容器: {centrifuge_vessel}") + except ValueError as e: + raise ValueError(f"无法找到离心机容器: {str(e)}") + + # === 简化的体积计算策略 === + if source_volume > 0: + # 如果能检测到液体体积,使用实际体积的大部分 + transfer_volume = min(source_volume * 0.9, 15.0) # 90%或最多15mL(离心管通常较小) + print(f"CENTRIFUGE: 检测到液体体积,将转移 {transfer_volume} mL") + else: + # 如果检测不到液体体积,默认转移标准量 + transfer_volume = 10.0 # 标准离心管体积 + print(f"CENTRIFUGE: 未检测到液体体积,默认转移 {transfer_volume} mL") + + # === 第一步:将待离心溶液转移到离心机容器 === + print(f"CENTRIFUGE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {centrifuge_vessel}") + try: + # 使用成熟的 pump_protocol 算法进行液体转移 + transfer_to_centrifuge_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=centrifuge_vessel, + volume=transfer_volume, + flowrate=1.0, # 离心转移用慢速,避免气泡 + transfer_flowrate=1.0 + ) + action_sequence.extend(transfer_to_centrifuge_actions) + except Exception as e: + raise ValueError(f"无法将溶液转移到离心机: {str(e)}") + + # 转移后等待 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 5} + } + action_sequence.append(wait_action) + + # === 第二步:执行离心操作 === + print(f"CENTRIFUGE: 执行离心操作") + centrifuge_action = { + "device_id": centrifuge_id, + "action_name": "centrifuge", + "action_kwargs": { + "vessel": centrifuge_vessel, + "speed": speed, + "time": time, + "temp": temp + } + } + action_sequence.append(centrifuge_action) + + # 离心后等待系统稳定 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 10} # 离心后等待稍长,让沉淀稳定 + } + action_sequence.append(wait_action) + + # === 第三步:将上清液转移回原容器 === + print(f"CENTRIFUGE: 将上清液从离心机转移回 {vessel}") + try: + # 估算上清液体积(约为转移体积的80% - 假设20%成为沉淀) + supernatant_volume = transfer_volume * 0.8 + print(f"CENTRIFUGE: 预计上清液体积 {supernatant_volume} mL") + + transfer_back_actions = generate_pump_protocol( + G=G, + from_vessel=centrifuge_vessel, + to_vessel=vessel, + volume=supernatant_volume, + flowrate=0.5, # 上清液转移更慢,避免扰动沉淀 + transfer_flowrate=0.5 + ) + action_sequence.extend(transfer_back_actions) + except Exception as e: + print(f"CENTRIFUGE: 将上清液转移回容器失败: {str(e)}") + + # === 第四步:清洗离心机容器 === + print(f"CENTRIFUGE: 清洗离心机容器") + try: + # 查找清洗溶剂 + cleaning_solvent = None + for solvent in ["flask_water", "flask_ethanol", "flask_acetone"]: + if solvent in G.nodes(): + cleaning_solvent = solvent + break + + if cleaning_solvent: + # 用少量溶剂清洗离心管 + cleaning_volume = 5.0 # 5mL清洗 + print(f"CENTRIFUGE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗") + + # 清洗溶剂加入 + cleaning_actions = generate_pump_protocol( + G=G, + from_vessel=cleaning_solvent, + to_vessel=centrifuge_vessel, + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(cleaning_actions) + + # 将清洗液转移到废液 + if "waste_workup" in G.nodes(): + waste_actions = generate_pump_protocol( + G=G, + from_vessel=centrifuge_vessel, + to_vessel="waste_workup", + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(waste_actions) + + except Exception as e: + print(f"CENTRIFUGE: 清洗步骤失败: {str(e)}") + + print(f"CENTRIFUGE: 生成了 {len(action_sequence)} 个动作") + print(f"CENTRIFUGE: 离心协议生成完成") + print(f"CENTRIFUGE: 总处理体积: {transfer_volume} mL") + + return action_sequence + + +# 便捷函数:常用离心方案 +def generate_low_speed_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 300.0 # 5分钟 +) -> List[Dict[str, Any]]: + """低速离心:细胞分离或大颗粒沉淀""" + return generate_centrifuge_protocol(G, vessel, 1000.0, time, 4.0) + + +def generate_high_speed_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 600.0 # 10分钟 +) -> List[Dict[str, Any]]: + """高速离心:蛋白质沉淀或小颗粒分离""" + return generate_centrifuge_protocol(G, vessel, 12000.0, time, 4.0) + + +def generate_standard_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 600.0 # 10分钟 +) -> List[Dict[str, Any]]: + """标准离心:常规样品处理""" + return generate_centrifuge_protocol(G, vessel, 5000.0, time, 25.0) + + +def generate_cold_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + speed: float = 5000.0, + time: float = 600.0 +) -> List[Dict[str, Any]]: + """冷冻离心:热敏感样品处理""" + return generate_centrifuge_protocol(G, vessel, speed, time, 4.0) + + +def generate_ultra_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 1800.0 # 30分钟 +) -> List[Dict[str, Any]]: + """超高速离心:超细颗粒分离""" + return generate_centrifuge_protocol(G, vessel, 15000.0, time, 4.0) \ No newline at end of file diff --git a/unilabos/compile/clean_vessel_protocol.py b/unilabos/compile/clean_vessel_protocol.py new file mode 100644 index 00000000..28abc954 --- /dev/null +++ b/unilabos/compile/clean_vessel_protocol.py @@ -0,0 +1,439 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """ + 查找溶剂容器,支持多种匹配模式: + 1. 容器名称匹配(如 flask_water, reagent_bottle_1-DMF) + 2. 容器内液体类型匹配(如 liquid_type: "DMF", "ethanol") + """ + print(f"CLEAN_VESSEL: 正在查找溶剂 '{solvent}' 的容器...") + + # 第一步:通过容器名称匹配 + possible_names = [ + f"flask_{solvent}", # flask_water, flask_ethanol + f"bottle_{solvent}", # bottle_water, bottle_ethanol + f"vessel_{solvent}", # vessel_water, vessel_ethanol + f"{solvent}_flask", # water_flask, ethanol_flask + f"{solvent}_bottle", # water_bottle, ethanol_bottle + f"{solvent}", # 直接用溶剂名 + f"solvent_{solvent}", # solvent_water, solvent_ethanol + f"reagent_bottle_{solvent}", # reagent_bottle_DMF + ] + + # 尝试名称匹配 + for vessel_name in possible_names: + if vessel_name in G.nodes(): + print(f"CLEAN_VESSEL: 通过名称匹配找到容器: {vessel_name}") + return vessel_name + + # 第二步:通过模糊名称匹配(名称中包含溶剂名) + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + # 检查节点ID或名称中是否包含溶剂名 + node_name = G.nodes[node_id].get('name', '').lower() + if (solvent.lower() in node_id.lower() or + solvent.lower() in node_name): + print(f"CLEAN_VESSEL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})") + return node_id + + # 第三步:通过液体类型匹配 + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + vessel_data = G.nodes[node_id].get('data', {}) + liquids = vessel_data.get('liquid', []) + + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式的液体类型字段 + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + reagent_name = vessel_data.get('reagent_name', '') + config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '') + + # 检查多个可能的字段 + if (liquid_type.lower() == solvent.lower() or + reagent_name.lower() == solvent.lower() or + config_reagent.lower() == solvent.lower()): + print(f"CLEAN_VESSEL: 通过液体类型匹配找到容器: {node_id}") + print(f" - liquid_type: {liquid_type}") + print(f" - reagent_name: {reagent_name}") + print(f" - config.reagent: {config_reagent}") + return node_id + + # 第四步:列出所有可用的容器信息帮助调试 + available_containers = [] + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + vessel_data = G.nodes[node_id].get('data', {}) + config_data = G.nodes[node_id].get('config', {}) + liquids = vessel_data.get('liquid', []) + + container_info = { + 'id': node_id, + 'name': G.nodes[node_id].get('name', ''), + 'liquid_types': [], + 'reagent_name': vessel_data.get('reagent_name', ''), + 'config_reagent': config_data.get('reagent', '') + } + + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type: + container_info['liquid_types'].append(liquid_type) + + available_containers.append(container_info) + + print(f"CLEAN_VESSEL: 可用容器列表:") + for container in available_containers: + print(f" - {container['id']}: {container['name']}") + print(f" 液体类型: {container['liquid_types']}") + print(f" 试剂名称: {container['reagent_name']}") + print(f" 配置试剂: {container['config_reagent']}") + + raise ValueError(f"未找到溶剂 '{solvent}' 的容器。尝试了名称匹配: {possible_names}") + + +def find_solvent_vessel_by_any_match(G: nx.DiGraph, solvent: str) -> str: + """ + 增强版溶剂容器查找,支持各种匹配方式的别名函数 + """ + return find_solvent_vessel(G, solvent) + + +def find_waste_vessel(G: nx.DiGraph) -> str: + """ + 查找废液容器 + """ + possible_waste_names = [ + "waste_workup", + "flask_waste", + "bottle_waste", + "waste", + "waste_vessel", + "waste_container" + ] + + for waste_name in possible_waste_names: + if waste_name in G.nodes(): + return waste_name + + raise ValueError(f"未找到废液容器。尝试了以下名称: {possible_waste_names}") + + +def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: + """ + 查找与指定容器相连的加热冷却设备 + """ + # 查找所有加热冷却设备节点 + heatchill_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_heatchill'] + + # 检查哪个加热设备与目标容器相连(机械连接) + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + return heatchill + + # 如果没有直接连接,返回第一个可用的加热设备 + if heatchill_nodes: + return heatchill_nodes[0] + + return None # 没有加热设备也可以工作,只是不能加热 + + +def generate_clean_vessel_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + temp: float, + repeats: int = 1 +) -> List[Dict[str, Any]]: + """ + 生成容器清洗操作的协议序列,复用 pump_protocol 的成熟算法 + + 清洗流程: + 1. 查找溶剂容器和废液容器 + 2. 如果需要加热,启动加热设备 + 3. 重复以下操作 repeats 次: + a. 使用 pump_protocol 将溶剂从溶剂容器转移到目标容器 + b. (可选) 等待清洗作用时间 + c. 使用 pump_protocol 将清洗液从目标容器转移到废液容器 + 4. 如果加热了,停止加热 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 要清洗的容器名称 + solvent: 用于清洗的溶剂名称 + volume: 每次清洗使用的溶剂体积 + temp: 清洗时的温度 + repeats: 清洗操作的重复次数,默认为 1 + + Returns: + List[Dict[str, Any]]: 容器清洗操作的动作序列 + + Raises: + ValueError: 当找不到必要的容器或设备时抛出异常 + + Examples: + clean_protocol = generate_clean_vessel_protocol(G, "main_reactor", "water", 100.0, 60.0, 2) + """ + action_sequence = [] + + print(f"CLEAN_VESSEL: 开始生成容器清洗协议") + print(f" - 目标容器: {vessel}") + print(f" - 清洗溶剂: {solvent}") + print(f" - 清洗体积: {volume} mL") + print(f" - 清洗温度: {temp}°C") + print(f" - 重复次数: {repeats}") + + # 验证目标容器存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + + # 查找溶剂容器 + try: + solvent_vessel = find_solvent_vessel(G, solvent) + print(f"CLEAN_VESSEL: 找到溶剂容器: {solvent_vessel}") + except ValueError as e: + raise ValueError(f"无法找到溶剂容器: {str(e)}") + + # 查找废液容器 + try: + waste_vessel = find_waste_vessel(G) + print(f"CLEAN_VESSEL: 找到废液容器: {waste_vessel}") + except ValueError as e: + raise ValueError(f"无法找到废液容器: {str(e)}") + + # 查找加热设备(可选) + heatchill_id = find_connected_heatchill(G, vessel) + if heatchill_id: + print(f"CLEAN_VESSEL: 找到加热设备: {heatchill_id}") + else: + print(f"CLEAN_VESSEL: 未找到加热设备,将在室温下清洗") + + # 第一步:如果需要加热且有加热设备,启动加热 + if temp > 25.0 and heatchill_id: + print(f"CLEAN_VESSEL: 启动加热至 {temp}°C") + heatchill_start_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"cleaning with {solvent}" + } + } + action_sequence.append(heatchill_start_action) + + # 等待温度稳定 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 30} # 等待30秒让温度稳定 + } + action_sequence.append(wait_action) + + # 第二步:重复清洗操作 + for repeat in range(repeats): + print(f"CLEAN_VESSEL: 执行第 {repeat + 1} 次清洗") + + # 2a. 使用 pump_protocol 将溶剂转移到目标容器 + print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel}") + try: + # 调用成熟的 pump_protocol 算法 + add_solvent_actions = generate_pump_protocol( + G=G, + from_vessel=solvent_vessel, + to_vessel=vessel, + volume=volume, + flowrate=2.5, # 适中的流速,避免飞溅 + transfer_flowrate=2.5 + ) + action_sequence.extend(add_solvent_actions) + except Exception as e: + raise ValueError(f"无法将溶剂转移到容器: {str(e)}") + + # 2b. 等待清洗作用时间(让溶剂充分清洗容器) + cleaning_wait_time = 60 if temp > 50.0 else 30 # 高温下等待更久 + print(f"CLEAN_VESSEL: 等待清洗作用 {cleaning_wait_time} 秒") + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": cleaning_wait_time} + } + action_sequence.append(wait_action) + + # 2c. 使用 pump_protocol 将清洗液转移到废液容器 + print(f"CLEAN_VESSEL: 将清洗液从 {vessel} 转移到废液容器") + try: + # 调用成熟的 pump_protocol 算法 + remove_waste_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=waste_vessel, + volume=volume, + flowrate=2.5, # 适中的流速 + transfer_flowrate=2.5 + ) + action_sequence.extend(remove_waste_actions) + except Exception as e: + raise ValueError(f"无法将清洗液转移到废液容器: {str(e)}") + + # 2d. 清洗循环间的短暂等待 + if repeat < repeats - 1: # 不是最后一次清洗 + print(f"CLEAN_VESSEL: 清洗循环间等待") + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 10} + } + action_sequence.append(wait_action) + + # 第三步:如果加热了,停止加热 + if temp > 25.0 and heatchill_id: + print(f"CLEAN_VESSEL: 停止加热") + heatchill_stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + } + action_sequence.append(heatchill_stop_action) + + print(f"CLEAN_VESSEL: 生成了 {len(action_sequence)} 个动作") + print(f"CLEAN_VESSEL: 清洗协议生成完成") + + return action_sequence + + +# 便捷函数:常用清洗方案 +def generate_quick_clean_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str = "water", + volume: float = 100.0 +) -> List[Dict[str, Any]]: + """快速清洗:室温,单次清洗""" + return generate_clean_vessel_protocol(G, vessel, solvent, volume, 25.0, 1) + + +def generate_thorough_clean_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str = "water", + volume: float = 150.0, + temp: float = 60.0 +) -> List[Dict[str, Any]]: + """深度清洗:加热,多次清洗""" + return generate_clean_vessel_protocol(G, vessel, solvent, volume, temp, 3) + + +def generate_organic_clean_protocol( + G: nx.DiGraph, + vessel: str, + volume: float = 100.0 +) -> List[Dict[str, Any]]: + """有机清洗:先用有机溶剂,再用水清洗""" + action_sequence = [] + + # 第一步:有机溶剂清洗 + try: + organic_actions = generate_clean_vessel_protocol( + G, vessel, "acetone", volume, 25.0, 2 + ) + action_sequence.extend(organic_actions) + except ValueError: + # 如果没有丙酮,尝试乙醇 + try: + organic_actions = generate_clean_vessel_protocol( + G, vessel, "ethanol", volume, 25.0, 2 + ) + action_sequence.extend(organic_actions) + except ValueError: + print("警告:未找到有机溶剂,跳过有机清洗步骤") + + # 第二步:水清洗 + water_actions = generate_clean_vessel_protocol( + G, vessel, "water", volume, 25.0, 2 + ) + action_sequence.extend(water_actions) + + return action_sequence + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积(修复版)""" + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) + volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) + total_volume += volume + + return total_volume + + +def get_vessel_liquid_types(G: nx.DiGraph, vessel: str) -> List[str]: + """获取容器中所有液体的类型""" + if vessel not in G.nodes(): + return [] + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + liquid_types = [] + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式的液体类型字段 + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type: + liquid_types.append(liquid_type) + + return liquid_types + + +def find_vessel_by_content(G: nx.DiGraph, content: str) -> List[str]: + """ + 根据内容物查找所有匹配的容器 + 返回匹配容器的ID列表 + """ + matching_vessels = [] + + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + # 检查容器名称匹配 + node_name = G.nodes[node_id].get('name', '').lower() + if content.lower() in node_id.lower() or content.lower() in node_name: + matching_vessels.append(node_id) + continue + + # 检查液体类型匹配 + vessel_data = G.nodes[node_id].get('data', {}) + liquids = vessel_data.get('liquid', []) + config_data = G.nodes[node_id].get('config', {}) + + # 检查 reagent_name 和 config.reagent + reagent_name = vessel_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + if (content.lower() == reagent_name or + content.lower() == config_reagent): + matching_vessels.append(node_id) + continue + + # 检查液体列表 + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type.lower() == content.lower(): + matching_vessels.append(node_id) + break + + return matching_vessels \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py new file mode 100644 index 00000000..3da0d537 --- /dev/null +++ b/unilabos/compile/dissolve_protocol.py @@ -0,0 +1,359 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """ + 查找溶剂容器 + """ + # 按照pump_protocol的命名规则查找溶剂瓶 + solvent_vessel_id = f"flask_{solvent}" + + if solvent_vessel_id in G.nodes(): + return solvent_vessel_id + + # 如果直接匹配失败,尝试模糊匹配 + for node in G.nodes(): + if node.startswith('flask_') and solvent.lower() in node.lower(): + return node + + # 如果还是找不到,列出所有可用的溶剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + + +def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: + """ + 查找与指定容器相连的加热搅拌器 + """ + # 查找所有加热搅拌器节点 + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + # 检查哪个加热器与目标容器相连 + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + return heatchill + + # 如果没有直接连接,返回第一个可用的加热器 + return heatchill_nodes[0] if heatchill_nodes else None + + +def generate_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + temp: float = 25.0, + time: float = 0.0, + stir_speed: float = 300.0 +) -> List[Dict[str, Any]]: + """ + 生成溶解操作的协议序列,复用 pump_protocol 的成熟算法 + + 溶解流程: + 1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器 + 2. 启动加热搅拌:设置温度和搅拌 + 3. 等待溶解:监控溶解过程 + 4. 停止加热搅拌:完成溶解 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 目标容器(要进行溶解的容器) + solvent: 溶剂名称(用于查找对应的溶剂瓶) + volume: 溶剂体积 (mL) + amount: 要溶解的物质描述 + temp: 溶解温度 (°C),默认25°C(室温) + time: 溶解时间 (秒),默认0(立即完成) + stir_speed: 搅拌速度 (RPM),默认300 RPM + + Returns: + List[Dict[str, Any]]: 溶解操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备或容器时 + + Examples: + dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0) + """ + action_sequence = [] + + print(f"DISSOLVE: 开始生成溶解协议") + print(f" - 目标容器: {vessel}") + print(f" - 溶剂: {solvent}") + print(f" - 溶剂体积: {volume} mL") + print(f" - 要溶解的物质: {amount}") + print(f" - 溶解温度: {temp}°C") + print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成") + print(f" - 搅拌速度: {stir_speed} RPM") + + # 验证目标容器存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + + # 查找溶剂瓶 + try: + solvent_vessel = find_solvent_vessel(G, solvent) + print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}") + except ValueError as e: + raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") + + # 验证是否存在从溶剂瓶到目标容器的路径 + try: + path = nx.shortest_path(G, source=solvent_vessel, target=vessel) + print(f"DISSOLVE: 找到路径 {solvent_vessel} -> {vessel}: {path}") + except nx.NetworkXNoPath: + raise ValueError(f"从溶剂瓶 '{solvent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + + # 查找加热搅拌器 + heatchill_id = None + if temp > 25.0 or stir_speed > 0 or time > 0: + try: + heatchill_id = find_connected_heatchill(G, vessel) + if heatchill_id: + print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}") + else: + print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器") + except Exception as e: + print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}") + + # === 第一步:启动加热搅拌(在添加溶剂前) === + if heatchill_id and (temp > 25.0 or time > 0): + print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C") + + if time > 0: + # 如果指定了时间,使用定时加热搅拌 + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "time": time, + "stir": True, + "stir_speed": stir_speed, + "purpose": f"溶解 {amount} 在 {solvent} 中" + } + } + else: + # 如果没有指定时间,使用持续加热搅拌 + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"溶解 {amount} 在 {solvent} 中" + } + } + + action_sequence.append(heatchill_action) + + # 等待温度稳定 + if temp > 25.0: + wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": wait_time} + }) + + # === 第二步:添加溶剂到目标容器 === + if volume > 0: + print(f"DISSOLVE: 将 {volume} mL {solvent} 从 {solvent_vessel} 转移到 {vessel}") + + # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 + transfer_flowrate = 1.0 # 较慢的转移速度 + flowrate = 0.5 # 较慢的注入速度 + + try: + # 使用成熟的 pump_protocol 算法进行液体转移 + pump_actions = generate_pump_protocol( + G=G, + from_vessel=solvent_vessel, + to_vessel=vessel, + volume=volume, + flowrate=flowrate, # 注入速度 - 较慢避免飞溅 + transfer_flowrate=transfer_flowrate # 转移速度 + ) + + action_sequence.extend(pump_actions) + + except Exception as e: + raise ValueError(f"生成泵协议时出错: {str(e)}") + + # 溶剂添加后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 === + if time > 0 and heatchill_id and temp <= 25.0: + # 只需要搅拌等待,不需要加热 + print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解") + + stir_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": 25.0, # 室温 + "time": time, + "stir": True, + "stir_speed": stir_speed, + "purpose": f"室温搅拌溶解 {amount}" + } + } + action_sequence.append(stir_action) + + # === 第四步:如果使用了持续加热,需要手动停止 === + if heatchill_id and time == 0 and temp > 25.0: + print(f"DISSOLVE: 停止加热搅拌器") + + stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + } + action_sequence.append(stop_action) + + print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作") + print(f"DISSOLVE: 溶解协议生成完成") + + return action_sequence + + +# 便捷函数:常用溶解方案 +def generate_room_temp_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + stir_time: float = 300.0 # 5分钟 +) -> List[Dict[str, Any]]: + """室温溶解:快速搅拌,短时间""" + return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0) + + +def generate_heated_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + temp: float = 60.0, + dissolve_time: float = 900.0 # 15分钟 +) -> List[Dict[str, Any]]: + """加热溶解:中等温度,较长时间""" + return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0) + + +def generate_gentle_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + temp: float = 40.0, + dissolve_time: float = 1800.0 # 30分钟 +) -> List[Dict[str, Any]]: + """温和溶解:低温,长时间,慢搅拌""" + return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0) + + +def generate_hot_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + amount: str = "", + temp: float = 80.0, + dissolve_time: float = 600.0 # 10分钟 +) -> List[Dict[str, Any]]: + """高温溶解:高温,中等时间,快搅拌""" + return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0) + + +def generate_sequential_dissolve_protocol( + G: nx.DiGraph, + vessel: str, + dissolve_steps: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + """ + 生成连续溶解多种物质的协议 + + Args: + G: 网络图 + vessel: 目标容器 + dissolve_steps: 溶解步骤列表,每个元素包含溶解参数 + + Returns: + List[Dict[str, Any]]: 完整的动作序列 + + Example: + dissolve_steps = [ + { + "solvent": "water", + "volume": 5.0, + "amount": "NaCl 1g", + "temp": 25.0, + "time": 300.0, + "stir_speed": 300.0 + }, + { + "solvent": "ethanol", + "volume": 2.0, + "amount": "organic compound 0.5g", + "temp": 40.0, + "time": 600.0, + "stir_speed": 400.0 + } + ] + """ + action_sequence = [] + + for i, step in enumerate(dissolve_steps): + print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤") + + # 生成单个溶解步骤的协议 + dissolve_actions = generate_dissolve_protocol( + G=G, + vessel=vessel, + solvent=step.get('solvent'), + volume=step.get('volume', 0.0), + amount=step.get('amount', ''), + temp=step.get('temp', 25.0), + time=step.get('time', 0.0), + stir_speed=step.get('stir_speed', 300.0) + ) + + action_sequence.extend(dissolve_actions) + + # 在步骤之间加入等待时间 + if i < len(dissolve_steps) - 1: # 不是最后一个步骤 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10} + }) + + print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 测试函数 +def test_dissolve_protocol(): + """测试溶解协议的示例""" + print("=== DISSOLVE PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index 9cde400f..96e057d3 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,143 +1,437 @@ import numpy as np import networkx as nx +from typing import List, Dict, Any, Optional +from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol + + +def find_gas_source(G: nx.DiGraph, gas: str) -> str: + """根据气体名称查找对应的气源""" + # 按照命名规则查找气源 + gas_source_patterns = [ + f"gas_source_{gas}", + f"gas_{gas}", + f"flask_{gas}", + f"{gas}_source" + ] + + for pattern in gas_source_patterns: + if pattern in G.nodes(): + return pattern + + # 模糊匹配 + for node in G.nodes(): + node_class = G.nodes[node].get('class', '') or '' + if 'gas_source' in node_class and gas.lower() in node.lower(): + return node + if node.startswith('flask_') and gas.lower() in node.lower(): + return node + + # 查找所有可用的气源 + available_gas_sources = [ + node for node in G.nodes() + if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source') + or ('gas' in node and 'source' in node) + or (node.startswith('flask_') and any(g in node.lower() for g in ['air', 'nitrogen', 'argon', 'vacuum']))) + ] + + raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}") + + +def find_vacuum_pump(G: nx.DiGraph) -> str: + """查找真空泵设备""" + vacuum_pumps = [ + node for node in G.nodes() + if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump') + or 'vacuum_pump' in node + or 'vacuum' in (G.nodes[node].get('class') or '')) + ] + + if not vacuum_pumps: + raise ValueError("系统中未找到真空泵设备") + + return vacuum_pumps[0] + + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找与指定容器相连的搅拌器""" + stirrer_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + return stirrer + + return stirrer_nodes[0] if stirrer_nodes else None + + +def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]: + """查找与指定设备相关联的电磁阀""" + solenoid_valves = [ + node for node in G.nodes() + if ('solenoid' in (G.nodes[node].get('class') or '').lower() + or 'solenoid_valve' in node) + ] + + # 通过网络连接查找直接相连的电磁阀 + for solenoid in solenoid_valves: + if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id): + return solenoid + + # 通过命名规则查找关联的电磁阀 + device_type = "" + if 'vacuum' in device_id.lower(): + device_type = "vacuum" + elif 'gas' in device_id.lower(): + device_type = "gas" + + if device_type: + for solenoid in solenoid_valves: + if device_type in solenoid.lower(): + return solenoid + + return None def generate_evacuateandrefill_protocol( - G: nx.DiGraph, - vessel: str, - gas: str, + G: nx.DiGraph, + vessel: str, + gas: str, repeats: int = 1 -) -> list[dict]: +) -> List[Dict[str, Any]]: """ - 生成泵操作的动作序列。 + 生成抽真空和充气操作的动作序列 - :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 - :param from_vessel: 容器A - :param to_vessel: 容器B - :param volume: 转移的体积 - :param flowrate: 最终注入容器B时的流速 - :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) - :return: 泵操作的动作序列 + **修复版本**: 正确调用 pump_protocol 并处理异常 """ + action_sequence = [] - # 生成电磁阀、真空泵、气源操作的动作序列 - vacuum_action_sequence = [] - nodes = G.nodes(data=True) + # 参数设置 - 关键修复:减小体积避免超出泵容量 + VACUUM_VOLUME = 20.0 # 减小抽真空体积 + REFILL_VOLUME = 20.0 # 减小充气体积 + PUMP_FLOW_RATE = 2.5 # 降低流速 + STIR_SPEED = 300.0 - # 找到和 vessel 相连的电磁阀和真空泵、气源 - vacuum_backbone = {"vessel": vessel} + print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}") - for neighbor in G.neighbors(vessel): - if nodes[neighbor]["class"].startswith("solenoid_valve"): - for neighbor2 in G.neighbors(neighbor): - if neighbor2 == vessel: - continue - if nodes[neighbor2]["class"].startswith("vacuum_pump"): - vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2}) - break - elif nodes[neighbor2]["class"].startswith("gas_source"): - vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2}) - break - # 判断是否设备齐全 - if len(vacuum_backbone) < 5: - print(f"\n\n\n{vacuum_backbone}\n\n\n") - raise ValueError("Not all devices are connected to the vessel.") + # 1. 验证设备存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - # 生成操作的动作序列 - for i in range(repeats): - # 打开真空泵阀门、关闭气源阀门 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["vacuum_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "OPEN" - } - }, - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - } - ]) + # 2. 查找设备 + try: + vacuum_pump = find_vacuum_pump(G) + vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump) + gas_source = find_gas_source(G, gas) + gas_solenoid = find_associated_solenoid_valve(G, gas_source) + stirrer_id = find_connected_stirrer(G, vessel) - # 打开真空泵、关闭气源 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["pump"], - "action_name": "set_status", - "action_kwargs": { - "command": "ON" - } - }, - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "command": "OFF" - } - } - ]) - vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + print(f"EVACUATE_REFILL: 找到设备") + print(f" - 真空泵: {vacuum_pump}") + print(f" - 气源: {gas_source}") + print(f" - 真空电磁阀: {vacuum_solenoid}") + print(f" - 气源电磁阀: {gas_solenoid}") + print(f" - 搅拌器: {stirrer_id}") - # 关闭真空泵阀门、打开气源阀门 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["vacuum_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - }, - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "OPEN" - } - } - ]) + except ValueError as e: + raise ValueError(f"设备查找失败: {str(e)}") + + # 3. **关键修复**: 验证路径存在性 + try: + # 验证抽真空路径 + vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) + print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}") - # 关闭真空泵、打开气源 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["pump"], - "action_name": "set_status", - "action_kwargs": { - "command": "OFF" - } - }, - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "command": "ON" - } + # 验证充气路径 + gas_path = nx.shortest_path(G, source=gas_source, target=vessel) + print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}") + + # **新增**: 检查路径中的边数据 + for i in range(len(vacuum_path) - 1): + nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1] + edge_data = G.get_edge_data(nodeA, nodeB) + if not edge_data or 'port' not in edge_data: + raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") + print(f" 抽真空路径边 {nodeA} → {nodeB}: {edge_data}") + + for i in range(len(gas_path) - 1): + nodeA, nodeB = gas_path[i], gas_path[i + 1] + edge_data = G.get_edge_data(nodeA, nodeB) + if not edge_data or 'port' not in edge_data: + raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") + print(f" 充气路径边 {nodeA} → {nodeB}: {edge_data}") + + except nx.NetworkXNoPath as e: + raise ValueError(f"路径不存在: {str(e)}") + except Exception as e: + raise ValueError(f"路径验证失败: {str(e)}") + + # 4. 启动搅拌器 + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": STIR_SPEED, + "purpose": "抽真空充气操作前启动搅拌" } - ]) - vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + }) + + # 5. 执行多次抽真空-充气循环 + for cycle in range(repeats): + print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===") + + # ============ 抽真空阶段 ============ + print(f"EVACUATE_REFILL: 抽真空阶段开始") + + # 启动真空泵 + action_sequence.append({ + "device_id": vacuum_pump, + "action_name": "set_status", + "action_kwargs": {"string": "ON"} + }) + + # 开启真空电磁阀 + if vacuum_solenoid: + action_sequence.append({ + "device_id": vacuum_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "OPEN"} + }) + + # **关键修复**: 改进 pump_protocol 调用和错误处理 + print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel} → {vacuum_pump}") + print(f" - 体积: {VACUUM_VOLUME} mL") + print(f" - 流速: {PUMP_FLOW_RATE} mL/s") + + try: + vacuum_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=vessel, + to_vessel=vacuum_pump, + volume=VACUUM_VOLUME, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=PUMP_FLOW_RATE, + transfer_flowrate=PUMP_FLOW_RATE + ) + + if vacuum_transfer_actions: + action_sequence.extend(vacuum_transfer_actions) + print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作") + else: + print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列") + # **修复**: 添加手动泵动作作为备选 + action_sequence.extend([ + { + "device_id": "multiway_valve_1", + "action_name": "set_valve_position", + "action_kwargs": {"command": "5"} # 连接到反应器 + }, + { + "device_id": "transfer_pump_1", + "action_name": "set_position", + "action_kwargs": { + "position": VACUUM_VOLUME, + "max_velocity": PUMP_FLOW_RATE + } + } + ]) + print(f"EVACUATE_REFILL: 使用备选手动泵动作") + + except Exception as e: + print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}") + import traceback + print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") + + # **修复**: 添加手动动作而不是忽略错误 + print(f"EVACUATE_REFILL: 使用手动备选方案") + action_sequence.extend([ + { + "device_id": "multiway_valve_1", + "action_name": "set_valve_position", + "action_kwargs": {"command": "5"} # 反应器端口 + }, + { + "device_id": "transfer_pump_1", + "action_name": "set_position", + "action_kwargs": { + "position": VACUUM_VOLUME, + "max_velocity": PUMP_FLOW_RATE + } + } + ]) + + # 关闭真空电磁阀 + if vacuum_solenoid: + action_sequence.append({ + "device_id": vacuum_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "CLOSED"} + }) + + # 关闭真空泵 + action_sequence.append({ + "device_id": vacuum_pump, + "action_name": "set_status", + "action_kwargs": {"string": "OFF"} + }) + + # ============ 充气阶段 ============ + print(f"EVACUATE_REFILL: 充气阶段开始") + + # 启动气源 + action_sequence.append({ + "device_id": gas_source, + "action_name": "set_status", + "action_kwargs": {"string": "ON"} + }) + + # 开启气源电磁阀 + if gas_solenoid: + action_sequence.append({ + "device_id": gas_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "OPEN"} + }) + + # **关键修复**: 改进充气 pump_protocol 调用 + print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source} → {vessel}") + + try: + gas_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=gas_source, + to_vessel=vessel, + volume=REFILL_VOLUME, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=PUMP_FLOW_RATE, + transfer_flowrate=PUMP_FLOW_RATE + ) + + if gas_transfer_actions: + action_sequence.extend(gas_transfer_actions) + print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作") + else: + print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列") + # **修复**: 添加手动充气动作 + action_sequence.extend([ + { + "device_id": "multiway_valve_2", + "action_name": "set_valve_position", + "action_kwargs": {"command": "8"} # 氮气端口 + }, + { + "device_id": "transfer_pump_2", + "action_name": "set_position", + "action_kwargs": { + "position": REFILL_VOLUME, + "max_velocity": PUMP_FLOW_RATE + } + }, + { + "device_id": "multiway_valve_2", + "action_name": "set_valve_position", + "action_kwargs": {"command": "5"} # 反应器端口 + }, + { + "device_id": "transfer_pump_2", + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": PUMP_FLOW_RATE + } + } + ]) + + except Exception as e: + print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}") + import traceback + print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") + + # **修复**: 使用手动充气动作 + print(f"EVACUATE_REFILL: 使用手动充气方案") + action_sequence.extend([ + { + "device_id": "multiway_valve_2", + "action_name": "set_valve_position", + "action_kwargs": {"command": "8"} # 连接气源 + }, + { + "device_id": "transfer_pump_2", + "action_name": "set_position", + "action_kwargs": { + "position": REFILL_VOLUME, + "max_velocity": PUMP_FLOW_RATE + } + }, + { + "device_id": "multiway_valve_2", + "action_name": "set_valve_position", + "action_kwargs": {"command": "5"} # 连接反应器 + }, + { + "device_id": "transfer_pump_2", + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": PUMP_FLOW_RATE + } + } + ]) + + # 关闭气源电磁阀 + if gas_solenoid: + action_sequence.append({ + "device_id": gas_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "CLOSED"} + }) # 关闭气源 - vacuum_action_sequence.append( - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "command": "OFF" - } - } - ) + action_sequence.append({ + "device_id": gas_source, + "action_name": "set_status", + "action_kwargs": {"string": "OFF"} + }) - # 关闭阀门 - vacuum_action_sequence.append( - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - } - ) - return vacuum_action_sequence + # 等待下一次循环 + if cycle < repeats - 1: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 2.0} + }) + + # 停止搅拌器 + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": {"vessel": vessel} + }) + + print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 测试函数 +def test_evacuateandrefill_protocol(): + """测试抽真空充气协议""" + print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol_old.py b/unilabos/compile/evacuateandrefill_protocol_old.py new file mode 100644 index 00000000..9f891ea2 --- /dev/null +++ b/unilabos/compile/evacuateandrefill_protocol_old.py @@ -0,0 +1,143 @@ +# import numpy as np +# import networkx as nx + + +# def generate_evacuateandrefill_protocol( +# G: nx.DiGraph, +# vessel: str, +# gas: str, +# repeats: int = 1 +# ) -> list[dict]: +# """ +# 生成泵操作的动作序列。 + +# :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 +# :param from_vessel: 容器A +# :param to_vessel: 容器B +# :param volume: 转移的体积 +# :param flowrate: 最终注入容器B时的流速 +# :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) +# :return: 泵操作的动作序列 +# """ + +# # 生成电磁阀、真空泵、气源操作的动作序列 +# vacuum_action_sequence = [] +# nodes = G.nodes(data=True) + +# # 找到和 vessel 相连的电磁阀和真空泵、气源 +# vacuum_backbone = {"vessel": vessel} + +# for neighbor in G.neighbors(vessel): +# if nodes[neighbor]["class"].startswith("solenoid_valve"): +# for neighbor2 in G.neighbors(neighbor): +# if neighbor2 == vessel: +# continue +# if nodes[neighbor2]["class"].startswith("vacuum_pump"): +# vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2}) +# break +# elif nodes[neighbor2]["class"].startswith("gas_source"): +# vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2}) +# break +# # 判断是否设备齐全 +# if len(vacuum_backbone) < 5: +# print(f"\n\n\n{vacuum_backbone}\n\n\n") +# raise ValueError("Not all devices are connected to the vessel.") + +# # 生成操作的动作序列 +# for i in range(repeats): +# # 打开真空泵阀门、关闭气源阀门 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["vacuum_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "OPEN" +# } +# }, +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# } +# ]) + +# # 打开真空泵、关闭气源 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["pump"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "ON" +# } +# }, +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# } +# ]) +# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + +# # 关闭真空泵阀门、打开气源阀门 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["vacuum_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# }, +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "OPEN" +# } +# } +# ]) + +# # 关闭真空泵、打开气源 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["pump"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# }, +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "ON" +# } +# } +# ]) +# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + +# # 关闭气源 +# vacuum_action_sequence.append( +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# } +# ) + +# # 关闭阀门 +# vacuum_action_sequence.append( +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# } +# ) +# return vacuum_action_sequence diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 15af5e11..4cee78d9 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,81 +1,326 @@ -import numpy as np +from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """ + 获取容器中的液体体积 + """ + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict) and 'liquid_volume' in liquid: + total_volume += liquid['liquid_volume'] + + return total_volume + + +def find_rotavap_device(G: nx.DiGraph) -> str: + """查找旋转蒸发仪设备""" + rotavap_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_rotavap'] + + if rotavap_nodes: + return rotavap_nodes[0] + + raise ValueError("系统中未找到旋转蒸发仪设备") + + +def find_solvent_recovery_vessel(G: nx.DiGraph) -> str: + """查找溶剂回收容器""" + possible_names = [ + "flask_distillate", + "bottle_distillate", + "vessel_distillate", + "distillate", + "solvent_recovery", + "flask_solvent_recovery", + "collection_flask" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + # 如果找不到专门的回收容器,使用废液容器 + waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"] + for vessel_name in waste_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}") def generate_evaporate_protocol( - G: nx.DiGraph, + G: nx.DiGraph, vessel: str, - pressure: float, - temp: float, - time: float, - stir_speed: float -) -> list[dict]: + pressure: float = 0.1, + temp: float = 60.0, + time: float = 1800.0, + stir_speed: float = 100.0 +) -> List[Dict[str, Any]]: """ - Generate a protocol to evaporate a solution from a vessel. + 生成蒸发操作的协议序列 - :param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections. - :param vessel: Vessel to clean. - :param solvent: Solvent to clean vessel with. - :param volume: Volume of solvent to clean vessel with. - :param temp: Temperature to heat vessel to while cleaning. - :param repeats: Number of cleaning cycles to perform. - :return: List of actions to clean vessel. + 蒸发流程: + 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪 + 2. 蒸发操作:执行旋转蒸发 + 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器 + 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待蒸发溶液的容器名称 + pressure: 蒸发时的真空度 (bar),默认0.1 bar + temp: 蒸发时的加热温度 (°C),默认60°C + time: 蒸发时间 (秒),默认1800秒(30分钟) + stir_speed: 旋转速度 (RPM),默认100 RPM + + Returns: + List[Dict[str, Any]]: 蒸发操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0) """ + action_sequence = [] - # 生成泵操作的动作序列 - pump_action_sequence = [] - reactor_volume = 500.0 - transfer_flowrate = flowrate = 2.5 + print(f"EVAPORATE: 开始生成蒸发协议") + print(f" - 源容器: {vessel}") + print(f" - 真空度: {pressure} bar") + print(f" - 温度: {temp}°C") + print(f" - 时间: {time}s ({time/60:.1f}分钟)") + print(f" - 旋转速度: {stir_speed} RPM") - # 开启冷凝器 - pump_action_sequence.append({ - "device_id": "rotavap_chiller", - "action_name": "set_temperature", - "action_kwargs": { - "command": "-40" - } - }) - # TODO: 通过温度反馈改为 HeatChillToTemp,而非等待固定时间 - pump_action_sequence.append({ + # 验证源容器存在 + if vessel not in G.nodes(): + raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, vessel) + print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体") + + # 查找旋转蒸发仪 + try: + rotavap_id = find_rotavap_device(G) + print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}") + except ValueError as e: + raise ValueError(f"无法找到旋转蒸发仪: {str(e)}") + + # 查找旋转蒸发仪样品容器 + rotavap_vessel = None + possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"] + for rv in possible_rotavap_vessels: + if rv in G.nodes(): + rotavap_vessel = rv + break + + if not rotavap_vessel: + raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}") + + print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}") + + # 查找溶剂回收容器 + try: + distillate_vessel = find_solvent_recovery_vessel(G) + print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}") + except ValueError as e: + print(f"EVAPORATE: 警告 - {str(e)}") + distillate_vessel = None + + # === 简化的体积计算策略 === + if source_volume > 0: + # 如果能检测到液体体积,使用实际体积的大部分 + transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL + print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL") + else: + # 如果检测不到液体体积,默认转移一整瓶 250mL + transfer_volume = 250.0 + print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL") + + # === 第一步:将待蒸发溶液转移到旋转蒸发仪 === + print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}") + try: + transfer_to_rotavap_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=rotavap_vessel, + volume=transfer_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(transfer_to_rotavap_actions) + except Exception as e: + raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}") + + # 转移后等待 + wait_action = { "action_name": "wait", - "action_kwargs": { - "time": 1800 - } - }) + "action_kwargs": {"time": 10} + } + action_sequence.append(wait_action) - # 开启旋蒸真空泵、旋转,在液体转移后运行time时间 - pump_action_sequence.append({ - "device_id": "rotavap_controller", - "action_name": "set_pump_time", + # === 第二步:执行旋转蒸发 === + print(f"EVAPORATE: 执行旋转蒸发操作") + evaporate_action = { + "device_id": rotavap_id, + "action_name": "evaporate", "action_kwargs": { - "command": str(time + reactor_volume / flowrate * 3) + "vessel": rotavap_vessel, + "pressure": pressure, + "temp": temp, + "time": time, + "stir_speed": stir_speed } - }) - pump_action_sequence.append({ - "device_id": "rotavap_controller", - "action_name": "set_pump_time", - "action_kwargs": { - "command": str(time + reactor_volume / flowrate * 3) - } - }) + } + action_sequence.append(evaporate_action) - # 液体转入旋转蒸发器 - pump_action_sequence.append({ - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": vessel, - "to_vessel": "rotavap", - "volume": reactor_volume, - "time": reactor_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - }) - - pump_action_sequence.append({ + # 蒸发后等待系统稳定 + wait_action = { "action_name": "wait", - "action_kwargs": { - "time": time - } - }) - return pump_action_sequence + "action_kwargs": {"time": 30} + } + action_sequence.append(wait_action) + + # === 第三步:溶剂回收(如果有回收容器)=== + if distillate_vessel: + print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}") + try: + condenser_vessel = "rotavap_condenser" + if condenser_vessel in G.nodes(): + # 估算回收体积(约为转移体积的70% - 大部分溶剂被蒸发回收) + recovery_volume = transfer_volume * 0.7 + print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂") + + recovery_actions = generate_pump_protocol( + G=G, + from_vessel=condenser_vessel, + to_vessel=distillate_vessel, + volume=recovery_volume, + flowrate=3.0, + transfer_flowrate=3.0 + ) + action_sequence.extend(recovery_actions) + else: + print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收") + except Exception as e: + print(f"EVAPORATE: 溶剂回收失败: {str(e)}") + + # === 第四步:将浓缩物转移回原容器 === + print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}") + try: + # 估算浓缩物体积(约为转移体积的20% - 大部分溶剂已蒸发) + concentrate_volume = transfer_volume * 0.2 + print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL") + + transfer_back_actions = generate_pump_protocol( + G=G, + from_vessel=rotavap_vessel, + to_vessel=vessel, + volume=concentrate_volume, + flowrate=1.0, # 浓缩物可能粘稠,用较慢流速 + transfer_flowrate=1.0 + ) + action_sequence.extend(transfer_back_actions) + except Exception as e: + print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}") + + # === 第五步:清洗旋转蒸发仪 === + print(f"EVAPORATE: 清洗旋转蒸发仪") + try: + # 查找清洗溶剂 + cleaning_solvent = None + for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]: + if solvent in G.nodes(): + cleaning_solvent = solvent + break + + if cleaning_solvent and distillate_vessel: + # 用固定量溶剂清洗(不依赖检测体积) + cleaning_volume = 50.0 # 固定50mL清洗 + print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗") + + # 清洗溶剂加入 + cleaning_actions = generate_pump_protocol( + G=G, + from_vessel=cleaning_solvent, + to_vessel=rotavap_vessel, + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(cleaning_actions) + + # 将清洗液转移到废液/回收容器 + waste_actions = generate_pump_protocol( + G=G, + from_vessel=rotavap_vessel, + to_vessel=distillate_vessel, # 使用回收容器作为废液 + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(waste_actions) + + except Exception as e: + print(f"EVAPORATE: 清洗步骤失败: {str(e)}") + + print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作") + print(f"EVAPORATE: 蒸发协议生成完成") + print(f"EVAPORATE: 总处理体积: {transfer_volume} mL") + + return action_sequence + + +# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积 +def generate_quick_evaporate_protocol( + G: nx.DiGraph, + vessel: str, + temp: float = 40.0, + time: float = 900.0 # 15分钟 +) -> List[Dict[str, Any]]: + """快速蒸发:低温、短时间、整瓶处理""" + return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0) + + +def generate_gentle_evaporate_protocol( + G: nx.DiGraph, + vessel: str, + temp: float = 50.0, + time: float = 2700.0 # 45分钟 +) -> List[Dict[str, Any]]: + """温和蒸发:中等条件、较长时间、整瓶处理""" + return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0) + + +def generate_high_vacuum_evaporate_protocol( + G: nx.DiGraph, + vessel: str, + temp: float = 35.0, + time: float = 3600.0 # 1小时 +) -> List[Dict[str, Any]]: + """高真空蒸发:低温、高真空、长时间、整瓶处理""" + return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0) + + +def generate_standard_evaporate_protocol( + G: nx.DiGraph, + vessel: str +) -> List[Dict[str, Any]]: + """标准蒸发:常用参数、整瓶250mL处理""" + return generate_evaporate_protocol( + G=G, + vessel=vessel, + pressure=0.1, # 标准真空度 + temp=60.0, # 适中温度 + time=1800.0, # 30分钟 + stir_speed=100.0 # 适中旋转速度 + ) diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py new file mode 100644 index 00000000..7e3ca6b4 --- /dev/null +++ b/unilabos/compile/filter_protocol.py @@ -0,0 +1,304 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积""" + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict) and 'liquid_volume' in liquid: + total_volume += liquid['liquid_volume'] + + return total_volume + + +def find_filter_device(G: nx.DiGraph) -> str: + """查找过滤器设备""" + filter_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_filter'] + + if filter_nodes: + return filter_nodes[0] + + raise ValueError("系统中未找到过滤器设备") + + +def find_filter_vessel(G: nx.DiGraph) -> str: + """查找过滤器专用容器""" + possible_names = [ + "filter_vessel", # 标准过滤器容器 + "filtration_vessel", # 备选名称 + "vessel_filter", # 备选名称 + "filter_unit", # 备选名称 + "filter" # 简单名称 + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}") + + +def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: + """查找滤液收集容器""" + if filtrate_vessel and filtrate_vessel in G.nodes(): + return filtrate_vessel + + # 自动查找滤液容器 + possible_names = [ + "filtrate_vessel", + "collection_bottle_1", + "collection_bottle_2", + "waste_workup" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}") + + +def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: + """查找与指定容器相连的加热搅拌器""" + # 查找所有加热搅拌器节点 + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + # 检查哪个加热器与目标容器相连 + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + return heatchill + + # 如果没有直接连接,返回第一个可用的加热器 + if heatchill_nodes: + return heatchill_nodes[0] + + raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器") + + +def generate_filter_protocol( + G: nx.DiGraph, + vessel: str, + filtrate_vessel: str = "", + stir: bool = False, + stir_speed: float = 300.0, + temp: float = 25.0, + continue_heatchill: bool = False, + volume: float = 0.0 +) -> List[Dict[str, Any]]: + """ + 生成过滤操作的协议序列,复用 pump_protocol 的成熟算法 + + 过滤流程: + 1. 液体转移:将待过滤溶液从源容器转移到过滤器 + 2. 启动加热搅拌:设置温度和搅拌 + 3. 执行过滤:通过过滤器分离固液 + 4. (可选) 继续或停止加热搅拌 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待过滤溶液的容器名称 + filtrate_vessel: 滤液收集容器(可选,自动查找) + stir: 是否在过滤过程中搅拌 + stir_speed: 搅拌速度 (RPM) + temp: 过滤温度 (°C) + continue_heatchill: 过滤后是否继续加热搅拌 + volume: 预期过滤体积 (mL),0表示全部过滤 + + Returns: + List[Dict[str, Any]]: 过滤操作的动作序列 + """ + action_sequence = [] + + print(f"FILTER: 开始生成过滤协议") + print(f" - 源容器: {vessel}") + print(f" - 滤液容器: {filtrate_vessel}") + print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否") + print(f" - 过滤温度: {temp}°C") + print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部") + print(f" - 继续加热搅拌: {continue_heatchill}") + + # 验证源容器存在 + if vessel not in G.nodes(): + raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, vessel) + print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体") + + # 查找过滤器设备 + try: + filter_id = find_filter_device(G) + print(f"FILTER: 找到过滤器: {filter_id}") + except ValueError as e: + raise ValueError(f"无法找到过滤器: {str(e)}") + + # 查找过滤器容器 + try: + filter_vessel_id = find_filter_vessel(G) + print(f"FILTER: 找到过滤器容器: {filter_vessel_id}") + except ValueError as e: + raise ValueError(f"无法找到过滤器容器: {str(e)}") + + # 查找滤液收集容器 + try: + actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) + print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}") + except ValueError as e: + raise ValueError(f"无法找到滤液收集容器: {str(e)}") + + # 查找加热搅拌器(如果需要温度控制或搅拌) + heatchill_id = None + if temp != 25.0 or stir or continue_heatchill: + try: + heatchill_id = find_connected_heatchill(G, filter_vessel_id) + print(f"FILTER: 找到加热搅拌器: {heatchill_id}") + except ValueError as e: + print(f"FILTER: 警告 - {str(e)}") + + # === 简化的体积计算策略 === + if volume > 0: + transfer_volume = min(volume, source_volume if source_volume > 0 else volume) + print(f"FILTER: 指定过滤体积 {transfer_volume} mL") + elif source_volume > 0: + transfer_volume = source_volume * 0.9 # 90% + print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL") + else: + transfer_volume = 50.0 # 默认过滤量 + print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL") + + # === 第一步:启动加热搅拌器(在转移前预热) === + if heatchill_id and (temp != 25.0 or stir): + print(f"FILTER: 启动加热搅拌器,温度: {temp}°C,搅拌: {stir}") + + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": filter_vessel_id, + "temp": temp, + "purpose": f"过滤过程温度控制和搅拌" + } + } + action_sequence.append(heatchill_action) + + # 等待温度稳定 + if temp != 25.0: + wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": wait_time} + }) + + # === 第二步:将待过滤溶液转移到过滤器 === + print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}") + try: + # 使用成熟的 pump_protocol 算法进行液体转移 + transfer_to_filter_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=filter_vessel_id, + volume=transfer_volume, + flowrate=1.0, # 过滤转移用较慢速度,避免扰动 + transfer_flowrate=1.5 + ) + action_sequence.extend(transfer_to_filter_actions) + except Exception as e: + raise ValueError(f"无法将溶液转移到过滤器: {str(e)}") + + # 转移后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # === 第三步:执行过滤操作(完全按照 Filter.action 参数) === + print(f"FILTER: 执行过滤操作") + filter_action = { + "device_id": filter_id, + "action_name": "filter", + "action_kwargs": { + "vessel": filter_vessel_id, + "filtrate_vessel": actual_filtrate_vessel, + "stir": stir, + "stir_speed": stir_speed, + "temp": temp, + "continue_heatchill": continue_heatchill, + "volume": transfer_volume + } + } + action_sequence.append(filter_action) + + # 过滤后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10} + }) + + # === 第四步:如果不继续加热搅拌,停止加热器 === + if heatchill_id and not continue_heatchill and (temp != 25.0 or stir): + print(f"FILTER: 停止加热搅拌器") + + stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": filter_vessel_id + } + } + action_sequence.append(stop_action) + + print(f"FILTER: 生成了 {len(action_sequence)} 个动作") + print(f"FILTER: 过滤协议生成完成") + + return action_sequence + + +# 便捷函数:常用过滤方案 +def generate_gravity_filter_protocol( + G: nx.DiGraph, + vessel: str, + filtrate_vessel: str = "" +) -> List[Dict[str, Any]]: + """重力过滤:室温,无搅拌""" + return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0) + + +def generate_hot_filter_protocol( + G: nx.DiGraph, + vessel: str, + filtrate_vessel: str = "", + temp: float = 60.0 +) -> List[Dict[str, Any]]: + """热过滤:高温过滤,防止结晶析出""" + return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0) + + +def generate_stirred_filter_protocol( + G: nx.DiGraph, + vessel: str, + filtrate_vessel: str = "", + stir_speed: float = 200.0 +) -> List[Dict[str, Any]]: + """搅拌过滤:低速搅拌,防止滤饼堵塞""" + return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0) + + +def generate_hot_stirred_filter_protocol( + G: nx.DiGraph, + vessel: str, + filtrate_vessel: str = "", + temp: float = 60.0, + stir_speed: float = 300.0 +) -> List[Dict[str, Any]]: + """热搅拌过滤:高温搅拌过滤""" + return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0) \ No newline at end of file diff --git a/unilabos/compile/filter_through_protocol.py b/unilabos/compile/filter_through_protocol.py new file mode 100644 index 00000000..314dbc74 --- /dev/null +++ b/unilabos/compile/filter_through_protocol.py @@ -0,0 +1,387 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积""" + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict) and 'liquid_volume' in liquid: + total_volume += liquid['liquid_volume'] + + return total_volume + + +def find_filter_through_vessel(G: nx.DiGraph, filter_through: str) -> str: + """查找过滤介质容器""" + # 直接使用 filter_through 参数作为容器名称 + if filter_through in G.nodes(): + return filter_through + + # 尝试常见的过滤介质容器命名 + possible_names = [ + f"filter_{filter_through}", + f"{filter_through}_filter", + f"column_{filter_through}", + f"{filter_through}_column", + "filter_through_vessel", + "column_vessel", + "chromatography_column", + "filter_column" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到过滤介质容器 '{filter_through}'。尝试了以下名称: {[filter_through] + possible_names}") + + +def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str: + """查找洗脱溶剂容器""" + if not eluting_solvent: + return "" + + # 按照命名规则查找溶剂瓶 + solvent_vessel_id = f"flask_{eluting_solvent}" + + if solvent_vessel_id in G.nodes(): + return solvent_vessel_id + + # 如果直接匹配失败,尝试模糊匹配 + for node in G.nodes(): + if node.startswith('flask_') and eluting_solvent.lower() in node.lower(): + return node + + # 如果还是找不到,列出所有可用的溶剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + + +def generate_filter_through_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + filter_through: str, + eluting_solvent: str = "", + eluting_volume: float = 0.0, + eluting_repeats: int = 0, + residence_time: float = 0.0 +) -> List[Dict[str, Any]]: + """ + 生成通过过滤介质过滤的协议序列,复用 pump_protocol 的成熟算法 + + 过滤流程: + 1. 液体转移:将样品从源容器转移到过滤介质 + 2. 重力过滤:液体通过过滤介质自动流到目标容器 + 3. 洗脱操作:将洗脱溶剂通过过滤介质洗脱目标物质 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + from_vessel: 源容器的名称,即物质起始所在的容器 + to_vessel: 目标容器的名称,物质过滤后要到达的容器 + filter_through: 过滤时所通过的介质,如滤纸、柱子等 + eluting_solvent: 洗脱溶剂的名称,可选参数 + eluting_volume: 洗脱溶剂的体积,可选参数 + eluting_repeats: 洗脱操作的重复次数,默认为 0 + residence_time: 物质在过滤介质中的停留时间,可选参数 + + Returns: + List[Dict[str, Any]]: 过滤操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备或容器时 + + Examples: + filter_through_actions = generate_filter_through_protocol( + G, "reaction_mixture", "collection_bottle_1", "celite", "ethanol", 20.0, 2, 30.0 + ) + """ + action_sequence = [] + + print(f"FILTER_THROUGH: 开始生成通过过滤协议") + print(f" - 源容器: {from_vessel}") + print(f" - 目标容器: {to_vessel}") + print(f" - 过滤介质: {filter_through}") + print(f" - 洗脱溶剂: {eluting_solvent}") + print(f" - 洗脱体积: {eluting_volume} mL" if eluting_volume > 0 else " - 洗脱体积: 无") + print(f" - 洗脱重复次数: {eluting_repeats}") + print(f" - 停留时间: {residence_time}s" if residence_time > 0 else " - 停留时间: 无") + + # 验证源容器和目标容器存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中") + + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, from_vessel) + print(f"FILTER_THROUGH: 源容器 {from_vessel} 中有 {source_volume} mL 液体") + + # 查找过滤介质容器 + try: + filter_through_vessel = find_filter_through_vessel(G, filter_through) + print(f"FILTER_THROUGH: 找到过滤介质容器: {filter_through_vessel}") + except ValueError as e: + raise ValueError(f"无法找到过滤介质容器: {str(e)}") + + # 查找洗脱溶剂容器(如果需要) + eluting_vessel = "" + if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0: + try: + eluting_vessel = find_eluting_solvent_vessel(G, eluting_solvent) + print(f"FILTER_THROUGH: 找到洗脱溶剂容器: {eluting_vessel}") + except ValueError as e: + raise ValueError(f"无法找到洗脱溶剂容器: {str(e)}") + + # === 第一步:将样品从源容器转移到过滤介质 === + transfer_volume = source_volume if source_volume > 0 else 100.0 # 默认100mL + print(f"FILTER_THROUGH: 将 {transfer_volume} mL 样品从 {from_vessel} 转移到 {filter_through_vessel}") + + try: + # 使用成熟的 pump_protocol 算法进行液体转移 + sample_transfer_actions = generate_pump_protocol( + G=G, + from_vessel=from_vessel, + to_vessel=filter_through_vessel, + volume=transfer_volume, + flowrate=0.8, # 较慢的流速,避免冲击过滤介质 + transfer_flowrate=1.2 + ) + action_sequence.extend(sample_transfer_actions) + except Exception as e: + raise ValueError(f"无法将样品转移到过滤介质: {str(e)}") + + # === 第二步:等待样品通过过滤介质(停留时间) === + if residence_time > 0: + print(f"FILTER_THROUGH: 等待样品在过滤介质中停留 {residence_time}s") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": residence_time} + }) + else: + # 即使没有指定停留时间,也等待一段时间让液体通过 + default_wait_time = max(10, transfer_volume / 10) # 根据体积估算等待时间 + print(f"FILTER_THROUGH: 等待样品通过过滤介质 {default_wait_time}s") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": default_wait_time} + }) + + # === 第三步:洗脱操作(如果指定了洗脱参数) === + if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0 and eluting_vessel: + print(f"FILTER_THROUGH: 开始洗脱操作 - {eluting_repeats} 次,每次 {eluting_volume} mL {eluting_solvent}") + + for repeat_idx in range(eluting_repeats): + print(f"FILTER_THROUGH: 第 {repeat_idx + 1}/{eluting_repeats} 次洗脱") + + try: + # 将洗脱溶剂转移到过滤介质 + eluting_transfer_actions = generate_pump_protocol( + G=G, + from_vessel=eluting_vessel, + to_vessel=filter_through_vessel, + volume=eluting_volume, + flowrate=0.6, # 洗脱用更慢的流速 + transfer_flowrate=1.0 + ) + action_sequence.extend(eluting_transfer_actions) + except Exception as e: + raise ValueError(f"第 {repeat_idx + 1} 次洗脱转移失败: {str(e)}") + + # 等待洗脱溶剂通过过滤介质 + eluting_wait_time = max(30, eluting_volume / 5) # 根据洗脱体积估算等待时间 + print(f"FILTER_THROUGH: 等待第 {repeat_idx + 1} 次洗脱液通过 {eluting_wait_time}s") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": eluting_wait_time} + }) + + # 洗脱间隔等待 + if repeat_idx < eluting_repeats - 1: # 不是最后一次洗脱 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10} + }) + + # === 第四步:最终等待,确保所有液体完全通过 === + print(f"FILTER_THROUGH: 最终等待,确保所有液体完全通过过滤介质") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 20} + }) + + print(f"FILTER_THROUGH: 生成了 {len(action_sequence)} 个动作") + print(f"FILTER_THROUGH: 通过过滤协议生成完成") + print(f"FILTER_THROUGH: 样品从 {from_vessel} 通过 {filter_through} 到达 {to_vessel}") + if eluting_repeats > 0: + total_eluting_volume = eluting_volume * eluting_repeats + print(f"FILTER_THROUGH: 总洗脱体积: {total_eluting_volume} mL {eluting_solvent}") + + return action_sequence + + +# 便捷函数:常用过滤方案 +def generate_gravity_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "silica_gel" +) -> List[Dict[str, Any]]: + """重力柱层析:简单重力过滤,无洗脱""" + return generate_filter_through_protocol(G, from_vessel, to_vessel, column_material, "", 0.0, 0, 0.0) + + +def generate_celite_filter_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + wash_solvent: str = "ethanol", + wash_volume: float = 20.0 +) -> List[Dict[str, Any]]: + """硅藻土过滤:用于去除固体杂质""" + return generate_filter_through_protocol(G, from_vessel, to_vessel, "celite", wash_solvent, wash_volume, 1, 30.0) + + +def generate_column_chromatography_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "silica_gel", + eluting_solvent: str = "ethyl_acetate", + eluting_volume: float = 30.0, + eluting_repeats: int = 3 +) -> List[Dict[str, Any]]: + """柱层析:多次洗脱分离""" + return generate_filter_through_protocol( + G, from_vessel, to_vessel, column_material, eluting_solvent, eluting_volume, eluting_repeats, 60.0 + ) + + +def generate_solid_phase_extraction_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + spe_cartridge: str = "C18", + eluting_solvent: str = "methanol", + eluting_volume: float = 15.0, + eluting_repeats: int = 2 +) -> List[Dict[str, Any]]: + """固相萃取:C18柱或其他SPE柱""" + return generate_filter_through_protocol( + G, from_vessel, to_vessel, spe_cartridge, eluting_solvent, eluting_volume, eluting_repeats, 120.0 + ) + + +def generate_resin_filter_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + resin_type: str = "ion_exchange", + regeneration_solvent: str = "NaCl_solution", + regeneration_volume: float = 25.0 +) -> List[Dict[str, Any]]: + """树脂过滤:离子交换树脂或其他功能树脂""" + return generate_filter_through_protocol( + G, from_vessel, to_vessel, resin_type, regeneration_solvent, regeneration_volume, 1, 180.0 + ) + + +def generate_multi_step_purification_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + filter_steps: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + """ + 多步骤纯化:连续多个过滤介质 + + Args: + G: 网络图 + from_vessel: 源容器 + to_vessel: 最终目标容器 + filter_steps: 过滤步骤列表,每个元素包含过滤参数 + + Returns: + List[Dict[str, Any]]: 完整的动作序列 + + Example: + filter_steps = [ + { + "to_vessel": "intermediate_vessel_1", + "filter_through": "celite", + "eluting_solvent": "", + "eluting_volume": 0.0, + "eluting_repeats": 0, + "residence_time": 30.0 + }, + { + "from_vessel": "intermediate_vessel_1", + "to_vessel": "final_vessel", + "filter_through": "silica_gel", + "eluting_solvent": "ethyl_acetate", + "eluting_volume": 20.0, + "eluting_repeats": 2, + "residence_time": 60.0 + } + ] + """ + action_sequence = [] + + current_from_vessel = from_vessel + + for i, step in enumerate(filter_steps): + print(f"FILTER_THROUGH: 处理第 {i+1}/{len(filter_steps)} 个过滤步骤") + + # 使用步骤中指定的参数,或使用默认值 + step_from_vessel = step.get('from_vessel', current_from_vessel) + step_to_vessel = step.get('to_vessel', to_vessel if i == len(filter_steps) - 1 else f"intermediate_vessel_{i+1}") + + # 生成单个过滤步骤的协议 + step_actions = generate_filter_through_protocol( + G=G, + from_vessel=step_from_vessel, + to_vessel=step_to_vessel, + filter_through=step.get('filter_through', 'silica_gel'), + eluting_solvent=step.get('eluting_solvent', ''), + eluting_volume=step.get('eluting_volume', 0.0), + eluting_repeats=step.get('eluting_repeats', 0), + residence_time=step.get('residence_time', 0.0) + ) + + action_sequence.extend(step_actions) + + # 更新下一步的源容器 + current_from_vessel = step_to_vessel + + # 在步骤之间加入等待时间 + if i < len(filter_steps) - 1: # 不是最后一个步骤 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 15} + }) + + print(f"FILTER_THROUGH: 多步骤纯化协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 测试函数 +def test_filter_through_protocol(): + """测试通过过滤协议的示例""" + print("=== FILTER THROUGH PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_filter_through_protocol() \ No newline at end of file diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py new file mode 100644 index 00000000..5ce09928 --- /dev/null +++ b/unilabos/compile/heatchill_protocol.py @@ -0,0 +1,373 @@ +from typing import List, Dict, Any, Optional +import networkx as nx + + +def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: + """ + 查找与指定容器相连的加热/冷却设备 + """ + # 查找所有加热/冷却设备节点 + heatchill_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_heatchill'] + + # 检查哪个加热/冷却设备与目标容器相连(机械连接) + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + return heatchill + + # 如果没有直接连接,返回第一个可用的加热/冷却设备 + if heatchill_nodes: + return heatchill_nodes[0] + + raise ValueError("系统中未找到可用的加热/冷却设备") + + +def generate_heat_chill_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + time: float, + stir: bool = False, + stir_speed: float = 300.0, + purpose: str = "加热/冷却操作" +) -> List[Dict[str, Any]]: + """ + 生成加热/冷却操作的协议序列 - 带时间限制的完整操作 + """ + action_sequence = [] + + print(f"HEATCHILL: 开始生成加热/冷却协议") + print(f" - 容器: {vessel}") + print(f" - 目标温度: {temp}°C") + print(f" - 持续时间: {time}秒") + print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM") + print(f" - 目的: {purpose}") + + # 1. 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行加热/冷却操作 + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "time": time, + "stir": stir, + "stir_speed": stir_speed, + "status": "start" + } + } + + action_sequence.append(heatchill_action) + + print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +def generate_heat_chill_start_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + purpose: str = "开始加热/冷却" +) -> List[Dict[str, Any]]: + """ + 生成开始加热/冷却操作的协议序列 + """ + action_sequence = [] + + print(f"HEATCHILL_START: 开始生成加热/冷却启动协议") + print(f" - 容器: {vessel}") + print(f" - 目标温度: {temp}°C") + print(f" - 目的: {purpose}") + + # 1. 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行开始加热/冷却操作 + heatchill_start_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": purpose + } + } + + action_sequence.append(heatchill_start_action) + + print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +def generate_heat_chill_stop_protocol( + G: nx.DiGraph, + vessel: str +) -> List[Dict[str, Any]]: + """ + 生成停止加热/冷却操作的协议序列 + """ + action_sequence = [] + + print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议") + print(f" - 容器: {vessel}") + + # 1. 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行停止加热/冷却操作 + heatchill_stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + } + + action_sequence.append(heatchill_stop_action) + + print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +def generate_heat_chill_to_temp_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + active: bool = True, + continue_heatchill: bool = False, + stir: bool = False, + stir_speed: Optional[float] = None, + purpose: Optional[str] = None +) -> List[Dict[str, Any]]: + """ + 生成加热/冷却到指定温度的协议序列 - 智能温控协议 + + **关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件 + """ + action_sequence = [] + + # 设置默认值 + if stir_speed is None: + stir_speed = 300.0 + if purpose is None: + purpose = f"智能温控到 {temp}°C" + + print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议") + print(f" - 容器: {vessel}") + print(f" - 目标温度: {temp}°C") + print(f" - 主动控温: {active}") + print(f" - 达到温度后继续: {continue_heatchill}") + print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM") + print(f" - 目的: {purpose}") + + # 1. 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式) + if not active: + print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": { + "time": 10.0, + "purpose": f"等待容器 {vessel} 自然达到 {temp}°C" + } + }) + else: + if continue_heatchill: + # 持续模式:使用 heat_chill_start 基础动作 + print(f"HEATCHILL_TO_TEMP: 使用持续温控模式") + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", # ← 直接使用设备基础动作 + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"{purpose} (持续保温)" + } + }) + else: + # 一次性模式:使用 heat_chill 基础动作 + print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式") + estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) + print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}秒") + + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill", # ← 直接使用设备基础动作 + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "time": estimated_time, + "stir": stir, + "stir_speed": stir_speed, + "status": "start" + } + }) + + print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +# 扩展版本的加热/冷却协议,集成智能温控功能 +def generate_smart_heat_chill_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + time: float = 0.0, # 0表示自动估算 + active: bool = True, + continue_heatchill: bool = False, + stir: bool = False, + stir_speed: float = 300.0, + purpose: str = "智能加热/冷却" +) -> List[Dict[str, Any]]: + """ + 这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑, + 但使用现有的 Action 类型 + """ + # 如果时间为0,自动估算 + if time == 0.0: + estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) + time = estimated_time + + if continue_heatchill: + # 使用持续模式 + return generate_heat_chill_start_protocol(G, vessel, temp, purpose) + else: + # 使用定时模式 + return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose) + + +# 便捷函数 +def generate_heating_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + time: float = 300.0, + stir: bool = True, + stir_speed: float = 300.0 +) -> List[Dict[str, Any]]: + """生成加热协议的便捷函数""" + return generate_heat_chill_protocol( + G=G, vessel=vessel, temp=temp, time=time, + stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C" + ) + + +def generate_cooling_protocol( + G: nx.DiGraph, + vessel: str, + temp: float, + time: float = 600.0, + stir: bool = True, + stir_speed: float = 200.0 +) -> List[Dict[str, Any]]: + """生成冷却协议的便捷函数""" + return generate_heat_chill_protocol( + G=G, vessel=vessel, temp=temp, time=time, + stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C" + ) + + +# # 温度预设快捷函数 +# def generate_room_temp_protocol( +# G: nx.DiGraph, +# vessel: str, +# stir: bool = False +# ) -> List[Dict[str, Any]]: +# """返回室温的快捷函数""" +# return generate_heat_chill_to_temp_protocol( +# G=G, +# vessel=vessel, +# temp=25.0, +# active=True, +# continue_heatchill=False, +# stir=stir, +# purpose="冷却到室温" +# ) + + +# def generate_reflux_heating_protocol( +# G: nx.DiGraph, +# vessel: str, +# temp: float, +# time: float = 3600.0 # 1小时回流 +# ) -> List[Dict[str, Any]]: +# """回流加热的快捷函数""" +# return generate_heat_chill_protocol( +# G=G, +# vessel=vessel, +# temp=temp, +# time=time, +# stir=True, +# stir_speed=400.0, # 回流时较快搅拌 +# purpose=f"回流加热到 {temp}°C" +# ) + + +# def generate_ice_bath_protocol( +# G: nx.DiGraph, +# vessel: str, +# time: float = 600.0 # 10分钟冰浴 +# ) -> List[Dict[str, Any]]: +# """冰浴冷却的快捷函数""" +# return generate_heat_chill_protocol( +# G=G, +# vessel=vessel, +# temp=0.0, +# time=time, +# stir=True, +# stir_speed=150.0, # 冰浴时缓慢搅拌 +# purpose="冰浴冷却到 0°C" +# ) + + +# 测试函数 +def test_heatchill_protocol(): + """测试加热/冷却协议的示例""" + print("=== HEAT CHILL PROTOCOL 测试 ===") + print("完整的四个协议函数:") + print("1. generate_heat_chill_protocol - 带时间限制的完整操作") + print("2. generate_heat_chill_start_protocol - 持续加热/冷却") + print("3. generate_heat_chill_stop_protocol - 停止加热/冷却") + print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)") + print("测试完成") + + +if __name__ == "__main__": + test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index 60670286..cddb8632 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -2,17 +2,43 @@ import numpy as np import networkx as nx +def is_integrated_pump(node_name): + return "pump" in node_name and "valve" in node_name + + +def find_connected_pump(G, valve_node): + for neighbor in G.neighbors(valve_node): + node_class = G.nodes[neighbor].get("class") or "" # 防止 None + if "pump" in node_class: + return neighbor + raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点") + + +def build_pump_valve_maps(G, pump_backbone): + pumps_from_node = {} + valve_from_node = {} + for node in pump_backbone: + if is_integrated_pump(node): + pumps_from_node[node] = node + valve_from_node[node] = node + else: + pump_node = find_connected_pump(G, node) + pumps_from_node[node] = pump_node + valve_from_node[node] = node + return pumps_from_node, valve_from_node + + def generate_pump_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - flowrate: float = 0.5, - transfer_flowrate: float = 0, + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 0.5, + transfer_flowrate: float = 0, ) -> list[dict]: """ 生成泵操作的动作序列。 - + :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 :param from_vessel: 容器A :param to_vessel: 容器B @@ -21,7 +47,7 @@ def generate_pump_protocol( :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) :return: 泵操作的动作序列 """ - + # 生成泵操作的动作序列 pump_action_sequence = [] nodes = G.nodes(data=True) @@ -34,31 +60,33 @@ def generate_pump_protocol( pump_backbone = pump_backbone[1:] if not to_vessel.startswith("pump"): pump_backbone = pump_backbone[:-1] - + if transfer_flowrate == 0: transfer_flowrate = flowrate - - min_transfer_volume = min([nodes[pump]["max_volume"] for pump in pump_backbone]) + + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + + min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) repeats = int(np.ceil(volume / min_transfer_volume)) if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") - + volume_left = volume - + # 生成泵操作的动作序列 for i in range(repeats): # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 if not from_vessel.startswith("pump"): pump_action_sequence.extend([ { - "device_id": pump_backbone[0], + "device_id": valve_from_node[pump_backbone[0]], "action_name": "set_valve_position", "action_kwargs": { "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] } }, { - "device_id": pump_backbone[0], + "device_id": pumps_from_node[pump_backbone[0]], "action_name": "set_position", "action_kwargs": { "position": float(min(volume_left, min_transfer_volume)), @@ -67,57 +95,57 @@ def generate_pump_protocol( } ]) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) - for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]): + for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): # 相邻两泵同时切换阀门至连通位置 pump_action_sequence.append([ - { - "device_id": pumpA, - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pumpA, pumpB)["port"][pumpA] + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA] + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB], + } } - }, - { - "device_id": pumpB, - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pumpB, pumpA)["port"][pumpB], - } - } ]) # 相邻两泵液体转移:泵A排出液体,泵B吸入液体 pump_action_sequence.append([ - { - "device_id": pumpA, - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": transfer_flowrate + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(min(volume_left, min_transfer_volume)), + "max_velocity": transfer_flowrate + } } - }, - { - "device_id": pumpB, - "action_name": "set_position", - "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), - "max_velocity": transfer_flowrate - } - } ]) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) - + if not to_vessel.startswith("pump"): # 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B pump_action_sequence.extend([ { - "device_id": pump_backbone[-1], + "device_id": valve_from_node[pump_backbone[-1]], "action_name": "set_valve_position", "action_kwargs": { "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] } }, { - "device_id": pump_backbone[-1], + "device_id": pumps_from_node[pump_backbone[-1]], "action_name": "set_position", "action_kwargs": { "position": 0.0, @@ -126,30 +154,30 @@ def generate_pump_protocol( } ]) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) - + volume_left -= min_transfer_volume return pump_action_sequence # Pump protocol compilation def generate_pump_protocol_with_rinsing( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - amount: str = "", - time: float = 0, - viscous: bool = False, - rinsing_solvent: str = "air", - rinsing_volume: float = 5.0, - rinsing_repeats: int = 2, - solid: bool = False, - flowrate: float = 2.5, - transfer_flowrate: float = 0.5, + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + amount: str = "", + time: float = 0, + viscous: bool = False, + rinsing_solvent: str = "air", + rinsing_volume: float = 5.0, + rinsing_repeats: int = 2, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, ) -> list[dict]: """ Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph. - + Args: G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 from_vessel (str): The name of the vessel to transfer from. @@ -164,50 +192,64 @@ def generate_pump_protocol_with_rinsing( solid (bool, optional): Indicates if the transfer involves a solid (default is False). flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速 transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同) - + Returns: list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列. - + Raises: AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats. - + Examples: pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") """ air_vessel = "flask_air" waste_vessel = f"waste_workup" - + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) pump_backbone = shortest_path[1: -1] nodes = G.nodes(data=True) - min_transfer_volume = float(min([nodes[pump]["max_volume"] for pump in pump_backbone])) + + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + + min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) if time != 0: flowrate = transfer_flowrate = volume / time - + pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) - if rinsing_solvent != "air": + if rinsing_solvent != "air" and rinsing_solvent != "": if "," in rinsing_solvent: rinsing_solvents = rinsing_solvent.split(",") - assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." + assert len( + rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." else: rinsing_solvents = [rinsing_solvent] * rinsing_repeats - + for rinsing_solvent in rinsing_solvents: solvent_vessel = f"flask_{rinsing_solvent}" # 清洗泵 pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, + transfer_flowrate) + + generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, + transfer_flowrate) + + generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, + transfer_flowrate) ) # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 if rinsing_solvent == rinsing_solvents[0]: - pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) - pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) - + pump_action_sequence.extend( + generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) + pump_action_sequence.extend( + generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) + pump_action_sequence.extend( + generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) + pump_action_sequence.extend( + generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) + if rinsing_solvent != "": + pump_action_sequence.extend( + generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + pump_action_sequence.extend( + generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + return pump_action_sequence # End Protocols diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py new file mode 100644 index 00000000..f6b9214f --- /dev/null +++ b/unilabos/compile/run_column_protocol.py @@ -0,0 +1,312 @@ +from typing import List, Dict, Any +import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积""" + if vessel not in G.nodes(): + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + liquids = vessel_data.get('liquid', []) + + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict): + # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) + volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) + total_volume += volume + + return total_volume + + +def find_column_device(G: nx.DiGraph, column: str) -> str: + """查找柱层析设备""" + # 首先检查是否有虚拟柱设备 + column_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_column'] + + if column_nodes: + return column_nodes[0] + + # 如果没有虚拟柱设备,抛出异常 + raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备") + + +def find_column_vessel(G: nx.DiGraph, column: str) -> str: + """查找柱容器""" + # 直接使用 column 参数作为容器名称 + if column in G.nodes(): + return column + + # 尝试常见的柱容器命名规则 + possible_names = [ + f"column_{column}", + f"{column}_column", + f"vessel_{column}", + f"{column}_vessel", + "column_vessel", + "chromatography_column", + "silica_column", + "preparative_column" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}") + + +def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str: + """查找洗脱溶剂容器""" + if not eluting_solvent: + return "" + + # 按照命名规则查找溶剂瓶 + solvent_vessel_id = f"flask_{eluting_solvent}" + + if solvent_vessel_id in G.nodes(): + return solvent_vessel_id + + # 如果直接匹配失败,尝试模糊匹配 + for node in G.nodes(): + if node.startswith('flask_') and eluting_solvent.lower() in node.lower(): + return node + + # 如果还是找不到,列出所有可用的溶剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + + +def generate_run_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column: str +) -> List[Dict[str, Any]]: + """ + 生成柱层析分离的协议序列 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + from_vessel: 源容器的名称,即样品起始所在的容器 + to_vessel: 目标容器的名称,分离后的样品要到达的容器 + column: 所使用的柱子的名称 + + Returns: + List[Dict[str, Any]]: 柱层析分离操作的动作序列 + """ + action_sequence = [] + + print(f"RUN_COLUMN: 开始生成柱层析协议") + print(f" - 源容器: {from_vessel}") + print(f" - 目标容器: {to_vessel}") + print(f" - 柱子: {column}") + + # 验证源容器和目标容器存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中") + + # 查找柱层析设备 + column_device_id = None + column_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_column'] + + if column_nodes: + column_device_id = column_nodes[0] + print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}") + else: + print(f"RUN_COLUMN: 警告 - 未找到柱层析设备") + + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, from_vessel) + print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体") + + # === 第一步:样品转移到柱子(如果柱子是容器) === + if column in G.nodes() and G.nodes[column].get('type') == 'container': + print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}") + + try: + sample_transfer_actions = generate_pump_protocol( + G=G, + from_vessel=from_vessel, + to_vessel=column, + volume=source_volume if source_volume > 0 else 100.0, + flowrate=2.0 + ) + action_sequence.extend(sample_transfer_actions) + except Exception as e: + print(f"RUN_COLUMN: 样品转移失败: {str(e)}") + + # === 第二步:使用柱层析设备执行分离 === + if column_device_id: + print(f"RUN_COLUMN: 使用柱层析设备执行分离") + + column_separation_action = { + "device_id": column_device_id, + "action_name": "run_column", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "column": column + } + } + action_sequence.append(column_separation_action) + + # 等待柱层析设备完成分离 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 60} + }) + + # === 第三步:从柱子转移到目标容器(如果需要) === + if column in G.nodes() and column != to_vessel: + print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}") + + try: + product_transfer_actions = generate_pump_protocol( + G=G, + from_vessel=column, + to_vessel=to_vessel, + volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失 + flowrate=1.5 + ) + action_sequence.extend(product_transfer_actions) + except Exception as e: + print(f"RUN_COLUMN: 产物转移失败: {str(e)}") + + print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +# 便捷函数:常用柱层析方案 +def generate_flash_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "silica_gel", + mobile_phase: str = "ethyl_acetate", + mobile_phase_volume: float = 100.0 +) -> List[Dict[str, Any]]: + """快速柱层析:高流速分离""" + return generate_run_column_protocol( + G, from_vessel, to_vessel, column_material, + mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0 + ) + + +def generate_preparative_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "silica_gel", + equilibration_solvent: str = "hexane", + eluting_solvent: str = "ethyl_acetate", + eluting_volume: float = 50.0, + eluting_repeats: int = 3 +) -> List[Dict[str, Any]]: + """制备柱层析:带平衡和多次洗脱""" + return generate_run_column_protocol( + G, from_vessel, to_vessel, column_material, + eluting_solvent, eluting_volume, eluting_repeats, + equilibration_solvent, 30.0, 1.5 + ) + + +def generate_gradient_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "silica_gel", + gradient_solvents: List[str] = None, + gradient_volumes: List[float] = None +) -> List[Dict[str, Any]]: + """梯度洗脱柱层析:多种溶剂系统""" + if gradient_solvents is None: + gradient_solvents = ["hexane", "ethyl_acetate", "methanol"] + if gradient_volumes is None: + gradient_volumes = [50.0, 50.0, 30.0] + + action_sequence = [] + + # 每种溶剂单独执行一次柱层析 + for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)): + print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}") + + # 第一步使用源容器,后续步骤使用柱子作为源 + step_from_vessel = from_vessel if i == 0 else column_material + # 最后一步使用目标容器,其他步骤使用柱子作为目标 + step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material + + step_actions = generate_run_column_protocol( + G, step_from_vessel, step_to_vessel, column_material, + solvent, volume, 1, "", 0.0, 1.0 + ) + action_sequence.extend(step_actions) + + # 在梯度步骤之间加入等待时间 + if i < len(gradient_solvents) - 1: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 20} + }) + + return action_sequence + + +def generate_reverse_phase_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "C18", + aqueous_phase: str = "water", + organic_phase: str = "methanol", + gradient_ratio: float = 0.5 +) -> List[Dict[str, Any]]: + """反相柱层析:C18柱,水-有机相梯度""" + # 先用水相平衡 + equilibration_volume = 20.0 + # 然后用有机相洗脱 + eluting_volume = 30.0 * gradient_ratio + + return generate_run_column_protocol( + G, from_vessel, to_vessel, column_material, + organic_phase, eluting_volume, 2, + aqueous_phase, equilibration_volume, 0.8 + ) + + +def generate_ion_exchange_column_protocol( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + column_material: str = "ion_exchange", + buffer_solution: str = "buffer", + salt_solution: str = "NaCl_solution", + salt_volume: float = 40.0 +) -> List[Dict[str, Any]]: + """离子交换柱层析:缓冲液平衡,盐溶液洗脱""" + return generate_run_column_protocol( + G, from_vessel, to_vessel, column_material, + salt_solution, salt_volume, 1, + buffer_solution, 25.0, 0.5 + ) + + +# 测试函数 +def test_run_column_protocol(): + """测试柱层析协议的示例""" + print("=== RUN COLUMN PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_run_column_protocol() \ No newline at end of file diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py new file mode 100644 index 00000000..6fc865cd --- /dev/null +++ b/unilabos/compile/stir_protocol.py @@ -0,0 +1,166 @@ +from typing import List, Dict, Any +import networkx as nx + + +def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: + """ + 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备 + """ + # 查找所有搅拌设备节点 + stirrer_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + + if vessel: + # 检查哪个搅拌设备与目标容器相连(机械连接) + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + return stirrer + + # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备 + if stirrer_nodes: + return stirrer_nodes[0] + + raise ValueError("系统中未找到可用的搅拌设备") + + +def generate_stir_protocol( + G: nx.DiGraph, + stir_time: float, + stir_speed: float, + settling_time: float +) -> List[Dict[str, Any]]: + """ + 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 + """ + action_sequence = [] + + print(f"STIR: 开始生成搅拌协议") + print(f" - 搅拌时间: {stir_time}秒") + print(f" - 搅拌速度: {stir_speed} RPM") + print(f" - 沉降时间: {settling_time}秒") + + # 查找搅拌设备 + try: + stirrer_id = find_connected_stirrer(G) + print(f"STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") + + # 执行搅拌操作 + stir_action = { + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": stir_time, + "stir_speed": stir_speed, + "settling_time": settling_time + } + } + + action_sequence.append(stir_action) + + print(f"STIR: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +def generate_start_stir_protocol( + G: nx.DiGraph, + vessel: str, + stir_speed: float, + purpose: str +) -> List[Dict[str, Any]]: + """ + 生成开始搅拌操作的协议序列 - 持续搅拌 + """ + action_sequence = [] + + print(f"START_STIR: 开始生成启动搅拌协议") + print(f" - 容器: {vessel}") + print(f" - 搅拌速度: {stir_speed} RPM") + print(f" - 目的: {purpose}") + + # 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 查找搅拌设备 + try: + stirrer_id = find_connected_stirrer(G, vessel) + print(f"START_STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") + + # 执行开始搅拌操作 + start_stir_action = { + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": purpose + } + } + + action_sequence.append(start_stir_action) + + print(f"START_STIR: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +def generate_stop_stir_protocol( + G: nx.DiGraph, + vessel: str +) -> List[Dict[str, Any]]: + """ + 生成停止搅拌操作的协议序列 + """ + action_sequence = [] + + print(f"STOP_STIR: 开始生成停止搅拌协议") + print(f" - 容器: {vessel}") + + # 验证容器存在 + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 查找搅拌设备 + try: + stirrer_id = find_connected_stirrer(G, vessel) + print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") + + # 执行停止搅拌操作 + stop_stir_action = { + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + } + + action_sequence.append(stop_stir_action) + + print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +# 便捷函数 +def generate_fast_stir_protocol( + G: nx.DiGraph, + time: float = 300.0, + speed: float = 800.0, + settling: float = 60.0 +) -> List[Dict[str, Any]]: + """快速搅拌的便捷函数""" + return generate_stir_protocol(G, time, speed, settling) + + +def generate_gentle_stir_protocol( + G: nx.DiGraph, + time: float = 600.0, + speed: float = 200.0, + settling: float = 120.0 +) -> List[Dict[str, Any]]: + """温和搅拌的便捷函数""" + return generate_stir_protocol(G, time, speed, settling) \ No newline at end of file diff --git a/unilabos/compile/transfer_protocol.py b/unilabos/compile/transfer_protocol.py new file mode 100644 index 00000000..202b009f --- /dev/null +++ b/unilabos/compile/transfer_protocol.py @@ -0,0 +1,79 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_transfer_protocol( + G: nx.DiGraph, + 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 +) -> List[Dict[str, Any]]: + """ + 生成液体转移操作的协议序列 + + Args: + G: 有向图,节点为设备和容器 + from_vessel: 源容器 + to_vessel: 目标容器 + volume: 转移体积 (mL) + amount: 数量描述 (可选) + time: 转移时间 (秒,可选) + viscous: 是否为粘性液体 + rinsing_solvent: 冲洗溶剂 (可选) + rinsing_volume: 冲洗体积 (mL,可选) + rinsing_repeats: 冲洗重复次数 + solid: 是否涉及固体 + + Returns: + List[Dict[str, Any]]: 转移操作的动作序列 + + Raises: + ValueError: 当找不到合适的转移设备时抛出异常 + + Examples: + transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0) + """ + action_sequence = [] + + # 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备进行液体转移") + + # 使用第一个可用的泵 + pump_id = pump_nodes[0] + + # 验证容器是否存在 + if from_vessel not in G.nodes(): + raise ValueError(f"源容器 {from_vessel} 不存在于图中") + + if to_vessel not in G.nodes(): + raise ValueError(f"目标容器 {to_vessel} 不存在于图中") + + # 执行液体转移操作 - 参数完全匹配Transfer.action + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "volume": volume, + "amount": amount, + "time": time, + "viscous": viscous, + "rinsing_solvent": rinsing_solvent, + "rinsing_volume": rinsing_volume, + "rinsing_repeats": rinsing_repeats, + "solid": solid + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py new file mode 100644 index 00000000..a792b8f0 --- /dev/null +++ b/unilabos/compile/wash_solid_protocol.py @@ -0,0 +1,216 @@ +from typing import List, Dict, Any +import networkx as nx + +def generate_wash_solid_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + filtrate_vessel: str = "", + temp: float = 25.0, + stir: bool = False, + stir_speed: float = 0.0, + time: float = 0.0, + repeats: int = 1 +) -> List[Dict[str, Any]]: + """ + 生成固体清洗的协议序列 + + Args: + G: 有向图,节点为设备和容器 + vessel: 装有固体物质的容器名称 + solvent: 用于清洗固体的溶剂名称 + volume: 清洗溶剂的体积 + filtrate_vessel: 滤液要收集到的容器名称,可选参数 + temp: 清洗时的温度,可选参数 + stir: 是否在清洗过程中搅拌,默认为 False + stir_speed: 搅拌速度,可选参数 + time: 清洗的时间,可选参数 + repeats: 清洗操作的重复次数,默认为 1 + + Returns: + List[Dict[str, Any]]: 固体清洗操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + wash_solid_protocol = generate_wash_solid_protocol( + G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3 + ) + """ + action_sequence = [] + + # 验证容器是否存在 + if vessel not in G.nodes(): + raise ValueError(f"固体容器 {vessel} 不存在于图中") + + if filtrate_vessel and filtrate_vessel not in G.nodes(): + raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + + # 查找转移泵设备(用于添加溶剂和转移滤液) + pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not pump_nodes: + raise ValueError("没有找到可用的转移泵设备") + + pump_id = pump_nodes[0] + + # 查找加热设备(如果需要加热) + heatchill_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_heatchill'] + + heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + + # 查找搅拌设备(如果需要搅拌) + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + stirrer_id = stirrer_nodes[0] if stirrer_nodes else None + + # 查找过滤设备(用于分离固体和滤液) + filter_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_filter'] + + filter_id = filter_nodes[0] if filter_nodes else None + + # 查找溶剂容器 + solvent_vessel = f"flask_{solvent}" + if solvent_vessel not in G.nodes(): + # 如果没有找到特定溶剂容器,查找可用的源容器 + available_vessels = [node for node in G.nodes() + if node.startswith('flask_') and + G.nodes[node].get('type') == 'container'] + if available_vessels: + solvent_vessel = available_vessels[0] + else: + raise ValueError(f"没有找到溶剂容器 {solvent}") + + # 如果没有指定滤液容器,使用废液容器 + if not filtrate_vessel: + waste_vessels = [node for node in G.nodes() + if 'waste' in node.lower() and + G.nodes[node].get('type') == 'container'] + filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask" + + # 重复清洗操作 + for repeat in range(repeats): + repeat_num = repeat + 1 + + # 步骤1:如果需要加热,先设置温度 + if temp > 25.0 and heatchill_id: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"固体清洗 - 第 {repeat_num} 次" + } + }) + + # 步骤2:添加清洗溶剂到固体容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": solvent_vessel, + "to_vessel": vessel, + "volume": volume, + "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤3:如果需要搅拌,开始搅拌 + if stir and stir_speed > 0 and stirrer_id: + if time > 0: + # 定时搅拌 + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "stir_time": time, + "stir_speed": stir_speed, + "settling_time": 30.0 # 搅拌后静置30秒 + } + }) + else: + # 开始搅拌(需要手动停止) + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"固体清洗搅拌 - 第 {repeat_num} 次" + } + }) + + # 步骤4:如果指定了清洗时间但没有搅拌,等待清洗时间 + if time > 0 and (not stir or stir_speed == 0): + # 这里可以添加等待操作,暂时跳过 + pass + + # 步骤5:如果有搅拌且没有定时,停止搅拌 + if stir and stir_speed > 0 and time == 0 and stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": { + "vessel": vessel + } + }) + + # 步骤6:过滤分离固体和滤液 + if filter_id: + action_sequence.append({ + "device_id": filter_id, + "action_name": "filter_sample", + "action_kwargs": { + "vessel": vessel, + "filtrate_vessel": filtrate_vessel, + "stir": False, + "stir_speed": 0.0, + "temp": temp, + "continue_heatchill": temp > 25.0, + "volume": volume + } + }) + else: + # 没有专门的过滤设备,使用转移泵模拟过滤过程 + # 将滤液转移到滤液容器 + action_sequence.append({ + "device_id": pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": vessel, + "to_vessel": filtrate_vessel, + "volume": volume, + "amount": f"转移滤液 - 第 {repeat_num} 次清洗", + "time": 0.0, + "viscous": False, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 步骤7:如果加热了,停止加热(在最后一次清洗后) + if temp > 25.0 and heatchill_id and repeat_num == repeats: + action_sequence.append({ + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } + }) + + return action_sequence \ No newline at end of file diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 0cf999e6..32ebf682 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -10,8 +10,9 @@ from unilabos.utils import logger class BasicConfig: ENV = "pro" # 'test' config_path = "" - is_host_mode = True # 从registry.py移动过来 + is_host_mode = True slave_no_host = False # 是否跳过rclient.wait_for_service() + upload_registry = False machine_name = "undefined" vis_2d_enable = False diff --git a/unilabos/devices/mock/__init__.py b/unilabos/devices/mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/mock/mock_chiller.py b/unilabos/devices/mock/mock_chiller.py new file mode 100644 index 00000000..fbb823c9 --- /dev/null +++ b/unilabos/devices/mock/mock_chiller.py @@ -0,0 +1,177 @@ +import time +import threading + + +class MockChiller: + def __init__(self, port: str = "MOCK"): + self.port = port + self._current_temperature: float = 25.0 # 室温开始 + self._target_temperature: float = 25.0 + self._status: str = "Idle" + self._is_cooling: bool = False + self._is_heating: bool = False + self._vessel = "Unknown" + self._purpose = "Unknown" + + # 模拟温度变化的线程 + self._temperature_thread = None + self._running = True + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + @property + def current_temperature(self) -> float: + """当前温度 - 会被自动识别的设备属性""" + return self._current_temperature + + @property + def target_temperature(self) -> float: + """目标温度""" + return self._target_temperature + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def is_cooling(self) -> bool: + """是否正在冷却""" + return self._is_cooling + + @property + def is_heating(self) -> bool: + """是否正在加热""" + return self._is_heating + + @property + def vessel(self) -> str: + """当前操作的容器名称""" + return self._vessel + + @property + def purpose(self) -> str: + """当前操作目的""" + return self._purpose + + def heat_chill_start(self, vessel: str, temp: float, purpose: str): + """设置目标温度并记录容器和目的""" + self._vessel = str(vessel) + self._purpose = str(purpose) + self._target_temperature = float(temp) + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_cooling = False + self._is_heating = False + elif diff < 0: + self._status = "Cooling" + self._is_cooling = True + self._is_heating = False + else: + self._status = "Heating" + self._is_heating = True + self._is_cooling = False + + self._start_temperature_control() + return True + + def heat_chill_stop(self, vessel: str): + """停止加热/制冷""" + if vessel != self._vessel: + return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"} + + # 停止温度控制线程,锁定当前温度 + self._stop_temperature_control() + + # 更新状态 + self._status = "Stopped" + self._is_cooling = False + self._is_heating = False + + # 重新启动线程但保持温度 + self._running = True + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + return {"success": True, "status": self._status} + + def _start_temperature_control(self): + """启动温度控制线程""" + self._running = True + if self._temperature_thread is None or not self._temperature_thread.is_alive(): + self._temperature_thread = threading.Thread(target=self._temperature_control_loop) + self._temperature_thread.daemon = True + self._temperature_thread.start() + + def _stop_temperature_control(self): + """停止温度控制""" + self._running = False + if self._temperature_thread: + self._temperature_thread.join(timeout=1.0) + + def _temperature_control_loop(self): + """温度控制循环 - 模拟真实冷却器的温度变化""" + while self._running: + # 如果状态是 Stopped,不改变温度 + if self._status == "Stopped": + time.sleep(1.0) + continue + + temp_diff = self._target_temperature - self._current_temperature + + if abs(temp_diff) < 0.1: + self._status = "At Target Temperature" + self._is_cooling = False + self._is_heating = False + elif temp_diff < 0: + self._status = "Cooling" + self._is_cooling = True + self._is_heating = False + self._current_temperature -= 0.5 + else: + self._status = "Heating" + self._is_heating = True + self._is_cooling = False + self._current_temperature += 0.3 + + time.sleep(1.0) + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_temperature_control() + self._is_cooling = False + self._is_heating = False + + def get_status_info(self) -> dict: + """获取完整状态信息""" + return { + "current_temperature": self._current_temperature, + "target_temperature": self._target_temperature, + "status": self._status, + "is_cooling": self._is_cooling, + "is_heating": self._is_heating, + "vessel": self._vessel, + "purpose": self._purpose, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + chiller = MockChiller() + + # 测试基本功能 + print("启动冷却器测试...") + print(f"初始状态: {chiller.get_status_info()}") + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}") + + chiller.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_filter.py b/unilabos/devices/mock/mock_filter.py new file mode 100644 index 00000000..f54e41ed --- /dev/null +++ b/unilabos/devices/mock/mock_filter.py @@ -0,0 +1,235 @@ +import time +import threading + + +class MockFilter: + def __init__(self, port: str = "MOCK"): + # 基本参数初始化 + self.port = port + self._status: str = "Idle" + self._is_filtering: bool = False + + # 过滤性能参数 + self._flow_rate: float = 1.0 # 流速(L/min) + self._pressure_drop: float = 0.0 # 压降(Pa) + self._filter_life: float = 100.0 # 滤芯寿命(%) + + # 过滤操作参数 + self._vessel: str = "" # 源容器 + self._filtrate_vessel: str = "" # 目标容器 + self._stir: bool = False # 是否搅拌 + self._stir_speed: float = 0.0 # 搅拌速度 + self._temperature: float = 25.0 # 温度(℃) + self._continue_heatchill: bool = False # 是否继续加热/制冷 + self._target_volume: float = 0.0 # 目标过滤体积(L) + self._filtered_volume: float = 0.0 # 已过滤体积(L) + self._progress: float = 0.0 # 过滤进度(%) + + # 线程控制 + self._filter_thread = None + self._running = False + + @property + def status(self) -> str: + return self._status + + @property + def is_filtering(self) -> bool: + return self._is_filtering + + @property + def flow_rate(self) -> float: + return self._flow_rate + + @property + def pressure_drop(self) -> float: + return self._pressure_drop + + @property + def filter_life(self) -> float: + return self._filter_life + # 新增 property + @property + def vessel(self) -> str: + return self._vessel + + @property + def filtrate_vessel(self) -> str: + return self._filtrate_vessel + + @property + def filtered_volume(self) -> float: + return self._filtered_volume + + @property + def progress(self) -> float: + return self._progress + + @property + def stir(self) -> bool: + return self._stir + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def temperature(self) -> float: + return self._temperature + + @property + def continue_heatchill(self) -> bool: + return self._continue_heatchill + + @property + def target_volume(self) -> float: + return self._target_volume + + def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict: + """新的过滤操作""" + # 停止任何正在进行的过滤 + if self._is_filtering: + self.stop_filtering() + # 验证参数 + if volume <= 0: + return {"success": False, "message": "Target volume must be greater than 0"} + # 设置新的过滤参数 + self._vessel = vessel + self._filtrate_vessel = filtrate_vessel + self._stir = stir + self._stir_speed = stir_speed + self._temperature = temp + self._continue_heatchill = continue_heatchill + self._target_volume = volume + # 重置过滤状态 + self._filtered_volume = 0.0 + self._progress = 0.0 + self._status = "Starting Filter" + # 启动过滤过程 + self._flow_rate = 1.0 # 设置默认流速 + self._start_filter_process() + + return {"success": True, "message": "Filter started"} + + def stop_filtering(self): + """停止过滤""" + self._status = "Stopping Filter" + self._stop_filter_process() + self._flow_rate = 0.0 + self._is_filtering = False + self._status = "Stopped" + return True + + def replace_filter(self): + """更换滤芯""" + self._filter_life = 100.0 + self._status = "Filter Replaced" + return True + + def _start_filter_process(self): + """启动过滤过程线程""" + if not self._running: + self._running = True + self._is_filtering = True + self._filter_thread = threading.Thread(target=self._filter_loop) + self._filter_thread.daemon = True + self._filter_thread.start() + + def _stop_filter_process(self): + """停止过滤过程""" + self._running = False + if self._filter_thread: + self._filter_thread.join(timeout=1.0) + + def _filter_loop(self): + """过滤进程主循环""" + update_interval = 1.0 # 更新间隔(秒) + + while self._running and self._is_filtering: + try: + self._status = "Filtering" + + # 计算这一秒过滤的体积 (L/min -> L/s) + volume_increment = (self._flow_rate / 60.0) * update_interval + + # 更新已过滤体积 + self._filtered_volume += volume_increment + + # 更新进度 (避免除零错误) + if self._target_volume > 0: + self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0) + + # 更新滤芯寿命 (每过滤1L减少0.5%寿命) + self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5)) + + # 更新压降 (根据滤芯寿命和流速动态计算) + life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子 + flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速) + base_pressure = 100.0 # 基础压降 + # 压降随滤芯寿命降低而增加,随流速增加而增加 + self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor + + # 检查是否完成目标体积 + if self._target_volume > 0 and self._filtered_volume >= self._target_volume: + self._status = "Completed" + self._progress = 100.0 + self.stop_filtering() + break + + # 检查滤芯寿命 + if self._filter_life <= 10.0: + self._status = "Filter Needs Replacement" + + time.sleep(update_interval) + + except Exception as e: + print(f"Error in filter loop: {e}") + self.emergency_stop() + break + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_filter_process() + self._is_filtering = False + self._flow_rate = 0.0 + + def get_status_info(self) -> dict: + """扩展的状态信息""" + return { + "status": self._status, + "is_filtering": self._is_filtering, + "flow_rate": self._flow_rate, + "pressure_drop": self._pressure_drop, + "filter_life": self._filter_life, + "vessel": self._vessel, + "filtrate_vessel": self._filtrate_vessel, + "filtered_volume": self._filtered_volume, + "target_volume": self._target_volume, + "progress": self._progress, + "temperature": self._temperature, + "stir": self._stir, + "stir_speed": self._stir_speed + } + + +# 用于测试的主函数 +if __name__ == "__main__": + filter_device = MockFilter() + + # 测试基本功能 + print("启动过滤器测试...") + print(f"初始状态: {filter_device.get_status_info()}") + + + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print( + f"第{i+1}秒: " + f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}" + ) + + filter_device.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_heater.py b/unilabos/devices/mock/mock_heater.py new file mode 100644 index 00000000..47dd8d85 --- /dev/null +++ b/unilabos/devices/mock/mock_heater.py @@ -0,0 +1,247 @@ +import time +import threading + +class MockHeater: + def __init__(self, port: str = "MOCK"): + self.port = port + self._current_temperature: float = 25.0 # 室温开始 + self._target_temperature: float = 25.0 + self._status: str = "Idle" + self._is_heating: bool = False + self._heating_power: float = 0.0 # 加热功率百分比 0-100 + self._max_temperature: float = 300.0 # 最大加热温度 + + # 新增加的属性 + self._vessel: str = "Unknown" + self._purpose: str = "Unknown" + self._stir: bool = False + self._stir_speed: float = 0.0 + + # 模拟加热过程的线程 + self._heating_thread = None + self._running = True + self._heating_thread = threading.Thread(target=self._heating_control_loop) + self._heating_thread.daemon = True + self._heating_thread.start() + + @property + def current_temperature(self) -> float: + """当前温度 - 会被自动识别的设备属性""" + return self._current_temperature + + @property + def target_temperature(self) -> float: + """目标温度""" + return self._target_temperature + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def is_heating(self) -> bool: + """是否正在加热""" + return self._is_heating + + @property + def heating_power(self) -> float: + """加热功率百分比""" + return self._heating_power + + @property + def max_temperature(self) -> float: + """最大加热温度""" + return self._max_temperature + + @property + def vessel(self) -> str: + """当前操作的容器名称""" + return self._vessel + + @property + def purpose(self) -> str: + """操作目的""" + return self._purpose + + @property + def stir(self) -> bool: + """是否搅拌""" + return self._stir + + @property + def stir_speed(self) -> float: + """搅拌速度""" + return self._stir_speed + + def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict: + """开始加热/制冷过程""" + self._vessel = str(vessel) + self._purpose = str(purpose) + self._target_temperature = float(temp) + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + elif diff > 0: + self._status = "Heating" + self._is_heating = True + else: + self._status = "Cooling Down" + self._is_heating = False + + return {"success": True, "status": self._status} + + def heat_chill_stop(self, vessel: str) -> dict: + """停止加热/制冷""" + if vessel != self._vessel: + return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"} + + self._status = "Stopped" + self._is_heating = False + self._heating_power = 0.0 + + return {"success": True, "status": self._status} + + def heat_chill(self, vessel: str, temp: float, time: float, + stir: bool = False, stir_speed: float = 0.0, + purpose: str = "Unknown") -> dict: + """完整的加热/制冷控制""" + self._vessel = str(vessel) + self._target_temperature = float(temp) + self._purpose = str(purpose) + self._stir = stir + self._stir_speed = stir_speed + + diff = self._target_temperature - self._current_temperature + if abs(diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + elif diff > 0: + self._status = "Heating" + self._is_heating = True + else: + self._status = "Cooling Down" + self._is_heating = False + + return {"success": True, "status": self._status} + + def set_temperature(self, temperature: float): + """设置目标温度 - 需要在注册表添加的设备动作""" + try: + temperature = float(temperature) + except ValueError: + self._status = "Error: Invalid temperature value" + return False + + if temperature > self._max_temperature: + self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)" + return False + + self._target_temperature = temperature + self._status = "Setting Temperature" + + # 启动加热控制 + self._start_heating_control() + return True + + def set_heating_power(self, power: float): + """设置加热功率""" + try: + power = float(power) + except ValueError: + self._status = "Error: Invalid power value" + return False + + self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100% + return True + + def _start_heating_control(self): + """启动加热控制线程""" + if not self._running: + self._running = True + self._heating_thread = threading.Thread(target=self._heating_control_loop) + self._heating_thread.daemon = True + self._heating_thread.start() + + def _stop_heating_control(self): + """停止加热控制""" + self._running = False + if self._heating_thread: + self._heating_thread.join(timeout=1.0) + + def _heating_control_loop(self): + """加热控制循环""" + while self._running: + # 如果状态是 Stopped,不改变温度 + if self._status == "Stopped": + time.sleep(1.0) + continue + + temp_diff = self._target_temperature - self._current_temperature + + if abs(temp_diff) < 0.1: + self._status = "At Target Temperature" + self._is_heating = False + self._heating_power = 10.0 + elif temp_diff > 0: + self._status = "Heating" + self._is_heating = True + self._heating_power = min(100.0, abs(temp_diff) * 2) + self._current_temperature += 0.5 + else: + self._status = "Cooling Down" + self._is_heating = False + self._heating_power = 0.0 + self._current_temperature -= 0.2 + + time.sleep(1.0) + + def emergency_stop(self): + """紧急停止""" + self._status = "Emergency Stop" + self._stop_heating_control() + self._is_heating = False + self._heating_power = 0.0 + + def get_status_info(self) -> dict: + """获取完整状态信息""" + return { + "current_temperature": self._current_temperature, + "target_temperature": self._target_temperature, + "status": self._status, + "is_heating": self._is_heating, + "heating_power": self._heating_power, + "max_temperature": self._max_temperature, + "vessel": self._vessel, + "purpose": self._purpose, + "stir": self._stir, + "stir_speed": self._stir_speed + } + +# 用于测试的主函数 +if __name__ == "__main__": + heater = MockHeater() + + print("启动加热器测试...") + print(f"初始状态: {heater.get_status_info()}") + + # 设置目标温度为80度 + heater.set_temperature(80.0) + + # 模拟运行15秒 + try: + for i in range(15): + time.sleep(1) + status = heater.get_status_info() + print( + f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | " + f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}", + end="" + ) + except KeyboardInterrupt: + heater.emergency_stop() + print("\n测试被手动停止") + + print("\n测试完成") \ No newline at end of file diff --git a/unilabos/devices/mock/mock_pump.py b/unilabos/devices/mock/mock_pump.py new file mode 100644 index 00000000..43cbf007 --- /dev/null +++ b/unilabos/devices/mock/mock_pump.py @@ -0,0 +1,360 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockPump: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 设备基本状态属性 + self._current_device = "MockPump1" # 设备标识符 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused + + # 流量相关属性 + self._flow_rate: float = 0.0 # 当前流速 (mL/min) + self._target_flow_rate: float = 0.0 # 目标流速 (mL/min) + self._max_flow_rate: float = 100.0 # 最大流速 (mL/min) + self._total_volume: float = 0.0 # 累计流量 (mL) + + # 压力相关属性 + self._pressure: float = 0.0 # 当前压力 (bar) + self._max_pressure: float = 10.0 # 最大压力 (bar) + + # 运行控制线程 + self._pump_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 新增 PumpTransfer 相关属性 + self._from_vessel: str = "" + self._to_vessel: str = "" + self._transfer_volume: float = 0.0 + self._amount: str = "" + self._transfer_time: float = 0.0 + self._is_viscous: bool = False + self._rinsing_solvent: str = "" + self._rinsing_volume: float = 0.0 + self._rinsing_repeats: int = 0 + self._is_solid: bool = False + + # 时间追踪 + self._start_time: datetime = None + self._time_spent: timedelta = timedelta() + self._time_remaining: timedelta = timedelta() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def current_device(self) -> str: + """当前设备标识符""" + return self._current_device + + @property + def pump_state(self) -> str: + return self._pump_state + + @property + def flow_rate(self) -> float: + return self._flow_rate + + @property + def target_flow_rate(self) -> float: + return self._target_flow_rate + + @property + def pressure(self) -> float: + return self._pressure + + @property + def total_volume(self) -> float: + return self._total_volume + + @property + def max_flow_rate(self) -> float: + return self._max_flow_rate + + @property + def max_pressure(self) -> float: + return self._max_pressure + + # 添加新的属性访问器 + @property + def from_vessel(self) -> str: + return self._from_vessel + + @property + def to_vessel(self) -> str: + return self._to_vessel + + @property + def transfer_volume(self) -> float: + return self._transfer_volume + + @property + def amount(self) -> str: + return self._amount + + @property + def transfer_time(self) -> float: + return self._transfer_time + + @property + def is_viscous(self) -> bool: + return self._is_viscous + + @property + def rinsing_solvent(self) -> str: + return self._rinsing_solvent + + @property + def rinsing_volume(self) -> float: + return self._rinsing_volume + + @property + def rinsing_repeats(self) -> int: + return self._rinsing_repeats + + @property + def is_solid(self) -> bool: + return self._is_solid + + # 修改这两个属性装饰器 + @property + def time_spent(self) -> float: + """已用时间(秒)""" + if isinstance(self._time_spent, timedelta): + return self._time_spent.total_seconds() + return float(self._time_spent) + + @property + def time_remaining(self) -> float: + """剩余时间(秒)""" + if isinstance(self._time_remaining, timedelta): + return self._time_remaining.total_seconds() + return float(self._time_remaining) + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float, + amount: str = "", time: float = 0.0, viscous: bool = False, + rinsing_solvent: str = "", rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, solid: bool = False) -> dict: + """Execute pump transfer operation""" + # Stop any existing operation first + self._stop_pump_operation() + + # Set transfer parameters + self._from_vessel = from_vessel + self._to_vessel = to_vessel + self._transfer_volume = float(volume) + self._amount = amount + self._transfer_time = float(time) + self._is_viscous = viscous + self._rinsing_solvent = rinsing_solvent + self._rinsing_volume = float(rinsing_volume) + self._rinsing_repeats = int(rinsing_repeats) + self._is_solid = solid + + # Calculate flow rate + if self._transfer_time > 0 and self._transfer_volume > 0: + self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0 + else: + self._target_flow_rate = 10.0 if not self._is_viscous else 5.0 + + # Reset timers and counters + self._start_time = datetime.now() + self._time_spent = timedelta() + self._time_remaining = timedelta(seconds=self._transfer_time) + self._total_volume = 0.0 + self._flow_rate = 0.0 + + # Start pump operation + self._pump_state = "Running" + self._status = "Starting Transfer" + self._running = True + + # Start pump operation thread + self._pump_thread = threading.Thread(target=self._pump_operation_loop) + self._pump_thread.daemon = True + self._pump_thread.start() + + # Wait briefly to ensure thread starts + time.sleep(0.1) + + return { + "success": True, + "status": self._status, + "current_device": self._current_device, + "time_spent": 0.0, + "time_remaining": float(self._transfer_time) + } + + def pause_pump(self) -> str: + + if self._pump_state != "Running": + self._status = "Error: Pump not running" + return "Error" + + self._pump_state = "Paused" + self._status = "Pump Paused" + self._stop_pump_operation() + + return "Success" + + def resume_pump(self) -> str: + + if self._pump_state != "Paused": + self._status = "Error: Pump not paused" + return "Error" + + self._pump_state = "Running" + self._status = "Resuming Pump" + self._start_pump_operation() + + return "Success" + + def reset_volume_counter(self) -> str: + self._total_volume = 0.0 + self._status = "Volume counter reset" + return "Success" + + def emergency_stop(self) -> str: + self._status = "Emergency Stop" + self._pump_state = "Stopped" + self._stop_pump_operation() + self._flow_rate = 0.0 + self._pressure = 0.0 + self._target_flow_rate = 0.0 + + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_pump_operation(self): + with self._thread_lock: + if not self._running: + self._running = True + self._pump_thread = threading.Thread(target=self._pump_operation_loop) + self._pump_thread.daemon = True + self._pump_thread.start() + + def _stop_pump_operation(self): + with self._thread_lock: + self._running = False + if self._pump_thread and self._pump_thread.is_alive(): + self._pump_thread.join(timeout=2.0) + + def _pump_operation_loop(self): + """泵运行主循环""" + print("Pump operation loop started") # Debug print + + while self._running and self._pump_state == "Running": + try: + # Calculate flow rate adjustment + flow_diff = self._target_flow_rate - self._flow_rate + + # Adjust flow rate more aggressively (50% of difference) + adjustment = flow_diff * 0.5 + self._flow_rate += adjustment + + # Ensure flow rate is within bounds + self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate)) + + # Update status based on flow rate + if abs(flow_diff) < 0.1: + self._status = "Running at Target Flow Rate" + else: + self._status = "Adjusting Flow Rate" + + # Calculate volume increment + volume_increment = (self._flow_rate / 60.0) # mL/s + self._total_volume += volume_increment + + # Update time tracking + self._time_spent = datetime.now() - self._start_time + if self._transfer_time > 0: + remaining = self._transfer_time - self._time_spent.total_seconds() + self._time_remaining = timedelta(seconds=max(0, remaining)) + + # Check completion + if self._total_volume >= self._transfer_volume: + self._status = "Transfer Completed" + self._pump_state = "Stopped" + self._running = False + break + + # Update pressure + self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure + + print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print + + time.sleep(1.0) + + except Exception as e: + print(f"Error in pump operation: {str(e)}") + self._status = "Error in pump operation" + self._pump_state = "Stopped" + self._running = False + break + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "pump_state": self._pump_state, + "flow_rate": self._flow_rate, + "target_flow_rate": self._target_flow_rate, + "pressure": self._pressure, + "total_volume": self._total_volume, + "max_flow_rate": self._max_flow_rate, + "max_pressure": self._max_pressure, + "current_device": self._current_device, + "from_vessel": self._from_vessel, + "to_vessel": self._to_vessel, + "transfer_volume": self._transfer_volume, + "amount": self._amount, + "transfer_time": self._transfer_time, + "is_viscous": self._is_viscous, + "rinsing_solvent": self._rinsing_solvent, + "rinsing_volume": self._rinsing_volume, + "rinsing_repeats": self._rinsing_repeats, + "is_solid": self._is_solid, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + +# 用于测试的主函数 +if __name__ == "__main__": + pump = MockPump() + + # 测试基本功能 + print("启动泵设备测试...") + print(f"初始状态: {pump.get_status_info()}") + + # 设置流速并启动 + pump.set_flow_rate(50.0) + pump.start_pump() + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print(f"第{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}") + + # 测试方向切换 + print("切换泵方向...") + + + pump.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_rotavap.py b/unilabos/devices/mock/mock_rotavap.py new file mode 100644 index 00000000..9b2ea914 --- /dev/null +++ b/unilabos/devices/mock/mock_rotavap.py @@ -0,0 +1,390 @@ +import time +import threading +import json + + +class MockRotavap: + """ + 模拟旋转蒸发器设备类 + + 这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、 + 真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockRotavap实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + + # 旋转相关属性 + self._rotate_state: str = "Stopped" # 旋转状态:Running, Stopped + self._rotate_time: float = 0.0 # 旋转剩余时间 (秒) + self._rotate_speed: float = 0.0 # 旋转速度 (rpm) + self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm) + + # 真空泵相关属性 + self._pump_state: str = "Stopped" # 泵状态:Running, Stopped + self._pump_time: float = 0.0 # 泵剩余时间 (秒) + self._vacuum_level: float = 0.0 # 真空度 (mbar) + self._target_vacuum: float = 50.0 # 目标真空度 (mbar) + + # 温度相关属性 + self._temperature: float = 25.0 # 水浴温度 (°C) + self._target_temperature: float = 25.0 # 目标温度 (°C) + self._max_temperature: float = 180.0 # 最大温度 (°C) + + # 运行控制线程 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 操作成功标志 + self.success: str = "True" # 使用字符串而不是布尔值 + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def rotate_state(self) -> str: + return self._rotate_state + + @property + def rotate_time(self) -> float: + return self._rotate_time + + @property + def rotate_speed(self) -> float: + return self._rotate_speed + + @property + def pump_state(self) -> str: + return self._pump_state + + @property + def pump_time(self) -> float: + return self._pump_time + + @property + def vacuum_level(self) -> float: + return self._vacuum_level + + @property + def temperature(self) -> float: + return self._temperature + + @property + def target_temperature(self) -> float: + return self._target_temperature + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def set_timer(self, command: str) -> str: + """ + 设置定时器 - 兼容现有RotavapOne接口 + + Args: + command (str): JSON格式的命令字符串,包含rotate_time和pump_time + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + try: + timer = json.loads(command) + rotate_time = timer.get("rotate_time", 0) + pump_time = timer.get("pump_time", 0) + + self.success = "False" + self._rotate_time = float(rotate_time) + self._pump_time = float(pump_time) + self.success = "True" + + self._status = "Timer Set" + return "Success" + + except (json.JSONDecodeError, ValueError, KeyError) as e: + self._status = f"Error: Invalid command format - {str(e)}" + self.success = "False" + return "Error" + + def set_rotate_time(self, time_seconds: float) -> str: + """ + 设置旋转时间 + + Args: + time_seconds (float): 旋转时间 (秒) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + self.success = "False" + self._rotate_time = max(0.0, float(time_seconds)) + self.success = "True" + self._status = "Rotate time set" + return "Success" + + def set_pump_time(self, time_seconds: float) -> str: + """ + 设置泵时间 + + Args: + time_seconds (float): 泵时间 (秒) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + self.success = "False" + self._pump_time = max(0.0, float(time_seconds)) + self.success = "True" + self._status = "Pump time set" + return "Success" + + def set_rotate_speed(self, speed: float) -> str: + """ + 设置旋转速度 + + Args: + speed (float): 旋转速度 (rpm) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if speed < 0 or speed > self._max_rotate_speed: + self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})" + return "Error" + + self._rotate_speed = speed + self._status = "Rotate speed set" + return "Success" + + def set_temperature(self, temperature: float) -> str: + """ + 设置水浴温度 + + Args: + temperature (float): 目标温度 (°C) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if temperature < 0 or temperature > self._max_temperature: + self._status = f"Error: Temperature out of range (0-{self._max_temperature})" + return "Error" + + self._target_temperature = temperature + self._status = "Temperature set" + + # 启动操作线程以开始温度控制 + self._start_operation() + + return "Success" + + def start_rotation(self) -> str: + """ + 启动旋转 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if self._rotate_time <= 0: + self._status = "Error: No rotate time set" + return "Error" + + self._rotate_state = "Running" + self._status = "Rotation started" + return "Success" + + def start_pump(self) -> str: + """ + 启动真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + + if self._pump_time <= 0: + self._status = "Error: No pump time set" + return "Error" + + self._pump_state = "Running" + self._status = "Pump started" + return "Success" + + def stop_all_operations(self) -> str: + """ + 停止所有操作 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._rotate_state = "Stopped" + self._pump_state = "Stopped" + self._stop_operation() + self._rotate_time = 0.0 + self._pump_time = 0.0 + self._vacuum_level = 0.0 + self._status = "All operations stopped" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self.stop_all_operations() + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_operation(self): + """ + 启动操作线程 + + 这个方法启动一个后台线程来模拟旋蒸的实际运行过程。 + """ + with self._thread_lock: + if not self._running: + self._running = True + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + def _stop_operation(self): + """ + 停止操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=2.0) + + def _operation_loop(self): + """ + 操作主循环 + + 这个方法在后台线程中运行,模拟真实旋蒸的工作过程: + 1. 时间倒计时 + 2. 温度控制 + 3. 真空度控制 + 4. 状态更新 + """ + while self._running: + try: + # 处理旋转时间倒计时 + if self._rotate_time > 0: + self._rotate_state = "Running" + self._rotate_time = max(0.0, self._rotate_time - 1.0) + else: + self._rotate_state = "Stopped" + + # 处理泵时间倒计时 + if self._pump_time > 0: + self._pump_state = "Running" + self._pump_time = max(0.0, self._pump_time - 1.0) + # 模拟真空度变化 + if self._vacuum_level > self._target_vacuum: + self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0) + else: + self._pump_state = "Stopped" + # 真空度逐渐回升 + self._vacuum_level = min(1013.25, self._vacuum_level + 2.0) + + # 模拟温度控制 + temp_diff = self._target_temperature - self._temperature + if abs(temp_diff) > 0.5: + if temp_diff > 0: + self._temperature += min(1.0, temp_diff * 0.1) + else: + self._temperature += max(-1.0, temp_diff * 0.1) + + # 更新整体状态 + if self._rotate_state == "Running" or self._pump_state == "Running": + self._status = "Operating" + elif self._rotate_time > 0 or self._pump_time > 0: + self._status = "Ready" + else: + self._status = "Idle" + + # 等待1秒后继续下一次循环 + time.sleep(1.0) + + except Exception as e: + self._status = f"Error in operation: {str(e)}" + break + + # 循环结束时的清理工作 + self._status = "Idle" + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "rotate_state": self._rotate_state, + "rotate_time": self._rotate_time, + "rotate_speed": self._rotate_speed, + "pump_state": self._pump_state, + "pump_time": self._pump_time, + "vacuum_level": self._vacuum_level, + "temperature": self._temperature, + "target_temperature": self._target_temperature, + "success": self.success, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + rotavap = MockRotavap() + + # 测试基本功能 + print("启动旋转蒸发器测试...") + print(f"初始状态: {rotavap.get_status_info()}") + + # 设置定时器 + timer_command = '{"rotate_time": 300, "pump_time": 600}' + rotavap.set_timer(timer_command) + + # 设置温度和转速 + rotavap.set_temperature(60.0) + rotavap.set_rotate_speed(120.0) + + # 启动操作 + rotavap.start_rotation() + rotavap.start_pump() + + # 模拟运行10秒 + for i in range(10): + time.sleep(1) + print( + f"第{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, " + f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar" + ) + + rotavap.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_separator.py b/unilabos/devices/mock/mock_separator.py new file mode 100644 index 00000000..222cb2ed --- /dev/null +++ b/unilabos/devices/mock/mock_separator.py @@ -0,0 +1,399 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockSeparator: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 基本状态属性 + self._status: str = "Idle" # 当前总体状态 + self._valve_state: str = "Closed" # 阀门状态:Open 或 Closed + self._settling_time: float = 0.0 # 静置时间(秒) + + # 搅拌相关属性 + self._shake_time: float = 0.0 # 剩余摇摆时间(秒) + self._shake_status: str = "Not Shaking" # 摇摆状态 + + # 用于后台模拟 shake 动作 + self._operation_thread = None + self._thread_lock = threading.Lock() + self._running = False + + # Separate action 相关属性 + self._current_device: str = "MockSeparator1" + self._purpose: str = "" # wash or extract + self._product_phase: str = "" # top or bottom + self._from_vessel: str = "" + self._separation_vessel: str = "" + self._to_vessel: str = "" + self._waste_phase_to_vessel: str = "" + self._solvent: str = "" + self._solvent_volume: float = 0.0 + self._through: str = "" + self._repeats: int = 1 + self._stir_time: float = 0.0 + self._stir_speed: float = 0.0 + self._time_spent = timedelta() + self._time_remaining = timedelta() + self._start_time = datetime.now() # 添加这一行 + + @property + def current_device(self) -> str: + return self._current_device + + @property + def purpose(self) -> str: + return self._purpose + + @property + def valve_state(self) -> str: + return self._valve_state + + @property + def settling_time(self) -> float: + return self._settling_time + + @property + def status(self) -> str: + return self._status + + @property + def shake_time(self) -> float: + with self._thread_lock: + return self._shake_time + + @property + def shake_status(self) -> str: + with self._thread_lock: + return self._shake_status + + @property + def product_phase(self) -> str: + return self._product_phase + + @property + def from_vessel(self) -> str: + return self._from_vessel + + @property + def separation_vessel(self) -> str: + return self._separation_vessel + + @property + def to_vessel(self) -> str: + return self._to_vessel + + @property + def waste_phase_to_vessel(self) -> str: + return self._waste_phase_to_vessel + + @property + def solvent(self) -> str: + return self._solvent + + @property + def solvent_volume(self) -> float: + return self._solvent_volume + + @property + def through(self) -> str: + return self._through + + @property + def repeats(self) -> int: + return self._repeats + + @property + def stir_time(self) -> float: + return self._stir_time + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def time_spent(self) -> float: + if self._running: + self._time_spent = datetime.now() - self._start_time + return self._time_spent.total_seconds() + + @property + def time_remaining(self) -> float: + if self._running: + elapsed = (datetime.now() - self._start_time).total_seconds() + total_time = (self._stir_time + self._settling_time + 10) * self._repeats + remain = max(0, total_time - elapsed) + self._time_remaining = timedelta(seconds=remain) + return self._time_remaining.total_seconds() + + def separate(self, purpose: str, product_phase: str, from_vessel: str, + separation_vessel: str, to_vessel: str, waste_phase_to_vessel: str = "", + solvent: str = "", solvent_volume: float = 0.0, through: str = "", + repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0, + settling_time: float = 60.0) -> dict: + """ + 执行分离操作 + """ + with self._thread_lock: + # 检查是否已经在运行 + if self._running: + return { + "success": False, + "status": "Error: Operation already in progress" + } + # 必填参数验证 + if not all([from_vessel, separation_vessel, to_vessel]): + self._status = "Error: Missing required vessel parameters" + return {"success": False} + # 验证参数 + if purpose not in ["wash", "extract"]: + self._status = "Error: Invalid purpose" + return {"success": False} + + if product_phase not in ["top", "bottom"]: + self._status = "Error: Invalid product phase" + return {"success": False} + # 数值参数验证 + try: + solvent_volume = float(solvent_volume) + repeats = int(repeats) + stir_time = float(stir_time) + stir_speed = float(stir_speed) + settling_time = float(settling_time) + except ValueError: + self._status = "Error: Invalid numeric parameters" + return {"success": False} + + # 设置参数 + self._purpose = purpose + self._product_phase = product_phase + self._from_vessel = from_vessel + self._separation_vessel = separation_vessel + self._to_vessel = to_vessel + self._waste_phase_to_vessel = waste_phase_to_vessel + self._solvent = solvent + self._solvent_volume = float(solvent_volume) + self._through = through + self._repeats = int(repeats) + self._stir_time = float(stir_time) + self._stir_speed = float(stir_speed) + self._settling_time = float(settling_time) + + # 重置计时器 + self._start_time = datetime.now() + self._time_spent = timedelta() + total_time = (self._stir_time + self._settling_time + 10) * self._repeats + self._time_remaining = timedelta(seconds=total_time) + + # 启动分离操作 + self._status = "Starting Separation" + self._running = True + + # 在锁内创建和启动线程 + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + # 等待确认操作已经开始 + time.sleep(0.1) # 短暂等待确保操作线程已启动 + + return { + "success": True, + "status": self._status, + "current_device": self._current_device, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + def shake(self, shake_time: float) -> str: + """ + 模拟 shake(搅拌)操作: + - 进入 "Shaking" 状态,倒计时 shake_time 秒 + - shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒 + - 最后恢复为 Idle + """ + try: + shake_time = float(shake_time) + except ValueError: + self._status = "Error: Invalid shake time" + return "Error" + + with self._thread_lock: + self._status = "Shaking" + self._settling_time = 0.0 + self._shake_time = shake_time + self._shake_status = "Shaking" + + def _run_shake(): + remaining = shake_time + while remaining > 0: + time.sleep(1) + remaining -= 1 + with self._thread_lock: + self._shake_time = remaining + with self._thread_lock: + self._status = "Settling" + self._settling_time = 60.0 # 固定静置时间为60秒 + self._shake_status = "Settling" + while True: + with self._thread_lock: + if self._settling_time <= 0: + self._status = "Idle" + self._shake_status = "Idle" + break + time.sleep(1) + with self._thread_lock: + self._settling_time = max(0.0, self._settling_time - 1) + + self._operation_thread = threading.Thread(target=_run_shake) + self._operation_thread.daemon = True + self._operation_thread.start() + return "Success" + + def set_valve(self, command: str) -> str: + """ + 阀门控制命令:传入 "open" 或 "close" + """ + + command = command.lower() + if command == "open": + self._valve_state = "Open" + self._status = "Valve Opened" + elif command == "close": + self._valve_state = "Closed" + self._status = "Valve Closed" + else: + self._status = "Error: Invalid valve command" + return "Error" + return "Success" + + def _operation_loop(self): + """分离操作主循环""" + try: + current_repeat = 1 + + # 立即更新状态,确保不会停留在Starting Separation + with self._thread_lock: + self._status = f"Separation Cycle {current_repeat}/{self._repeats}" + + while self._running and current_repeat <= self._repeats: + # 第一步:搅拌 + if self._stir_time > 0: + with self._thread_lock: + self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})" + remaining_stir = self._stir_time + while remaining_stir > 0 and self._running: + time.sleep(1) + remaining_stir -= 1 + + # 第二步:静置 + if self._settling_time > 0: + with self._thread_lock: + self._status = f"Settling (Repeat {current_repeat}/{self._repeats})" + remaining_settle = self._settling_time + while remaining_settle > 0 and self._running: + time.sleep(1) + remaining_settle -= 1 + + # 第三步:打开阀门排出 + with self._thread_lock: + self._valve_state = "Open" + self._status = f"Draining (Repeat {current_repeat}/{self._repeats})" + + # 模拟排出时间(5秒) + time.sleep(10) + + # 关闭阀门 + with self._thread_lock: + self._valve_state = "Closed" + + # 检查是否继续下一次重复 + if current_repeat < self._repeats: + current_repeat += 1 + else: + with self._thread_lock: + self._status = "Separation Complete" + break + + except Exception as e: + with self._thread_lock: + self._status = f"Error in separation: {str(e)}" + finally: + with self._thread_lock: + self._running = False + self._valve_state = "Closed" + if self._status == "Starting Separation": + self._status = "Error: Operation failed to start" + elif self._status != "Separation Complete": + self._status = "Stopped" + + def stop_operations(self) -> str: + """停止任何正在执行的操作""" + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=1.0) + self._operation_thread = None + self._settling_time = 0.0 + self._status = "Idle" + self._shake_status = "Idle" + self._shake_time = 0.0 + self._time_remaining = timedelta() + return "Success" + + def get_status_info(self) -> dict: + """获取当前设备状态信息""" + with self._thread_lock: + current_time = datetime.now() + if self._start_time: + self._time_spent = current_time - self._start_time + + return { + "status": self._status, + "valve_state": self._valve_state, + "settling_time": self._settling_time, + "shake_time": self._shake_time, + "shake_status": self._shake_status, + "current_device": self._current_device, + "purpose": self._purpose, + "product_phase": self._product_phase, + "from_vessel": self._from_vessel, + "separation_vessel": self._separation_vessel, + "to_vessel": self._to_vessel, + "waste_phase_to_vessel": self._waste_phase_to_vessel, + "solvent": self._solvent, + "solvent_volume": self._solvent_volume, + "through": self._through, + "repeats": self._repeats, + "stir_time": self._stir_time, + "stir_speed": self._stir_speed, + "time_spent": self._time_spent.total_seconds(), + "time_remaining": self._time_remaining.total_seconds() + } + + +# 主函数用于测试 +if __name__ == "__main__": + separator = MockSeparator() + + print("启动简单版分离器测试...") + print("初始状态:", separator.get_status_info()) + + # 触发 shake 操作,模拟 10 秒的搅拌 + print("执行 shake 操作...") + print(separator.shake(10.0)) + + # 循环显示状态变化 + for i in range(20): + time.sleep(1) + info = separator.get_status_info() + print( + f"第{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, " + f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, " + f"shake_status={info['shake_status']}" + ) + + # 模拟打开阀门 + print("打开阀门...", separator.set_valve("open")) + print("最终状态:", separator.get_status_info()) diff --git a/unilabos/devices/mock/mock_solenoid_valve.py b/unilabos/devices/mock/mock_solenoid_valve.py new file mode 100644 index 00000000..0f0fbe55 --- /dev/null +++ b/unilabos/devices/mock/mock_solenoid_valve.py @@ -0,0 +1,89 @@ +import time + + +class MockSolenoidValve: + """ + 模拟电磁阀设备类 - 简化版本 + + 这个类提供了电磁阀的基本功能:开启、关闭和状态查询 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockSolenoidValve实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + self._status: str = "Idle" + self._valve_status: str = "Closed" # 阀门位置:Open, Closed + + @property + def status(self) -> str: + """设备状态 - 会被自动识别的设备属性""" + return self._status + + @property + def valve_status(self) -> str: + """阀门状态""" + return self._valve_status + + def set_valve_status(self, status: str) -> str: + """ + 设置阀门位置 + + Args: + position (str): 阀门位置,可选值:"Open", "Closed" + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if status not in ["Open", "Closed"]: + self._status = "Error: Invalid position" + return "Error" + + self._status = "Moving" + time.sleep(1) # 模拟阀门动作时间 + + self._valve_status = status + self._status = "Idle" + return "Success" + + def open_valve(self) -> str: + """打开阀门""" + return self.set_valve_status("Open") + + def close_valve(self) -> str: + """关闭阀门""" + return self.set_valve_status("Closed") + + def get_valve_status(self) -> str: + """获取阀门位置""" + return self._valve_status + + def is_open(self) -> bool: + """检查阀门是否打开""" + return self._valve_status == "Open" + + def is_closed(self) -> bool: + """检查阀门是否关闭""" + return self._valve_status == "Closed" + + +# 用于测试的主函数 +if __name__ == "__main__": + valve = MockSolenoidValve() + + print("启动电磁阀测试...") + print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}") + + # 测试开启阀门 + valve.open_valve() + print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}") + + # 测试关闭阀门 + valve.close_valve() + print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}") + + print("测试完成") diff --git a/unilabos/devices/mock/mock_stirrer.py b/unilabos/devices/mock/mock_stirrer.py new file mode 100644 index 00000000..a1f2c51d --- /dev/null +++ b/unilabos/devices/mock/mock_stirrer.py @@ -0,0 +1,307 @@ +import time +import threading + + +class MockStirrer: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + + # 搅拌相关属性 + self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm) + self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm) + self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm) + self._stir_state: str = "Stopped" # 搅拌状态:Running, Stopped + + # 温度相关属性 + self._temperature: float = 25.0 # 当前温度 (°C) + self._target_temperature: float = 25.0 # 目标温度 (°C) + self._max_temperature: float = 300.0 # 最大温度 (°C) + self._heating_state: str = "Off" # 加热状态:On, Off + self._heating_power: float = 0.0 # 加热功率百分比 0-100 + + # 运行控制线程 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + return self._status + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def target_stir_speed(self) -> float: + return self._target_stir_speed + + @property + def stir_state(self) -> str: + return self._stir_state + + @property + def temperature(self) -> float: + """ + 当前温度 + + Returns: + float: 当前温度 (°C) + """ + return self._temperature + + @property + def target_temperature(self) -> float: + """ + 目标温度 + + Returns: + float: 目标温度 (°C) + """ + return self._target_temperature + + @property + def heating_state(self) -> str: + return self._heating_state + + @property + def heating_power(self) -> float: + return self._heating_power + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def max_temperature(self) -> float: + return self._max_temperature + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def set_stir_speed(self, speed: float) -> str: + + speed = float(speed) # 确保传入的速度是浮点数 + + if speed < 0 or speed > self._max_stir_speed: + self._status = f"Error: Speed out of range (0-{self._max_stir_speed})" + return "Error" + + self._target_stir_speed = speed + self._status = "Setting Stir Speed" + + # 如果设置了非零速度,启动搅拌 + if speed > 0: + self._stir_state = "Running" + else: + self._stir_state = "Stopped" + + return "Success" + + def set_temperature(self, temperature: float) -> str: + temperature = float(temperature) # 确保传入的温度是浮点数 + + if temperature < 0 or temperature > self._max_temperature: + self._status = f"Error: Temperature out of range (0-{self._max_temperature})" + return "Error" + + self._target_temperature = temperature + self._status = "Setting Temperature" + + return "Success" + + def start_stirring(self) -> str: + + if self._target_stir_speed <= 0: + self._status = "Error: No target speed set" + return "Error" + + self._stir_state = "Running" + self._status = "Stirring Started" + return "Success" + + def stop_stirring(self) -> str: + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._status = "Stirring Stopped" + return "Success" + + def heating_control(self, heating_state: str = "On") -> str: + + if heating_state not in ["On", "Off"]: + self._status = "Error: Invalid heating state" + return "Error" + + self._heating_state = heating_state + + if heating_state == "On": + self._status = "Heating On" + else: + self._status = "Heating Off" + self._heating_power = 0.0 + + return "Success" + + def stop_all_operations(self) -> str: + self._stir_state = "Stopped" + self._heating_state = "Off" + self._stop_operation() + self._stir_speed = 0.0 + self._target_stir_speed = 0.0 + self._heating_power = 0.0 + self._status = "All operations stopped" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self.stop_all_operations() + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_operation(self): + with self._thread_lock: + if not self._running: + self._running = True + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + def _stop_operation(self): + """ + 停止操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._operation_thread and self._operation_thread.is_alive(): + self._operation_thread.join(timeout=2.0) + + def _operation_loop(self): + while self._running: + try: + # 处理搅拌速度控制 + if self._stir_state == "Running": + speed_diff = self._target_stir_speed - self._stir_speed + + if abs(speed_diff) < 1.0: # 速度接近目标值 + self._stir_speed = self._target_stir_speed + if self._stir_speed > 0: + self._status = "Stirring at Target Speed" + else: + # 模拟速度调节,每秒调整10%的差值 + adjustment = speed_diff * 0.1 + self._stir_speed += adjustment + self._status = "Adjusting Stir Speed" + + # 确保速度在合理范围内 + self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed)) + else: + # 搅拌停止时,速度逐渐降为0 + if self._stir_speed > 0: + self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm + + # 处理温度控制 + if self._heating_state == "On": + temp_diff = self._target_temperature - self._temperature + + if abs(temp_diff) < 0.5: # 温度接近目标值 + self._heating_power = 20.0 # 维持温度的最小功率 + elif temp_diff > 0: # 需要加热 + # 根据温差调整加热功率 + if temp_diff > 50: + self._heating_power = 100.0 + elif temp_diff > 20: + self._heating_power = 80.0 + elif temp_diff > 10: + self._heating_power = 60.0 + else: + self._heating_power = 40.0 + + # 模拟加热过程 + heating_rate = self._heating_power / 100.0 * 1.5 # 最大每秒升温1.5度 + self._temperature += heating_rate + else: # 目标温度低于当前温度 + self._heating_power = 0.0 + # 自然冷却 + self._temperature -= 0.1 + else: + self._heating_power = 0.0 + # 自然冷却到室温 + if self._temperature > 25.0: + self._temperature -= 0.2 + + # 限制温度范围 + self._temperature = max(20.0, min(self._max_temperature, self._temperature)) + + # 更新整体状态 + if self._stir_state == "Running" and self._heating_state == "On": + self._status = "Stirring and Heating" + elif self._stir_state == "Running": + self._status = "Stirring Only" + elif self._heating_state == "On": + self._status = "Heating Only" + else: + self._status = "Idle" + + # 等待1秒后继续下一次循环 + time.sleep(1.0) + + except Exception as e: + self._status = f"Error in operation: {str(e)}" + break + + # 循环结束时的清理工作 + self._status = "Idle" + + def get_status_info(self) -> dict: + return { + "status": self._status, + "stir_speed": self._stir_speed, + "target_stir_speed": self._target_stir_speed, + "stir_state": self._stir_state, + "temperature": self._temperature, + "target_temperature": self._target_temperature, + "heating_state": self._heating_state, + "heating_power": self._heating_power, + "max_stir_speed": self._max_stir_speed, + "max_temperature": self._max_temperature, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + stirrer = MockStirrer() + + # 测试基本功能 + print("启动搅拌器测试...") + print(f"初始状态: {stirrer.get_status_info()}") + + # 设置搅拌速度和温度 + stirrer.set_stir_speed(800.0) + stirrer.set_temperature(60.0) + stirrer.heating_control("On") + + # 模拟运行15秒 + for i in range(15): + time.sleep(1) + print( + f"第{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, " + f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}" + ) + + stirrer.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/mock/mock_stirrer_new.py b/unilabos/devices/mock/mock_stirrer_new.py new file mode 100644 index 00000000..ac429db5 --- /dev/null +++ b/unilabos/devices/mock/mock_stirrer_new.py @@ -0,0 +1,229 @@ +import time +import threading +from datetime import datetime, timedelta + +class MockStirrer_new: + def __init__(self, port: str = "MOCK"): + self.port = port + + # 基本状态属性 + self._status: str = "Idle" + self._vessel: str = "" + self._purpose: str = "" + + # 搅拌相关属性 + self._stir_speed: float = 0.0 + self._target_stir_speed: float = 0.0 + self._max_stir_speed: float = 2000.0 + self._stir_state: str = "Stopped" + + # 计时相关 + self._stir_time: float = 0.0 + self._settling_time: float = 0.0 + self._start_time = datetime.now() + self._time_remaining = timedelta() + + # 运行控制 + self._operation_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # 创建操作线程 + self._operation_thread = threading.Thread(target=self._operation_loop) + self._operation_thread.daemon = True + self._operation_thread.start() + + # ==================== 状态属性 ==================== + @property + def status(self) -> str: + return self._status + + @property + def stir_speed(self) -> float: + return self._stir_speed + + @property + def target_stir_speed(self) -> float: + return self._target_stir_speed + + @property + def stir_state(self) -> str: + return self._stir_state + + @property + def vessel(self) -> str: + return self._vessel + + @property + def purpose(self) -> str: + return self._purpose + + @property + def stir_time(self) -> float: + return self._stir_time + + @property + def settling_time(self) -> float: + return self._settling_time + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def progress(self) -> float: + """返回当前操作的进度(0-100)""" + if not self._running: + return 0.0 + elapsed = (datetime.now() - self._start_time).total_seconds() + total_time = self._stir_time + self._settling_time + if total_time <= 0: + return 100.0 + return min(100.0, (elapsed / total_time) * 100) + + # ==================== Action Server 方法 ==================== + def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict: + """ + StartStir.action 对应的方法 + """ + with self._thread_lock: + if self._running: + return { + "success": False, + "message": "Operation already in progress" + } + + try: + # 重置所有参数 + self._vessel = vessel + self._purpose = purpose + self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间 + self._settling_time = 0.0 + self._start_time = datetime.now() # 重置开始时间 + + if stir_speed > 0: + self._target_stir_speed = min(stir_speed, self._max_stir_speed) + + self._stir_state = "Running" + self._status = "Stirring Started" + self._running = True + + return { + "success": True, + "message": "Stirring started successfully" + } + + except Exception as e: + return { + "success": False, + "message": f"Error: {str(e)}" + } + + def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict: + """ + Stir.action 对应的方法 + """ + with self._thread_lock: + try: + # 如果已经在运行,先停止当前操作 + if self._running: + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + time.sleep(0.1) # 给一个短暂的停止时间 + + + # 重置所有参数 + self._stir_time = float(stir_time) + self._settling_time = float(settling_time) + self._target_stir_speed = min(float(stir_speed), self._max_stir_speed) + self._start_time = datetime.now() # 重置开始时间 + self._stir_state = "Running" + self._status = "Stirring" + self._running = True + + return {"success": True} + + except ValueError: + self._status = "Error: Invalid parameters" + return {"success": False} + + def stop_stir(self, vessel: str) -> dict: + """ + StopStir.action 对应的方法 + """ + with self._thread_lock: + if vessel != self._vessel: + return { + "success": False, + "message": "Vessel mismatch" + } + + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._status = "Stirring Stopped" + + return { + "success": True, + "message": "Stirring stopped successfully" + } + + # ==================== 内部控制方法 ==================== + + def _operation_loop(self): + """操作主循环""" + while True: + try: + current_time = datetime.now() + + with self._thread_lock: # 添加锁保护 + if self._stir_state == "Running": + # 实际搅拌逻辑 + speed_diff = self._target_stir_speed - self._stir_speed + if abs(speed_diff) > 0.1: + adjustment = speed_diff * 0.1 + self._stir_speed += adjustment + else: + self._stir_speed = self._target_stir_speed + + # 更新进度 + if self._running: + if self._stir_time > 0: # 定时搅拌模式 + elapsed = (current_time - self._start_time).total_seconds() + if elapsed >= self._stir_time + self._settling_time: + self._running = False + self._stir_state = "Stopped" + self._target_stir_speed = 0.0 + self._stir_speed = 0.0 + self._status = "Stirring Complete" + elif elapsed >= self._stir_time: + self._status = "Settling" + else: # 连续搅拌模式 + self._status = "Stirring" + else: + # 停止状态下慢慢降低速度 + if self._stir_speed > 0: + self._stir_speed = max(0, self._stir_speed - 20.0) + + time.sleep(0.1) + + except Exception as e: + print(f"Error in operation loop: {str(e)}") # 添加错误输出 + self._status = f"Error: {str(e)}" + time.sleep(1.0) # 错误发生时等待较长时间 + + def get_status_info(self) -> dict: + """获取设备状态信息""" + return { + "status": self._status, + "vessel": self._vessel, + "purpose": self._purpose, + "stir_speed": self._stir_speed, + "target_stir_speed": self._target_stir_speed, + "stir_state": self._stir_state, + "stir_time": self._stir_time, # 添加 + "settling_time": self._settling_time, # 添加 + "progress": self.progress, + "max_stir_speed": self._max_stir_speed + } \ No newline at end of file diff --git a/unilabos/devices/mock/mock_vacuum.py b/unilabos/devices/mock/mock_vacuum.py new file mode 100644 index 00000000..9e368a90 --- /dev/null +++ b/unilabos/devices/mock/mock_vacuum.py @@ -0,0 +1,410 @@ +import time +import threading + + +class MockVacuum: + """ + 模拟真空泵设备类 + + 这个类模拟了一个实验室真空泵的行为,包括真空度控制、 + 压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。 + """ + + def __init__(self, port: str = "MOCK"): + """ + 初始化MockVacuum实例 + + Args: + port (str): 设备端口,默认为"MOCK"表示模拟设备 + """ + self.port = port + + # 设备基本状态属性 + self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped + self._power_state: str = "Off" # 电源状态:On, Off + self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused + + # 真空相关属性 + self._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始 + self._target_vacuum: float = 50.0 # 目标真空度 (mbar) + self._min_vacuum: float = 1.0 # 最小真空度 (mbar) + self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压 + + # 泵性能相关属性 + self._pump_speed: float = 0.0 # 泵速 (L/s) + self._max_pump_speed: float = 100.0 # 最大泵速 (L/s) + self._pump_efficiency: float = 95.0 # 泵效率百分比 + + # 运行控制线程 + self._vacuum_thread = None + self._running = False + self._thread_lock = threading.Lock() + + # ==================== 状态属性 ==================== + # 这些属性会被Uni-Lab系统自动识别并定时对外广播 + + @property + def status(self) -> str: + """ + 设备状态 - 会被自动识别的设备属性 + + Returns: + str: 当前设备状态 (Idle, Running, Error, Stopped) + """ + return self._status + + @property + def power_state(self) -> str: + """ + 电源状态 + + Returns: + str: 电源状态 (On, Off) + """ + return self._power_state + + @property + def pump_state(self) -> str: + """ + 泵运行状态 + + Returns: + str: 泵状态 (Running, Stopped, Paused) + """ + return self._pump_state + + @property + def vacuum_level(self) -> float: + """ + 当前真空度 + + Returns: + float: 当前真空度 (mbar) + """ + return self._vacuum_level + + @property + def target_vacuum(self) -> float: + """ + 目标真空度 + + Returns: + float: 目标真空度 (mbar) + """ + return self._target_vacuum + + @property + def pump_speed(self) -> float: + """ + 泵速 + + Returns: + float: 泵速 (L/s) + """ + return self._pump_speed + + @property + def pump_efficiency(self) -> float: + """ + 泵效率 + + Returns: + float: 泵效率百分比 + """ + return self._pump_efficiency + + @property + def max_pump_speed(self) -> float: + """ + 最大泵速 + + Returns: + float: 最大泵速 (L/s) + """ + return self._max_pump_speed + + # ==================== 设备控制方法 ==================== + # 这些方法需要在注册表中添加,会作为ActionServer接受控制指令 + + def power_control(self, power_state: str = "On") -> str: + """ + 电源控制方法 + + Args: + power_state (str): 电源状态,可选值:"On", "Off" + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if power_state not in ["On", "Off"]: + self._status = "Error: Invalid power state" + return "Error" + + self._power_state = power_state + + if power_state == "On": + self._status = "Power On" + self._start_vacuum_operation() + else: + self._status = "Power Off" + self.stop_vacuum() + + return "Success" + + def set_vacuum_level(self, vacuum_level: float) -> str: + """ + 设置目标真空度 + + Args: + vacuum_level (float): 目标真空度 (mbar) + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + try: + vacuum_level = float(vacuum_level) + except ValueError: + self._status = "Error: Invalid vacuum level" + return "Error" + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum: + self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})" + return "Error" + + self._target_vacuum = vacuum_level + self._status = "Setting Vacuum Level" + + return "Success" + + def start_vacuum(self) -> str: + """ + 启动真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + self._pump_state = "Running" + self._status = "Starting Vacuum Pump" + self._start_vacuum_operation() + + return "Success" + + def stop_vacuum(self) -> str: + """ + 停止真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._pump_state = "Stopped" + self._status = "Stopping Vacuum Pump" + self._stop_vacuum_operation() + self._pump_speed = 0.0 + + return "Success" + + def pause_vacuum(self) -> str: + """ + 暂停真空泵 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._pump_state != "Running": + self._status = "Error: Pump not running" + return "Error" + + self._pump_state = "Paused" + self._status = "Vacuum Pump Paused" + self._stop_vacuum_operation() + + return "Success" + + def resume_vacuum(self) -> str: + """ + 恢复真空泵运行 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + if self._pump_state != "Paused": + self._status = "Error: Pump not paused" + return "Error" + + if self._power_state != "On": + self._status = "Error: Power Off" + return "Error" + + self._pump_state = "Running" + self._status = "Resuming Vacuum Pump" + self._start_vacuum_operation() + + return "Success" + + def vent_to_atmosphere(self) -> str: + """ + 通大气 - 将真空度恢复到大气压 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._target_vacuum = self._max_vacuum # 设置为大气压 + self._status = "Venting to Atmosphere" + return "Success" + + def emergency_stop(self) -> str: + """ + 紧急停止 + + Returns: + str: 操作结果状态 ("Success", "Error") + """ + self._status = "Emergency Stop" + self._pump_state = "Stopped" + self._stop_vacuum_operation() + self._pump_speed = 0.0 + + return "Success" + + # ==================== 内部控制方法 ==================== + + def _start_vacuum_operation(self): + """ + 启动真空操作线程 + + 这个方法启动一个后台线程来模拟真空泵的实际运行过程。 + """ + with self._thread_lock: + if not self._running and self._power_state == "On": + self._running = True + self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop) + self._vacuum_thread.daemon = True + self._vacuum_thread.start() + + def _stop_vacuum_operation(self): + """ + 停止真空操作线程 + + 安全地停止后台运行线程并等待其完成。 + """ + with self._thread_lock: + self._running = False + if self._vacuum_thread and self._vacuum_thread.is_alive(): + self._vacuum_thread.join(timeout=2.0) + + def _vacuum_operation_loop(self): + """ + 真空操作主循环 + + 这个方法在后台线程中运行,模拟真空泵的工作过程: + 1. 检查电源状态和运行状态 + 2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度 + 3. 否则等待 + """ + while self._running and self._power_state == "On": + try: + with self._thread_lock: + # 只有泵状态为 Running 时才进行更新 + if self._pump_state == "Running": + vacuum_diff = self._vacuum_level - self._target_vacuum + + if abs(vacuum_diff) < 1.0: # 真空度接近目标值 + self._status = "At Target Vacuum" + self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速 + elif vacuum_diff > 0: # 需要抽真空(降低压力) + self._status = "Pumping Down" + if vacuum_diff > 500: + self._pump_speed = self._max_pump_speed + elif vacuum_diff > 100: + self._pump_speed = self._max_pump_speed * 0.8 + elif vacuum_diff > 50: + self._pump_speed = self._max_pump_speed * 0.6 + else: + self._pump_speed = self._max_pump_speed * 0.4 + + # 根据泵速和效率计算真空降幅 + pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0 + vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar + self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction) + else: # 目标真空度高于当前值,需要通气 + self._status = "Venting" + self._pump_speed = 0.0 + self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0) + + # 限制真空度范围 + self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level)) + else: + # 当泵状态不是 Running 时,可保持原状态 + self._status = "Vacuum Pump Not Running" + # 释放锁后等待1秒钟 + time.sleep(1.0) + except Exception as e: + with self._thread_lock: + self._status = f"Error in vacuum operation: {str(e)}" + break + + # 循环结束后的清理工作 + if self._pump_state == "Running": + self._status = "Idle" + # 停止泵后,真空度逐渐回升到大气压 + while self._vacuum_level < self._max_vacuum * 0.9: + with self._thread_lock: + self._vacuum_level += 2.0 + time.sleep(0.1) + + def get_status_info(self) -> dict: + """ + 获取完整的设备状态信息 + + Returns: + dict: 包含所有设备状态的字典 + """ + return { + "status": self._status, + "power_state": self._power_state, + "pump_state": self._pump_state, + "vacuum_level": self._vacuum_level, + "target_vacuum": self._target_vacuum, + "pump_speed": self._pump_speed, + "pump_efficiency": self._pump_efficiency, + "max_pump_speed": self._max_pump_speed, + } + + +# 用于测试的主函数 +if __name__ == "__main__": + vacuum = MockVacuum() + + # 测试基本功能 + print("启动真空泵测试...") + vacuum.power_control("On") + print(f"初始状态: {vacuum.get_status_info()}") + + # 设置目标真空度并启动 + vacuum.set_vacuum_level(10.0) # 设置为10mbar + vacuum.start_vacuum() + + # 模拟运行15秒 + for i in range(15): + time.sleep(1) + print( + f"第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}" + ) + # 测试通大气 + print("测试通大气...") + vacuum.vent_to_atmosphere() + + # 继续运行5秒观察通大气过程 + for i in range(5): + time.sleep(1) + print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}") + + vacuum.emergency_stop() + print("测试完成") diff --git a/unilabos/devices/pump_and_valve/vacuum_pump_mock.py b/unilabos/devices/pump_and_valve/vacuum_pump_mock.py index 96a48426..35655122 100644 --- a/unilabos/devices/pump_and_valve/vacuum_pump_mock.py +++ b/unilabos/devices/pump_and_valve/vacuum_pump_mock.py @@ -12,10 +12,8 @@ class VacuumPumpMock: def get_status(self) -> str: return self._status - def set_status(self, position): - time.sleep(5) - - self._status = position + def set_status(self, string): + self._status = string time.sleep(5) def open(self): diff --git a/unilabos/devices/virtual/__init__.py b/unilabos/devices/virtual/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/virtual/virtual_centrifuge.py b/unilabos/devices/virtual/virtual_centrifuge.py new file mode 100644 index 00000000..79f9dce0 --- /dev/null +++ b/unilabos/devices/virtual/virtual_centrifuge.py @@ -0,0 +1,213 @@ +import asyncio +import logging +import time as time_module +from typing import Dict, Any, Optional + + +class VirtualCentrifuge: + """Virtual centrifuge device - 简化版,只保留核心功能""" + + 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_centrifuge" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualCentrifuge.{self.device_id}") + self.data = {} + + # 从config或kwargs中获取配置参数 + self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL") + self._max_speed = self.config.get("max_speed") or kwargs.get("max_speed", 15000.0) + self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 40.0) + self._min_temp = self.config.get("min_temp") or kwargs.get("min_temp", 4.0) + + # 处理其他kwargs参数 + skip_keys = {"port", "max_speed", "max_temp", "min_temp"} + 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 centrifuge""" + self.logger.info(f"Initializing virtual centrifuge {self.device_id}") + + # 只保留核心状态 + self.data.update({ + "status": "Idle", + "centrifuge_state": "Stopped", # Stopped, Running, Completed, Error + "current_speed": 0.0, + "target_speed": 0.0, + "current_temp": 25.0, + "target_temp": 25.0, + "time_remaining": 0.0, + "progress": 0.0, + "message": "Ready for centrifugation" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual centrifuge""" + self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}") + + self.data.update({ + "status": "Offline", + "centrifuge_state": "Offline", + "current_speed": 0.0, + "current_temp": 25.0, + "message": "System offline" + }) + return True + + async def centrifuge( + self, + vessel: str, + speed: float, + time: float, + temp: float = 25.0 + ) -> bool: + """Execute centrifuge action - 简化的离心流程""" + self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} rpm, time={time}s, temp={temp}°C") + + # 验证参数 + if speed > self._max_speed or speed < 100.0: + error_msg = f"离心速度 {speed} rpm 超出范围 (100-{self._max_speed} rpm)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "centrifuge_state": "Error", + "message": error_msg + }) + return False + + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "centrifuge_state": "Error", + "message": error_msg + }) + return False + + # 开始离心 + self.data.update({ + "status": f"离心中: {vessel}", + "centrifuge_state": "Running", + "current_speed": speed, + "target_speed": speed, + "current_temp": temp, + "target_temp": temp, + "time_remaining": time, + "progress": 0.0, + "message": f"Centrifuging {vessel} at {speed} rpm, {temp}°C" + }) + + try: + # 离心过程 - 实时更新进度 + start_time = time_module.time() + total_time = time + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + progress = min(100.0, (elapsed / total_time) * 100) + + # 更新状态 + self.data.update({ + "time_remaining": remaining, + "progress": progress, + "status": f"离心中: {vessel} | {speed} rpm | {temp}°C | {progress:.1f}% | 剩余: {remaining:.0f}s", + "message": f"Centrifuging: {progress:.1f}% complete, {remaining:.0f}s remaining" + }) + + # 时间到了,退出循环 + if remaining <= 0: + break + + # 每秒更新一次 + await asyncio.sleep(1.0) + + # 离心完成 + self.data.update({ + "status": f"离心完成: {vessel} | {speed} rpm | {time}s", + "centrifuge_state": "Completed", + "progress": 100.0, + "time_remaining": 0.0, + "current_speed": 0.0, # 停止旋转 + "current_temp": 25.0, # 恢复室温 + "message": f"Centrifugation completed: {vessel} at {speed} rpm for {time}s" + }) + + self.logger.info(f"Centrifugation completed: {vessel} at {speed} rpm for {time}s") + return True + + except Exception as e: + # 出错处理 + self.logger.error(f"Error during centrifugation: {str(e)}") + + self.data.update({ + "status": f"离心错误: {str(e)}", + "centrifuge_state": "Error", + "current_speed": 0.0, + "current_temp": 25.0, + "progress": 0.0, + "time_remaining": 0.0, + "message": f"Centrifugation failed: {str(e)}" + }) + return False + + # === 核心状态属性 === + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def centrifuge_state(self) -> str: + return self.data.get("centrifuge_state", "Unknown") + + @property + def current_speed(self) -> float: + return self.data.get("current_speed", 0.0) + + @property + def target_speed(self) -> float: + return self.data.get("target_speed", 0.0) + + @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_speed(self) -> float: + return self._max_speed + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def min_temp(self) -> float: + return self._min_temp + + @property + def time_remaining(self) -> float: + return self.data.get("time_remaining", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def message(self) -> str: + return self.data.get("message", "") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py new file mode 100644 index 00000000..c83da1c2 --- /dev/null +++ b/unilabos/devices/virtual/virtual_column.py @@ -0,0 +1,132 @@ +import asyncio +import logging +from typing import Dict, Any, Optional + +class VirtualColumn: + """Virtual column device for RunColumn protocol""" + + 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.device_id = device_id or "unknown_column" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualColumn.{self.device_id}") + self.data = {} + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_flow_rate = self.config.get('max_flow_rate') or kwargs.get('max_flow_rate', 10.0) + self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0) + self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0) + + print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===") + + async def initialize(self) -> bool: + """Initialize virtual column""" + self.logger.info(f"Initializing virtual column {self.device_id}") + self.data.update({ + "status": "Idle", + "column_state": "Ready", + "current_flow_rate": 0.0, + "max_flow_rate": self._max_flow_rate, + "column_length": self._column_length, + "column_diameter": self._column_diameter, + "processed_volume": 0.0, + "progress": 0.0, + "current_status": "Ready" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual column""" + self.logger.info(f"Cleaning up virtual column {self.device_id}") + return True + + async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool: + """Execute column chromatography run - matches RunColumn action""" + self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}") + + # 更新设备状态 + self.data.update({ + "status": "Running", + "column_state": "Separating", + "current_status": "Column separation in progress", + "progress": 0.0, + "processed_volume": 0.0 + }) + + # 模拟柱层析分离过程 + # 假设处理时间基于流速和柱子长度 + separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + + steps = 20 # 分20个步骤模拟分离过程 + step_time = separation_time / steps + + for i in range(steps): + await asyncio.sleep(step_time) + + progress = (i + 1) / steps * 100 + volume_processed = (i + 1) * 5.0 # 假设每步处理5mL + + # 更新状态 + self.data.update({ + "progress": progress, + "processed_volume": volume_processed, + "current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL" + }) + + self.logger.info(f"Column separation progress: {progress:.1f}%") + + # 分离完成 + self.data.update({ + "status": "Idle", + "column_state": "Ready", + "current_status": "Column separation completed", + "progress": 100.0 + }) + + self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def column_state(self) -> str: + return self.data.get("column_state", "Unknown") + + @property + def current_flow_rate(self) -> float: + return self.data.get("current_flow_rate", 0.0) + + @property + def max_flow_rate(self) -> float: + return self.data.get("max_flow_rate", 0.0) + + @property + def column_length(self) -> float: + return self.data.get("column_length", 0.0) + + @property + def column_diameter(self) -> float: + return self.data.get("column_diameter", 0.0) + + @property + def processed_volume(self) -> float: + return self.data.get("processed_volume", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def current_status(self) -> str: + return self.data.get("current_status", "Ready") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py new file mode 100644 index 00000000..ca2e8b2c --- /dev/null +++ b/unilabos/devices/virtual/virtual_filter.py @@ -0,0 +1,221 @@ +import asyncio +import logging +import time as time_module +from typing import Dict, Any, Optional + + +class VirtualFilter: + """Virtual filter device - 完全按照 Filter.action 规范""" + + 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_filter" + self.config = config or {} + self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}") + self.data = {} + + # 从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', 100.0) + self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) + self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0) + + # 处理其他kwargs参数 + skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'} + 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 filter""" + self.logger.info(f"Initializing virtual filter {self.device_id}") + + # 按照 Filter.action 的 feedback 字段初始化 + self.data.update({ + "status": "Idle", + "progress": 0.0, # Filter.action feedback + "current_temp": 25.0, # Filter.action feedback + "filtered_volume": 0.0, # Filter.action feedback + "current_status": "Ready for filtration", # Filter.action feedback + "message": "Ready for filtration" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual filter""" + self.logger.info(f"Cleaning up virtual filter {self.device_id}") + + self.data.update({ + "status": "Offline", + "current_status": "System offline", + "message": "System offline" + }) + return True + + async def filter( + self, + vessel: str, + filtrate_vessel: str = "", + stir: bool = False, + stir_speed: float = 300.0, + temp: float = 25.0, + continue_heatchill: bool = False, + volume: float = 0.0 + ) -> bool: + """Execute filter action - 完全按照 Filter.action 参数""" + self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}") + self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}") + self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}") + + # 验证参数 + if temp > self._max_temp or temp < 4.0: + error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "message": error_msg + }) + return False + + if stir and stir_speed > self._max_stir_speed: + error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "message": error_msg + }) + return False + + if volume > self._max_volume: + error_msg = f"过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "message": error_msg + }) + return False + + # 开始过滤 + filter_volume = volume if volume > 0 else 50.0 + + self.data.update({ + "status": f"过滤中: {vessel}", + "current_temp": temp, + "filtered_volume": 0.0, + "progress": 0.0, + "current_status": f"Filtering {vessel} → {filtrate_vessel}", + "message": f"Starting filtration: {vessel} → {filtrate_vessel}" + }) + + try: + # 过滤过程 - 实时更新进度 + start_time = time_module.time() + # 根据体积和搅拌估算过滤时间 + base_time = filter_volume / 5.0 # 5mL/s 基础速度 + if stir: + base_time *= 0.8 # 搅拌加速过滤 + if temp > 50.0: + base_time *= 0.7 # 高温加速过滤 + filter_time = max(base_time, 10.0) # 最少10秒 + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, filter_time - elapsed) + progress = min(100.0, (elapsed / filter_time) * 100) + current_filtered = (progress / 100.0) * filter_volume + + # 更新状态 - 按照 Filter.action feedback 字段 + status_msg = f"过滤中: {vessel}" + if stir: + status_msg += f" | 搅拌: {stir_speed} RPM" + status_msg += f" | {temp}°C | {progress:.1f}% | 已过滤: {current_filtered:.1f}mL" + + self.data.update({ + "progress": progress, # Filter.action feedback + "current_temp": temp, # Filter.action feedback + "filtered_volume": current_filtered, # Filter.action feedback + "current_status": f"Filtering: {progress:.1f}% complete", # Filter.action feedback + "status": status_msg, + "message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" + }) + + if remaining <= 0: + break + + await asyncio.sleep(1.0) + + # 过滤完成 + final_temp = temp if continue_heatchill else 25.0 + final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}" + if continue_heatchill: + final_status += " | 继续加热搅拌" + + self.data.update({ + "status": final_status, + "progress": 100.0, # Filter.action feedback + "current_temp": final_temp, # Filter.action feedback + "filtered_volume": filter_volume, # Filter.action feedback + "current_status": f"Filtration completed: {filter_volume}mL", # Filter.action feedback + "message": f"Filtration completed: {filter_volume}mL filtered from {vessel}" + }) + + self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}") + return True + + except Exception as e: + self.logger.error(f"Error during filtration: {str(e)}") + self.data.update({ + "status": f"过滤错误: {str(e)}", + "current_status": f"Filtration failed: {str(e)}", + "message": f"Filtration failed: {str(e)}" + }) + return False + + # === 核心状态属性 - 按照 Filter.action feedback 字段 === + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def progress(self) -> float: + """Filter.action feedback 字段""" + return self.data.get("progress", 0.0) + + @property + def current_temp(self) -> float: + """Filter.action feedback 字段""" + return self.data.get("current_temp", 25.0) + + @property + def filtered_volume(self) -> float: + """Filter.action feedback 字段""" + return self.data.get("filtered_volume", 0.0) + + @property + def current_status(self) -> str: + """Filter.action feedback 字段""" + return self.data.get("current_status", "") + + @property + def message(self) -> str: + return self.data.get("message", "") + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def max_volume(self) -> float: + return self._max_volume \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_gas_source.py b/unilabos/devices/virtual/virtual_gas_source.py new file mode 100644 index 00000000..90528826 --- /dev/null +++ b/unilabos/devices/virtual/virtual_gas_source.py @@ -0,0 +1,46 @@ +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 \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py new file mode 100644 index 00000000..541434af --- /dev/null +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -0,0 +1,210 @@ +import asyncio +import logging +import time as time_module # 重命名time模块,避免与参数冲突 +from typing import Dict, Any + +class VirtualHeatChill: + """Virtual heat chill device for HeatChillProtocol testing""" + + 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.device_id = device_id or "unknown_heatchill" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualHeatChill.{self.device_id}") + self.data = {} + + # 从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', 200.0) + self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', -80.0) + self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) + + # 处理其他kwargs参数 + skip_keys = {'port', 'max_temp', 'min_temp', 'max_stir_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 heat chill""" + self.logger.info(f"Initializing virtual heat chill {self.device_id}") + + # 初始化状态信息 + self.data.update({ + "status": "Idle", + "operation_mode": "Idle", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0, + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual heat chill""" + self.logger.info(f"Cleaning up virtual heat chill {self.device_id}") + self.data.update({ + "status": "Offline", + "operation_mode": "Offline", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0 + }) + return True + + async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, + stir_speed: float, purpose: str) -> bool: + """Execute heat chill action - 按实际时间运行,实时更新剩余时间""" + self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + if stir and stir_speed > self._max_stir_speed: + error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + # 确定操作模式 + if temp > 25.0: + operation_mode = "Heating" + status_action = "加热" + elif temp < 25.0: + operation_mode = "Cooling" + status_action = "冷却" + else: + operation_mode = "Maintaining" + status_action = "保温" + + # **修复**: 使用重命名的time模块 + start_time = time_module.time() + total_time = time + + # 开始操作 + stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ + "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {total_time:.0f}s{stir_info}", + "operation_mode": operation_mode, + "is_stirring": stir, + "stir_speed": stir_speed if stir else 0.0, + "remaining_time": total_time, + }) + + # **修复**: 在等待过程中每秒更新剩余时间 + while True: + current_time = time_module.time() # 使用重命名的time模块 + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + + # 更新剩余时间和状态 + self.data.update({ + "remaining_time": remaining, + "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {remaining:.0f}s{stir_info}" + }) + + # 如果时间到了,退出循环 + if remaining <= 0: + break + + # 等待1秒后再次检查 + await asyncio.sleep(1.0) + + # 操作完成 + final_stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ + "status": f"完成: {vessel} 已达到 {temp}°C | 用时: {total_time:.0f}s{final_stir_info}", + "operation_mode": "Completed", + "remaining_time": 0.0, + "is_stirring": False, + "stir_speed": 0.0 + }) + + self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s") + return True + + async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: + """Start continuous heat chill""" + self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + # 确定操作模式 + if temp > 25.0: + operation_mode = "Heating" + status_action = "持续加热" + elif temp < 25.0: + operation_mode = "Cooling" + status_action = "持续冷却" + else: + operation_mode = "Maintaining" + status_action = "恒温保持" + + self.data.update({ + "status": f"启动: {status_action} {vessel} 至 {temp}°C | 持续运行", + "operation_mode": operation_mode, + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": -1.0, # -1 表示持续运行 + }) + + return True + + async def heat_chill_stop(self, vessel: str) -> bool: + """Stop heat chill""" + self.logger.info(f"HeatChillStop: vessel={vessel}") + + self.data.update({ + "status": f"已停止: {vessel} 温控停止", + "operation_mode": "Stopped", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0, + }) + + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Idle") + + @property + def operation_mode(self) -> str: + return self.data.get("operation_mode", "Idle") + + @property + def is_stirring(self) -> bool: + return self.data.get("is_stirring", False) + + @property + def stir_speed(self) -> float: + return self.data.get("stir_speed", 0.0) + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py new file mode 100644 index 00000000..c24b7b1c --- /dev/null +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -0,0 +1,231 @@ +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})" + + def set_valve_position(self, command: Union[int, str]): + """ + 设置阀门位置 - 兼容pump_protocol调用 + 这是set_position的别名方法,用于兼容pump_protocol.py + + Args: + command: 目标位置 (0-8) 或位置字符串 + """ + return self.set_position(command) + + +# 使用示例 +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()}") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_pump.py b/unilabos/devices/virtual/virtual_pump.py new file mode 100644 index 00000000..d134319a --- /dev/null +++ b/unilabos/devices/virtual/virtual_pump.py @@ -0,0 +1,197 @@ +import asyncio +import logging +from typing import Dict, Any, Optional + +class VirtualPump: + """Virtual pump device for transfer and cleaning operations""" + + 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.device_id = device_id or "unknown_pump" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualPump.{self.device_id}") + self.data = {} + + # 从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', 10.0) + + print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===") + + async def initialize(self) -> bool: + """Initialize virtual pump""" + self.logger.info(f"Initializing virtual pump {self.device_id}") + self.data.update({ + "status": "Idle", + "valve_position": 0, + "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: + """Cleanup virtual pump""" + self.logger.info(f"Cleaning up virtual pump {self.device_id}") + return True + + async def transfer(self, from_vessel: str, to_vessel: str, volume: float, + amount: str = "", time: float = 0.0, viscous: bool = False, + rinsing_solvent: str = "", rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, solid: bool = False) -> bool: + """Execute transfer operation""" + self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}") + + # 计算转移时间 + transfer_time = volume / self._transfer_rate if time == 0 else time + + 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 + current_volume = step_volume * (i + 1) + + self.data.update({ + "progress": progress, + "transferred_volume": current_volume, + "current_status": f"Transferring: {progress:.1f}%" + }) + + self.logger.info(f"Transfer progress: {progress:.1f}%") + + self.data.update({ + "status": "Idle", + "current_status": "Transfer completed", + "progress": 100.0, + "transferred_volume": volume + }) + + return True + + async def clean_vessel(self, vessel: str, solvent: str, volume: float, + temp: float, repeats: int = 1) -> bool: + """Execute vessel cleaning operation - matches CleanVessel action""" + self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)") + + # 更新设备状态 + self.data.update({ + "status": "Running", + "from_vessel": f"flask_{solvent}", + "to_vessel": vessel, + "current_status": "Cleaning in progress", + "progress": 0.0, + "transferred_volume": 0.0 + }) + + # 计算清洗时间(基于体积和重复次数) + # 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放) + cleaning_rate = self._transfer_rate / 2 + cleaning_time_per_cycle = volume / cleaning_rate + total_cleaning_time = cleaning_time_per_cycle * repeats + + # 模拟清洗过程 + steps_per_repeat = 10 # 每次重复清洗分10个步骤 + total_steps = steps_per_repeat * repeats + step_time = total_cleaning_time / total_steps + + for repeat in range(repeats): + self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}") + + for step in range(steps_per_repeat): + await asyncio.sleep(step_time) + + # 计算当前进度 + current_step = repeat * steps_per_repeat + step + 1 + progress = (current_step / total_steps) * 100 + + # 计算已处理的体积 + volume_processed = (current_step / total_steps) * volume * repeats + + # 更新状态 + self.data.update({ + "progress": progress, + "transferred_volume": volume_processed, + "current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)" + }) + + self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})") + + # 清洗完成 + self.data.update({ + "status": "Idle", + "current_status": "Cleaning completed successfully", + "progress": 100.0, + "transferred_volume": volume * repeats, + "from_vessel": "", + "to_vessel": "" + }) + + self.logger.info(f"Vessel cleaning completed: {vessel}") + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def valve_position(self) -> int: + return self.data.get("valve_position", 0) + + @property + def current_volume(self) -> float: + return self.data.get("current_volume", 0.0) + + @property + def max_volume(self) -> float: + return self.data.get("max_volume", 0.0) + + @property + def transfer_rate(self) -> float: + return self.data.get("transfer_rate", 0.0) + + @property + def from_vessel(self) -> str: + return self.data.get("from_vessel", "") + + @property + def to_vessel(self) -> str: + return self.data.get("to_vessel", "") + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def transferred_volume(self) -> float: + return self.data.get("transferred_volume", 0.0) + + @property + def current_status(self) -> str: + return self.data.get("current_status", "Ready") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py new file mode 100644 index 00000000..ba01c7b7 --- /dev/null +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -0,0 +1,228 @@ +import asyncio +import logging +import time as time_module +from typing import Dict, Any, Optional + + +class VirtualRotavap: + """Virtual rotary evaporator device - 简化版,只保留核心功能""" + + 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 = {} + + # 从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""" + self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}") + + # 只保留核心状态 + self.data.update({ + "status": "Idle", + "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error + "current_temp": 25.0, + "target_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, # 大气压 + "evaporated_volume": 0.0, + "progress": 0.0, + "remaining_time": 0.0, + "message": "Ready for evaporation" + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual rotary evaporator""" + self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}") + + self.data.update({ + "status": "Offline", + "rotavap_state": "Offline", + "current_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": "System offline" + }) + return True + + async def evaporate( + self, + vessel: str, + pressure: float = 0.1, + temp: float = 60.0, + time: float = 1800.0, # 30分钟默认 + stir_speed: float = 100.0 + ) -> bool: + """Execute evaporate action - 简化的蒸发流程""" + self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM") + + # 验证参数 + if temp > self._max_temp or temp < 10.0: + error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) + return False + + if stir_speed > self._max_rotation_speed or stir_speed < 10.0: + error_msg = f"旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) + return False + + if pressure < 0.01 or pressure > 1.0: + error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) + return False + + # 开始蒸发 + self.data.update({ + "status": f"蒸发中: {vessel}", + "rotavap_state": "Evaporating", + "current_temp": temp, + "target_temp": temp, + "rotation_speed": stir_speed, + "vacuum_pressure": pressure, + "remaining_time": time, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" + }) + + try: + # 蒸发过程 - 实时更新进度 + start_time = time_module.time() + total_time = time + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + progress = min(100.0, (elapsed / total_time) * 100) + + # 模拟蒸发体积 + evaporated_vol = progress * 0.8 # 假设最多蒸发80mL + + # 更新状态 + self.data.update({ + "remaining_time": remaining, + "progress": progress, + "evaporated_volume": evaporated_vol, + "status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s", + "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining" + }) + + # 时间到了,退出循环 + if remaining <= 0: + break + + # 每秒更新一次 + await asyncio.sleep(1.0) + + # 蒸发完成 + final_evaporated = 80.0 + self.data.update({ + "status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL", + "rotavap_state": "Completed", + "evaporated_volume": final_evaporated, + "progress": 100.0, + "remaining_time": 0.0, + "current_temp": 25.0, # 冷却下来 + "rotation_speed": 0.0, # 停止旋转 + "vacuum_pressure": 1.0, # 恢复大气压 + "message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}" + }) + + self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}") + return True + + except Exception as e: + # 出错处理 + self.logger.error(f"Error during evaporation: {str(e)}") + + self.data.update({ + "status": f"蒸发错误: {str(e)}", + "rotavap_state": "Error", + "current_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": f"Evaporation failed: {str(e)}" + }) + return False + + # === 核心状态属性 === + @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 rotation_speed(self) -> float: + return self.data.get("rotation_speed", 0.0) + + @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", "") + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def max_rotation_speed(self) -> float: + return self._max_rotation_speed + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) diff --git a/unilabos/devices/virtual/virtual_separator.py b/unilabos/devices/virtual/virtual_separator.py new file mode 100644 index 00000000..e1c46128 --- /dev/null +++ b/unilabos/devices/virtual/virtual_separator.py @@ -0,0 +1,184 @@ +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", "") diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py new file mode 100644 index 00000000..f25cc843 --- /dev/null +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -0,0 +1,147 @@ +import time +import asyncio +from typing import Union + + +class VirtualSolenoidValve: + """ + 虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态 + """ + def __init__(self, device_id: str = None, config: dict = None, **kwargs): + # 从配置中获取参数,提供默认值 + if config is None: + config = {} + + self.device_id = device_id + self.port = config.get("port", "VIRTUAL") + self.voltage = config.get("voltage", 12.0) + self.response_time = config.get("response_time", 0.1) + + # 状态属性 + self._status = "Idle" + self._valve_state = "Closed" # "Open" or "Closed" + self._is_open = False + + async def initialize(self) -> bool: + """初始化设备""" + self._status = "Idle" + return True + + async def cleanup(self) -> bool: + """清理资源""" + return True + + @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" + + async def set_valve_position(self, command: str = None, **kwargs): + """ + 设置阀门位置 - ROS动作接口 + + Args: + command: "OPEN"/"CLOSED" 或其他控制命令 + """ + if command is None: + return {"success": False, "message": "Missing command parameter"} + + print(f"SOLENOID_VALVE: {self.device_id} 接收到命令: {command}") + + self._status = "Busy" + + # 模拟阀门响应时间 + await asyncio.sleep(self.response_time) + + # 处理不同的命令格式 + if isinstance(command, str): + cmd_upper = command.upper() + if cmd_upper in ["OPEN", "ON", "TRUE", "1"]: + self._is_open = True + self._valve_state = "Open" + result_msg = f"Valve {self.device_id} opened" + elif cmd_upper in ["CLOSED", "CLOSE", "OFF", "FALSE", "0"]: + self._is_open = False + self._valve_state = "Closed" + result_msg = f"Valve {self.device_id} closed" + else: + # 可能是端口名称,处理路径设置 + # 对于简单电磁阀,任何非关闭命令都视为开启 + self._is_open = True + self._valve_state = "Open" + result_msg = f"Valve {self.device_id} set to position: {command}" + else: + self._status = "Error" + return {"success": False, "message": "Invalid command type"} + + self._status = "Idle" + print(f"SOLENOID_VALVE: {result_msg}") + + return { + "success": True, + "message": result_msg, + "valve_position": self.get_valve_position() + } + + async def open(self, **kwargs): + """打开电磁阀 - ROS动作接口""" + return await self.set_valve_position(command="OPEN") + + async def close(self, **kwargs): + """关闭电磁阀 - ROS动作接口""" + return await self.set_valve_position(command="CLOSED") + + async def set_state(self, command: Union[bool, str], **kwargs): + """ + 设置阀门状态 - 兼容 SendCmd 类型 + + Args: + command: True/False 或 "open"/"close" + """ + if isinstance(command, bool): + cmd_str = "OPEN" if command else "CLOSED" + elif isinstance(command, str): + cmd_str = command + else: + return {"success": False, "message": "Invalid command type"} + + return await self.set_valve_position(command=cmd_str) + + 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 { + "device_id": self.device_id, + "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() + } + + async def reset(self): + """重置阀门到关闭状态""" + return await self.close() \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py new file mode 100644 index 00000000..874f997a --- /dev/null +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -0,0 +1,215 @@ +import asyncio +import logging +import time as time_module +from typing import Dict, Any + +class VirtualStirrer: + """Virtual stirrer device for StirProtocol testing - 功能完整版""" + + 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.device_id = device_id or "unknown_stirrer" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualStirrer.{self.device_id}") + self.data = {} + + # 从config或kwargs中获取配置参数 + self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') + self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1500.0) + self._min_speed = self.config.get('min_speed') or kwargs.get('min_speed', 50.0) + + # 处理其他kwargs参数 + skip_keys = {'port', 'max_speed', 'min_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 stirrer""" + self.logger.info(f"Initializing virtual stirrer {self.device_id}") + + # 初始化状态信息 + self.data.update({ + "status": "Idle", + "operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error + "current_vessel": "", # 当前搅拌的容器 + "current_speed": 0.0, # 当前搅拌速度 + "is_stirring": False, # 是否正在搅拌 + "remaining_time": 0.0, # 剩余时间 + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual stirrer""" + self.logger.info(f"Cleaning up virtual stirrer {self.device_id}") + self.data.update({ + "status": "Offline", + "operation_mode": "Offline", + "current_vessel": "", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": 0.0, + }) + return True + + async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool: + """Execute stir action - 定时搅拌 + 沉降""" + self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s") + + # 验证参数 + if stir_speed > self._max_speed or stir_speed < self._min_speed: + error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + # === 第一阶段:搅拌 === + start_time = time_module.time() + total_stir_time = stir_time + + self.data.update({ + "status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s", + "operation_mode": "Stirring", + "current_speed": stir_speed, + "is_stirring": True, + "remaining_time": total_stir_time, + }) + + # 搅拌过程 - 实时更新剩余时间 + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_stir_time - elapsed) + + # 更新状态 + self.data.update({ + "remaining_time": remaining, + "status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s" + }) + + # 搅拌时间到了 + if remaining <= 0: + break + + await asyncio.sleep(1.0) + + # === 第二阶段:沉降(如果需要)=== + if settling_time > 0: + start_settling_time = time_module.time() + total_settling_time = settling_time + + self.data.update({ + "status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s", + "operation_mode": "Settling", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": total_settling_time, + }) + + # 沉降过程 - 实时更新剩余时间 + while True: + current_time = time_module.time() + elapsed = current_time - start_settling_time + remaining = max(0, total_settling_time - elapsed) + + # 更新状态 + self.data.update({ + "remaining_time": remaining, + "status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s" + }) + + # 沉降时间到了 + if remaining <= 0: + break + + await asyncio.sleep(1.0) + + # === 操作完成 === + settling_info = f" | 沉降: {settling_time:.0f}s" if settling_time > 0 else "" + self.data.update({ + "status": f"完成: 搅拌 {stir_speed} RPM, {stir_time:.0f}s{settling_info}", + "operation_mode": "Completed", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": 0.0, + }) + + self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s") + return True + + async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool: + """Start stir action - 开始持续搅拌""" + self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}") + + # 验证参数 + if stir_speed > self._max_speed or stir_speed < self._min_speed: + error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + self.data.update({ + "status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}", + "operation_mode": "Stirring", + "current_vessel": vessel, + "current_speed": stir_speed, + "is_stirring": True, + "remaining_time": -1.0, # -1 表示持续运行 + }) + + return True + + async def stop_stir(self, vessel: str) -> bool: + """Stop stir action - 停止搅拌""" + self.logger.info(f"StopStir: vessel={vessel}") + + current_speed = self.data.get("current_speed", 0.0) + + self.data.update({ + "status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", + "operation_mode": "Stopped", + "current_vessel": "", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": 0.0, + }) + + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Idle") + + @property + def operation_mode(self) -> str: + return self.data.get("operation_mode", "Idle") + + @property + def current_vessel(self) -> str: + return self.data.get("current_vessel", "") + + @property + def current_speed(self) -> float: + return self.data.get("current_speed", 0.0) + + @property + def is_stirring(self) -> bool: + return self.data.get("is_stirring", False) + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py new file mode 100644 index 00000000..a2cba9c4 --- /dev/null +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -0,0 +1,328 @@ +import asyncio +import time +from enum import Enum +from typing import Union, Optional +import logging + + +class VirtualPumpMode(Enum): + Normal = 0 + AccuratePos = 1 + AccuratePosVel = 2 + + +class VirtualTransferPump: + """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件""" + + def __init__(self, device_id: str = None, config: dict = None, **kwargs): + """ + 初始化虚拟转移泵 + + Args: + device_id: 设备ID + config: 配置字典,包含max_volume, port等参数 + **kwargs: 其他参数,确保兼容性 + """ + self.device_id = device_id or "virtual_transfer_pump" + + # 从config或kwargs中获取参数,确保类型正确 + if config: + self.max_volume = float(config.get('max_volume', 25.0)) + self.port = config.get('port', 'VIRTUAL') + else: + self.max_volume = float(kwargs.get('max_volume', 25.0)) + self.port = kwargs.get('port', 'VIRTUAL') + + self._transfer_rate = float(kwargs.get('transfer_rate', 0)) + self.mode = kwargs.get('mode', VirtualPumpMode.Normal) + + # 状态变量 - 确保都是正确类型 + self._status = "Idle" + self._position = 0.0 # float + self._max_velocity = 5.0 # float + self._current_volume = 0.0 # float + + self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") + + async def initialize(self) -> bool: + """初始化虚拟泵""" + self.logger.info(f"Initializing virtual pump {self.device_id}") + self._status = "Idle" + self._position = 0.0 + self._current_volume = 0.0 + return True + + async def cleanup(self) -> bool: + """清理虚拟泵""" + self.logger.info(f"Cleaning up virtual pump {self.device_id}") + self._status = "Idle" + return True + + # 基本属性 + @property + def status(self) -> str: + return self._status + + @property + def position(self) -> float: + """当前柱塞位置 (ml)""" + return self._position + + @property + def current_volume(self) -> float: + """当前注射器中的体积 (ml)""" + return self._current_volume + + @property + def max_velocity(self) -> float: + return self._max_velocity + + @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") + + def get_status(self) -> str: + """获取泵状态""" + return self._status + + async def _simulate_operation(self, duration: float): + """模拟操作延时""" + self._status = "Busy" + await asyncio.sleep(duration) + self._status = "Idle" + + def _calculate_duration(self, volume: float, velocity: float = None) -> float: + """计算操作持续时间""" + if velocity is None: + velocity = self._max_velocity + return abs(volume) / velocity + + # 新的set_position方法 - 专门用于SetPumpPosition动作 + async def set_position(self, position: float, max_velocity: float = None): + """ + 移动到绝对位置 - 专门用于SetPumpPosition动作 + + Args: + position (float): 目标位置 (ml) + max_velocity (float): 移动速度 (ml/s) + + Returns: + dict: 符合SetPumpPosition.action定义的结果 + """ + try: + # 验证并转换参数 + target_position = float(position) + velocity = float(max_velocity) if max_velocity is not None else self._max_velocity + + # 限制位置在有效范围内 + target_position = max(0.0, min(float(self.max_volume), target_position)) + + # 计算移动距离和时间 + volume_to_move = abs(target_position - self._position) + duration = self._calculate_duration(volume_to_move, velocity) + + self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s") + + # 模拟移动过程 + start_position = self._position + steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步 + step_duration = duration / steps if steps > 1 else duration + + for i in range(steps + 1): + # 计算当前位置和进度 + progress = (i / steps) * 100 if steps > 0 else 100 + current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + + # 更新状态 + self._status = "Moving" if i < steps else "Idle" + self._position = current_pos + self._current_volume = current_pos + + # 等待一小步时间 + if i < steps and step_duration > 0: + await asyncio.sleep(step_duration) + + # 确保最终位置准确 + self._position = target_position + self._current_volume = target_position + self._status = "Idle" + + self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml") + + # 返回符合action定义的结果 + return { + "success": True, + "message": f"Successfully moved to position {self._position} ml" + } + + except Exception as e: + error_msg = f"Failed to set position: {str(e)}" + self.logger.error(error_msg) + return { + "success": False, + "message": error_msg + } + + # 其他泵操作方法 + 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): + """吸液操作""" + await self.pull_plunger(volume, velocity) + + async def dispense(self, volume: float, velocity: float = None): + """排液操作""" + await self.push_plunger(volume, velocity) + + async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): + """转移操作(先吸后排)""" + # 吸液 + 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"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})" + + def __repr__(self): + return self.__str__() + + +# 使用示例 +async def demo(): + """虚拟泵使用示例""" + pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0}) + + await pump.initialize() + + print(f"Initial state: {pump}") + + # 测试set_position方法 + result = await pump.set_position(10.0, max_velocity=2.0) + print(f"Set position result: {result}") + print(f"After setting position to 10ml: {pump}") + + # 吸液测试 + await pump.aspirate(5.0, velocity=2.0) + print(f"After aspirating 5ml: {pump}") + + # 清空测试 + result = await pump.set_position(0.0) + print(f"Empty result: {result}") + print(f"After emptying: {pump}") + + print("\nPump info:", pump.get_pump_info()) + + +if __name__ == "__main__": + asyncio.run(demo()) diff --git a/unilabos/devices/virtual/virtual_vacuum_pump.py b/unilabos/devices/virtual/virtual_vacuum_pump.py new file mode 100644 index 00000000..ec355b68 --- /dev/null +++ b/unilabos/devices/virtual/virtual_vacuum_pump.py @@ -0,0 +1,47 @@ +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 \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_valve.py b/unilabos/devices/virtual/virtual_valve.py new file mode 100644 index 00000000..a665e005 --- /dev/null +++ b/unilabos/devices/virtual/virtual_valve.py @@ -0,0 +1,105 @@ +import asyncio +import logging +from typing import Dict, Any + +class VirtualValve: + """Virtual valve device for AddProtocol testing""" + + 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.device_id = device_id or "unknown_valve" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualValve.{self.device_id}") + self.data = {} + + print(f"=== VirtualValve {self.device_id} is being created! ===") + print(f"=== Config: {self.config} ===") + print(f"=== Kwargs: {kwargs} ===") + + # 处理所有配置参数,包括port + self.port = self.config.get('port', 'VIRTUAL') + self.positions = self.config.get('positions', 6) + self.current_position = 0 + + # 忽略其他可能的kwargs参数 + for key, value in kwargs.items(): + if not hasattr(self, key): + setattr(self, key, value) + + async def initialize(self) -> bool: + """Initialize virtual valve""" + print(f"=== VirtualValve {self.device_id} initialize() called! ===") + self.logger.info(f"Initializing virtual valve {self.device_id}") + self.data.update({ + "status": "Idle", + "valve_state": "Closed", + "current_position": 0, + "target_position": 0, + "max_positions": self.positions + }) + return True + + async def cleanup(self) -> bool: + """Cleanup virtual valve""" + self.logger.info(f"Cleaning up virtual valve {self.device_id}") + return True + + async def set_position(self, position: int) -> bool: + """Set valve position - matches SendCmd action""" + if 0 <= position <= self.positions: + self.logger.info(f"Setting valve position to {position}") + self.data.update({ + "target_position": position, + "current_position": position, + "valve_state": "Open" if position > 0 else "Closed" + }) + return True + else: + self.logger.error(f"Invalid position {position}. Must be 0-{self.positions}") + return False + + async def open(self) -> bool: + """Open valve - matches EmptyIn action""" + self.logger.info("Opening valve") + self.data.update({ + "valve_state": "Open", + "current_position": 1 + }) + return True + + async def close(self) -> bool: + """Close valve - matches EmptyIn action""" + self.logger.info("Closing valve") + self.data.update({ + "valve_state": "Closed", + "current_position": 0 + }) + return True + + # 状态属性 + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def valve_state(self) -> str: + return self.data.get("valve_state", "Unknown") + + @property + def current_position(self) -> int: + return self.data.get("current_position", 0) + + @property + def target_position(self) -> int: + return self.data.get("target_position", 0) + + @property + def max_positions(self) -> int: + return self.data.get("max_positions", 6) \ No newline at end of file diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 7bff6dc5..16947e69 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -33,19 +33,19 @@ class CleanProtocol(BaseModel): class SeparateProtocol(BaseModel): - purpose: str # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'. - product_phase: str # 'top' or 'bottom'. Phase that product will be in. - from_vessel: str #Contents of from_vessel are transferred to separation_vessel and separation is performed. - separation_vessel: str # Vessel in which separation of phases will be carried out. - to_vessel: str # Vessel to send product phase to. - waste_phase_to_vessel: str # Optional. Vessel to send waste phase to. - solvent: str # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases. - solvent_volume: float # Optional. Volume of solvent to add. - through: str # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. - repeats: int # Optional. Number of separations to perform. - stir_time: float # Optional. Time stir for after adding solvent, before separation of phases. - stir_speed: float # Optional. Speed to stir at after adding solvent, before separation of phases. - settling_time: float # Optional. Time + purpose: str + product_phase: str + from_vessel: str + separation_vessel: str + to_vessel: str + waste_phase_to_vessel: str + solvent: str + solvent_volume: float + through: str + repeats: int + stir_time: float + stir_speed: float + settling_time: float class EvaporateProtocol(BaseModel): @@ -68,6 +68,123 @@ class AGVTransferProtocol(BaseModel): from_repo_position: str to_repo_position: str +#=============新添加的新的协议================ +class AddProtocol(BaseModel): + vessel: str + reagent: str + volume: float + mass: float + amount: str + time: float + stir: bool + stir_speed: float + viscous: bool + purpose: str -__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol"] +class CentrifugeProtocol(BaseModel): + vessel: str + speed: float + time: float + temp: float + +class FilterProtocol(BaseModel): + vessel: str + filtrate_vessel: str + stir: bool + stir_speed: float + temp: float + continue_heatchill: bool + volume: float + +class HeatChillProtocol(BaseModel): + vessel: str + temp: float + time: float + stir: bool + stir_speed: float + purpose: str + +class HeatChillStartProtocol(BaseModel): + vessel: str + temp: float + purpose: str + +class HeatChillStopProtocol(BaseModel): + vessel: str + +class StirProtocol(BaseModel): + stir_time: float + stir_speed: float + settling_time: float + +class StartStirProtocol(BaseModel): + vessel: str + stir_speed: float + purpose: str + +class StopStirProtocol(BaseModel): + vessel: str + +class TransferProtocol(BaseModel): + 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 + +class CleanVesselProtocol(BaseModel): + vessel: str + solvent: str + volume: float + temp: float + repeats: int = 1 + +class DissolveProtocol(BaseModel): + vessel: str + solvent: str + volume: float + amount: str = "" + temp: float = 25.0 + time: float = 0.0 + stir_speed: float = 0.0 + +class FilterThroughProtocol(BaseModel): + from_vessel: str + to_vessel: str + filter_through: str + eluting_solvent: str = "" + eluting_volume: float = 0.0 + eluting_repeats: int = 0 + residence_time: float = 0.0 + +class RunColumnProtocol(BaseModel): + from_vessel: str + to_vessel: str + column: str + +class WashSolidProtocol(BaseModel): + vessel: str + solvent: str + volume: float + filtrate_vessel: str = "" + temp: float = 25.0 + stir: bool = False + stir_speed: float = 0.0 + time: float = 0.0 + repeats: int = 1 + +__all__ = [ + "Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", + "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", + "CentrifugeProtocol", "AddProtocol", "FilterProtocol", + "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", + "StirProtocol", "StartStirProtocol", "StopStirProtocol", + "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", + "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol" +] # End Protocols diff --git a/unilabos/registry/device_comms/modbus_ioboard.yaml b/unilabos/registry/device_comms/modbus_ioboard.yaml index b1d04eec..fcea4d7e 100644 --- a/unilabos/registry/device_comms/modbus_ioboard.yaml +++ b/unilabos/registry/device_comms/modbus_ioboard.yaml @@ -1,7 +1,7 @@ io_snrd: description: IO Board with 16 IOs class: - module: unilabos.device_comms.SRND_16_IO:SRND_16_IO + module: ilabos.device_comms.SRND_16_IO:SRND_16_IO type: python hardware_interface: name: modbus_client diff --git a/unilabos/registry/devices/mock_devices.yaml b/unilabos/registry/devices/mock_devices.yaml new file mode 100644 index 00000000..93f52a85 --- /dev/null +++ b/unilabos/registry/devices/mock_devices.yaml @@ -0,0 +1,892 @@ +mock_chiller: + description: Mock Chiller Device + class: + module: unilabos.devices.mock.mock_chiller:MockChiller + type: python + status_types: + current_temperature: Float64 + target_temperature: Float64 + status: String + is_cooling: Bool + is_heating: Bool + vessel: String # 新增 + purpose: String # 新增 + action_value_mappings: + emergency_stop: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: {} + result: + success: success + status: status + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: {} + result: + success: success + status: status + schema: + type: object + properties: + current_temperature: + type: number + description: Current temperature of the chiller in °C + target_temperature: + type: number + description: Target temperature setting in °C + status: + type: string + description: Current status of the device + is_cooling: + type: boolean + description: Whether the device is actively cooling + is_heating: + type: boolean + description: Whether the device is actively heating + vessel: # 新增 + type: string + description: Current vessel being processed + purpose: # 新增 + type: string + description: Purpose of the current operation + required: + - current_temperature + - target_temperature + - status + - vessel + - purpose + additionalProperties: false +mock_filter: + description: Mock Filter Device + class: + module: unilabos.devices.mock.mock_filter:MockFilter + type: python + status_types: + status: String + is_filtering: Bool + flow_rate: Float64 + pressure_drop: Float64 + filter_life: Float64 + vessel: String + filtrate_vessel: String + filtered_volume: Float64 + progress: Float64 + stir: Bool + stir_speed: Float64 + temperature: Float64 + continue_heatchill: Bool + target_volume: Float64 + action_value_mappings: + filter: + type: Filter + goal: + vessel: vessel + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + continue_heatchill: continue_heatchill + volume: volume + feedback: + progress: progress + current_temp: current_temp + filtered_volume: filtered_volume + current_status: current_status + result: + success: success + message: message + stop_filtering: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + replace_filter: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the filter + is_filtering: + type: boolean + description: Whether the filter is actively filtering + flow_rate: + type: number + description: Current flow rate in L/min + pressure_drop: + type: number + description: Pressure drop across the filter in Pa + filter_life: + type: number + description: Remaining filter life percentage + power_on: + type: boolean + description: Power state of the device + required: + - status + - is_filtering + - flow_rate + - filter_life + - vessel + - filtrate_vessel + - filtered_volume + - progress + additionalProperties: false +mock_heater: + description: Mock Heater Device + class: + module: unilabos.devices.mock.mock_heater:MockHeater + type: python + status_types: + current_temperature: Float64 + target_temperature: Float64 + status: String + is_heating: Bool + heating_power: Float64 + max_temperature: Float64 + vessel: String + purpose: String + stir: Bool + stir_speed: Float64 + action_value_mappings: + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: + status: status + result: + success: success + heat_chill: + type: HeatChill + goal: + vessel: vessel + temp: temp + time: time + stir: stir + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + emergency_stop: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + current_temperature: + type: number + description: Current temperature of the heater in °C + target_temperature: + type: number + description: Target temperature setting in °C + status: + type: string + description: Current status of the device + is_heating: + type: boolean + description: Whether the device is actively heating + heating_power: + type: number + description: Current heating power percentage + max_temperature: + type: number + description: Maximum temperature limit + vessel: + type: string + description: Current vessel being heated + purpose: + type: string + description: Purpose of the heating operation + stir: + type: boolean + description: Whether stirring is enabled + stir_speed: + type: number + description: Current stirring speed + required: + - current_temperature + - target_temperature + - status + - vessel + - purpose + additionalProperties: false +mock_pump: + description: Mock Pump Device + class: + module: unilabos.devices.mock.mock_pump:MockPump + type: python + status_types: + status: String + pump_state: String + flow_rate: Float64 + target_flow_rate: Float64 + pressure: Float64 + total_volume: Float64 + max_flow_rate: Float64 + max_pressure: Float64 + from_vessel: String + to_vessel: String + transfer_volume: Float64 + amount: String + transfer_time: Float64 + is_viscous: Bool + rinsing_solvent: String + rinsing_volume: Float64 + rinsing_repeats: Int32 + is_solid: Bool + time_spent: Float64 + time_remaining: Float64 + current_device: String + action_value_mappings: + pump_transfer: + type: PumpTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + status: status + current_device: current_device + time_spent: time_spent + time_remaining: time_remaining + result: + success: success + pause_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + resume_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + reset_volume_counter: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the pump + pump_state: + type: string + description: Pump operation state (Running/Stopped/Paused) + flow_rate: + type: number + description: Current flow rate in mL/min + target_flow_rate: + type: number + description: Target flow rate in mL/min + pressure: + type: number + description: Current pressure in bar + total_volume: + type: number + description: Total accumulated volume in mL + max_flow_rate: + type: number + description: Maximum flow rate in mL/min + max_pressure: + type: number + description: Maximum pressure in bar + from_vessel: + type: string + description: Source vessel for transfer + to_vessel: + type: string + description: Target vessel for transfer + transfer_volume: + type: number + description: Volume to transfer in mL + amount: + type: string + description: Amount description + transfer_time: + type: number + description: Transfer time in seconds + is_viscous: + type: boolean + description: Whether the liquid is viscous + rinsing_solvent: + type: string + description: Solvent used for rinsing + rinsing_volume: + type: number + description: Volume used for rinsing + rinsing_repeats: + type: integer + description: Number of rinsing cycles + is_solid: + type: boolean + description: Whether transferring solid material + current_device: + type: string + description: Current device identifier + required: + - status + - pump_state + - flow_rate + - from_vessel + - to_vessel + additionalProperties: false +mock_rotavap: + description: Mock Rotavap Device + class: + module: unilabos.devices.mock.mock_rotavap:MockRotavap + type: python + status_types: + status: String + rotate_state: String + rotate_time: Float64 + rotate_speed: Float64 + pump_state: String + pump_time: Float64 + vacuum_level: Float64 + temperature: Float64 + target_temperature: Float64 + success: String + action_value_mappings: + set_timer: + type: StrSingleInput + goal: + string: command + feedback: {} + result: + success: success + set_rotate_time: + type: FloatSingleInput + goal: + float_in: time_seconds + feedback: {} + result: + success: success + set_pump_time: + type: FloatSingleInput + goal: + float_in: time_seconds + feedback: {} + result: + success: success + set_rotate_speed: + type: FloatSingleInput + goal: + float_in: speed + feedback: {} + result: + success: success + set_temperature: + type: FloatSingleInput + goal: + float_in: temperature + feedback: {} + result: + success: success + start_rotation: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + start_pump: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the rotavap + rotate_state: + type: string + description: Rotation state (Running/Stopped) + rotate_time: + type: number + description: Remaining rotation time in seconds + rotate_speed: + type: number + description: Rotation speed in rpm + pump_state: + type: string + description: Pump state (Running/Stopped) + pump_time: + type: number + description: Remaining pump time in seconds + vacuum_level: + type: number + description: Current vacuum level in mbar + temperature: + type: number + description: Current water bath temperature + target_temperature: + type: number + description: Target water bath temperature + success: + type: string + description: Operation success status + required: + - status + - rotate_time + - pump_time + - temperature + additionalProperties: false +mock_separator: + description: Simplified Mock Separator Device + class: + module: unilabos.devices.mock.mock_separator:MockSeparator + type: python + status_types: + status: String + settling_time: Float64 + valve_state: String + shake_time: Float64 + shake_status: String + current_device: String + purpose: String + product_phase: String + from_vessel: String + separation_vessel: String + to_vessel: String + waste_phase_to_vessel: String + solvent: String + solvent_volume: Float64 + through: String + repeats: Int32 + stir_time: Float64 + stir_speed: Float64 + time_spent: Float64 + time_remaining: Float64 + action_value_mappings: + separate: + type: Separate + goal: + purpose: purpose + product_phase: product_phase + from_vessel: from_vessel + separation_vessel: separation_vessel + to_vessel: to_vessel + waste_phase_to_vessel: waste_phase_to_vessel + solvent: solvent + solvent_volume: solvent_volume + through: through + repeats: repeats + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + status: status + current_device: current_device + time_spent: time_spent + time_remaining: time_remaining + result: + success: success + shake: + type: FloatSingleInput + goal: + float_in: shake_time + feedback: + status: status + result: + success: success + stop_operations: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + set_valve: + type: StrSingleInput + goal: + string: command + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the separator + settling_time: + type: number + description: Settling time in seconds + valve_state: + type: string + description: Valve state (Open/Closed) + shake_time: + type: number + description: Remaining shake time in seconds + shake_status: + type: string + description: Current shake state + purpose: + type: string + description: Separation purpose (wash/extract) + product_phase: + type: string + description: Product phase (top/bottom) + from_vessel: + type: string + description: Source vessel + separation_vessel: + type: string + description: Vessel for separation + to_vessel: + type: string + description: Target vessel + required: + - status + - valve_state + - shake_status + - current_device + additionalProperties: false +mock_solenoid_valve: + description: Mock Solenoid Valve Device + class: + module: unilabos.devices.mock.mock_solenoid_valve:MockSolenoidValve + type: python + status_types: + status: String + valve_status: String + action_value_mappings: + set_valve_status: + type: StrSingleInput + goal: + string: status + feedback: {} + result: + success: success + open_valve: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + close_valve: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the valve + valve_status: + type: string + description: Valve status (Open/Closed) + required: + - status + - valve_status + additionalProperties: false +mock_stirrer: + description: Mock Stirrer Device + class: + module: unilabos.devices.mock.mock_stirrer:MockStirrer + type: python + status_types: + status: String + stir_speed: Float64 + target_stir_speed: Float64 + stir_state: String + temperature: Float64 + target_temperature: Float64 + heating_state: String + heating_power: Float64 + max_stir_speed: Float64 + max_temperature: Float64 + action_value_mappings: + set_stir_speed: + type: FloatSingleInput + goal: + float_in: speed + feedback: {} + result: + success: success + set_temperature: + type: FloatSingleInput + goal: + float_in: temperature + feedback: {} + result: + success: success + start_stirring: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + stop_stirring: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + heating_control: + type: StrSingleInput + goal: + string: heating_state + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the stirrer + stir_speed: + type: number + description: Current stirring speed in rpm + target_stir_speed: + type: number + description: Target stirring speed in rpm + stir_state: + type: string + description: Stirring state (Running/Stopped) + temperature: + type: number + description: Current temperature in °C + target_temperature: + type: number + description: Target temperature in °C + heating_state: + type: string + description: Heating state (On/Off) + heating_power: + type: number + description: Current heating power percentage + max_stir_speed: + type: number + description: Maximum stirring speed in rpm + max_temperature: + type: number + description: Maximum temperature in °C + required: + - status + - stir_speed + - temperature + - power_state + additionalProperties: false +mock_stirrer_new: + description: Mock Stirrer Device (Copy Version) + class: + module: unilabos.devices.mock.mock_stirrer_new:MockStirrer_new + type: python + status_types: + status: String + vessel: String + purpose: String + stir_speed: Float64 + target_stir_speed: Float64 + stir_state: String + stir_time: Float64 + settling_time: Float64 + progress: Float64 + max_stir_speed: Float64 + action_value_mappings: + start_stir: + type: StartStir + goal: + vessel: vessel + stir_speed: stir_speed + purpose: purpose + feedback: + progress: progress + current_speed: stir_speed + current_status: status + result: + success: success + message: message + stir: + type: Stir + goal: + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + status: status + result: + success: success + stop_stir: + type: StopStir + goal: + vessel: vessel + feedback: + progress: progress + current_status: status + result: + success: success + message: message + schema: + type: object + properties: + status: + type: string + vessel: + type: string + purpose: + type: string + stir_speed: + type: number + target_stir_speed: + type: number + stir_state: + type: string + stir_time: + type: number + settling_time: + type: number + progress: + type: number + max_stir_speed: + type: number + required: + - status + - stir_speed + - stir_state + - vessel + additionalProperties: false +mock_vacuum: + description: Mock Vacuum Pump Device + class: + module: unilabos.devices.mock.mock_vacuum:MockVacuum + type: python + status_types: + status: String + power_state: String + pump_state: String + vacuum_level: Float64 + target_vacuum: Float64 + pump_speed: Float64 + pump_efficiency: Float64 + max_pump_speed: Float64 + action_value_mappings: + power_control: + type: StrSingleInput + goal: + string: power_state + feedback: {} + result: + success: success + set_vacuum_level: + type: FloatSingleInput + goal: + float_in: vacuum_level + feedback: {} + result: + success: success + start_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + stop_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + pause_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + resume_vacuum: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + vent_to_atmosphere: + type: EmptyIn + goal: {} + feedback: {} + result: + success: success + schema: + type: object + properties: + status: + type: string + description: Current status of the vacuum pump + power_state: + type: string + description: Power state (On/Off) + pump_state: + type: string + description: Pump operation state (Running/Stopped/Paused) + vacuum_level: + type: number + description: Current vacuum level in mbar + target_vacuum: + type: number + description: Target vacuum level in mbar + pump_speed: + type: number + description: Current pump speed in L/s + pump_efficiency: + type: number + description: Pump efficiency percentage + max_pump_speed: + type: number + description: Maximum pump speed in L/s + required: + - status + - power_state + - pump_state + - vacuum_level + additionalProperties: false diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index fd5dd98e..651a25f8 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -48,14 +48,16 @@ solenoid_valve.mock: feedback: {} result: {} handles: - input: - - handler_key: fluid-input - label: Fluid Input + - handler_key: in + label: in + io_type: target data_type: fluid - output: - - handler_key: fluid-output - label: Fluid Output + side: NORTH + - handler_key: out + label: out + io_type: source data_type: fluid + side: SOUTH init_param_schema: type: object properties: @@ -71,3 +73,13 @@ solenoid_valve: class: module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve type: python + status_types: + status: String + valve_position: String + action_value_mappings: + set_valve_position: + type: StrSingleInput + goal: + string: position + feedback: {} + result: {} \ No newline at end of file diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 1c01b4e8..662ee016 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -62,4 +62,4 @@ tempsensor: command: command feedback: {} result: - success: success \ No newline at end of file + success: success diff --git a/unilabos/registry/devices/vacuum_and_purge.yaml b/unilabos/registry/devices/vacuum_and_purge.yaml index b6108203..84fb58cf 100644 --- a/unilabos/registry/devices/vacuum_and_purge.yaml +++ b/unilabos/registry/devices/vacuum_and_purge.yaml @@ -23,20 +23,12 @@ vacuum_pump.mock: feedback: {} result: {} handles: - 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 + - handler_key: out + label: out data_type: fluid io_type: source - data_source: executor - data_key: fluid_out + data_source: handle + data_key: fluid_in init_param_schema: type: object properties: @@ -72,16 +64,8 @@ gas_source.mock: feedback: {} result: {} handles: - 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 + - handler_key: out + label: out data_type: fluid io_type: source data_source: executor @@ -94,4 +78,4 @@ gas_source.mock: description: "通信端口" default: "COM6" required: - - port + - port \ No newline at end of file diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml new file mode 100644 index 00000000..e25c0d6b --- /dev/null +++ b/unilabos/registry/devices/virtual_device.yaml @@ -0,0 +1,961 @@ +# 虚拟设备清单及连接特性 + +# 1. virtual_pump - 虚拟泵 +# 描述:具有多通道阀门特性的泵,根据valve_position可连接多个容器 +# 连接特性:1个输入口 + 1个输出口 +# 数据类型:fluid(流体连接) + +# 2. virtual_stirrer - 虚拟搅拌器 +# 描述:机械连接设备,提供搅拌功能 +# 连接特性:1个连接点 +# 数据类型:mechanical(机械连接) + +# 3a. virtual_valve - 虚拟八通阀门 +# 描述:8通阀门,可切换流向 +# 连接特性:1个口连接注射泵 + 多个输出口 +# 数据类型:fluid(流体连接) + +# 3b. virtual_solenoid_valve (电磁阀门) +# 描述:简单的开关型电磁阀,只有开启和关闭两个状态 +# 连接特性:1个输入口 + 1个输出口,控制通断 +# 数据类型:fluid(流体连接) + +# 4. virtual_centrifuge - 虚拟离心机 +# 描述:单个样品处理设备,原地处理样品 +# 连接特性:1个输入口 + 1个输出口 +# 数据类型:resource(资源/样品连接) + +# 5. virtual_filter - 虚拟过滤器 +# 描述:分离设备,将样品分离为滤液和滤渣 +# 连接特性:1个输入口 + 2个输出口(滤液和滤渣) +# 数据类型:resource(资源/样品连接) + +# 6. virtual_heatchill - 虚拟加热/冷却器 +# 描述:温控设备,容器直接放置在设备上进行温度控制 +# 连接特性:1个连接点 +# 数据类型:mechanical(机械/物理接触连接) + +# 7. virtual_transfer_pump - 虚拟转移泵(注射器式) +# 描述:注射器式转移泵,通过同一个口吸入和排出液体 +# 连接特性:1个连接点 +# 数据类型:fluid(流体连接) + +# 8. virtual_column - 虚拟色谱柱 +# 描述:分离纯化设备,用于样品纯化 +# 连接特性:1个输入口 + 1个输出口 +# 数据类型:resource(资源/样品连接) + +# 9. virtual_rotavap - 虚拟旋转蒸发仪 +# 描述:旋转蒸发仪用于溶剂蒸发和浓缩,具有加热、旋转和真空功能 +# 连接特性:1个输入口(样品),1个输出口(浓缩物),1个冷凝器出口(回收溶剂) +# 数据类型:resource(资源/样品连接) + +# 10. virtual_separator - 虚拟分液器 +# 描述:分液器用于两相液体的分离,可进行萃取和洗涤操作 +# 连接特性:1个输入口(混合液),2个输出口(上相和下相) +# 数据类型:fluid(流体连接) + +# 11. virtual_vacuum_pump - 虚拟真空泵 +# 描述:真空泵设备,用于抽真空操作和真空/充气循环 +# 连接特性:1个输出口(连接需要抽真空的系统) +# 数据类型:fluid(流体连接) + +# 12. virtual_gas_source - 虚拟气源 +# 描述:气源设备,用于充气操作和真空/充气循环 +# 连接特性:1个输出口(向系统提供加压气体) +# 数据类型:fluid(流体连接) + +virtual_pump: + description: Virtual Pump for PumpTransferProtocol Testing + class: + module: unilabos.devices.virtual.virtual_pump:VirtualPump + type: python + status_types: + status: String + position: Float64 + valve_position: Int32 + max_volume: Float64 + current_volume: Float64 + action_value_mappings: + transfer: + type: PumpTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + status: status + result: + success: success + set_valve_position: + type: FloatSingleInput + goal: + float_in: valve_position + feedback: + status: status + result: + success: success + handles: + - handler_key: pumpio + label: pumpio + data_type: fluid + io_type: source + data_source: handle + data_key: fluid_in + description: "泵的输出口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_volume: + type: number + default: 25.0 + additionalProperties: false + +virtual_stirrer: + description: Virtual Stirrer for StirProtocol Testing + icon: Stirrer.webp + class: + module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer + type: python + status_types: + status: String + operation_mode: String + current_vessel: String + current_speed: Float64 + is_stirring: Bool + remaining_time: Float64 + action_value_mappings: + stir: + type: Stir + goal: + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + status: status + result: + success: success + start_stir: + type: StartStir + goal: + vessel: vessel + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + stop_stir: + type: StopStir + goal: + vessel: vessel + feedback: + status: status + result: + success: success + handles: + - handler_key: stirrer + label: stirrer + data_type: mechanical + side: NORTH + io_type: source + data_source: handle + data_key: vessel + description: "搅拌器的机械连接口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_speed: + type: number + default: 1500.0 + min_speed: + type: number + default: 50.0 + additionalProperties: false + +virtual_multiway_valve: + description: Virtual 8-Way Valve for flow direction control + icon: EightPipeline.webp + class: + module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve + type: python + status_types: + status: String + valve_state: String + current_position: Int32 + target_position: Int32 + max_positions: Int32 + action_value_mappings: + set_position: + type: SendCmd + goal: + command: command + feedback: {} + result: + success: success + set_valve_position: + type: SendCmd + goal: + command: command + feedback: {} + result: + success: success + handles: + - handler_key: transferpump + label: transferpump + data_type: fluid + side: NORTH + io_type: target + data_source: handle + data_key: fluid_in + description: "八通阀门进液口" + - handler_key: 1 + label: 1 + data_type: fluid + side: NORTH + io_type: source + data_source: executor + data_key: fluid_port_1 + description: "八通阀门端口1" + - handler_key: 2 + label: 2 + data_type: fluid + side: EAST + io_type: source + data_source: executor + data_key: fluid_port_2 + description: "八通阀门端口2" + - handler_key: 3 + label: 3 + data_type: fluid + side: EAST + io_type: source + data_source: executor + data_key: fluid_port_3 + description: "八通阀门端口3" + - handler_key: 4 + label: 4 + data_type: fluid + side: SOUTH + io_type: source + data_source: executor + data_key: fluid_port_4 + description: "八通阀门端口4" + - handler_key: 5 + label: 5 + data_type: fluid + side: SOUTH + io_type: source + data_source: executor + data_key: fluid_port_5 + description: "八通阀门端口5" + - handler_key: 6 + label: 6 + data_type: fluid + side: WEST + io_type: source + data_source: executor + data_key: fluid_port_6 + description: "八通阀门端口6" + - handler_key: 7 + label: 7 + data_type: fluid + side: WEST + io_type: source + data_source: executor + data_key: fluid_port_7 + description: "八通阀门端口7" + - handler_key: 8 + label: 8 + data_type: fluid + side: NORTH + io_type: source + data_source: executor + data_key: fluid_port_8 + description: "八通阀门端口8" + schema: + type: object + properties: + port: + type: string + 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 + is_open: Bool + action_value_mappings: + set_valve_position: + type: SendCmd + goal: + command: command + feedback: {} + result: + success: success + open: + type: SendCmd + goal: + command: "OPEN" + feedback: {} + result: + success: success + close: + type: SendCmd + goal: + command: "CLOSED" + feedback: {} + result: + success: success + set_state: + type: SendCmd + goal: + command: command + feedback: {} + result: + success: success + handles: + - handler_key: in + label: in + data_type: fluid + side: NORTH + io_type: target + data_source: handle + data_key: fluid_port_in + description: "电磁阀的进液口" + - handler_key: out + label: out + data_type: fluid + side: SOUTH + io_type: source + data_source: handle + data_key: fluid_port_out + description: "电磁阀的出液口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + voltage: + type: number + default: 12.0 + response_time: + type: number + default: 0.1 + additionalProperties: false + +virtual_centrifuge: + description: Virtual Centrifuge for CentrifugeProtocol Testing + class: + module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge + type: python + status_types: + status: String + current_speed: Float64 + target_speed: Float64 + current_temp: Float64 + target_temp: Float64 + max_speed: Float64 + max_temp: Float64 + min_temp: Float64 + centrifuge_state: String + time_remaining: Float64 + progress: Float64 + message: String + action_value_mappings: + centrifuge: + type: Centrifuge + goal: + vessel: vessel + speed: speed + time: time + temp: temp + feedback: + progress: progress + current_speed: current_speed + current_temp: current_temp + current_status: status + result: + success: success + message: message + handles: + - handler_key: centrifuge + label: centrifuge + data_type: transport + side: NORTH + io_type: target + data_source: handle + data_key: vessel + description: "需要离心的样品容器" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_speed: + type: number + default: 15000.0 + max_temp: + type: number + default: 40.0 + min_temp: + type: number + default: 4.0 + additionalProperties: false + +virtual_filter: + description: Virtual Filter for FilterProtocol Testing + class: + module: unilabos.devices.virtual.virtual_filter:VirtualFilter + type: python + status_types: + status: String + progress: Float64 + current_temp: Float64 + filtered_volume: Float64 + current_status: String + message: String + max_temp: Float64 + max_stir_speed: Float64 + max_volume: Float64 + action_value_mappings: + filter: + type: Filter + goal: + vessel: vessel + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + continue_heatchill: continue_heatchill + volume: volume + feedback: + progress: progress + current_temp: current_temp + filtered_volume: filtered_volume + current_status: current_status + result: + success: success + message: message + return_info: message + handles: + - handler_key: filter_in + label: filter_in + data_type: transport + side: NORTH + io_type: target + data_source: handle + data_key: vessel_in + description: "需要过滤的样品容器" + - handler_key: filtrate_out + label: filtrate_out + data_type: transport + side: SOUTH + io_type: source + data_source: handle + data_key: filtrate_out + description: "滤液出口" + - handler_key: retentate_out + label: retentate_out + data_type: transport + side: EAST + io_type: source + data_source: handle + data_key: retentate_out + description: "滤渣/固体出口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_temp: + type: number + default: 100.0 + max_stir_speed: + type: number + default: 1000.0 + max_volume: + type: number + default: 500.0 + additionalProperties: false + +virtual_heatchill: + description: Virtual HeatChill for HeatChillProtocol Testing + icon: Heater.webp + class: + module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill + type: python + status_types: + status: String + operation_mode: String + is_stirring: Bool + stir_speed: Float64 + action_value_mappings: + heat_chill: + type: HeatChill + goal: + vessel: vessel + temp: temp + time: time + stir: stir + stir_speed: stir_speed + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_start: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: + status: status + result: + success: success + heat_chill_stop: + type: HeatChillStop + goal: + vessel: vessel + feedback: + status: status + result: + success: success + handles: + - handler_key: heatchill + label: heatchill + data_type: mechanical + side: NORTH + io_type: source + data_source: handle + data_key: vessel + description: "加热/冷却器的物理连接口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_temp: + type: number + default: 200.0 + min_temp: + type: number + default: -80 + max_stir_speed: + type: number + default: 1000.0 + additionalProperties: false + +virtual_transfer_pump: + description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) + icon: Pump.webp + class: + module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump + type: python + status_types: + status: String + current_volume: Float64 + max_volume: Float64 + transfer_rate: Float64 + position: Float64 + action_value_mappings: + transfer: + type: Transfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: + progress: progress + transferred_volume: transferred_volume + current_status: current_status + result: + success: success + message: message + set_position: + type: SetPumpPosition + goal: + position: position + max_velocity: max_velocity + feedback: + status: status + current_position: current_position + progress: progress + result: + success: success + message: message + handles: + - handler_key: transferpump + label: transferpump + data_type: fluid + side: SOUTH + io_type: source + data_source: handle + data_key: fluid_port + description: "注射器式转移泵的连接口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + description: "通信端口" + max_volume: + type: number + default: 50.0 + description: "最大注射器容量 (mL)" + transfer_rate: + type: number + default: 5.0 + description: "默认转移速率 (mL/s)" + additionalProperties: false + +virtual_column: + description: Virtual Column Chromatography Device for RunColumn Protocol Testing + class: + module: unilabos.devices.virtual.virtual_column:VirtualColumn + type: python + status_types: + status: String + column_state: String + current_flow_rate: Float64 + max_flow_rate: Float64 + column_length: Float64 + column_diameter: Float64 + processed_volume: Float64 + progress: Float64 + current_status: String + action_value_mappings: + run_column: + type: RunColumn + goal: + from_vessel: from_vessel + to_vessel: to_vessel + column: column + feedback: + progress: progress + processed_volume: processed_volume + current_status: current_status + result: + success: success + message: current_status + return_info: current_status + handles: + - handler_key: columnin + label: columnin + data_type: transport + side: WEST + io_type: target + data_source: handle + data_key: from_vessel + description: "样品输入口" + - handler_key: columnout + label: columnout + data_type: transport + side: EAST + io_type: source + data_source: handle + data_key: to_vessel + description: "产物输出口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + max_flow_rate: + type: number + default: 10.0 + column_length: + type: number + default: 25.0 + column_diameter: + type: number + default: 2.0 + additionalProperties: false + +virtual_rotavap: + description: Virtual Rotary Evaporator for EvaporateProtocol Testing + icon: Rotaryevaporator.webp + class: + module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap + type: python + status_types: + status: String + rotavap_state: String + current_temp: Float64 + rotation_speed: Float64 + vacuum_pressure: Float64 + evaporated_volume: Float64 + progress: Float64 + remaining_time: Float64 + message: String + max_temp: Float64 + max_rotation_speed: Float64 + 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 + status: status + result: + success: success + message: message + handles: + - handler_key: sample_in + label: sample_in + data_type: fluid + side: NORTH + io_type: target + data_source: handle + data_key: vessel_in + description: "样品连接口" + - handler_key: product_out + label: product_out + data_type: fluid + side: SOUTH + io_type: source + data_source: handle + data_key: product_out + description: "浓缩产物出口" + - handler_key: solvent_out + label: solvent_out + data_type: fluid + side: EAST + io_type: source + data_source: handle + data_key: solvent_out + 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 + icon: Separator.webp + 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 + handles: + - handler_key: separator_in + label: separator_in + data_type: fluid + side: NORTH + io_type: target + data_source: handle + data_key: from_vessel + description: "需要分离的混合液体输入口" + - handler_key: bottom_phase_out + label: bottom_phase_out + data_type: fluid + side: SOUTH + io_type: source + data_source: executor + data_key: bottom_outlet + description: "下相(重相)液体输出口" + - handler_key: top_phase_out + label: top_phase_out + data_type: fluid + side: EAST + io_type: source + data_source: executor + data_key: top_outlet + description: "上相(轻相)液体输出口" + - handler_key: bind + label: bind + io_type: target + data_type: mechanical + side: WEST + data_source: handle + data_key: mechanical_port + 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 + icon: Vacuum.webp + 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: vacuumpump + label: vacuumpump + data_type: fluid + side: SOUTH + io_type: source + data_source: handle + data_key: fluid_in + description: "真空泵进气口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + description: "通信端口" + additionalProperties: false + +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: gassource + label: gassource + data_type: fluid + side: SOUTH + io_type: source + data_source: executor + data_key: fluid_out + description: "气源出气口" + schema: + type: object + properties: + port: + type: string + default: "VIRTUAL" + description: "通信端口" + gas_type: + type: string + default: "nitrogen" + description: "气体类型" + max_pressure: + type: number + default: 5.0 + description: "最大输出压力 (bar)" + additionalProperties: false \ No newline at end of file diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index d3a37338..c8c0324d 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -3,5 +3,623 @@ workstation: class: module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode type: ros2 - schema: - properties: {} + action_value_mappings: + AddProtocol: + type: Add + goal: + vessel: vessel + reagent: reagent + volume: volume + mass: mass + amount: amount + time: time + stir: stir + stir_speed: stir_speed + viscous: viscous + purpose: purpose + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: reagent + label: Reagent + data_type: resource + data_source: handle + data_key: reagent + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + AGVTransferProtocol: + type: AGVTransfer + goal: + from_repo: from_repo + from_repo_position: from_repo_position + to_repo: to_repo + to_repo_position: to_repo_position + feedback: {} + result: {} + + CentrifugeProtocol: + type: Centrifuge + goal: + vessel: vessel + speed: speed + time: time + temp: temp + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + CleanProtocol: + type: Clean + goal: + vessel: vessel + solvent: solvent + volume: volume + temp: temp + repeats: repeats + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: solvent + label: Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + CleanVesselProtocol: + type: CleanVessel + goal: + vessel: vessel + solvent: solvent + volume: volume + temp: temp + repeats: repeats + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: solvent + label: Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + DissolveProtocol: + type: Dissolve + goal: + vessel: vessel + solvent: solvent + volume: volume + amount: amount + temp: temp + time: time + stir_speed: stir_speed + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: solvent + label: Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + EvacuateAndRefillProtocol: + type: EvacuateAndRefill + goal: + vessel: vessel + gas: gas + repeats: repeats + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + EvaporateProtocol: + type: Evaporate + goal: + vessel: vessel + pressure: pressure + temp: temp + time: time + stir_speed: stir_speed + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + FilterProtocol: + type: Filter + goal: + vessel: vessel + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + continue_heatchill: continue_heatchill + volume: volume + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: filtrate_vessel + label: Filtrate Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + - handler_key: filtrate_out + label: Filtrate Vessel + data_type: resource + data_source: executor + data_key: vessel + + FilterThroughProtocol: + type: FilterThrough + goal: + from_vessel: from_vessel + to_vessel: to_vessel + filter_through: filter_through + eluting_solvent: eluting_solvent + eluting_volume: eluting_volume + eluting_repeats: eluting_repeats + residence_time: residence_time + feedback: {} + result: {} + handles: + input: + - handler_key: from_vessel + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + - handler_key: solvent + label: Eluting Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: from_vessel_out + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel_out + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + + HeatChillProtocol: + type: HeatChill + goal: + vessel: vessel + temp: temp + time: time + stir: stir + stir_speed: stir_speed + purpose: purpose + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + HeatChillStartProtocol: + type: HeatChillStart + goal: + vessel: vessel + temp: temp + purpose: purpose + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + HeatChillStopProtocol: + type: HeatChillStop + goal: + vessel: vessel + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + PumpTransferProtocol: + type: PumpTransfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: {} + result: {} + handles: + input: + - handler_key: from_vessel + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + - handler_key: solvent + label: Rinsing Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: from_vessel_out + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel_out + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + + RunColumnProtocol: + type: RunColumn + goal: + from_vessel: from_vessel + to_vessel: to_vessel + column: column + feedback: {} + result: {} + handles: + input: + - handler_key: from_vessel + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + output: + - handler_key: from_vessel_out + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel_out + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + + SeparateProtocol: + 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: {} + result: {} + handles: + input: + - handler_key: from_vessel + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + - handler_key: solvent + label: Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: from_vessel_out + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel_out + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + + StartStirProtocol: + type: StartStir + goal: + vessel: vessel + stir_speed: stir_speed + purpose: purpose + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + StirProtocol: + type: Stir + goal: + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + StopStirProtocol: + type: StopStir + goal: + vessel: vessel + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel + data_type: resource + data_source: executor + data_key: vessel + + TransferProtocol: + type: Transfer + goal: + from_vessel: from_vessel + to_vessel: to_vessel + volume: volume + amount: amount + time: time + viscous: viscous + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + rinsing_repeats: rinsing_repeats + solid: solid + feedback: {} + result: {} + handles: + input: + - handler_key: from_vessel + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + - handler_key: solvent + label: Rinsing Solvent + data_type: resource + data_source: handle + data_key: solvent + output: + - handler_key: from_vessel_out + label: From Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: to_vessel_out + label: To Vessel + data_type: resource + data_source: executor + data_key: vessel + + WashSolidProtocol: + type: WashSolid + goal: + vessel: vessel + solvent: solvent + volume: volume + filtrate_vessel: filtrate_vessel + temp: temp + stir: stir + stir_speed: stir_speed + time: time + repeats: repeats + feedback: {} + result: {} + handles: + input: + - handler_key: vessel + label: Vessel + data_type: resource + data_source: handle + data_key: vessel + - handler_key: solvent + label: Solvent + data_type: resource + data_source: handle + data_key: solvent + - handler_key: filtrate_vessel + label: Filtrate Vessel + data_type: resource + data_source: executor + data_key: vessel + output: + - handler_key: vessel_out + label: Vessel Out + data_type: resource + data_source: handle + data_key: vessel + - handler_key: filtrate_vessel_out + label: Filtrate Vessel + data_type: resource + data_source: executor + data_key: vessel \ No newline at end of file diff --git a/unilabos/registry/resources/organic/container.yaml b/unilabos/registry/resources/organic/container.yaml new file mode 100644 index 00000000..2186b700 --- /dev/null +++ b/unilabos/registry/resources/organic/container.yaml @@ -0,0 +1,28 @@ +container: + description: regular organic container + icon: Flask.webp + class: + module: unilabos.resources.container:RegularContainer + type: unilabos + handles: + - handler_key: top + label: top + io_type: target + data_type: fluid + side: NORTH + data_source: handle + data_key: fluid_in + - handler_key: bottom + label: bottom + io_type: source + data_type: fluid + side: SOUTH + data_source: handle + data_key: fluid_out + - handler_key: bind + label: bind + io_type: target + data_type: mechanical + side: SOUTH + data_source: handle + data_key: mechanical_port \ No newline at end of file diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py new file mode 100644 index 00000000..644bfe88 --- /dev/null +++ b/unilabos/resources/container.py @@ -0,0 +1,67 @@ +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}" \ No newline at end of file diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index cca7a35b..3ed2e36f 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -1,9 +1,13 @@ import importlib import inspect import json -from typing import Union +from typing import Union, Any 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 @@ -80,6 +84,8 @@ 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): @@ -153,7 +159,27 @@ 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 + 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") + edge["id"] = f"reactflow__edge-{source}-{edge['sourceHandle']}-{target}-{edge['targetHandle']}" + for key in ["source_port", "target_port"]: + if key in edge: + edge.pop(key) + return data def read_graphml(graphml_file): @@ -178,7 +204,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 + return physical_setup_graph, data def dict_from_graph(graph: nx.Graph) -> dict: @@ -466,6 +492,10 @@ 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()] diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index bbd6359e..7ee26839 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -45,6 +45,7 @@ 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] = [], @@ -62,6 +63,7 @@ def main( "host_node", devices_config, resources_config, + resources_edge_config, graph, controllers_config, bridges, @@ -97,6 +99,7 @@ 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] = [], diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 62ab606d..2bcaadb0 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -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"): + if py_msgtype not in _action_mapping and (py_msgtype.endswith("Protocol") or py_msgtype.startswith("Protocol")): try: protocol_class = msg_converter_manager.get_class(f"unilabos.messages.{py_msgtype}") action_name = py_msgtype.replace("Protocol", "") @@ -117,6 +117,7 @@ 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消息转换器 diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index eafdd71c..e119e214 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -19,6 +19,7 @@ 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, @@ -344,6 +345,7 @@ 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 # 本地拿到这个物料,可能需要先做初始化? @@ -362,6 +364,28 @@ 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" @@ -385,7 +409,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): res.response = serialize_result_info(traceback.format_exc(), False, {}) return res # 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中 - resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) + if bind_parent_id != self.node_name: + resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) # 拿到父节点,进行具体assign等操作 # request.resources = [convert_to_ros_msg(Resource, resources)] try: @@ -435,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): "bind_parent_id": bind_parent_id, } ) - future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4()) + future = action_client.send_goal_async(goal) def done_cb(*args): self.lab_logger().info(f"向meshmanager发送新增resource完成") @@ -601,10 +626,10 @@ class BaseROS2DeviceNode(Node, Generic[T]): goal = goal_handle.request # 从目标消息中提取参数, 并调用对应的方法 - if "sequence" in self._action_value_mappings: + if "sequence" in action_value_mapping: # 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用 def ACTION(**kwargs): - for i, action in enumerate(self._action_value_mappings["sequence"]): + for i, action in enumerate(action_value_mapping["sequence"]): if i == 0: self.lab_logger().info(f"执行序列动作第一步: {action}") self.get_real_function(self.driver_instance, action)[0](**kwargs) @@ -612,9 +637,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.lab_logger().info(f"执行序列动作后续步骤: {action}") self.get_real_function(self.driver_instance, action)[0]() - action_paramtypes = get_type_hints( - self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0]) - )[1] + action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1] else: ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name) @@ -903,9 +926,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) + self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker) else: - self._driver_creator = DeviceClassCreator(driver_class) + self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker) if driver_is_ros: driver_params["device_id"] = device_id diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 712e9b3a..66651651 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -22,6 +22,7 @@ from unilabos_msgs.srv import ( ) # type: ignore from unique_identifier_msgs.msg import UUID +from unilabos.config.config import BasicConfig from unilabos.registry.registry import lab_registry from unilabos.resources.graphio import initialize_resource from unilabos.resources.registry import add_schema @@ -58,6 +59,7 @@ 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, @@ -96,6 +98,7 @@ 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 = {} @@ -144,13 +147,15 @@ class HostNode(BaseROS2DeviceNode): self.device_status = {} # 用来存储设备状态 self.device_status_timestamps = {} # 用来存储设备状态最后更新时间 + if BasicConfig.upload_registry: + from unilabos.app.mq import mqtt_client - from unilabos.app.mq import mqtt_client - - for device_info in lab_registry.obtain_registry_device_info(): - mqtt_client.publish_registry(device_info["id"], device_info) - for resource_info in lab_registry.obtain_registry_resource_info(): - mqtt_client.publish_registry(resource_info["id"], resource_info) + for device_info in lab_registry.obtain_registry_device_info(): + mqtt_client.publish_registry(device_info["id"], device_info) + for resource_info in lab_registry.obtain_registry_resource_info(): + mqtt_client.publish_registry(resource_info["id"], resource_info) + else: + self.lab_logger().warning("本次启动注册表不报送云端,如果您需要联网调试,请使用unilab-register命令进行单独报送,或者在启动命令增加--upload_registry") time.sleep(1) # 等待MQTT连接稳定 # 首次发现网络中的设备 self._discover_devices() @@ -191,24 +196,36 @@ 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_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}" - continue + # 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 = bridge.resource_add(add_schema(resource_with_parent_name), True) + resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False) 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()) @@ -383,18 +400,24 @@ class HostNode(BaseROS2DeviceNode): liquid_volume: list[int], slot_on_deck: str, ): - 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的格式 + 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的格式 resources = init_new_res # initialize_resource已经返回list[dict] device_ids = [device_id] bind_parent_id = [parent] @@ -751,8 +774,10 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().info(f"[Host Node-Resource] Add request received: {len(resources)} resources") success = False - if len(self.bridges) > 0: - r = self.bridges[-1].resource_add(add_schema(resources)) + 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) success = bool(r) response.success = success diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index 23e08d0d..4da8c62d 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -110,7 +110,8 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): def initialize_device(self, device_id, device_config): """初始化设备并创建相应的动作客户端""" - device_id_abs = f"{self.device_id}/{device_id}" + # device_id_abs = f"{self.device_id}/{device_id}" + device_id_abs = f"{device_id}" self.lab_logger().info(f"初始化子设备: {device_id_abs}") d = self.sub_devices[device_id] = initialize_device_from_dict(device_id_abs, device_config) @@ -213,7 +214,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): if device_id in ["", None, "self"]: action_id = f"/devices/{self.device_id}/{action_name}" else: - action_id = f"/devices/{self.device_id}/{device_id}/{action_name}" + action_id = f"/devices/{device_id}/{action_name}" # 执行时取消了主节点信息 /{self.device_id} # 检查动作客户端是否存在 if action_id not in self._action_clients: @@ -256,12 +257,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): return write_func(*args, **kwargs) if read_method: - bound_read = MethodType(_read, device.driver_instance) - setattr(device.driver_instance, read_method, bound_read) + # bound_read = MethodType(_read, device.driver_instance) + setattr(device.driver_instance, read_method, _read) if write_method: - bound_write = MethodType(_write, device.driver_instance) - setattr(device.driver_instance, write_method, bound_write) + # bound_write = MethodType(_write, device.driver_instance) + setattr(device.driver_instance, write_method, _write) async def _update_resources(self, goal, protocol_kwargs): diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index ff1a7797..10cc4bc6 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -25,7 +25,7 @@ class DeviceNodeResourceTracker(object): def clear_resource(self): self.resources = [] - def figure_resource(self, query_resource): + def figure_resource(self, query_resource, try_mode=False): 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,10 +45,14 @@ class DeviceNodeResourceTracker(object): res_list.extend( self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key)) ) - assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}" + 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] + # 后续加入其他对比方式 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]]: @@ -57,8 +61,12 @@ 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) or target_resource_cls_type == dict: - if hasattr(resource, identifier_key): + 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 getattr(resource, identifier_key) == compare_value: res_list.append((parent_res, resource)) return res_list diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 9f223f9f..7fd726b2 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -33,7 +33,7 @@ class DeviceClassCreator(Generic[T]): 这个类提供了从任意类创建实例的通用方法。 """ - def __init__(self, cls: Type[T]): + def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker): """ 初始化设备类创建器 @@ -42,6 +42,18 @@ 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: """ @@ -60,6 +72,7 @@ class DeviceClassCreator(Generic[T]): } ) self.post_create() + self.attach_resource() return self.device_instance def get_instance(self) -> Optional[T]: @@ -90,14 +103,15 @@ class PyLabRobotCreator(DeviceClassCreator[T]): cls: PyLabRobot设备类 children: 子资源字典,用于资源替换 """ - super().__init__(cls) - self.children = children - self.resource_tracker = resource_tracker + super().__init__(cls, children, 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 @@ -260,7 +274,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]): 这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。 """ - def __init__(self, cls: Type[T], children: Dict[str, Any]): + def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker): """ 初始化ProtocolNode设备类创建器 @@ -268,8 +282,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]): cls: ProtocolNode设备类 children: 子资源字典,用于资源替换 """ - super().__init__(cls) - self.children = children + super().__init__(cls, children, resource_tracker) def create_instance(self, data: Dict[str, Any]) -> T: """ @@ -282,8 +295,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]): ProtocolNode设备类实例 """ try: - - # 创建实例 + # 创建实例,额外补充一个给protocol node的字段,后面考虑取消 data["children"] = self.children self.device_instance = super(ProtocolNodeCreator, self).create_instance(data) self.post_create() diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 3e98ce6e..db641168 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -29,8 +29,25 @@ 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/SetPumpPosition.action" "action/LiquidHandlerProtocolCreation.action" - "action/LiquidHandlerAspirate.action" "action/LiquidHandlerDiscardTips.action" "action/LiquidHandlerDispense.action" diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action new file mode 100644 index 00000000..de06c6a0 --- /dev/null +++ b/unilabos_msgs/action/Add.action @@ -0,0 +1,20 @@ +# Goal - 添加试剂的目标参数 +string vessel # 目标容器 +string reagent # 试剂名称 +float64 volume # 体积 (可选) +float64 mass # 质量 (可选) +string amount # 数量描述 (可选) +float64 time # 添加时间 (可选) +bool stir # 是否搅拌 +float64 stir_speed # 搅拌速度 (可选) +bool viscous # 是否为粘性液体 +string purpose # 添加目的 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/Centrifuge.action b/unilabos_msgs/action/Centrifuge.action new file mode 100644 index 00000000..356ccb9f --- /dev/null +++ b/unilabos_msgs/action/Centrifuge.action @@ -0,0 +1,16 @@ +# Goal - 离心操作的目标参数 +string vessel # 离心容器 +float64 speed # 离心速度 (rpm) +float64 time # 离心时间 (秒) +float64 temp # 温度 (可选,摄氏度) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_speed # 当前转速 +float64 current_temp # 当前温度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/CleanVessel.action b/unilabos_msgs/action/CleanVessel.action new file mode 100644 index 00000000..cba232a7 --- /dev/null +++ b/unilabos_msgs/action/CleanVessel.action @@ -0,0 +1,12 @@ +string vessel # 要清洗的容器名称 +string solvent # 用于清洗容器的溶剂名称 +float64 volume # 清洗溶剂的体积,可选参数 +float64 temp # 清洗时的温度,可选参数 +int32 repeats # 清洗操作的重复次数,默认为 1 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Crystallize.action b/unilabos_msgs/action/Crystallize.action new file mode 100644 index 00000000..50d26cf6 --- /dev/null +++ b/unilabos_msgs/action/Crystallize.action @@ -0,0 +1,14 @@ +# Goal - 结晶操作的目标参数 +string vessel # 结晶容器 +float64 ramp_time # 升温/降温时间 (可选,秒) +float64 ramp_temp # 目标温度 (可选,摄氏度) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action new file mode 100644 index 00000000..6b860d07 --- /dev/null +++ b/unilabos_msgs/action/Dissolve.action @@ -0,0 +1,14 @@ +string vessel # 装有要溶解物质的容器名称 +string solvent # 用于溶解物质的溶剂名称 +float64 volume # 溶剂的体积,可选参数 +string amount # 要溶解物质的量,可选参数 +float64 temp # 溶解时的温度,可选参数 +float64 time # 溶解的时间,可选参数 +float64 stir_speed # 搅拌速度,可选参数 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Dry.action b/unilabos_msgs/action/Dry.action new file mode 100644 index 00000000..5692ef28 --- /dev/null +++ b/unilabos_msgs/action/Dry.action @@ -0,0 +1,17 @@ +# Goal - 干燥操作的目标参数 +string vessel # 干燥容器 +float64 time # 干燥时间 (可选,秒) +float64 pressure # 压力 (可选,Pa) +float64 temp # 温度 (可选,摄氏度) +bool continue_heatchill # 是否继续加热冷却 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +float64 current_pressure # 当前压力 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/Filter.action b/unilabos_msgs/action/Filter.action new file mode 100644 index 00000000..564df1a5 --- /dev/null +++ b/unilabos_msgs/action/Filter.action @@ -0,0 +1,19 @@ +# Goal - 过滤操作的目标参数 +string vessel # 过滤容器 +string filtrate_vessel # 滤液容器 (可选) +bool stir # 是否搅拌 +float64 stir_speed # 搅拌速度 (可选) +float64 temp # 温度 (可选,摄氏度) +bool continue_heatchill # 是否继续加热冷却 +float64 volume # 过滤体积 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_temp # 当前温度 +float64 filtered_volume # 已过滤体积 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/FilterThrough.action b/unilabos_msgs/action/FilterThrough.action new file mode 100644 index 00000000..dbabd129 --- /dev/null +++ b/unilabos_msgs/action/FilterThrough.action @@ -0,0 +1,14 @@ +string from_vessel # 源容器的名称,即物质起始所在的容器 +string to_vessel # 目标容器的名称,物质过滤后要到达的容器 +string filter_through # 过滤时所通过的介质,如滤纸、柱子等 +string eluting_solvent # 洗脱溶剂的名称,可选参数 +float64 eluting_volume # 洗脱溶剂的体积,可选参数 +int32 eluting_repeats # 洗脱操作的重复次数,默认为 0 +float64 residence_time # 物质在过滤介质中的停留时间,可选参数 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Purge.action b/unilabos_msgs/action/Purge.action new file mode 100644 index 00000000..00d76b31 --- /dev/null +++ b/unilabos_msgs/action/Purge.action @@ -0,0 +1,17 @@ +# Goal - 清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +string gas # 清洗气体 (可选) +float64 time # 清洗时间 (可选,秒) +float64 pressure # 压力 (可选,Pa) +float64 flow_rate # 流速 (可选,mL/min) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_pressure # 当前压力 +float64 current_flow_rate # 当前流速 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action new file mode 100644 index 00000000..3fba948a --- /dev/null +++ b/unilabos_msgs/action/RunColumn.action @@ -0,0 +1,10 @@ +string from_vessel # 源容器的名称,即样品起始所在的容器 +string to_vessel # 目标容器的名称,分离后的样品要到达的容器 +string column # 所使用的柱子的名称 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/SetPumpPosition.action b/unilabos_msgs/action/SetPumpPosition.action new file mode 100644 index 00000000..5bbc1160 --- /dev/null +++ b/unilabos_msgs/action/SetPumpPosition.action @@ -0,0 +1,13 @@ +# Goal - 目标参数 +float64 position # 目标位置 (ml) +float64 max_velocity # 最大速度 (ml/s) +--- +# Result - 结果 +string return_info +bool success # 操作是否成功 +string message # 操作结果消息 +--- +# Feedback - 反馈 +string status # 当前状态 +float64 current_position # 当前位置 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/StartPurge.action b/unilabos_msgs/action/StartPurge.action new file mode 100644 index 00000000..f5500a61 --- /dev/null +++ b/unilabos_msgs/action/StartPurge.action @@ -0,0 +1,16 @@ +# Goal - 启动清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +string gas # 清洗气体 (可选) +float64 pressure # 压力 (可选,Pa) +float64 flow_rate # 流速 (可选,mL/min) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_pressure # 当前压力 +float64 current_flow_rate # 当前流速 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/StartStir.action b/unilabos_msgs/action/StartStir.action new file mode 100644 index 00000000..534c9f31 --- /dev/null +++ b/unilabos_msgs/action/StartStir.action @@ -0,0 +1,14 @@ +# Goal - 启动搅拌操作的目标参数 +string vessel # 搅拌容器 +float64 stir_speed # 搅拌速度 (可选,rpm) +string purpose # 搅拌目的 (可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 current_speed # 当前搅拌速度 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/StopPurge.action b/unilabos_msgs/action/StopPurge.action new file mode 100644 index 00000000..b7db8913 --- /dev/null +++ b/unilabos_msgs/action/StopPurge.action @@ -0,0 +1,11 @@ +# Goal - 停止清洗/吹扫操作的目标参数 +string vessel # 清洗容器 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/StopStir.action b/unilabos_msgs/action/StopStir.action new file mode 100644 index 00000000..a3205987 --- /dev/null +++ b/unilabos_msgs/action/StopStir.action @@ -0,0 +1,11 @@ +# Goal - 停止搅拌操作的目标参数 +string vessel # 搅拌容器 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/Transfer.action b/unilabos_msgs/action/Transfer.action new file mode 100644 index 00000000..f31f9dde --- /dev/null +++ b/unilabos_msgs/action/Transfer.action @@ -0,0 +1,20 @@ +string from_vessel # 源容器 +string to_vessel # 目标容器 +float64 volume # 转移体积 (可选) +string amount # 数量描述 (可选) +float64 time # 转移时间 (可选,秒) +bool viscous # 是否为粘性液体 +string rinsing_solvent # 冲洗溶剂 (可选) +float64 rinsing_volume # 冲洗体积 (可选) +int32 rinsing_repeats # 冲洗重复次数 +bool solid # 是否涉及固体 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +# Feedback - 实时反馈 +float64 progress # 进度百分比 (0-100) +float64 transferred_volume # 已转移体积 +string current_status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/Wait.action b/unilabos_msgs/action/Wait.action new file mode 100644 index 00000000..d4c49429 --- /dev/null +++ b/unilabos_msgs/action/Wait.action @@ -0,0 +1,9 @@ +int32 time # 等待时间(秒) +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) +int32 remaining_time # 剩余等待时间(秒) \ No newline at end of file diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action new file mode 100644 index 00000000..cb57e5cc --- /dev/null +++ b/unilabos_msgs/action/WashSolid.action @@ -0,0 +1,16 @@ +string vessel # 装有固体物质的容器名称 +string solvent # 用于清洗固体的溶剂名称 +float64 volume # 清洗溶剂的体积 +string filtrate_vessel # 滤液要收集到的容器名称,可选参数 +float64 temp # 清洗时的温度,可选参数 +bool stir # 是否在清洗过程中搅拌,默认为 False +float64 stir_speed # 搅拌速度,可选参数 +float64 time # 清洗的时间,可选参数 +int32 repeats # 清洗操作的重复次数,默认为 1 +--- +bool success # 操作是否成功 +string message # 结果消息 +string return_info +--- +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file