mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
Compare commits
140 Commits
v0.9.7
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5805f94e9a | ||
|
|
3adcc41ce8 | ||
|
|
243922caf4 | ||
|
|
079ec9d1b4 | ||
|
|
54cfaf15f3 | ||
|
|
1c9d2ee98a | ||
|
|
3fe8f4ca44 | ||
|
|
2476821dcc | ||
|
|
7b426ed5ae | ||
|
|
9bbae96447 | ||
|
|
10aabb7592 | ||
|
|
a5397ffe12 | ||
|
|
196e0f7e2b | ||
|
|
a632fd495e | ||
|
|
a8cc02a126 | ||
|
|
ad2e1432c6 | ||
|
|
c3b9583eac | ||
|
|
5c47cd0c8a | ||
|
|
63ab1af45d | ||
|
|
a8419dc0c3 | ||
|
|
34f05f2e25 | ||
|
|
0dc2488f02 | ||
|
|
f13156e792 | ||
|
|
13fd1ac572 | ||
|
|
f8ef6e0686 | ||
|
|
94a7b8aaca | ||
|
|
301bea639e | ||
|
|
4b5a83efa4 | ||
|
|
2889e9be2c | ||
|
|
304aebbba7 | ||
|
|
091c9fa247 | ||
|
|
67ca45a240 | ||
|
|
7aab2ea493 | ||
|
|
62f3a6d696 | ||
|
|
eb70ad0e18 | ||
|
|
768f43880e | ||
|
|
762c3c737c | ||
|
|
ace98a4472 | ||
|
|
41eaa88c6f | ||
|
|
a1a55a2c0a | ||
|
|
2eaa0ca729 | ||
|
|
6f8f070f40 | ||
|
|
da4bd927e0 | ||
|
|
01f8816597 | ||
|
|
e5006285df | ||
|
|
573c724a5c | ||
|
|
09549d2839 | ||
|
|
50c7777cea | ||
|
|
4888f02c09 | ||
|
|
779c9693d9 | ||
|
|
ffa841a41a | ||
|
|
fc669f09f8 | ||
|
|
2ca0311de6 | ||
|
|
94cdcbf24e | ||
|
|
1cd07915e7 | ||
|
|
b600fc666d | ||
|
|
9e214c56c1 | ||
|
|
bdf27a7e82 | ||
|
|
2493fb9f94 | ||
|
|
c7a0ff67a9 | ||
|
|
711a7c65fa | ||
|
|
cde7956896 | ||
|
|
95b6fd0451 | ||
|
|
513e848d89 | ||
|
|
58d1cc4720 | ||
|
|
5676dd6589 | ||
|
|
1ae274a833 | ||
|
|
22b88c8441 | ||
|
|
81bcc1907d | ||
|
|
8cffd3dc21 | ||
|
|
a722636938 | ||
|
|
f68340d932 | ||
|
|
361eae2f6d | ||
|
|
c25283ae04 | ||
|
|
961752fb0d | ||
|
|
55165024dd | ||
|
|
6ddceb8393 | ||
|
|
4e52c7d2f4 | ||
|
|
0b56efc89d | ||
|
|
a27b93396a | ||
|
|
2a60a6c27e | ||
|
|
5dda94044d | ||
|
|
0cfc6f45e3 | ||
|
|
831f4549f9 | ||
|
|
f4d4eb06d3 | ||
|
|
e3b8164f6b | ||
|
|
78c04acc2e | ||
|
|
cd0428ea78 | ||
|
|
bdddbd57ba | ||
|
|
a312de08a5 | ||
|
|
68513b5745 | ||
|
|
19027350fb | ||
|
|
bbbdb06bbc | ||
|
|
cd84e26126 | ||
|
|
ce5bab3af1 | ||
|
|
82d9ef6bf7 | ||
|
|
332b33c6f4 | ||
|
|
1ec642ee3a | ||
|
|
7d8e6d029b | ||
|
|
5ec8a57a1f | ||
|
|
ae3c1100ae | ||
|
|
14bc2e6cda | ||
|
|
9f823a4198 | ||
|
|
02c79363c1 | ||
|
|
227ff1284a | ||
|
|
4b7bde6be5 | ||
|
|
8a669ac35a | ||
|
|
a1538da39e | ||
|
|
0063df4cf3 | ||
|
|
e570ba4976 | ||
|
|
e8c1f76dbb | ||
|
|
f791c1a342 | ||
|
|
ea60cbe891 | ||
|
|
eac9b8ab3d | ||
|
|
573bcf1a6c | ||
|
|
50e93cb1af | ||
|
|
fe1a029a9b | ||
|
|
662c063f50 | ||
|
|
01cbbba0b3 | ||
|
|
e6c556cf19 | ||
|
|
0605f305ed | ||
|
|
37d8108ec4 | ||
|
|
6081dac561 | ||
|
|
5b2d066127 | ||
|
|
06e66765e7 | ||
|
|
98ce360088 | ||
|
|
5cd0f72fbd | ||
|
|
343f394203 | ||
|
|
46aa7a7bd2 | ||
|
|
a66369e2c3 | ||
|
|
8beb80f0e7 | ||
|
|
09c1e8ca73 | ||
|
|
e7b6b8190a | ||
|
|
933e84bf13 | ||
|
|
0b56378287 | ||
|
|
51b47596ce | ||
|
|
42e8befec4 | ||
|
|
cbe7963ad0 | ||
|
|
280d83db57 | ||
|
|
4224008a92 |
89
.conda/recipe.yaml
Normal file
89
.conda/recipe.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.6
|
||||
|
||||
source:
|
||||
path: ../unilabos
|
||||
target_directory: unilabos
|
||||
|
||||
build:
|
||||
python:
|
||||
entry_points:
|
||||
- unilab = unilabos.app.main:main
|
||||
script:
|
||||
- set PIP_NO_INDEX=
|
||||
- if: win
|
||||
then:
|
||||
- copy %RECIPE_DIR%\..\MANIFEST.in %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\setup.cfg %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\setup.py %SRC_DIR%
|
||||
- call %PYTHON% -m pip install %SRC_DIR%
|
||||
- if: unix
|
||||
then:
|
||||
- cp $RECIPE_DIR/../MANIFEST.in $SRC_DIR
|
||||
- cp $RECIPE_DIR/../setup.cfg $SRC_DIR
|
||||
- cp $RECIPE_DIR/../setup.py $SRC_DIR
|
||||
- $PYTHON -m pip install $SRC_DIR
|
||||
|
||||
|
||||
requirements:
|
||||
host:
|
||||
- python ==3.11.11
|
||||
- pip
|
||||
- setuptools
|
||||
run:
|
||||
- conda-forge::python ==3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- zstd
|
||||
- ninja
|
||||
- if: unix
|
||||
then:
|
||||
- make
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
- numpy
|
||||
- scipy
|
||||
- pandas
|
||||
- networkx
|
||||
- matplotlib
|
||||
- pint
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
- pydantic
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websockets
|
||||
- ipython
|
||||
- jupyter
|
||||
- jupyros
|
||||
- colcon-common-extensions
|
||||
- robostack-staging::ros-humble-desktop-full
|
||||
- robostack-staging::ros-humble-control-msgs
|
||||
- robostack-staging::ros-humble-sensor-msgs
|
||||
- robostack-staging::ros-humble-trajectory-msgs
|
||||
- ros-humble-navigation2
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
- ros-humble-moveit-servo
|
||||
- ros-humble-simulation
|
||||
- ros-humble-tf-transformations
|
||||
- transforms3d
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
|
||||
about:
|
||||
repository: https://github.com/dptech-corp/Uni-Lab-OS
|
||||
license: GPL-3.0-only
|
||||
description: "Uni-Lab-OS"
|
||||
9
.conda/scripts/post-link.bat
Normal file
9
.conda/scripts/post-link.bat
Normal file
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM upgrade pip
|
||||
"%PREFIX%\python.exe" -m pip install --upgrade pip
|
||||
|
||||
REM install extra deps
|
||||
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
|
||||
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
9
.conda/scripts/post-link.sh
Normal file
9
.conda/scripts/post-link.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
# make sure pip is available
|
||||
"$PREFIX/bin/python" -m pip install --upgrade pip
|
||||
|
||||
# install extra deps
|
||||
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
|
||||
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
193
.github/workflows/multi-platform-build.yml
vendored
193
.github/workflows/multi-platform-build.yml
vendored
@@ -2,16 +2,21 @@ name: Multi-Platform Conda Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
tags: [ 'v*' ]
|
||||
branches: [main, dev]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [main, dev]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platforms:
|
||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||
required: false
|
||||
default: 'osx-arm64'
|
||||
upload_to_anaconda:
|
||||
description: '是否上传到Anaconda.org'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,18 +24,18 @@ jobs:
|
||||
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
|
||||
- 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 }}
|
||||
|
||||
@@ -39,94 +44,88 @@ jobs:
|
||||
shell: bash -l {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- 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: 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: 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: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge rattler-build anaconda-client
|
||||
|
||||
- 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: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
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: Build conda package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
else
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
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: List built packages
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Built packages in output directory:"
|
||||
find ./output -name "*.conda" | head -10
|
||||
ls -la ./output/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found"
|
||||
ls -la ./output/noarch/ || echo "noarch directory not found"
|
||||
echo "Output directory structure:"
|
||||
find ./output -type f -name "*.conda"
|
||||
|
||||
- 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: Prepare artifacts for upload
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mkdir -p conda-packages-temp
|
||||
find ./output -name "*.conda" -exec cp {} conda-packages-temp/ \;
|
||||
echo "Copied files to temp directory:"
|
||||
ls -la conda-packages-temp/
|
||||
|
||||
- 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: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conda-package-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
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
|
||||
- name: Upload to Anaconda.org (unilab organization)
|
||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
||||
run: |
|
||||
for package in $(find ./output -name "*.conda"); do
|
||||
echo "Uploading $package to unilab organization..."
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
124
.github/workflows/unilabos-conda-build.yml
vendored
Normal file
124
.github/workflows/unilabos-conda-build.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
name: UniLabOS 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: 'linux-64'
|
||||
upload_to_anaconda:
|
||||
description: '是否上传到Anaconda.org'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux-64
|
||||
- os: macos-13 # Intel
|
||||
platform: osx-64
|
||||
- os: macos-latest # ARM64
|
||||
platform: osx-arm64
|
||||
- os: windows-latest
|
||||
platform: win-64
|
||||
|
||||
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,uni-lab,defaults
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
auto-activate-base: false
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge rattler-build anaconda-client
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
echo "Building UniLabOS package"
|
||||
|
||||
- name: Build conda package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
rattler-build build -r .conda/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
|
||||
- name: List built packages
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Built packages in output directory:"
|
||||
find ./output -name "*.conda" | head -10
|
||||
ls -la ./output/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found"
|
||||
ls -la ./output/noarch/ || echo "noarch directory not found"
|
||||
echo "Output directory structure:"
|
||||
find ./output -type f -name "*.conda"
|
||||
|
||||
- name: Prepare artifacts for upload
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mkdir -p conda-packages-temp
|
||||
find ./output -name "*.conda" -exec cp {} conda-packages-temp/ \;
|
||||
echo "Copied files to temp directory:"
|
||||
ls -la conda-packages-temp/
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conda-package-unilabos-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to Anaconda.org (uni-lab organization)
|
||||
if: github.event.inputs.upload_to_anaconda == 'true'
|
||||
run: |
|
||||
for package in $(find ./output -name "*.conda"); do
|
||||
echo "Uploading $package to uni-lab organization..."
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
configs/
|
||||
temp/
|
||||
output/
|
||||
unilabos_data/
|
||||
## Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -237,4 +241,10 @@ unilabos/device_mesh/view_robot.rviz
|
||||
|
||||
|
||||
# Certs
|
||||
**/.certs
|
||||
**/.certs
|
||||
local_test2.py
|
||||
ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
||||
*.bz2
|
||||
test_config.py
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/web *.html
|
||||
recursive-include unilabos/app/web *.css
|
||||
recursive-include unilabos/app/web/static *
|
||||
recursive-include unilabos/app/web/templates *
|
||||
recursive-include unilabos/device_mesh/devices *
|
||||
recursive-include unilabos/device_mesh/resources *
|
||||
|
||||
23
README.md
23
README.md
@@ -5,6 +5,7 @@
|
||||
# Uni-Lab-OS
|
||||
|
||||
<!-- Language switcher -->
|
||||
|
||||
**English** | [中文](README_zh.md)
|
||||
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||
@@ -34,30 +35,14 @@ Detailed documentation can be found at:
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Configure Conda Environment
|
||||
|
||||
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||
|
||||
```bash
|
||||
# Create new environment
|
||||
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||
mamba activate unilab
|
||||
|
||||
# Or update existing environment
|
||||
# Where `[YOUR_OS]` can be `win64`, `linux-64`, `osx-64`, or `osx-arm64`.
|
||||
conda env update --file unilabos-[YOUR_OS].yml -n environment_name
|
||||
|
||||
# Currently, you need to install the `unilabos_msgs` package
|
||||
# You can download the system-specific package from the Release page
|
||||
conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2
|
||||
|
||||
# Install PyLabRobot and other prerequisites
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
cd plr_repo
|
||||
pip install .[opentrons]
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. Install Uni-Lab-OS:
|
||||
## Install Dev Uni-Lab-OS
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
@@ -90,4 +75,4 @@ This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for det
|
||||
|
||||
## Contact Us
|
||||
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
|
||||
25
README_zh.md
25
README_zh.md
@@ -5,6 +5,7 @@
|
||||
# Uni-Lab-OS
|
||||
|
||||
<!-- Language switcher -->
|
||||
|
||||
[English](README.md) | **中文**
|
||||
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||
@@ -12,7 +13,7 @@
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||
|
||||
Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
|
||||
## 🏆 比赛
|
||||
|
||||
@@ -34,30 +35,16 @@ Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 配置Conda环境
|
||||
1. 配置 Conda 环境
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||
|
||||
```bash
|
||||
# 创建新环境
|
||||
mamba env create -f unilabos-[YOUR_OS].yaml
|
||||
mamba activate unilab
|
||||
|
||||
# 或更新现有环境
|
||||
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
||||
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
|
||||
# 现阶段,需要安装 `unilabos_msgs` 包
|
||||
# 可以前往 Release 页面下载系统对应的包进行安装
|
||||
conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2
|
||||
|
||||
# 安装PyLabRobot等前置
|
||||
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
|
||||
cd plr_repo
|
||||
pip install .[opentrons]
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. 安装 Uni-Lab-OS:
|
||||
2. 安装开发版 Uni-Lab-OS:
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
@@ -90,4 +77,4 @@ Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在
|
||||
|
||||
## 联系我们
|
||||
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
|
||||
@@ -13,18 +13,16 @@
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": [
|
||||
"deck"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": ["deck"]
|
||||
},
|
||||
{
|
||||
"id": "deck",
|
||||
"name": "deck",
|
||||
@@ -32,12 +30,12 @@
|
||||
"class": null,
|
||||
"parent": "PLR_STATION",
|
||||
"children": [
|
||||
"trash",
|
||||
"trash_core96",
|
||||
"teaching_carrier",
|
||||
"tip_rack",
|
||||
"plate"
|
||||
]
|
||||
"trash",
|
||||
"trash_core96",
|
||||
"teaching_carrier",
|
||||
"tip_rack",
|
||||
"plate"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
@@ -45,6 +43,7 @@
|
||||
```
|
||||
|
||||
配置文件定义了移液站的组成部分,主要包括:
|
||||
|
||||
- 移液站本体(LiquidHandler)- 设备类型
|
||||
- 移液站携带物料实例(deck)- 物料类型
|
||||
|
||||
@@ -55,7 +54,7 @@
|
||||
使用以下命令启动移液站设备:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/plr_test.json --app_bridges ""
|
||||
unilab -g test/experiments/plr_test.json --ak [通过网页获取的ak值] --sk [通过网页获取的sk值]
|
||||
```
|
||||
|
||||
### 2. 执行枪头插入操作
|
||||
@@ -66,35 +65,50 @@ unilab -g test/experiments/plr_test.json --app_bridges ""
|
||||
ros2 action send_goal /devices/PLR_STATION/pick_up_tips unilabos_msgs/action/_liquid_handler_pick_up_tips/LiquidHandlerPickUpTips "{ tip_spots: [ { id: 'tip_rack_tipspot_0_0', name: 'tip_rack_tipspot_0_0', sample_id: null, children: [], parent: 'tip_rack', type: 'device', config: { position: { x: 7.2, y: 68.3, z: -83.5 }, size_x: 9.0, size_y: 9.0, size_z: 0, rotation: { x: 0, y: 0, z: 0, type: 'Rotation' }, category: 'tip_spot', model: null, type: 'TipSpot', prototype_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } }, data: { tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' }, tip_state: { liquids: [], pending_liquids: [], liquid_history: [] }, pending_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } } } ], use_channels: [ 0 ], offsets: [ { x: 0.0, y: 0.0, z: 0.0 } ] }"
|
||||
```
|
||||
|
||||
此命令会通过ros通信触发移液站执行枪头插入操作,得到如下的PyLabRobot的输出日志。
|
||||
此命令会通过 ros 通信触发移液站执行枪头插入操作,得到如下的 PyLabRobot 的输出日志。
|
||||
|
||||
```log
|
||||
Picking up tips:
|
||||
pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter
|
||||
p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes
|
||||
pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter
|
||||
p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes
|
||||
```
|
||||
|
||||
也可以登陆网页,给`tip_spots`选择`tip_rack_tipspot_0_0`,`use_channels`为`0`,`offsets`均填写`0`,同样可观察到上面的日志
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **重复插入枪头不成功**:操作编排应该符合实际操作顺序,可自行通过PyLabRobot进行测试
|
||||
1. **重复插入枪头不成功**:操作编排应该符合实际操作顺序,可自行通过 PyLabRobot 进行测试
|
||||
|
||||
## 移液站支持的操作
|
||||
|
||||
移液站支持多种操作,以下是当前系统支持的操作列表:
|
||||
|
||||
1. **LiquidHandlerAspirate** - 吸液操作
|
||||
2. **LiquidHandlerDispense** - 排液操作
|
||||
3. **LiquidHandlerDiscardTips** - 丢弃枪头
|
||||
4. **LiquidHandlerDropTips** - 卸下枪头
|
||||
5. **LiquidHandlerDropTips96** - 卸下96通道枪头
|
||||
6. **LiquidHandlerMoveLid** - 移动盖子
|
||||
7. **LiquidHandlerMovePlate** - 移动板子
|
||||
8. **LiquidHandlerMoveResource** - 移动资源
|
||||
9. **LiquidHandlerPickUpTips** - 插入枪头
|
||||
10. **LiquidHandlerPickUpTips96** - 插入96通道枪头
|
||||
11. **LiquidHandlerReturnTips** - 归还枪头
|
||||
12. **LiquidHandlerReturnTips96** - 归还96通道枪头
|
||||
13. **LiquidHandlerStamp** - 打印标记
|
||||
14. **LiquidHandlerTransfer** - 液体转移
|
||||
1. **LiquidHandlerProtocolCreation** - 协议创建
|
||||
2. **LiquidHandlerAspirate** - 吸液操作
|
||||
3. **LiquidHandlerDispense** - 排液操作
|
||||
4. **LiquidHandlerDiscardTips** - 丢弃枪头
|
||||
5. **LiquidHandlerDropTips** - 卸下枪头
|
||||
6. **LiquidHandlerDropTips96** - 卸下 96 通道枪头
|
||||
7. **LiquidHandlerMoveLid** - 移动盖子
|
||||
8. **LiquidHandlerMovePlate** - 移动板子
|
||||
9. **LiquidHandlerMoveResource** - 移动资源
|
||||
10. **LiquidHandlerPickUpTips** - 插入枪头
|
||||
11. **LiquidHandlerPickUpTips96** - 插入 96 通道枪头
|
||||
12. **LiquidHandlerReturnTips** - 归还枪头
|
||||
13. **LiquidHandlerReturnTips96** - 归还 96 通道枪头
|
||||
14. **LiquidHandlerSetLiquid** - 设置液体
|
||||
15. **LiquidHandlerSetTipRack** - 设置枪头架
|
||||
16. **LiquidHandlerStamp** - 打印标记
|
||||
17. **LiquidHandlerTransfer** - 液体转移
|
||||
18. **LiquidHandlerSetGroup** - 设置分组
|
||||
19. **LiquidHandlerTransferBiomek** - Biomek 液体转移
|
||||
20. **LiquidHandlerIncubateBiomek** - Biomek 孵育
|
||||
21. **LiquidHandlerMoveBiomek** - Biomek 移动
|
||||
22. **LiquidHandlerOscillateBiomek** - Biomek 振荡
|
||||
23. **LiquidHandlerTransferGroup** - 分组转移
|
||||
24. **LiquidHandlerAdd** - 添加操作
|
||||
25. **LiquidHandlerMix** - 混合操作
|
||||
26. **LiquidHandlerMoveTo** - 移动到指定位置
|
||||
27. **LiquidHandlerRemove** - 移除操作
|
||||
|
||||
这些操作可通过ROS2 Action接口进行调用,以实现复杂的移液流程。
|
||||
这些操作可通过 ROS2 Action 接口进行调用,以实现复杂的移液流程。
|
||||
|
||||
@@ -19,7 +19,7 @@ Uni-Lab 的组态图当前支持 node-link json 和 graphml 格式,其中包
|
||||
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
|
||||
|
||||
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
|
||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`ProtocolNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`WorkstationNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,26 +1,64 @@
|
||||
## 简单单变量动作函数
|
||||
|
||||
|
||||
### `SendCmd`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `StrSingleInput`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `IntSingleInput`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `FloatSingleInput`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Point3DSeparateInput`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Wait`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Wait.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常量有机化学操作
|
||||
|
||||
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
|
||||
|
||||
|
||||
|
||||
### `Clean`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Clean.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `EvacuateAndRefill`
|
||||
|
||||
@@ -28,7 +66,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Evaporate`
|
||||
|
||||
@@ -36,7 +74,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChill`
|
||||
|
||||
@@ -44,7 +82,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChillStart`
|
||||
|
||||
@@ -52,7 +90,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChillStop`
|
||||
|
||||
@@ -60,7 +98,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `PumpTransfer`
|
||||
|
||||
@@ -68,7 +106,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Separate`
|
||||
|
||||
@@ -76,7 +114,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Stir`
|
||||
|
||||
@@ -84,20 +122,179 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Add`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Add.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `AddSolid`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/AddSolid.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `AdjustPH`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Centrifuge`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `CleanVessel`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Crystallize`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Crystallize.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Dissolve`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Dissolve.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Dry`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Dry.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Filter`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Filter.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `FilterThrough`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Hydrogenate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Purge`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Purge.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Recrystallize`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `RunColumn`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/RunColumn.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `StartPurge`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/StartPurge.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `StartStir`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/StartStir.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `StopPurge`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/StopPurge.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `StopStir`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/StopStir.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Transfer`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Transfer.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `WashSolid`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/WashSolid.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 移液工作站及相关生物自动化设备操作
|
||||
|
||||
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
|
||||
|
||||
|
||||
|
||||
### `LiquidHandlerAspirate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDiscardTips`
|
||||
|
||||
@@ -105,7 +302,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDispense`
|
||||
|
||||
@@ -113,7 +310,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDropTips`
|
||||
|
||||
@@ -121,7 +318,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDropTips96`
|
||||
|
||||
@@ -129,7 +326,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveLid`
|
||||
|
||||
@@ -137,7 +334,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMovePlate`
|
||||
|
||||
@@ -145,7 +342,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveResource`
|
||||
|
||||
@@ -153,7 +350,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerPickUpTips`
|
||||
|
||||
@@ -161,7 +358,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerPickUpTips96`
|
||||
|
||||
@@ -169,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerReturnTips`
|
||||
|
||||
@@ -177,7 +374,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerReturnTips96`
|
||||
|
||||
@@ -185,7 +382,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerStamp`
|
||||
|
||||
@@ -193,7 +390,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerTransfer`
|
||||
|
||||
@@ -201,9 +398,113 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
## 多工作站及小车运行、物料转移
|
||||
---
|
||||
|
||||
### `LiquidHandlerAdd`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerIncubateBiomek`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerMix`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveBiomek`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveTo`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerOscillateBiomek`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerProtocolCreation`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerRemove`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerSetGroup`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerSetLiquid`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerSetTipRack`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerTransferBiomek`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiquidHandlerTransferGroup`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多工作站及小车运行、物料转移
|
||||
|
||||
### `AGVTransfer`
|
||||
|
||||
@@ -211,7 +512,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `WorkStationRun`
|
||||
|
||||
@@ -219,12 +520,64 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `ResetHandling`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ResourceCreateFromOuter`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `ResourceCreateFromOuterEasy`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `SetPumpPosition`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 固体分配与处理设备操作
|
||||
|
||||
### `SolidDispenseAddPowderTube`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 其他设备操作
|
||||
|
||||
### `EmptyIn`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 机械臂、夹爪等机器人设备
|
||||
|
||||
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`:
|
||||
|
||||
|
||||
### `FollowJointTrajectory`
|
||||
|
||||
```yaml
|
||||
@@ -292,7 +645,8 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `GripperCommand`
|
||||
|
||||
```yaml
|
||||
@@ -310,17 +664,19 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `JointTrajectory`
|
||||
|
||||
```yaml
|
||||
trajectory_msgs/JointTrajectory trajectory
|
||||
---
|
||||
---
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `PointHead`
|
||||
|
||||
```yaml
|
||||
@@ -330,12 +686,13 @@ string pointing_frame
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
|
||||
---
|
||||
float64 pointing_angle_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `SingleJointPosition`
|
||||
|
||||
```yaml
|
||||
@@ -343,15 +700,16 @@ float64 position
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
|
||||
---
|
||||
std_msgs/Header header
|
||||
float64 position
|
||||
float64 velocity
|
||||
float64 error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `AssistedTeleop`
|
||||
|
||||
```yaml
|
||||
@@ -363,10 +721,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback
|
||||
builtin_interfaces/Duration current_teleop_duration
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `BackUp`
|
||||
|
||||
```yaml
|
||||
@@ -380,10 +738,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `ComputePathThroughPoses`
|
||||
|
||||
```yaml
|
||||
@@ -398,10 +756,10 @@ nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `ComputePathToPose`
|
||||
|
||||
```yaml
|
||||
@@ -416,10 +774,10 @@ nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `DriveOnHeading`
|
||||
|
||||
```yaml
|
||||
@@ -433,10 +791,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `DummyBehavior`
|
||||
|
||||
```yaml
|
||||
@@ -447,10 +805,10 @@ std_msgs/String command
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `FollowPath`
|
||||
|
||||
```yaml
|
||||
@@ -465,10 +823,10 @@ std_msgs/Empty result
|
||||
#feedback definition
|
||||
float32 distance_to_goal
|
||||
float32 speed
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `FollowWaypoints`
|
||||
|
||||
```yaml
|
||||
@@ -480,10 +838,10 @@ int32[] missed_waypoints
|
||||
---
|
||||
#feedback definition
|
||||
uint32 current_waypoint
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `NavigateThroughPoses`
|
||||
|
||||
```yaml
|
||||
@@ -501,10 +859,10 @@ builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
int16 number_of_poses_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `NavigateToPose`
|
||||
|
||||
```yaml
|
||||
@@ -521,10 +879,10 @@ builtin_interfaces/Duration navigation_time
|
||||
builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `SmoothPath`
|
||||
|
||||
```yaml
|
||||
@@ -540,10 +898,10 @@ builtin_interfaces/Duration smoothing_duration
|
||||
bool was_completed
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Spin`
|
||||
|
||||
```yaml
|
||||
@@ -556,10 +914,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 angular_distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Wait`
|
||||
|
||||
```yaml
|
||||
@@ -571,7 +929,6 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
builtin_interfaces/Duration time_left
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
@@ -1,37 +1,142 @@
|
||||
# 添加新动作指令(Action)
|
||||
|
||||
1. 在 `unilabos_msgs/action` 中新建实验操作名和参数列表,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
本指南将引导你完成添加新动作指令的整个流程,包括编写、在线构建和测试。
|
||||
|
||||
## 1. 编写新的 Action
|
||||
|
||||
### 1.1 创建 Action 文件
|
||||
|
||||
在 `unilabos_msgs/action` 目录中新建实验操作文件,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
|
||||
```action
|
||||
# 目标(Goal)
|
||||
# 目标(Goal)- 定义动作执行所需的参数
|
||||
string command
|
||||
float64 timeout
|
||||
---
|
||||
# 结果(Result)
|
||||
bool success
|
||||
# 结果(Result)- 定义动作完成后返回的结果
|
||||
bool success # 要求必须包含success,以便回传执行结果
|
||||
string return_info # 要求必须包含return_info,以便回传执行结果
|
||||
... # 其他
|
||||
---
|
||||
# 反馈(Feedback)
|
||||
# 反馈(Feedback)- 定义动作执行过程中的反馈信息
|
||||
float64 progress
|
||||
string status
|
||||
```
|
||||
|
||||
2. 在 `unilabos_msgs/CMakeLists.txt` 中添加新定义的 action
|
||||
### 1.2 更新 CMakeLists.txt
|
||||
|
||||
在 `unilabos_msgs/CMakeLists.txt` 中的 `add_action_files()` 部分添加新定义的 action:
|
||||
|
||||
```cmake
|
||||
add_action_files(
|
||||
FILES
|
||||
MyDeviceCmd.action
|
||||
# 其他已有的 action 文件...
|
||||
)
|
||||
```
|
||||
|
||||
3. 因为在指令集中新建了指令,因此调试时需要编译,并在终端环境中加载临时路径:
|
||||
## 2. 在线构建和测试
|
||||
|
||||
为了简化开发流程并确保构建环境的一致性,我们使用 GitHub Actions 进行在线构建。
|
||||
|
||||
### 2.1 Fork 仓库并创建分支
|
||||
|
||||
1. **Fork 仓库**:在 GitHub 上 fork `Uni-Lab-OS` 仓库到你的个人账户
|
||||
|
||||
2. **Clone 你的 fork**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
```
|
||||
|
||||
3. **创建功能分支**:
|
||||
|
||||
```bash
|
||||
git checkout -b add-my-device-action
|
||||
```
|
||||
|
||||
4. **提交你的更改**:
|
||||
```bash
|
||||
git add unilabos_msgs/action/MyDeviceCmd.action
|
||||
git add unilabos_msgs/CMakeLists.txt
|
||||
git commit -m "Add MyDeviceCmd action for device control"
|
||||
git push origin add-my-device-action
|
||||
```
|
||||
|
||||
### 2.2 触发在线构建
|
||||
|
||||
1. **访问你的 fork 仓库**:在浏览器中打开你的 fork 仓库页面
|
||||
|
||||
2. **手动触发构建**:
|
||||
|
||||
- 点击 "Actions" 标签
|
||||
- 选择 "Multi-Platform Conda Build" 工作流
|
||||
- 点击 "Run workflow" 按钮
|
||||
|
||||
3. **监控构建状态**:
|
||||
- 构建过程大约需要 5-10 分钟
|
||||
- 在 Actions 页面可以实时查看构建日志
|
||||
- 构建完成后,可以下载生成的 conda 包进行测试
|
||||
|
||||
### 2.3 下载和测试构建包
|
||||
|
||||
1. **下载构建产物**:
|
||||
|
||||
- 在构建完成的 Action 页面,找到 "Artifacts" 部分
|
||||
- 下载对应平台的 `conda-package-*` 文件
|
||||
|
||||
2. **本地测试安装**:
|
||||
|
||||
```bash
|
||||
# 解压下载的构建产物
|
||||
unzip conda-package-linux-64.zip # 或其他平台
|
||||
|
||||
# 安装测试包
|
||||
mamba install ./ros-humble-unilabos-msgs-*.conda
|
||||
```
|
||||
|
||||
3. **验证 Action 是否正确添加**:
|
||||
```bash
|
||||
# 检查 action 是否可用
|
||||
ros2 interface show unilabos_msgs/action/MyDeviceCmd
|
||||
```
|
||||
|
||||
## 3. 提交 Pull Request
|
||||
|
||||
测试成功后,向主仓库提交 Pull Request:
|
||||
|
||||
1. **创建 Pull Request**:
|
||||
|
||||
- 在你的 fork 仓库页面,点击 "New Pull Request"
|
||||
- 选择你的功能分支作为源分支
|
||||
- 填写详细的 PR 描述,包括:
|
||||
- 添加的 Action 功能说明
|
||||
- 测试结果
|
||||
- 相关的设备或用例
|
||||
|
||||
2. **等待审核和合并**:
|
||||
- 维护者会审核你的代码
|
||||
- CI/CD 系统会自动运行完整的测试套件
|
||||
- 合并后,新的指令集会自动发布到官方 conda 仓库
|
||||
|
||||
## 4. 使用新的 Action
|
||||
|
||||
如果采用自己构建的action包,可以通过以下命令更新安装:
|
||||
|
||||
```bash
|
||||
cd unilabos_msgs
|
||||
colcon build
|
||||
source ./install/local_setup.sh
|
||||
cd ..
|
||||
mamba remove --force ros-humble-unilabos-msgs
|
||||
mamba config set safety_checks disabled # 如果没有提升版本号,会触发md5与网络上md5不一致,是正常现象,因此通过本指令关闭md5检查
|
||||
mamba install xxx.conda --offline
|
||||
```
|
||||
|
||||
调试成功后,发起 pull request,Uni-Lab 的 CI/CD 系统会自动将新的指令集编译打包,mamba执行升级即可永久生效:
|
||||
## 常见问题
|
||||
|
||||
```bash
|
||||
mamba update ros-humble-unilabos-msgs -c http://quetz.dp.tech:8088/get/unilab -c robostack-humble -c robostack-staging
|
||||
```
|
||||
**Q: 构建失败怎么办?**
|
||||
A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。
|
||||
|
||||
**Q: 如何测试特定平台?**
|
||||
A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。
|
||||
|
||||
**Q: 构建包在哪里下载?**
|
||||
A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。
|
||||
|
||||
@@ -13,36 +13,36 @@ class MockGripper:
|
||||
self._velocity: float = 2.0
|
||||
self._torque: float = 0.0
|
||||
self._status = "Idle"
|
||||
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
return self._position
|
||||
|
||||
|
||||
@property
|
||||
def velocity(self) -> float:
|
||||
return self._velocity
|
||||
|
||||
|
||||
@property
|
||||
def torque(self) -> float:
|
||||
return self._torque
|
||||
|
||||
|
||||
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
@status.setter
|
||||
def status(self, target):
|
||||
self._status = target
|
||||
|
||||
|
||||
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
def push_to(self, position: float, torque: float, velocity: float = 0.0):
|
||||
self._status = "Running"
|
||||
current_pos = self.position
|
||||
if velocity == 0.0:
|
||||
velocity = self.velocity
|
||||
|
||||
|
||||
move_time = abs(position - current_pos) / velocity
|
||||
for i in range(20):
|
||||
self._position = current_pos + (position - current_pos) / 20 * (i+1)
|
||||
@@ -68,7 +68,7 @@ public class MockGripper
|
||||
public double velocity { get; private set; } = 2.0;
|
||||
public double torque { get; private set; } = 0.0;
|
||||
public string status { get; private set; } = "Idle";
|
||||
|
||||
|
||||
// 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
public async Task PushToAsync(double Position, double Torque, double Velocity = 0.0)
|
||||
{
|
||||
@@ -94,107 +94,61 @@ public class MockGripper
|
||||
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
|
||||
|
||||
## 注册表文件位置
|
||||
## 快速开始:使用注册表编辑器(推荐)
|
||||
|
||||
Uni-Lab 启动时会自动读取默认注册表路径 `unilabos/registry/devices` 下的所有注册设备。您也可以任意维护自己的注册表路径,只需要在 Uni-Lab 启动时使用 `--registry` 参数将路径添加即可。
|
||||
推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
|
||||
|
||||
在 `<path-to-registry>/devices` 中新建一个 yaml 文件,即可开始撰写。您可以将多个设备写到同一个 yaml 文件中。
|
||||
1. 启动 Uni-Lab-OS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择您的 Python 设备驱动文件
|
||||
4. 点击"分析文件",让系统读取类信息
|
||||
5. 填写基本信息(设备描述、图标等)
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 保存到 `devices/` 目录下
|
||||
|
||||
## 注册表的结构
|
||||
---
|
||||
|
||||
1. 顶层名称:每个设备的注册表以设备名称开头,例如 `new_device`, `gripper.mock`。
|
||||
1. `class` 字段:定义设备的模块路径和驱动程序语言。
|
||||
1. `status_types` 字段:定义设备定时对 Uni-Lab 实验室内发送的属性名及其类型。
|
||||
1. `action_value_mappings` 字段:定义设备支持的动作及其目标、反馈和结果。
|
||||
1. `schema` 字段:定义设备定时对 Uni-Lab 云端监控发送的属性名及其类型、描述(非必须)
|
||||
## 手动编写注册表(简化版)
|
||||
|
||||
## 创建新的注册表教程
|
||||
如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
|
||||
|
||||
1. 创建文件
|
||||
在 devices 文件夹中创建一个新的 YAML 文件,例如 `new_device.yaml`。
|
||||
2. 定义设备名称
|
||||
在文件中定义设备的顶层名称,例如:`new_device` 或 `gripper.mock`
|
||||
3. 定义设备的类信息
|
||||
添加设备的模块路径和类型:
|
||||
### 最小配置示例
|
||||
|
||||
```yaml
|
||||
gripper.mock:
|
||||
class: # 定义设备的类信息
|
||||
module: unilabos.devices.gripper.mock:MockGripper
|
||||
type: python # 指定驱动语言为 Python
|
||||
status_types:
|
||||
position: Float64
|
||||
torque: Float64
|
||||
status: String
|
||||
my_device: # 设备唯一标识符
|
||||
class:
|
||||
module: unilabos.devices.your_module.my_device:MyDevice # Python 类路径
|
||||
type: python # 驱动类型
|
||||
```
|
||||
|
||||
4. 定义设备的定时发布属性。注意,对于 Python Class 来说,PROP 是 class 的 `property`,或满足能被 `getattr(cls, PROP)` 或 `cls.get_PROP` 读取到的属性值的对象。
|
||||
### 注册表文件位置
|
||||
|
||||
- 默认路径:`unilabos/registry/devices`
|
||||
- 自定义路径:启动时使用 `--registry` 参数指定
|
||||
- 可将多个设备写在同一个 yaml 文件中
|
||||
|
||||
### 系统自动生成的内容
|
||||
|
||||
系统会自动分析您的 Python 驱动类并生成:
|
||||
|
||||
- `status_types`:从 `get_*` 方法自动识别状态属性
|
||||
- `action_value_mappings`:从类方法自动生成动作映射
|
||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||
- `schema`:前端显示用的属性类型定义
|
||||
|
||||
### 完整结构概览
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
PROP: TYPE
|
||||
```
|
||||
5. 定义设备支持的动作
|
||||
添加设备支持的动作及其目标、反馈和结果:
|
||||
|
||||
```yaml
|
||||
action_value_mappings:
|
||||
set_speed:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.your_module.my_device:MyDevice
|
||||
type: python
|
||||
status_types: {} # 自动生成
|
||||
action_value_mappings: {} # 自动生成
|
||||
description: '' # 可选:设备描述
|
||||
icon: '' # 可选:设备图标
|
||||
init_param_schema: {} # 自动生成
|
||||
schema: {} # 自动生成
|
||||
```
|
||||
|
||||
在 devices 文件夹中的 YAML 文件中,action_value_mappings 是用来将驱动内的动作函数,映射到 Uni-Lab 标准动作(actions)及其目标参数值(goal)、反馈值(feedback)和结果值(result)的映射规则。若在 Uni-Lab 指令集内找不到符合心意的,请【创建新指令】。
|
||||
|
||||
```yaml
|
||||
action_value_mappings:
|
||||
<action_name>: # <action_name>:动作的名称
|
||||
# start:启动设备或某个功能。
|
||||
# stop:停止设备或某个功能。
|
||||
# set_speed:设置设备的速度。
|
||||
# set_temperature:设置设备的温度。
|
||||
# move_to_position:移动设备到指定位置。
|
||||
# stir:执行搅拌操作。
|
||||
# heatchill:执行加热或冷却操作。
|
||||
# send_nav_task:发送导航任务(例如机器人导航)。
|
||||
# set_timer:设置设备的计时器。
|
||||
# valve_open_cmd:打开阀门。
|
||||
# valve_close_cmd:关闭阀门。
|
||||
# execute_command_from_outer:执行外部命令。
|
||||
# push_to:控制设备推送到某个位置(例如机械爪)。
|
||||
# move_through_points:导航设备通过多个点。
|
||||
|
||||
type: <ActionType> # 动作的类型,表示动作的功能
|
||||
# 根据动作的功能选择合适的类型,请查阅 Uni-Lab 已支持的指令集。
|
||||
|
||||
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
|
||||
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
|
||||
|
||||
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
|
||||
<feedback_key>: <mapped_value>
|
||||
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
|
||||
<result_key>: <mapped_value>
|
||||
```
|
||||
|
||||
6. 定义设备的网页展示属性类型,这部分会被用于在 Uni-Lab 网页端渲染成状态监控
|
||||
添加设备的属性模式,包括属性类型和描述:
|
||||
|
||||
```yaml
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
speed:
|
||||
type: number
|
||||
description: The speed of the device
|
||||
required:
|
||||
- status
|
||||
- speed
|
||||
additionalProperties: false
|
||||
```
|
||||
详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <add_yaml>`。
|
||||
|
||||
@@ -1,95 +1,610 @@
|
||||
# yaml注册表编写指南
|
||||
# yaml 注册表编写指南
|
||||
|
||||
`注册表的结构`
|
||||
## 快速开始:使用注册表编辑器
|
||||
|
||||
1. 顶层名称:每个设备的注册表以设备名称开头,例如 new_device。
|
||||
2. class 字段:定义设备的模块路径和类型。
|
||||
3. schema 字段:定义设备的属性模式,包括属性类型、描述和必需字段。
|
||||
4. action_value_mappings 字段:定义设备支持的动作及其目标、反馈和结果。
|
||||
推荐使用 UniLabOS 自带的可视化编辑器,它能帮你自动生成大部分配置,省去手写的麻烦。
|
||||
|
||||
`创建新的注册表教程`
|
||||
1. 创建文件
|
||||
在 devices 文件夹中创建一个新的 YAML 文件,例如 new_device.yaml。
|
||||
### 怎么用编辑器
|
||||
|
||||
2. 定义设备名称
|
||||
在文件中定义设备的顶层名称,例如:new_device
|
||||
1. 启动 UniLabOS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择你的 Python 设备驱动文件
|
||||
4. 点击"分析文件",让系统读取你的类信息
|
||||
5. 填写一些基本信息(设备描述、图标啥的)
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 把内容保存到 `devices/` 目录下
|
||||
|
||||
3. 定义设备的类信息
|
||||
添加设备的模块路径和类型:
|
||||
我们为你准备了一个测试驱动,用于在界面上尝试注册表生成,参见目录:test\registry\example_devices.py
|
||||
|
||||
```python
|
||||
new_device: # 定义一个名为 linear_motion.grbl 的设备
|
||||
---
|
||||
|
||||
## 手动编写指南
|
||||
|
||||
class: # 定义设备的类信息
|
||||
module: unilabos.devices_names.new_device:NewDeviceClass # 指定模块路径和类名
|
||||
type: python # 指定类型为 Python 类
|
||||
status_types:
|
||||
```
|
||||
4. 定义设备支持的动作
|
||||
添加设备支持的动作及其目标、反馈和结果:
|
||||
```python
|
||||
action_value_mappings:
|
||||
set_speed:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
```
|
||||
`如何编写action_valve_mappings`
|
||||
1. 在 devices 文件夹中的 YAML 文件中,action_value_mappings 是用来定义设备支持的动作(actions)及其目标值(goal)、反馈值(feedback)和结果值(result)的映射规则。以下是规则和编写方法:
|
||||
```python
|
||||
action_value_mappings:
|
||||
<action_name>: # <action_name>:动作的名称
|
||||
# start:启动设备或某个功能。
|
||||
# stop:停止设备或某个功能。
|
||||
# set_speed:设置设备的速度。
|
||||
# set_temperature:设置设备的温度。
|
||||
# move_to_position:移动设备到指定位置。
|
||||
# stir:执行搅拌操作。
|
||||
# heatchill:执行加热或冷却操作。
|
||||
# send_nav_task:发送导航任务(例如机器人导航)。
|
||||
# set_timer:设置设备的计时器。
|
||||
# valve_open_cmd:打开阀门。
|
||||
# valve_close_cmd:关闭阀门。
|
||||
# execute_command_from_outer:执行外部命令。
|
||||
# push_to:控制设备推送到某个位置(例如机械爪)。
|
||||
# move_through_points:导航设备通过多个点。
|
||||
如果你想自己写 yaml 文件,或者想深入了解结构,查阅下方说明。
|
||||
|
||||
type: <ActionType> # 动作的类型,表示动作的功能
|
||||
# 根据动作的功能选择合适的类型:
|
||||
# SendCmd:发送简单命令。
|
||||
# NavigateThroughPoses:导航动作。
|
||||
# SingleJointPosition:设置单一关节的位置。
|
||||
# Stir:搅拌动作。
|
||||
# HeatChill:加热或冷却动作。
|
||||
## 注册表的基本结构
|
||||
|
||||
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
|
||||
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
|
||||
yaml 注册表就是设备的配置文件,里面定义了设备怎么用、有什么功能。好消息是系统会自动帮你填大部分内容,你只需要写两个必需的东西:设备名和 class 信息。
|
||||
|
||||
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
|
||||
<feedback_key>: <mapped_value>
|
||||
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
|
||||
<result_key>: <mapped_value>
|
||||
### 各字段用途
|
||||
|
||||
| 字段名 | 类型 | 需要手写 | 说明 |
|
||||
| ----------------- | ------ | -------- | ----------------------------------- |
|
||||
| 设备标识符 | string | 是 | 设备的唯一名字,比如 `mock_chiller` |
|
||||
| class | object | 部分 | 设备的核心信息,必须写 |
|
||||
| description | string | 否 | 设备描述,系统默认给空字符串 |
|
||||
| handles | array | 否 | 连接关系,默认是空的 |
|
||||
| icon | string | 否 | 图标路径,默认为空 |
|
||||
| init_param_schema | object | 否 | 初始化参数,系统自动分析生成 |
|
||||
| version | string | 否 | 版本号,默认 "1.0.0" |
|
||||
| category | array | 否 | 设备分类,默认用文件名 |
|
||||
| config_info | array | 否 | 嵌套配置,默认为空 |
|
||||
| file_path | string | 否 | 文件路径,系统自动设置 |
|
||||
| registry_type | string | 否 | 注册表类型,自动设为 "device" |
|
||||
|
||||
### class 字段里有啥
|
||||
|
||||
class 是核心部分,包含这些内容:
|
||||
|
||||
| 字段名 | 类型 | 需要手写 | 说明 |
|
||||
| --------------------- | ------ | -------- | ---------------------------------- |
|
||||
| module | string | 是 | Python 类的路径,必须写 |
|
||||
| type | string | 是 | 驱动类型,一般写 "python" |
|
||||
| status_types | object | 否 | 状态类型,系统自动分析生成 |
|
||||
| action_value_mappings | object | 部分 | 动作配置,系统会自动生成一些基础的 |
|
||||
|
||||
## 怎么创建新的注册表
|
||||
|
||||
### 创建文件
|
||||
|
||||
在 devices 文件夹里新建一个 yaml 文件,比如 `new_device.yaml`。
|
||||
|
||||
### 完整结构是什么样的
|
||||
|
||||
```yaml
|
||||
new_device: # 设备名,要唯一
|
||||
class: # 核心配置
|
||||
action_value_mappings: # 动作配置(后面会详细说)
|
||||
action_name:
|
||||
# 具体的动作设置
|
||||
module: unilabos.devices.your_module.new_device:NewDeviceClass # 你的 Python 类
|
||||
status_types: # 状态类型(系统会自动生成)
|
||||
status: str
|
||||
temperature: float
|
||||
# 其他状态
|
||||
type: python # 驱动类型,一般就是 python
|
||||
|
||||
description: New Device Description # 设备描述
|
||||
handles: [] # 连接关系,通常是空的
|
||||
icon: '' # 图标路径
|
||||
init_param_schema: # 初始化参数(系统会自动生成)
|
||||
config: # 初始化时需要的参数
|
||||
properties:
|
||||
port:
|
||||
default: DEFAULT_PORT
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data: # 前端显示用的数据类型
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
|
||||
version: 0.0.1 # 版本号
|
||||
category:
|
||||
- device_category # 设备类别
|
||||
config_info: [] # 嵌套配置,通常为空
|
||||
```
|
||||
|
||||
6. 定义设备的属性模式
|
||||
添加设备的属性模式,包括属性类型和描述:
|
||||
```python
|
||||
schema:
|
||||
type: object
|
||||
## action_value_mappings 怎么写
|
||||
|
||||
这个部分定义设备能做哪些动作。好消息是系统会自动生成大部分动作,你通常只需要添加一些特殊的自定义动作。
|
||||
|
||||
### 系统自动生成哪些动作
|
||||
|
||||
系统会帮你生成这些:
|
||||
|
||||
1. 以 `auto-` 开头的动作:从你 Python 类的方法自动生成
|
||||
2. 通用的驱动动作:
|
||||
- `_execute_driver_command`:同步执行驱动命令
|
||||
- `_execute_driver_command_async`:异步执行驱动命令
|
||||
|
||||
### 如果要手动定义动作
|
||||
|
||||
如果你需要自定义一些特殊动作,需要这些字段:
|
||||
|
||||
| 字段名 | 需要手写 | 说明 |
|
||||
| ---------------- | -------- | -------------------------------- |
|
||||
| type | 是 | 动作类型,必须指定 |
|
||||
| goal | 是 | 输入参数怎么映射 |
|
||||
| feedback | 否 | 实时反馈,通常为空 |
|
||||
| result | 是 | 结果怎么返回 |
|
||||
| goal_default | 部分 | 参数默认值,ROS 动作会自动生成 |
|
||||
| schema | 部分 | 前端表单配置,ROS 动作会自动生成 |
|
||||
| handles | 否 | 连接关系,默认为空 |
|
||||
| placeholder_keys | 否 | 特殊输入字段配置 |
|
||||
|
||||
### 动作类型有哪些
|
||||
|
||||
| 类型 | 什么时候用 | 系统会自动生成什么 |
|
||||
| ---------------------- | -------------------- | ---------------------- |
|
||||
| UniLabJsonCommand | 自定义同步 JSON 命令 | 啥都不生成 |
|
||||
| UniLabJsonCommandAsync | 自定义异步 JSON 命令 | 啥都不生成 |
|
||||
| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema |
|
||||
|
||||
常用的 ROS 动作类型:
|
||||
|
||||
- `SendCmd`:发送简单命令
|
||||
- `NavigateThroughPoses`:导航动作
|
||||
- `SingleJointPosition`:单关节位置控制
|
||||
- `Stir`:搅拌动作
|
||||
- `HeatChill`、`HeatChillStart`:加热冷却动作
|
||||
|
||||
### 复杂一点的例子
|
||||
|
||||
```yaml
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
purpose: purpose
|
||||
temp: temp
|
||||
goal_default: # ROS动作会自动生成,你也可以手动覆盖
|
||||
purpose: ''
|
||||
temp: 0.0
|
||||
handles:
|
||||
output:
|
||||
- handler_key: labware
|
||||
label: Labware
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
placeholder_keys:
|
||||
purpose: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
# schema 系统会自动生成,不用写
|
||||
```
|
||||
|
||||
### 动作名字怎么起
|
||||
|
||||
根据设备用途来起名字:
|
||||
|
||||
- 启动停止类:`start`、`stop`、`pause`、`resume`
|
||||
- 设置参数类:`set_speed`、`set_temperature`、`set_timer`
|
||||
- 移动控制类:`move_to_position`、`move_through_points`
|
||||
- 功能操作类:`stir`、`heat_chill_start`、`heat_chill_stop`
|
||||
- 开关控制类:`valve_open_cmd`、`valve_close_cmd`、`push_to`
|
||||
- 命令执行类:`send_nav_task`、`execute_command_from_outer`
|
||||
|
||||
### 常用的动作类型
|
||||
|
||||
- `UniLabJsonCommand`:自定义 JSON 命令(不走 ROS)
|
||||
- `UniLabJsonCommandAsync`:异步 JSON 命令(不走 ROS)
|
||||
- `SendCmd`:发送简单命令
|
||||
- `NavigateThroughPoses`:导航相关
|
||||
- `SingleJointPosition`:单关节控制
|
||||
- `Stir`:搅拌
|
||||
- `HeatChill`、`HeatChillStart`:加热冷却
|
||||
- 其他的 ROS 动作类型:看具体的 ROS 服务
|
||||
|
||||
### 示例:完整的动作配置
|
||||
|
||||
```yaml
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
purpose: purpose
|
||||
temp: temp
|
||||
goal_default:
|
||||
purpose: ''
|
||||
temp: 0.0
|
||||
handles:
|
||||
output:
|
||||
- handler_key: labware
|
||||
label: Labware
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
placeholder_keys:
|
||||
purpose: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
schema:
|
||||
description: '启动加热冷却功能'
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
purpose:
|
||||
type: string
|
||||
description: '用途说明'
|
||||
temp:
|
||||
type: number
|
||||
description: '目标温度'
|
||||
required:
|
||||
- purpose
|
||||
- temp
|
||||
title: HeatChillStart_Goal
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: HeatChillStart
|
||||
type: object
|
||||
feedback: {}
|
||||
```
|
||||
|
||||
## 系统自动生成的字段
|
||||
|
||||
### status_types
|
||||
|
||||
系统会扫描你的 Python 类,从状态方法自动生成这部分:
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
current_temperature: float # 从 get_current_temperature() 方法来的
|
||||
is_heating: bool # 从 get_is_heating() 方法来的
|
||||
status: str # 从 get_status() 方法来的
|
||||
```
|
||||
|
||||
注意几点:
|
||||
|
||||
- 系统会找所有 `get_` 开头的方法
|
||||
- 类型会自动转成 ROS 类型(比如 `str` 变成 `String`)
|
||||
- 如果类型是 `Any`、`None` 或者不知道的,就默认用 `String`
|
||||
|
||||
### init_param_schema
|
||||
|
||||
这个完全是系统自动生成的,你不用管:
|
||||
|
||||
```yaml
|
||||
init_param_schema:
|
||||
config: # 从你类的 __init__ 方法分析出来的
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
default: '/dev/ttyUSB0'
|
||||
baudrate:
|
||||
type: integer
|
||||
default: 9600
|
||||
required: []
|
||||
type: object
|
||||
|
||||
data: # 根据 status_types 生成的前端用的类型
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
is_heating:
|
||||
type: boolean
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
speed:
|
||||
type: number
|
||||
description: The speed of the device
|
||||
required:
|
||||
- status
|
||||
- speed
|
||||
additionalProperties: false
|
||||
type: object
|
||||
```
|
||||
# 写完yaml注册表后需要添加到哪些其他文件?
|
||||
|
||||
生成规则很简单:
|
||||
|
||||
- `config` 部分:看你类的 `__init__` 方法有什么参数,类型和默认值是啥
|
||||
- `data` 部分:根据 `status_types` 生成前端显示用的类型定义
|
||||
|
||||
### 其他自动填充的字段
|
||||
|
||||
```yaml
|
||||
version: '1.0.0' # 默认版本
|
||||
category: ['文件名'] # 用你的 yaml 文件名当类别
|
||||
description: '' # 默认为空,你可以手动改
|
||||
icon: '' # 默认为空,你可以加图标
|
||||
handles: [] # 默认空数组
|
||||
config_info: [] # 默认空数组
|
||||
file_path: '/path/to/file' # 系统自动填文件路径
|
||||
registry_type: 'device' # 自动设为设备类型
|
||||
```
|
||||
|
||||
### handles 字段
|
||||
|
||||
这个是定义设备连接关系的,类似动作里的 handles 一样:
|
||||
|
||||
```yaml
|
||||
handles: # 大多数时候都是空的,除非设备本身需要连接啥
|
||||
- handler_key: device_output
|
||||
label: Device Output
|
||||
data_type: resource
|
||||
data_source: value
|
||||
data_key: default_value
|
||||
```
|
||||
|
||||
### 其他可以配置的字段
|
||||
|
||||
```yaml
|
||||
description: '设备的详细描述' # 写清楚设备是干啥的
|
||||
|
||||
icon: 'device_icon.webp' # 设备图标,文件名(会上传到OSS)
|
||||
|
||||
version: '0.0.1' # 版本号
|
||||
|
||||
category: # 设备分类,前端会用这个分组
|
||||
- 'heating'
|
||||
- 'cooling'
|
||||
- 'temperature_control'
|
||||
|
||||
config_info: # 嵌套配置,如果设备包含子设备
|
||||
- children:
|
||||
- opentrons_24_tuberack_nest_1point5ml_snapcap_A1
|
||||
- other_nested_component
|
||||
```
|
||||
|
||||
## 完整的例子
|
||||
|
||||
这里是一个比较完整的设备配置示例:
|
||||
|
||||
```yaml
|
||||
my_temperature_controller:
|
||||
class:
|
||||
action_value_mappings:
|
||||
heat_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
target_temp: temp
|
||||
vessel: vessel
|
||||
goal_default:
|
||||
target_temp: 25.0
|
||||
vessel: ''
|
||||
handles:
|
||||
output:
|
||||
- handler_key: heated_sample
|
||||
label: Heated Sample
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: sample
|
||||
placeholder_keys:
|
||||
vessel: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
schema:
|
||||
description: '启动加热功能'
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
target_temp:
|
||||
type: number
|
||||
description: '目标温度'
|
||||
vessel:
|
||||
type: string
|
||||
description: '容器标识'
|
||||
required:
|
||||
- target_temp
|
||||
- vessel
|
||||
title: HeatStart_Goal
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: HeatStart
|
||||
type: object
|
||||
feedback: {}
|
||||
|
||||
stop:
|
||||
type: UniLabJsonCommand
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result:
|
||||
status: status
|
||||
schema:
|
||||
description: '停止设备'
|
||||
properties:
|
||||
goal:
|
||||
type: object
|
||||
title: Stop_Goal
|
||||
title: Stop
|
||||
type: object
|
||||
feedback: {}
|
||||
|
||||
module: unilabos.devices.temperature.my_controller:MyTemperatureController
|
||||
status_types:
|
||||
current_temperature: float
|
||||
target_temperature: float
|
||||
is_heating: bool
|
||||
is_cooling: bool
|
||||
status: str
|
||||
vessel: str
|
||||
type: python
|
||||
|
||||
description: '我的温度控制器设备'
|
||||
handles: []
|
||||
icon: 'temperature_controller.webp'
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
default: '/dev/ttyUSB0'
|
||||
type: string
|
||||
baudrate:
|
||||
default: 9600
|
||||
type: number
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
target_temperature:
|
||||
type: number
|
||||
is_heating:
|
||||
type: boolean
|
||||
is_cooling:
|
||||
type: boolean
|
||||
status:
|
||||
type: string
|
||||
vessel:
|
||||
type: string
|
||||
required:
|
||||
- current_temperature
|
||||
- target_temperature
|
||||
- status
|
||||
type: object
|
||||
|
||||
version: '1.0.0'
|
||||
category:
|
||||
- 'temperature_control'
|
||||
- 'heating'
|
||||
config_info: []
|
||||
```
|
||||
|
||||
## 怎么部署和使用
|
||||
|
||||
### 方法一:用编辑器(推荐)
|
||||
|
||||
1. 先写好你的 Python 驱动类
|
||||
2. 用注册表编辑器自动生成 yaml 配置
|
||||
3. 把生成的文件保存到 `devices/` 目录
|
||||
4. 重启 UniLabOS 就能用了
|
||||
|
||||
### 方法二:手动写(简化版)
|
||||
|
||||
1. 创建最简配置:
|
||||
|
||||
```yaml
|
||||
# devices/my_device.yaml
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.my_module.my_device:MyDevice
|
||||
type: python
|
||||
```
|
||||
|
||||
2. 启动系统时用 `complete_registry=True` 参数,让系统自动补全
|
||||
|
||||
3. 检查一下生成的配置是不是你想要的
|
||||
|
||||
### Python 驱动类要怎么写
|
||||
|
||||
你的设备类要符合这些要求:
|
||||
|
||||
```python
|
||||
from unilabos.common.device_base import DeviceBase
|
||||
|
||||
class MyDevice(DeviceBase):
|
||||
def __init__(self, config):
|
||||
"""初始化,参数会自动分析到 init_param_schema.config"""
|
||||
super().__init__(config)
|
||||
self.port = config.get('port', '/dev/ttyUSB0')
|
||||
|
||||
# 状态方法(会自动生成到 status_types)
|
||||
def get_status(self):
|
||||
"""返回设备状态"""
|
||||
return "idle"
|
||||
|
||||
def get_temperature(self):
|
||||
"""返回当前温度"""
|
||||
return 25.0
|
||||
|
||||
# 动作方法(会自动生成 auto- 开头的动作)
|
||||
async def start_heating(self, temperature: float):
|
||||
"""开始加热到指定温度"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""停止操作"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 系统集成
|
||||
|
||||
1. 把 yaml 文件放到 `devices/` 目录下
|
||||
2. 系统启动时会自动扫描并加载设备
|
||||
3. 系统会自动补全所有缺失的字段
|
||||
4. 设备马上就能在前端界面中使用
|
||||
|
||||
### 高级配置
|
||||
|
||||
如果需要特殊设置,可以手动加:
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.my_module.my_device:MyDevice
|
||||
type: python
|
||||
action_value_mappings:
|
||||
# 自定义动作
|
||||
special_command:
|
||||
type: UniLabJsonCommand
|
||||
goal: {}
|
||||
result: {}
|
||||
|
||||
# 可选的自定义配置
|
||||
description: '我的特殊设备'
|
||||
icon: 'my_device.webp'
|
||||
category: ['temperature', 'heating']
|
||||
```
|
||||
|
||||
## 常见问题怎么排查
|
||||
|
||||
### 设备加载不了
|
||||
|
||||
1. 检查模块路径:确认 `class.module` 路径写对了
|
||||
2. 确认类能导入:看看你的 Python 驱动类能不能正常导入
|
||||
3. 检查语法:用 yaml 验证器看看文件格式对不对
|
||||
4. 查看日志:看 UniLabOS 启动时有没有报错信息
|
||||
|
||||
### 自动生成失败了
|
||||
|
||||
1. 类分析出问题:确认你的类继承了正确的基类
|
||||
2. 方法类型不明确:确保状态方法的返回类型写清楚了
|
||||
3. 导入有问题:检查类能不能被动态导入
|
||||
4. 没开完整注册:确认启用了 `complete_registry=True`
|
||||
|
||||
### 前端显示有问题
|
||||
|
||||
1. 重新生成:删掉旧的 yaml 文件,用编辑器重新生成
|
||||
2. 清除缓存:清除浏览器缓存,重新加载页面
|
||||
3. 检查字段:确认必需的字段(比如 `schema`)都有
|
||||
4. 验证数据:检查 `goal_default` 和 `schema` 的数据类型是不是一致
|
||||
|
||||
### 动作执行出错
|
||||
|
||||
1. 方法名不对:确认动作方法名符合规范(比如 `execute_<action_name>`)
|
||||
2. 参数映射错误:检查 `goal` 字段的参数映射是否正确
|
||||
3. 返回格式不对:确认方法返回值格式符合 `result` 映射
|
||||
4. 没异常处理:在驱动类里加上异常处理
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 开发流程
|
||||
|
||||
1. **优先使用编辑器**:除非有特殊需求,否则优先使用注册表编辑器
|
||||
2. **最小化配置**:手动配置时只定义必要字段,让系统自动生成其他内容
|
||||
3. **增量开发**:先创建基本配置,后续根据需要添加特殊动作
|
||||
|
||||
### 代码规范
|
||||
|
||||
1. **方法命名**:状态方法使用 `get_` 前缀,动作方法使用动词开头
|
||||
2. **类型注解**:为方法参数和返回值添加类型注解
|
||||
3. **文档字符串**:为类和方法添加详细的文档字符串
|
||||
4. **异常处理**:实现完善的错误处理和日志记录
|
||||
|
||||
### 配置管理
|
||||
|
||||
1. **版本控制**:所有 yaml 文件纳入版本控制
|
||||
2. **命名一致性**:设备 ID、文件名、类名保持一致的命名风格
|
||||
3. **定期更新**:定期运行完整注册以更新自动生成的字段
|
||||
4. **备份配置**:在修改前备份重要的手动配置
|
||||
|
||||
### 测试验证
|
||||
|
||||
1. **本地测试**:在本地环境充分测试后再部署
|
||||
2. **渐进部署**:先部署到测试环境,验证无误后再上生产环境
|
||||
3. **监控日志**:密切监控设备加载和运行日志
|
||||
4. **回滚准备**:准备快速回滚机制,以应对紧急情况
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **按需加载**:只加载实际使用的设备类型
|
||||
2. **缓存利用**:充分利用系统的注册表缓存机制
|
||||
3. **资源管理**:合理管理设备连接和资源占用
|
||||
4. **监控指标**:设置关键性能指标的监控和告警
|
||||
|
||||
378
docs/developer_guide/workstation_architecture.md
Normal file
378
docs/developer_guide/workstation_architecture.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# 工作站基础架构设计文档
|
||||
|
||||
## 1. 整体架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "工作站基础架构"
|
||||
WB[WorkstationBase]
|
||||
WB --> |继承| RPN[ROS2WorkstationNode]
|
||||
WB --> |组合| WCB[WorkstationCommunicationBase]
|
||||
WB --> |组合| MMB[MaterialManagementBase]
|
||||
WB --> |组合| WHS[WorkstationHTTPService]
|
||||
end
|
||||
|
||||
subgraph "通信层实现"
|
||||
WCB --> |实现| PLC[PLCCommunication]
|
||||
WCB --> |实现| SER[SerialCommunication]
|
||||
WCB --> |实现| ETH[EthernetCommunication]
|
||||
end
|
||||
|
||||
subgraph "物料管理实现"
|
||||
MMB --> |实现| PLR[PyLabRobotMaterialManager]
|
||||
MMB --> |实现| BIO[BioyondMaterialManager]
|
||||
MMB --> |实现| SIM[SimpleMaterialManager]
|
||||
end
|
||||
|
||||
subgraph "HTTP服务"
|
||||
WHS --> |处理| LIMS[LIMS协议报送]
|
||||
WHS --> |处理| MAT[物料变更报送]
|
||||
WHS --> |处理| ERR[错误处理报送]
|
||||
end
|
||||
|
||||
subgraph "具体工作站实现"
|
||||
WB --> |继承| WS1[PLCWorkstation]
|
||||
WB --> |继承| WS2[ReportingWorkstation]
|
||||
WB --> |继承| WS3[HybridWorkstation]
|
||||
end
|
||||
|
||||
subgraph "外部系统"
|
||||
EXT1[PLC设备] --> |通信| PLC
|
||||
EXT2[外部工作站] --> |HTTP报送| WHS
|
||||
EXT3[LIMS系统] --> |HTTP报送| WHS
|
||||
EXT4[Bioyond物料系统] --> |查询| BIO
|
||||
end
|
||||
```
|
||||
|
||||
## 2. 类关系图
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class WorkstationBase {
|
||||
<<abstract>>
|
||||
+device_id: str
|
||||
+communication: WorkstationCommunicationBase
|
||||
+material_management: MaterialManagementBase
|
||||
+http_service: WorkstationHTTPService
|
||||
+workflow_status: WorkflowStatus
|
||||
+supported_workflows: Dict
|
||||
|
||||
+_create_communication_module()*
|
||||
+_create_material_management_module()*
|
||||
+_register_supported_workflows()*
|
||||
|
||||
+process_step_finish_report()
|
||||
+process_sample_finish_report()
|
||||
+process_order_finish_report()
|
||||
+process_material_change_report()
|
||||
+handle_external_error()
|
||||
|
||||
+start_workflow()
|
||||
+stop_workflow()
|
||||
+get_workflow_status()
|
||||
+get_device_status()
|
||||
}
|
||||
|
||||
class ROS2WorkstationNode {
|
||||
+sub_devices: Dict
|
||||
+protocol_names: List
|
||||
+execute_single_action()
|
||||
+create_ros_action_server()
|
||||
+initialize_device()
|
||||
}
|
||||
|
||||
class WorkstationCommunicationBase {
|
||||
<<abstract>>
|
||||
+config: CommunicationConfig
|
||||
+is_connected: bool
|
||||
+connect()
|
||||
+disconnect()
|
||||
+start_workflow()*
|
||||
+stop_workflow()*
|
||||
+get_device_status()*
|
||||
+write_register()
|
||||
+read_register()
|
||||
}
|
||||
|
||||
class MaterialManagementBase {
|
||||
<<abstract>>
|
||||
+device_id: str
|
||||
+deck_config: Dict
|
||||
+resource_tracker: DeviceNodeResourceTracker
|
||||
+plr_deck: Deck
|
||||
+find_materials_by_type()
|
||||
+update_material_location()
|
||||
+convert_to_unilab_format()
|
||||
+_create_resource_by_type()*
|
||||
}
|
||||
|
||||
class WorkstationHTTPService {
|
||||
+workstation_instance: WorkstationBase
|
||||
+host: str
|
||||
+port: int
|
||||
+start()
|
||||
+stop()
|
||||
+_handle_step_finish_report()
|
||||
+_handle_material_change_report()
|
||||
}
|
||||
|
||||
class PLCWorkstation {
|
||||
+plc_config: Dict
|
||||
+modbus_client: ModbusTCPClient
|
||||
+_create_communication_module()
|
||||
+_create_material_management_module()
|
||||
+_register_supported_workflows()
|
||||
}
|
||||
|
||||
class ReportingWorkstation {
|
||||
+report_handlers: Dict
|
||||
+_create_communication_module()
|
||||
+_create_material_management_module()
|
||||
+_register_supported_workflows()
|
||||
}
|
||||
|
||||
WorkstationBase --|> ROS2WorkstationNode
|
||||
WorkstationBase *-- WorkstationCommunicationBase
|
||||
WorkstationBase *-- MaterialManagementBase
|
||||
WorkstationBase *-- WorkstationHTTPService
|
||||
|
||||
PLCWorkstation --|> WorkstationBase
|
||||
ReportingWorkstation --|> WorkstationBase
|
||||
|
||||
WorkstationCommunicationBase <|-- PLCCommunication
|
||||
WorkstationCommunicationBase <|-- DummyCommunication
|
||||
|
||||
MaterialManagementBase <|-- PyLabRobotMaterialManager
|
||||
MaterialManagementBase <|-- SimpleMaterialManager
|
||||
```
|
||||
|
||||
## 3. 工作站启动时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant APP as Application
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant MAT as MaterialManager
|
||||
participant HTTP as HTTPService
|
||||
participant ROS as ROS2WorkstationNode
|
||||
|
||||
APP->>WS: 创建工作站实例
|
||||
WS->>ROS: 初始化ROS2WorkstationNode
|
||||
ROS->>ROS: 初始化子设备
|
||||
ROS->>ROS: 设置硬件接口代理
|
||||
|
||||
WS->>COMM: _create_communication_module()
|
||||
COMM->>COMM: 初始化通信配置
|
||||
COMM->>COMM: 建立PLC/串口连接
|
||||
COMM-->>WS: 返回通信模块实例
|
||||
|
||||
WS->>MAT: _create_material_management_module()
|
||||
MAT->>MAT: 创建PyLabRobot Deck
|
||||
MAT->>MAT: 初始化物料资源
|
||||
MAT->>MAT: 注册到ResourceTracker
|
||||
MAT-->>WS: 返回物料管理实例
|
||||
|
||||
WS->>WS: _register_supported_workflows()
|
||||
WS->>WS: _create_workstation_services()
|
||||
WS->>HTTP: _start_http_service()
|
||||
HTTP->>HTTP: 创建HTTP服务器
|
||||
HTTP->>HTTP: 启动监听线程
|
||||
HTTP-->>WS: HTTP服务启动完成
|
||||
|
||||
WS-->>APP: 工作站初始化完成
|
||||
```
|
||||
|
||||
## 4. 工作流执行时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant EXT as ExternalSystem
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant MAT as MaterialManager
|
||||
participant ROS as ROS2WorkstationNode
|
||||
participant DEV as SubDevice
|
||||
|
||||
EXT->>WS: start_workflow(type, params)
|
||||
WS->>WS: 验证工作流类型
|
||||
WS->>COMM: start_workflow(type, params)
|
||||
COMM->>COMM: 发送启动命令到PLC
|
||||
COMM-->>WS: 启动成功
|
||||
|
||||
WS->>WS: 更新workflow_status = RUNNING
|
||||
|
||||
loop 工作流步骤执行
|
||||
WS->>ROS: execute_single_action(device_id, action, params)
|
||||
ROS->>DEV: 发送ROS Action请求
|
||||
DEV->>DEV: 执行设备动作
|
||||
DEV-->>ROS: 返回执行结果
|
||||
ROS-->>WS: 返回动作结果
|
||||
|
||||
WS->>MAT: update_material_location(material_id, location)
|
||||
MAT->>MAT: 更新PyLabRobot资源状态
|
||||
MAT-->>WS: 更新完成
|
||||
end
|
||||
|
||||
WS->>COMM: get_workflow_status()
|
||||
COMM->>COMM: 查询PLC状态寄存器
|
||||
COMM-->>WS: 返回状态信息
|
||||
|
||||
WS->>WS: 更新workflow_status = COMPLETED
|
||||
WS-->>EXT: 工作流执行完成
|
||||
```
|
||||
|
||||
## 5. HTTP报送处理时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant EXT as ExternalWorkstation
|
||||
participant HTTP as HTTPService
|
||||
participant WS as WorkstationBase
|
||||
participant MAT as MaterialManager
|
||||
participant DB as DataStorage
|
||||
|
||||
EXT->>HTTP: POST /report/step_finish
|
||||
HTTP->>HTTP: 解析请求数据
|
||||
HTTP->>HTTP: 验证LIMS协议字段
|
||||
HTTP->>WS: process_step_finish_report(request)
|
||||
|
||||
WS->>WS: 增加接收计数
|
||||
WS->>WS: 记录步骤完成事件
|
||||
WS->>MAT: 更新相关物料状态
|
||||
MAT->>MAT: 更新PyLabRobot资源
|
||||
MAT-->>WS: 更新完成
|
||||
|
||||
WS->>DB: 保存报送记录
|
||||
DB-->>WS: 保存完成
|
||||
|
||||
WS-->>HTTP: 返回处理结果
|
||||
HTTP->>HTTP: 构造HTTP响应
|
||||
HTTP-->>EXT: 200 OK + acknowledgment_id
|
||||
|
||||
Note over EXT,DB: 类似处理sample_finish, order_finish, material_change等报送
|
||||
```
|
||||
|
||||
## 6. 错误处理时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant DEV as Device
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant HTTP as HTTPService
|
||||
participant EXT as ExternalSystem
|
||||
|
||||
DEV->>WS: 设备错误事件
|
||||
WS->>WS: handle_external_error(error_data)
|
||||
WS->>WS: 记录错误历史
|
||||
|
||||
alt 关键错误
|
||||
WS->>COMM: emergency_stop()
|
||||
COMM->>COMM: 发送紧急停止命令
|
||||
WS->>WS: 更新workflow_status = ERROR
|
||||
else 普通错误
|
||||
WS->>WS: 标记动作失败
|
||||
WS->>WS: 触发重试逻辑
|
||||
end
|
||||
|
||||
WS->>HTTP: 记录错误报送
|
||||
HTTP->>EXT: 主动通知错误状态
|
||||
|
||||
WS-->>DEV: 错误处理完成
|
||||
```
|
||||
|
||||
## 7. 典型工作站实现示例
|
||||
|
||||
### 7.1 PLC工作站实现
|
||||
|
||||
```python
|
||||
class PLCWorkstation(WorkstationBase):
|
||||
def _create_communication_module(self):
|
||||
return PLCCommunication(self.communication_config)
|
||||
|
||||
def _create_material_management_module(self):
|
||||
return PyLabRobotMaterialManager(
|
||||
self.device_id,
|
||||
self.deck_config,
|
||||
self.resource_tracker
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
self.supported_workflows = {
|
||||
"battery_assembly": WorkflowInfo(...),
|
||||
"quality_check": WorkflowInfo(...)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 报送接收工作站实现
|
||||
|
||||
```python
|
||||
class ReportingWorkstation(WorkstationBase):
|
||||
def _create_communication_module(self):
|
||||
return DummyCommunication(self.communication_config)
|
||||
|
||||
def _create_material_management_module(self):
|
||||
return SimpleMaterialManager(
|
||||
self.device_id,
|
||||
self.deck_config,
|
||||
self.resource_tracker
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
self.supported_workflows = {
|
||||
"data_collection": WorkflowInfo(...),
|
||||
"report_processing": WorkflowInfo(...)
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 核心接口说明
|
||||
|
||||
### 8.1 必须实现的抽象方法
|
||||
- `_create_communication_module()`: 创建通信模块
|
||||
- `_create_material_management_module()`: 创建物料管理模块
|
||||
- `_register_supported_workflows()`: 注册支持的工作流
|
||||
|
||||
### 8.2 可重写的报送处理方法
|
||||
- `process_step_finish_report()`: 步骤完成处理
|
||||
- `process_sample_finish_report()`: 样本完成处理
|
||||
- `process_order_finish_report()`: 订单完成处理
|
||||
- `process_material_change_report()`: 物料变更处理
|
||||
- `handle_external_error()`: 错误处理
|
||||
|
||||
### 8.3 工作流控制接口
|
||||
- `start_workflow()`: 启动工作流
|
||||
- `stop_workflow()`: 停止工作流
|
||||
- `get_workflow_status()`: 获取状态
|
||||
|
||||
## 9. 配置参数说明
|
||||
|
||||
```python
|
||||
workstation_config = {
|
||||
"communication_config": {
|
||||
"protocol": "modbus_tcp",
|
||||
"host": "192.168.1.100",
|
||||
"port": 502
|
||||
},
|
||||
"deck_config": {
|
||||
"size_x": 1000.0,
|
||||
"size_y": 1000.0,
|
||||
"size_z": 500.0
|
||||
},
|
||||
"http_service_config": {
|
||||
"enabled": True,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8081
|
||||
},
|
||||
"communication_interfaces": {
|
||||
"logical_device_1": CommunicationInterface(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个架构设计支持:
|
||||
1. **灵活的通信方式**: 通过CommunicationBase支持PLC、串口、以太网等
|
||||
2. **多样的物料管理**: 支持PyLabRobot、Bioyond、简单物料系统
|
||||
3. **统一的HTTP报送**: 基于LIMS协议的标准化报送接口
|
||||
4. **完整的工作流控制**: 支持动态和静态工作流
|
||||
5. **强大的错误处理**: 多层次的错误处理和恢复机制
|
||||
@@ -1,82 +1,73 @@
|
||||
# Uni-Lab 配置指南
|
||||
|
||||
Uni-Lab支持通过Python配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的Uni-Lab环境。
|
||||
Uni-Lab 支持通过 Python 配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的 Uni-Lab 环境。
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
Uni-Lab支持Python格式的配置文件,它比YAML或JSON提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
Uni-Lab 支持 Python 格式的配置文件,它比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
|
||||
### 基本配置示例
|
||||
### 默认配置示例
|
||||
|
||||
一个典型的配置文件包含以下部分:
|
||||
首次使用时,系统会自动创建一个基础配置文件 `local_config.py`:
|
||||
|
||||
```python
|
||||
# unilabos的配置文件
|
||||
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
|
||||
sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
|
||||
|
||||
|
||||
# WebSocket配置,一般无需调整
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
```
|
||||
|
||||
### 完整配置示例
|
||||
|
||||
您可以根据需要添加更多配置选项:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""Uni-Lab 配置文件"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
# 基础配置
|
||||
class BasicConfig:
|
||||
ak = "your_access_key" # 实验室访问密钥
|
||||
sk = "your_secret_key" # 实验室私钥
|
||||
working_dir = "" # 工作目录(通常自动设置)
|
||||
config_path = "" # 配置文件路径(自动设置)
|
||||
is_host_mode = True # 是否为主站模式
|
||||
slave_no_host = False # 从站模式下是否跳过等待主机服务
|
||||
upload_registry = False # 是否上传注册表
|
||||
machine_name = "undefined" # 机器名称(自动获取)
|
||||
vis_2d_enable = False # 是否启用2D可视化
|
||||
enable_resource_load = True # 是否启用资源加载
|
||||
communication_protocol = "websocket" # 通信协议
|
||||
|
||||
# 配置类定义
|
||||
# WebSocket配置
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "YOUR_LAB_ID"
|
||||
# 更多配置...
|
||||
# OSS上传配置
|
||||
class OSSUploadConfig:
|
||||
api_host = "" # API主机地址
|
||||
authorization = "" # 授权信息
|
||||
init_endpoint = "" # 初始化端点
|
||||
complete_endpoint = "" # 完成端点
|
||||
max_retries = 3 # 最大重试次数
|
||||
|
||||
# 其他配置类...
|
||||
```
|
||||
|
||||
## 配置选项说明
|
||||
|
||||
### MQTT配置 (MQConfig)
|
||||
|
||||
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
||||
|
||||
```python
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "7AAEDBEA" # 实验室唯一标识
|
||||
instance_id: str = "mqtt-cn-instance"
|
||||
access_key: str = "your-access-key"
|
||||
secret_key: str = "your-secret-key"
|
||||
group_id: str = "GID_labs"
|
||||
broker_url: str = "mqtt-cn-instance.mqtt.aliyuncs.com"
|
||||
port: int = 8883
|
||||
|
||||
# 可以直接提供证书文件路径
|
||||
ca_file: str = "/path/to/ca.pem" # 相对config.py所在目录的路径
|
||||
cert_file: str = "/path/to/cert.pem" # 相对config.py所在目录的路径
|
||||
key_file: str = "/path/to/key.pem" # 相对config.py所在目录的路径
|
||||
|
||||
# 或者直接提供证书内容
|
||||
ca_content: str = ""
|
||||
cert_content: str = ""
|
||||
key_content: str = ""
|
||||
```
|
||||
|
||||
#### 证书配置
|
||||
|
||||
MQTT连接支持两种方式配置证书:
|
||||
|
||||
1. **文件路径方式**(推荐):指定证书文件的路径,系统会自动读取文件内容
|
||||
2. **直接内容方式**:直接在配置中提供证书内容
|
||||
|
||||
推荐使用文件路径方式,便于证书的更新和管理。
|
||||
|
||||
### HTTP客户端配置 (HTTPConfig)
|
||||
|
||||
即将开放 Uni-Lab 云端实验室。
|
||||
|
||||
### ROS模块配置 (ROSConfig)
|
||||
|
||||
配置ROS消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "http://127.0.0.1:48197/api/v1" # 远程地址
|
||||
|
||||
# ROS配置
|
||||
class ROSConfig:
|
||||
"""ROS模块配置"""
|
||||
modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
@@ -85,25 +76,365 @@ class ROSConfig:
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
] # 需要加载的ROS模块
|
||||
```
|
||||
|
||||
## 命令行参数覆盖配置
|
||||
|
||||
Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。命令行参数的优先级高于配置文件。
|
||||
|
||||
### 支持命令行覆盖的配置项
|
||||
|
||||
以下配置项可以通过命令行参数进行覆盖:
|
||||
|
||||
| 配置类 | 配置字段 | 命令行参数 | 说明 |
|
||||
| ------------- | ----------------- | ------------------- | -------------------------------- |
|
||||
| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 |
|
||||
| `BasicConfig` | `sk` | `--sk` | 实验室私钥 |
|
||||
| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 |
|
||||
| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) |
|
||||
| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 |
|
||||
| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 |
|
||||
| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 |
|
||||
| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 |
|
||||
|
||||
### 特殊命令行参数
|
||||
|
||||
除了直接覆盖配置项的参数外,还有一些特殊的命令行参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| `--config` | 指定配置文件路径 |
|
||||
| `--port` | Web 服务端口(不影响配置文件) |
|
||||
| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) |
|
||||
| `--visual` | 可视化工具选择(不影响配置文件) |
|
||||
| `--skip_env_check` | 跳过环境检查(不影响配置文件) |
|
||||
|
||||
### 配置优先级
|
||||
|
||||
配置项的生效优先级从高到低为:
|
||||
|
||||
1. **命令行参数**:最高优先级
|
||||
2. **环境变量**:中等优先级
|
||||
3. **配置文件**:基础优先级
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 通过命令行覆盖认证信息
|
||||
unilab --ak "new_access_key" --sk "new_secret_key"
|
||||
|
||||
# 覆盖服务器地址
|
||||
unilab --addr "https://custom.server.com/api/v1"
|
||||
|
||||
# 启用从站模式并跳过等待主机
|
||||
unilab --is_slave --slave_no_host
|
||||
|
||||
# 启用上传注册表和2D可视化
|
||||
unilab --upload_registry --2d_vis
|
||||
|
||||
# 组合使用多个覆盖参数
|
||||
unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis
|
||||
```
|
||||
|
||||
### 预设环境地址
|
||||
|
||||
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL:
|
||||
|
||||
- `test` → `https://uni-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- `local` → `http://127.0.0.1:48197/api/v1`
|
||||
- 其他值 → 直接使用作为完整 URL
|
||||
|
||||
## 配置选项详解
|
||||
|
||||
### 基础配置 (BasicConfig)
|
||||
|
||||
基础配置包含了系统运行的核心参数:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------------- | ------------------------------------------ |
|
||||
| `ak` | str | `""` | 实验室访问密钥(必需) |
|
||||
| `sk` | str | `""` | 实验室私钥(必需) |
|
||||
| `working_dir` | str | `""` | 工作目录,通常自动设置 |
|
||||
| `is_host_mode` | bool | `True` | 是否为主站模式 |
|
||||
| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 |
|
||||
| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 |
|
||||
| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) |
|
||||
| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 |
|
||||
| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket |
|
||||
|
||||
#### 认证配置
|
||||
|
||||
`ak` 和 `sk` 是必需的认证参数:
|
||||
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
|
||||
2. **配置方式**:
|
||||
- **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级)
|
||||
- **配置文件**:在 `BasicConfig` 类中设置
|
||||
- **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK`
|
||||
3. **优先级顺序**:命令行参数 > 环境变量 > 配置文件
|
||||
4. **安全注意**:请妥善保管您的密钥信息
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
- 开发环境:使用配置文件
|
||||
- 生产环境:使用环境变量或命令行参数
|
||||
- 临时测试:使用命令行参数
|
||||
|
||||
### WebSocket 配置 (WSConfig)
|
||||
|
||||
WebSocket 是 Uni-Lab 的主要通信方式:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------ | ------------------ |
|
||||
| `reconnect_interval` | int | `5` | 断线重连间隔(秒) |
|
||||
| `max_reconnect_attempts` | int | `999` | 最大重连次数 |
|
||||
| `ping_interval` | int | `30` | 心跳检测间隔(秒) |
|
||||
|
||||
### HTTP 配置 (HTTPConfig)
|
||||
|
||||
HTTP 客户端配置用于与云端服务通信:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | --------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"http://127.0.0.1:48197/api/v1"` | 远程服务地址 |
|
||||
|
||||
**预设环境地址**:
|
||||
|
||||
- 生产环境:`https://uni-lab.bohrium.com/api/v1`
|
||||
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### ROS 配置 (ROSConfig)
|
||||
|
||||
配置 ROS 消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
class ROSConfig:
|
||||
modules = [
|
||||
"std_msgs.msg", # 标准消息类型
|
||||
"geometry_msgs.msg", # 几何消息类型
|
||||
"control_msgs.msg", # 控制消息类型
|
||||
"control_msgs.action", # 控制动作类型
|
||||
"nav2_msgs.action", # 导航动作类型
|
||||
"unilabos_msgs.msg", # UniLab 自定义消息类型
|
||||
"unilabos_msgs.action", # UniLab 自定义动作类型
|
||||
]
|
||||
```
|
||||
|
||||
您可以根据需要添加其他ROS模块。
|
||||
您可以根据实际使用的设备和功能添加其他 ROS 模块。
|
||||
|
||||
### 其他配置选项
|
||||
### OSS 上传配置 (OSSUploadConfig)
|
||||
|
||||
- **OSSUploadConfig**: 对象存储上传配置
|
||||
对象存储服务配置,用于文件上传功能:
|
||||
|
||||
## 如何使用配置文件
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------- | ---- | ------ | -------------------- |
|
||||
| `api_host` | str | `""` | OSS API 主机地址 |
|
||||
| `authorization` | str | `""` | 授权认证信息 |
|
||||
| `init_endpoint` | str | `""` | 上传初始化端点 |
|
||||
| `complete_endpoint` | str | `""` | 上传完成端点 |
|
||||
| `max_retries` | int | `3` | 上传失败最大重试次数 |
|
||||
|
||||
启动Uni-Lab时通过`--config`参数指定配置文件路径:
|
||||
## 环境变量支持
|
||||
|
||||
Uni-Lab 支持通过环境变量覆盖配置文件中的设置。环境变量格式为:
|
||||
|
||||
```
|
||||
UNILABOS_{配置类名}_{字段名}
|
||||
```
|
||||
|
||||
### 环境变量示例
|
||||
|
||||
```bash
|
||||
unilab --config path/to/your/config.py
|
||||
# 设置基础配置
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
export UNILABOS_BASICCONFIG_IS_HOST_MODE="true"
|
||||
|
||||
# 设置WebSocket配置
|
||||
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
|
||||
|
||||
# 设置HTTP配置
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
如果您不涉及多环境开发,可以在unilabos的安装路径中手动添加local_config.py的文件
|
||||
### 环境变量类型转换
|
||||
|
||||
# 启动Uni-Lab
|
||||
python -m unilabos.app.main --config path/to/your/config.py
|
||||
- **布尔值**:`"true"`, `"1"`, `"yes"` → `True`;其他 → `False`
|
||||
- **整数**:自动转换为 `int` 类型
|
||||
- **浮点数**:自动转换为 `float` 类型
|
||||
- **字符串**:保持原值
|
||||
|
||||
## 配置文件使用方法
|
||||
|
||||
### 1. 指定配置文件启动
|
||||
|
||||
```bash
|
||||
# 使用指定配置文件启动
|
||||
unilab --config /path/to/your/config.py
|
||||
```
|
||||
|
||||
### 2. 使用默认配置文件
|
||||
|
||||
如果不指定配置文件,系统会按以下顺序查找:
|
||||
|
||||
1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径
|
||||
2. 工作目录下的 `local_config.py`
|
||||
3. 首次使用时会引导创建配置文件
|
||||
|
||||
### 3. 配置文件验证
|
||||
|
||||
系统启动时会自动验证配置文件:
|
||||
|
||||
- **语法检查**:确保 Python 语法正确
|
||||
- **类型检查**:验证配置项类型是否匹配
|
||||
- **必需项检查**:确保 `ak` 和 `sk` 已配置
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 安全配置
|
||||
|
||||
- 不要将包含密钥的配置文件提交到版本控制系统
|
||||
- 使用环境变量或命令行参数在生产环境中配置敏感信息
|
||||
- 定期更换访问密钥
|
||||
- **推荐配置方式**:
|
||||
|
||||
```bash
|
||||
# 生产环境 - 使用环境变量
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
unilab
|
||||
|
||||
# 或使用命令行参数
|
||||
unilab --ak "your_access_key" --sk "your_secret_key"
|
||||
```
|
||||
|
||||
### 2. 多环境配置
|
||||
|
||||
为不同环境创建不同的配置文件并结合命令行参数:
|
||||
|
||||
```
|
||||
configs/
|
||||
├── local_config.py # 本地开发
|
||||
├── test_config.py # 测试环境
|
||||
├── prod_config.py # 生产环境
|
||||
└── example_config.py # 示例配置
|
||||
```
|
||||
|
||||
**环境切换示例**:
|
||||
|
||||
```bash
|
||||
# 本地开发环境
|
||||
unilab --config configs/local_config.py --addr local
|
||||
|
||||
# 测试环境
|
||||
unilab --config configs/test_config.py --addr test --upload_registry
|
||||
|
||||
# 生产环境
|
||||
unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK"
|
||||
```
|
||||
|
||||
### 3. 配置管理
|
||||
|
||||
- 保持配置文件简洁,只包含需要修改的配置项
|
||||
- 为配置项添加注释说明其作用
|
||||
- 定期检查和更新配置文件
|
||||
- **命令行参数优先使用场景**:
|
||||
- 临时测试不同配置
|
||||
- CI/CD 流水线中的动态配置
|
||||
- 不同环境间快速切换
|
||||
- 敏感信息的安全传递
|
||||
|
||||
### 4. 灵活配置策略
|
||||
|
||||
**基础配置文件 + 命令行覆盖**的推荐方式:
|
||||
|
||||
```python
|
||||
# base_config.py - 基础配置
|
||||
class BasicConfig:
|
||||
# 非敏感配置写在文件中
|
||||
is_host_mode = True
|
||||
upload_registry = False
|
||||
vis_2d_enable = False
|
||||
|
||||
class WSConfig:
|
||||
reconnect_interval = 5
|
||||
max_reconnect_attempts = 999
|
||||
ping_interval = 30
|
||||
```
|
||||
|
||||
```bash
|
||||
# 启动时通过命令行覆盖关键参数
|
||||
unilab --config base_config.py \
|
||||
--ak "$AK" \
|
||||
--sk "$SK" \
|
||||
--addr "test" \
|
||||
--upload_registry \
|
||||
--2d_vis
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 配置文件加载失败
|
||||
|
||||
**错误信息**:`[ENV] 配置文件 xxx 不存在`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认配置文件路径正确
|
||||
- 检查文件权限是否可读
|
||||
- 确保配置文件是 `.py` 格式
|
||||
|
||||
### 2. 语法错误
|
||||
|
||||
**错误信息**:`[ENV] 加载配置文件 xxx 失败`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 检查 Python 语法是否正确
|
||||
- 确认类名和字段名拼写正确
|
||||
- 验证缩进是否正确(使用空格而非制表符)
|
||||
|
||||
### 3. 认证失败
|
||||
|
||||
**错误信息**:`后续运行必须拥有一个实验室`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认 `ak` 和 `sk` 已正确配置
|
||||
- 检查密钥是否有效
|
||||
- 确认网络连接正常
|
||||
|
||||
### 4. 环境变量不生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认环境变量名格式正确(`UNILABOS_CLASS_FIELD`)
|
||||
- 检查环境变量是否已正确设置
|
||||
- 重启系统或重新加载环境变量
|
||||
|
||||
### 5. 命令行参数不生效
|
||||
|
||||
**错误现象**:设置了命令行参数但配置没有生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`)
|
||||
- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值)
|
||||
- 确认参数位置正确(所有参数都应在 `unilab` 之后)
|
||||
- 查看启动日志确认参数是否被正确解析
|
||||
|
||||
### 6. 配置优先级混淆
|
||||
|
||||
**错误现象**:不确定哪个配置生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 记住优先级:命令行参数 > 环境变量 > 配置文件
|
||||
- 使用 `--ak` 和 `--sk` 参数时会看到提示信息
|
||||
- 检查启动日志中的配置加载信息
|
||||
- 临时移除低优先级配置来测试高优先级配置是否生效
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
# **Uni-Lab 安装**
|
||||
|
||||
请先 `git clone` 本仓库,随后按照以下步骤安装项目:
|
||||
## 快速开始
|
||||
|
||||
`Uni-Lab` 建议您采用 `mamba` 管理环境。若需从头建立 `Uni-Lab` 的运行依赖环境,请执行
|
||||
1. **配置 Conda 环境**
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。创建新的环境:
|
||||
|
||||
```shell
|
||||
mamba env create -f unilabos-<YOUR_OS>.yaml
|
||||
mamba activate unilab
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
||||
|
||||
若需将依赖安装进当前环境,请执行
|
||||
2. **安装开发版 Uni-Lab-OS**
|
||||
|
||||
```shell
|
||||
conda env update --file unilabos-<YOUR_OS>.yml
|
||||
# 配置好conda环境后,克隆仓库
|
||||
git clone https://github.com/dptech-corp/Uni-Lab-OS.git -b dev
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 安装 Uni-Lab-OS
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
随后,可在本仓库安装 `unilabos` 的开发版:
|
||||
3. **安装开发版 ros-humble-unilabos-msgs**
|
||||
|
||||
**卸载老版本:**
|
||||
```shell
|
||||
pip install .
|
||||
conda activate unilab
|
||||
conda remove --force ros-humble-unilabos-msgs
|
||||
```
|
||||
有时相同的安装包版本会由于dev构建得到的md5不一样,触发安全检查,可输入 `config set safety_checks disabled` 来关闭安全检查。
|
||||
|
||||
**安装新版本:**
|
||||
|
||||
访问 https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/multi-platform-build.yml 选择最新的构建,下载对应平台的压缩包(仅解压一次,得到.conda文件)使用如下指令:
|
||||
```shell
|
||||
conda activate base
|
||||
conda install ros-humble-unilabos-msgs-<version>-<platform>.conda --offline -n <环境名>
|
||||
```
|
||||
|
||||
4. **启动 Uni-Lab 系统**
|
||||
|
||||
请参见{doc}`启动样例 <../boot_examples/index>`或{doc}`启动指南 <launch>`了解详细的启动方法。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Uni-Lab 启动
|
||||
# Uni-Lab 启动指南
|
||||
|
||||
安装完毕后,可以通过 `unilab` 命令行启动:
|
||||
|
||||
@@ -8,70 +8,240 @@ Start Uni-Lab Edge server.
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-g GRAPH, --graph GRAPH
|
||||
Physical setup graph.
|
||||
-d DEVICES, --devices DEVICES
|
||||
Devices config file.
|
||||
-r RESOURCES, --resources RESOURCES
|
||||
Resources config file.
|
||||
Physical setup graph file path.
|
||||
-c CONTROLLERS, --controllers CONTROLLERS
|
||||
Controllers config file.
|
||||
Controllers config file path.
|
||||
--registry_path REGISTRY_PATH
|
||||
Path to the registry
|
||||
Path to the registry directory
|
||||
--working_dir WORKING_DIR
|
||||
Path to the working directory
|
||||
--backend {ros,simple,automancer}
|
||||
Choose the backend to run with: 'ros', 'simple', or 'automancer'.
|
||||
--app_bridges APP_BRIDGES [APP_BRIDGES ...]
|
||||
Bridges to connect to. Now support 'mqtt' and 'fastapi'.
|
||||
--without_host Run the backend as slave (without host).
|
||||
--config CONFIG Configuration file path for system settings
|
||||
Bridges to connect to. Now support 'websocket' and 'fastapi'.
|
||||
--is_slave Run the backend as slave node (without host privileges).
|
||||
--slave_no_host Skip waiting for host service in slave mode
|
||||
--upload_registry Upload registry information when starting unilab
|
||||
--use_remote_resource Use remote resources when starting unilab
|
||||
--config CONFIG Configuration file path, supports .py format Python config files
|
||||
--port PORT Port for web service information page
|
||||
--disable_browser Disable opening information page on startup
|
||||
--2d_vis Enable 2D visualization when starting pylabrobot instance
|
||||
--visual {rviz,web,disable}
|
||||
Choose visualization tool: rviz, web, or disable
|
||||
--ak AK Access key for laboratory requests
|
||||
--sk SK Secret key for laboratory requests
|
||||
--addr ADDR Laboratory backend address
|
||||
--skip_env_check Skip environment dependency check on startup
|
||||
--complete_registry Complete registry information
|
||||
```
|
||||
|
||||
## 启动流程详解
|
||||
|
||||
Uni-Lab 的启动过程分为以下几个阶段:
|
||||
|
||||
### 1. 参数解析阶段
|
||||
|
||||
- 解析命令行参数
|
||||
- 处理参数格式转换(支持 dash 和 underscore 格式)
|
||||
|
||||
### 2. 环境检查阶段 (可选)
|
||||
|
||||
- 默认进行环境依赖检查并自动安装必需包
|
||||
- 使用 `--skip_env_check` 可跳过此步骤
|
||||
|
||||
### 3. 配置文件处理阶段
|
||||
|
||||
您可以直接跟随 unilabos 的提示进行,无需查阅本节
|
||||
|
||||
- **工作目录设置**:
|
||||
|
||||
- 如果当前目录以 `unilabos_data` 结尾,则使用当前目录
|
||||
- 否则使用 `当前目录/unilabos_data` 作为工作目录
|
||||
- 可通过 `--working_dir` 指定自定义工作目录
|
||||
|
||||
- **配置文件查找顺序**:
|
||||
1. 使用 `--config` 参数指定的配置文件
|
||||
2. 在工作目录中查找 `local_config.py`
|
||||
3. 首次使用时会引导创建配置文件
|
||||
|
||||
### 4. 服务器地址配置
|
||||
|
||||
支持多种后端环境:
|
||||
|
||||
- `--addr test`:测试环境 (`https://uni-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://uni-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr local`:本地环境 (`http://127.0.0.1:48197/api/v1`)
|
||||
- 自定义地址:直接指定完整 URL
|
||||
|
||||
### 5. 认证配置
|
||||
|
||||
- **必需参数**:`--ak` 和 `--sk` 必须同时提供
|
||||
- 命令行参数优先于配置文件中的设置
|
||||
- 未提供认证信息会导致启动失败并提示注册实验室
|
||||
|
||||
### 6. 设备图谱加载
|
||||
|
||||
支持两种方式:
|
||||
|
||||
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
||||
- **远程资源**:使用 `--use_remote_resource` 从云端获取
|
||||
|
||||
### 7. 注册表构建
|
||||
|
||||
- 构建设备和资源注册表
|
||||
- 支持自定义注册表路径 (`--registry_path`)
|
||||
- 可选择补全注册表信息 (`--complete_registry`)
|
||||
|
||||
### 8. 设备验证和注册
|
||||
|
||||
- 验证设备连接和端点配置
|
||||
- 自动注册设备到云端服务
|
||||
|
||||
### 9. 通信桥接配置
|
||||
|
||||
- **WebSocket**:实时通信和任务下发
|
||||
- **FastAPI**:HTTP API 服务和物料更新
|
||||
|
||||
### 10. 可视化和服务启动
|
||||
|
||||
- 可选启动可视化工具 (`--visual`)
|
||||
- 启动 Web 信息服务 (默认端口 8002)
|
||||
- 启动后端通信服务
|
||||
|
||||
## 使用配置文件
|
||||
|
||||
Uni-Lab支持使用Python格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
|
||||
Uni-Lab 支持使用 Python 格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
|
||||
|
||||
```bash
|
||||
# 使用配置文件启动
|
||||
unilab --config path/to/your/config.py
|
||||
```
|
||||
|
||||
配置文件包含MQTT、HTTP、ROS等系统设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
|
||||
配置文件包含实验室和 WebSocket 连接等设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
|
||||
|
||||
## 初始化信息来源
|
||||
|
||||
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备、耗材、通信、控制逻辑:
|
||||
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备:
|
||||
|
||||
### 1. 组态&拓扑图
|
||||
|
||||
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 graphml 和 node-link json 两种格式。格式可参照 `tests/experiments` 下的启动文件。
|
||||
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 GraphML 和 node-link JSON 两种格式。格式可参照 `tests/experiments` 下的启动文件。
|
||||
|
||||
### 2. 分别指定设备、耗材、控制逻辑
|
||||
### 2. 分别指定控制逻辑
|
||||
|
||||
分别使用 `-d, -r, -c` 依次传入设备组态配置、耗材列表、控制逻辑。
|
||||
使用 `-c` 传入控制逻辑配置。
|
||||
|
||||
可参照 `devices.json` 和 `resources.json`。
|
||||
|
||||
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry` 加入 `<your-registry-path>/{devices,device_comms,resources}`。
|
||||
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `<your-registry-path>/{devices,device_comms,resources}`。
|
||||
|
||||
## 通信中间件 `--backend`
|
||||
|
||||
目前 Uni-Lab 仅支持 ros2 作为通信中间件。
|
||||
目前 Uni-Lab 支持以下通信中间件:
|
||||
|
||||
- **ros** (默认):基于 ROS2 的通信
|
||||
- **simple**:简化通信模式
|
||||
- **automancer**:Automancer 兼容模式
|
||||
|
||||
## 端云桥接 `--app_bridges`
|
||||
|
||||
目前 Uni-Lab 提供 FastAPI (http), MQTT 两种端云通信方式。其中默认 MQTT 负责端对云状态同步和云对端任务下发,FastAPI 负责端对云物料更新。
|
||||
目前 Uni-Lab 提供 WebSocket、FastAPI (http) 两种端云通信方式:
|
||||
|
||||
- **WebSocket**:负责实时通信和任务下发
|
||||
- **FastAPI**:负责端对云物料更新和 HTTP API
|
||||
|
||||
## 分布式组网
|
||||
|
||||
启动 Uni-Lab 时,加入 `--without_host` 将作为从站,不加将作为主站,主站 (host) 持有物料修改权以及对云端的通信。局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
|
||||
启动 Uni-Lab 时,加入 `--is_slave` 将作为从站,不加将作为主站:
|
||||
|
||||
- **主站 (host)**:持有物料修改权以及对云端的通信
|
||||
- **从站 (slave)**:无主机权限,可选择跳过等待主机服务 (`--slave_no_host`)
|
||||
|
||||
局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
|
||||
|
||||
## 可视化选项
|
||||
|
||||
### 2D 可视化
|
||||
|
||||
使用 `--2d_vis` 在 PyLabRobot 实例启动时同时启动 2D 可视化。
|
||||
|
||||
### 3D 可视化
|
||||
|
||||
通过 `--visual` 参数选择:
|
||||
|
||||
- **rviz**:使用 RViz 进行 3D 可视化
|
||||
- **web**:使用 Web 界面进行可视化
|
||||
- **disable** (默认):禁用可视化
|
||||
|
||||
## 实验室管理
|
||||
|
||||
### 首次使用
|
||||
|
||||
如果是首次使用,系统会:
|
||||
|
||||
1. 提示前往 https://uni-lab.bohrium.com 注册实验室
|
||||
2. 引导创建配置文件
|
||||
3. 设置工作目录
|
||||
|
||||
### 认证设置
|
||||
|
||||
- `--ak`:实验室访问密钥
|
||||
- `--sk`:实验室私钥
|
||||
- 两者必须同时提供才能正常启动
|
||||
|
||||
## 完整启动示例
|
||||
|
||||
以下是一些常用的启动命令示例:
|
||||
|
||||
```bash
|
||||
# 使用配置文件和组态图启动
|
||||
unilab -g path/to/graph.json
|
||||
# 使用组态图启动,上传注册表
|
||||
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
||||
|
||||
# 使用配置文件和分离的设备/资源文件启动
|
||||
unilab -d devices.json -r resources.json
|
||||
# 使用远程资源启动
|
||||
unilab --ak your_ak --sk your_sk --use_remote_resource
|
||||
|
||||
# 更新注册表
|
||||
unilab --ak your_ak --sk your_sk --complete_registry
|
||||
|
||||
# 启动从站模式
|
||||
unilab --ak your_ak --sk your_sk --is_slave
|
||||
|
||||
# 启用可视化
|
||||
unilab --ak your_ak --sk your_sk --visual web --2d_vis
|
||||
|
||||
# 指定本地信息网页服务端口和禁用自动跳出浏览器
|
||||
unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 认证失败
|
||||
|
||||
如果提示 "后续运行必须拥有一个实验室",请确保:
|
||||
|
||||
- 已在 https://uni-lab.bohrium.com 注册实验室
|
||||
- 正确设置了 `--ak` 和 `--sk` 参数
|
||||
- 配置文件中包含正确的认证信息
|
||||
|
||||
### 2. 配置文件问题
|
||||
|
||||
如果配置文件加载失败:
|
||||
|
||||
- 确保配置文件是 `.py` 格式
|
||||
- 检查配置文件语法是否正确
|
||||
- 首次使用可让系统自动创建示例配置文件
|
||||
|
||||
### 3. 网络连接问题
|
||||
|
||||
如果无法连接到服务器:
|
||||
|
||||
- 检查网络连接
|
||||
- 确认服务器地址是否正确
|
||||
- 尝试使用不同的环境地址(test、uat、local)
|
||||
|
||||
### 4. 设备图谱问题
|
||||
|
||||
如果设备加载失败:
|
||||
|
||||
- 检查图谱文件格式是否正确
|
||||
- 验证设备连接和端点配置
|
||||
- 确保注册表路径正确
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
channel_sources:
|
||||
- robostack,robostack-staging,conda-forge,defaults
|
||||
|
||||
gazebo:
|
||||
- '11'
|
||||
libpqxx:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:: Generated by vinca http://github.com/RoboStack/vinca.
|
||||
:: DO NOT EDIT!
|
||||
setlocal EnableDelayedExpansion
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "PYTHONPATH=%LIBRARY_PREFIX%\lib\site-packages;%SP_DIR%"
|
||||
|
||||
@@ -16,9 +17,11 @@ pushd build
|
||||
|
||||
:: try to fix long paths issues by using default generator
|
||||
set "CMAKE_GENERATOR=Visual Studio %VS_MAJOR% %VS_YEAR%"
|
||||
set "SP_DIR_FORWARDSLASHES=%SP_DIR:\=/%"
|
||||
|
||||
set PYTHON="%PREFIX%\python.exe"
|
||||
set PYTHON=%PYTHON:\=/%
|
||||
set SP_DIR="..\Lib\site-packages"
|
||||
set SP_DIR=%SP_DIR:\=/%
|
||||
|
||||
cmake ^
|
||||
-G "%CMAKE_GENERATOR%" ^
|
||||
@@ -32,10 +35,10 @@ cmake ^
|
||||
-DBUILD_SHARED_LIBS=ON ^
|
||||
-DBUILD_TESTING=OFF ^
|
||||
-DCMAKE_OBJECT_PATH_MAX=255 ^
|
||||
-DPYTHON_INSTALL_DIR=%SP_DIR_FORWARDSLASHES% ^
|
||||
-DPYTHON_INSTALL_DIR=%SP_DIR% ^
|
||||
--compile-no-warning-as-error ^
|
||||
%SRC_DIR%\%PKG_NAME%\src\work
|
||||
%SRC_DIR%\src
|
||||
if errorlevel 1 exit 1
|
||||
|
||||
cmake --build . --config Release --target install
|
||||
if errorlevel 1 exit 1
|
||||
cmake --build . --config Release --target install -j8
|
||||
if errorlevel 1 exit 1
|
||||
@@ -24,7 +24,7 @@ echo "USING PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_EXECUTABLE}"
|
||||
export ROS_PYTHON_VERSION=`$PYTHON_EXECUTABLE -c "import sys; print('%i.%i' % (sys.version_info[0:2]))"`
|
||||
echo "Using Python ${ROS_PYTHON_VERSION}"
|
||||
# Fix up SP_DIR which for some reason might contain a path to a wrong Python version
|
||||
FIXED_SP_DIR=$(echo $SP_DIR | sed -E "s/python[0-9]+\.[0-9]+/python$ROS_PYTHON_VERSION/")
|
||||
FIXED_SP_DIR=$($PYTHON_EXECUTABLE -c "import site; print(site.getsitepackages()[0])")
|
||||
echo "Using site-package dir ${FIXED_SP_DIR}"
|
||||
|
||||
# see https://github.com/conda-forge/cross-python-feedstock/issues/24
|
||||
@@ -56,7 +56,6 @@ cmake \
|
||||
-DPYTHON_EXECUTABLE=$PYTHON_EXECUTABLE \
|
||||
-DPython_EXECUTABLE=$PYTHON_EXECUTABLE \
|
||||
-DPython3_EXECUTABLE=$PYTHON_EXECUTABLE \
|
||||
-DPython3_FIND_STRATEGY=LOCATION \
|
||||
-DPKG_CONFIG_EXECUTABLE=$PKG_CONFIG_EXECUTABLE \
|
||||
-DPYTHON_INSTALL_DIR=$FIXED_SP_DIR \
|
||||
-DSETUPTOOLS_DEB_LAYOUT=OFF \
|
||||
@@ -66,6 +65,6 @@ cmake \
|
||||
-DBUILD_TESTING=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=$OSX_DEPLOYMENT_TARGET \
|
||||
--compile-no-warning-as-error \
|
||||
$SRC_DIR/$PKG_NAME/src/work
|
||||
$SRC_DIR/src
|
||||
|
||||
cmake --build . --config Release --target install
|
||||
cmake --build . --config Release --target install -j8
|
||||
76
recipes/msgs/recipe.yaml
Normal file
76
recipes/msgs/recipe.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.10.6
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
target_directory: src
|
||||
|
||||
build:
|
||||
script:
|
||||
- if: win
|
||||
then:
|
||||
- copy %RECIPE_DIR%\bld_ament_cmake.bat %SRC_DIR%
|
||||
- call %SRC_DIR%\bld_ament_cmake.bat
|
||||
- if: unix
|
||||
then:
|
||||
- cp $RECIPE_DIR/build_ament_cmake.sh $SRC_DIR
|
||||
- bash $SRC_DIR/build_ament_cmake.sh
|
||||
|
||||
about:
|
||||
repository: https://github.com/dptech-corp/Uni-Lab-OS
|
||||
license: BSD-3-Clause
|
||||
description: "ros-humble-unilabos-msgs is a package that provides message definitions for Uni-Lab-OS."
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- ${{ compiler('cxx') }}
|
||||
- ${{ compiler('c') }}
|
||||
- python ==3.11.11
|
||||
- numpy
|
||||
- if: build_platform != target_platform
|
||||
then:
|
||||
- pkg-config
|
||||
- cross-python_${{ target_platform }}
|
||||
- if: linux and x86_64
|
||||
then:
|
||||
- sysroot_linux-64 ==2.17
|
||||
- ninja
|
||||
- setuptools
|
||||
- cython
|
||||
- cmake
|
||||
- if: unix
|
||||
then:
|
||||
- make
|
||||
- coreutils
|
||||
- if: osx
|
||||
then:
|
||||
- tapi
|
||||
- if: win
|
||||
then:
|
||||
- vs2022_win-64
|
||||
host:
|
||||
- numpy
|
||||
- pip
|
||||
- if: build_platform == target_platform
|
||||
then:
|
||||
- pkg-config
|
||||
- robostack-staging::ros-humble-action-msgs
|
||||
- robostack-staging::ros-humble-ament-cmake
|
||||
- robostack-staging::ros-humble-ament-lint-auto
|
||||
- robostack-staging::ros-humble-ament-lint-common
|
||||
- robostack-staging::ros-humble-ros-environment
|
||||
- robostack-staging::ros-humble-ros-workspace
|
||||
- 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
|
||||
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
|
||||
- if: osx and x86_64
|
||||
then:
|
||||
- __osx >=${{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||
@@ -1,61 +0,0 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.9.7
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
folder: ros-humble-unilabos-msgs/src/work
|
||||
|
||||
build:
|
||||
script:
|
||||
sel(win): bld_ament_cmake.bat
|
||||
sel(unix): build_ament_cmake.sh
|
||||
number: 5
|
||||
about:
|
||||
home: https://www.ros.org/
|
||||
license: BSD-3-Clause
|
||||
summary: |
|
||||
Robot Operating System
|
||||
|
||||
extra:
|
||||
recipe-maintainers:
|
||||
- ros-forge
|
||||
|
||||
requirements:
|
||||
build:
|
||||
- "{{ compiler('cxx') }}"
|
||||
- "{{ compiler('c') }}"
|
||||
- sel(linux64): sysroot_linux-64 2.17
|
||||
- ninja
|
||||
- setuptools
|
||||
- sel(unix): make
|
||||
- sel(unix): coreutils
|
||||
- sel(osx): tapi
|
||||
- sel(build_platform != target_platform): pkg-config
|
||||
- cmake
|
||||
- cython
|
||||
- sel(win): vs2022_win-64
|
||||
- sel(build_platform != target_platform): python
|
||||
- sel(build_platform != target_platform): cross-python_{{ target_platform }}
|
||||
- sel(build_platform != target_platform): numpy
|
||||
host:
|
||||
- numpy
|
||||
- pip
|
||||
- sel(build_platform == target_platform): pkg-config
|
||||
- robostack-staging::ros-humble-action-msgs
|
||||
- robostack-staging::ros-humble-ament-cmake
|
||||
- robostack-staging::ros-humble-ament-lint-auto
|
||||
- robostack-staging::ros-humble-ament-lint-common
|
||||
- robostack-staging::ros-humble-ros-environment
|
||||
- robostack-staging::ros-humble-ros-workspace
|
||||
- robostack-staging::ros-humble-rosidl-default-generators
|
||||
- robostack-staging::ros-humble-std-msgs
|
||||
- robostack-staging::ros-humble-geometry-msgs
|
||||
- robostack-staging::ros2-distro-mutex=0.5.*
|
||||
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.*
|
||||
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.9.7"
|
||||
version: "0.10.6"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
5
setup.py
5
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.9.7',
|
||||
version='0.10.6',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
@@ -16,8 +16,7 @@ setup(
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"unilab = unilabos.app.main:main",
|
||||
"unilab-register = unilabos.app.register:main"
|
||||
"unilab = unilabos.app.main:main"
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
45
test/experiments/camera.json
Normal file
45
test/experiments/camera.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "Camera",
|
||||
"name": "摄像头",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "camera",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"camera_index": 0,
|
||||
"period": 0.05
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Gripper1",
|
||||
"name": "假夹爪",
|
||||
"children": [
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "gripper.mock",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
|
||||
]
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
HeatChillProtocol: generate_heat_chill_protocol, (√)
|
||||
HeatChillStartProtocol: generate_heat_chill_start_protocol, (√)
|
||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol, (√)
|
||||
HeatChillToTempProtocol:
|
||||
StirProtocol: generate_stir_protocol, (√)
|
||||
StartStirProtocol: generate_start_stir_protocol, (√)
|
||||
StopStirProtocol: generate_stop_stir_protocol, (√)
|
||||
@@ -30,7 +31,228 @@
|
||||
CleanVesselProtocol: generate_clean_vessel_protocol, (√)
|
||||
DissolveProtocol: generate_dissolve_protocol, (√)
|
||||
FilterThroughProtocol: generate_filter_through_protocol, (√)
|
||||
RunColumnProtocol: generate_run_column_protocol, (×)
|
||||
WashSolidProtocol: generate_wash_solid_protocol, (×)
|
||||
RunColumnProtocol: generate_run_column_protocol, (√)<RunColumn Rf="?" column="column" from_vessel="rotavap" ratio="5:95" solvent1="methanol" solvent2="chloroform" to_vessel="rotavap"/>
|
||||
|
||||
上下文体积搜索
|
||||
上下文体积搜索
|
||||
3. 还没创建的protocol
|
||||
ResetHandling 写完了 <ResetHandling solvent="methanol"/>
|
||||
Dry 写完了 <Dry compound="product" vessel="filter"/>
|
||||
AdjustPH 写完了 <AdjustPH pH="8.0" reagent="hydrochloric acid" vessel="main_reactor"/>
|
||||
Recrystallize 写完了 <Recrystallize ratio="?" solvent1="dichloromethane" solvent2="methanol" vessel="filter" volume="?"/>
|
||||
TakeSample <TakeSample id="a" vessel="rotavap"/>
|
||||
Hydrogenate <Hydrogenate temp="45 °C" time="?" vessel="main_reactor"/>
|
||||
4. 参数对齐
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class PumpTransferProtocol(BaseModel):
|
||||
from_vessel: str
|
||||
to_vessel: str
|
||||
volume: float
|
||||
amount: str = ""
|
||||
time: float = 0
|
||||
viscous: bool = False
|
||||
rinsing_solvent: str = "air" <Transfer from_vessel="main_reactor" to_vessel="rotavap"/>
|
||||
rinsing_volume: float = 5000 <Transfer event="A" from_vessel="reactor" rate_spec="dropwise" to_vessel="main_reactor"/>
|
||||
rinsing_repeats: int = 2 <Transfer from_vessel="separator" through="cartridge" to_vessel="rotavap"/>
|
||||
solid: bool = False 测完了三个都能跑✅
|
||||
flowrate: float = 500
|
||||
transfer_flowrate: float = 2500
|
||||
|
||||
class SeparateProtocol(BaseModel):
|
||||
purpose: str
|
||||
product_phase: str
|
||||
from_vessel: str
|
||||
separation_vessel: str
|
||||
to_vessel: str
|
||||
waste_phase_to_vessel: str
|
||||
solvent: str
|
||||
solvent_volume: float <Separate product_phase="bottom" purpose="wash" solvent="water" vessel="separator" volume="?"/>
|
||||
through: str <Separate product_phase="top" purpose="separate" vessel="separator"/>
|
||||
repeats: int <Separate product_phase="bottom" purpose="extract" repeats="3" solvent="CH2Cl2" vessel="separator" volume="?"/>
|
||||
stir_time: float <Separate product_phase="top" product_vessel="flask" purpose="separate" vessel="separator" waste_vessel="separator"/>
|
||||
stir_speed: float
|
||||
settling_time: float 测完了能跑✅
|
||||
|
||||
|
||||
class EvaporateProtocol(BaseModel):
|
||||
vessel: str
|
||||
pressure: float
|
||||
temp: float <Evaporate solvent="ethanol" vessel="rotavap"/>
|
||||
time: float 测完了能跑✅
|
||||
stir_speed: float
|
||||
|
||||
|
||||
class EvacuateAndRefillProtocol(BaseModel):
|
||||
vessel: str
|
||||
gas: str <EvacuateAndRefill gas="nitrogen" vessel="main_reactor"/>
|
||||
repeats: int 测完了能跑✅
|
||||
|
||||
class AddProtocol(BaseModel):
|
||||
vessel: str
|
||||
reagent: str
|
||||
volume: float
|
||||
mass: float
|
||||
amount: str
|
||||
time: float
|
||||
stir: bool
|
||||
stir_speed: float <Add reagent="ethanol" vessel="main_reactor" volume="2.7 mL"/>
|
||||
<Add event="A" mass="19.3 g" mol="0.28 mol" rate_spec="portionwise" reagent="sodium nitrite" time="1 h" vessel="main_reactor"/>
|
||||
<Add mass="4.5 g" mol="16.2 mmol" reagent="(S)-2-phthalimido-6-hydroxyhexanoic acid" vessel="main_reactor"/>
|
||||
<Add purpose="dilute" reagent="hydrochloric acid" vessel="main_reactor" volume="?"/>
|
||||
<Add equiv="1.1" event="B" mol="25.2 mmol" rate_spec="dropwise" reagent="1-fluoro-2-nitrobenzene" time="20 min"
|
||||
vessel="main_reactor" volume="2.67 mL"/>
|
||||
<Add ratio="?" reagent="tetrahydrofuran|tert-butanol" vessel="main_reactor" volume="?"/>
|
||||
viscous: bool
|
||||
purpose: str 测完了能跑✅
|
||||
|
||||
class CentrifugeProtocol(BaseModel):
|
||||
vessel: str
|
||||
speed: float
|
||||
time: float 没毛病
|
||||
temp: float
|
||||
|
||||
class FilterProtocol(BaseModel):
|
||||
vessel: str
|
||||
filtrate_vessel: str
|
||||
stir: bool <Filter vessel="filter"/>
|
||||
stir_speed: float <Filter filtrate_vessel="rotavap" vessel="filter"/>
|
||||
temp: float 测完了能跑✅
|
||||
continue_heatchill: bool
|
||||
volume: float
|
||||
|
||||
class HeatChillProtocol(BaseModel):
|
||||
vessel: str
|
||||
temp: float
|
||||
time: float <HeatChill pressure="1 mbar" temp_spec="room temperature" time="?" vessel="main_reactor"/>
|
||||
<HeatChill temp_spec="room temperature" time_spec="overnight" vessel="main_reactor"/>
|
||||
<HeatChill temp="256 °C" time="?" vessel="main_reactor"/>
|
||||
<HeatChill reflux_solvent="methanol" temp_spec="reflux" time="2 h" vessel="main_reactor"/>
|
||||
<HeatChillToTemp temp_spec="room temperature" vessel="main_reactor"/>
|
||||
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 <Stir time="0.5 h" vessel="main_reactor"/>
|
||||
<Stir event="A" time="30 min" vessel="main_reactor"/>
|
||||
<Stir time_spec="several minutes" vessel="filter"/>
|
||||
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 这个protocol早该删掉了
|
||||
|
||||
class CleanVesselProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float
|
||||
temp: float
|
||||
repeats: int = 1 <CleanVessel vessel="centrifuge"/>
|
||||
|
||||
class DissolveProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float <Dissolve mass="2.9 g" mol="0.12 mol" reagent="magnesium" vessel="main_reactor"/>
|
||||
amount: str = "" <Dissolve mass="12.9 g" reagent="4-tert-butylbenzyl bromide" vessel="main_reactor"/>
|
||||
temp: float = 25.0 <Dissolve solvent="diisopropyl ether" vessel="rotavap" volume="?"/>
|
||||
time: float = 0.0 <Dissolve event="A" mass="?" reagent="pyridinone" vessel="main_reactor"/>
|
||||
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 <RunColumn Rf="?" column="column" from_vessel="rotavap" pct1="40 %" pct2="50 %" solvent1="ethyl acetate" solvent2="hexane" to_vessel="rotavap"/>
|
||||
column: str 测完了能跑✅
|
||||
|
||||
class WashSolidProtocol(BaseModel):
|
||||
vessel: str
|
||||
solvent: str
|
||||
volume: float
|
||||
filtrate_vessel: str = "" <WashSolid repeats="4" solvent="water" vessel="main_reactor" volume="400 mL"/>
|
||||
temp: float = 25.0 <WashSolid filtrate_vessel="rotavap" solvent="formic acid" vessel="main_reactor" volume="?"/>
|
||||
stir: bool = False <WashSolid solvent="acetone" vessel="rotavap" volume="5 mL"/>
|
||||
<WashSolid solvent="ethyl alcohol" vessel="main_reactor" volume_spec="small volume"/>
|
||||
<WashSolid filtrate_vessel="rotavap" mass="10 g" solvent="toluene" vessel="separator"/>
|
||||
<WashSolid repeats_spec="several" solvent="water" vessel="main_reactor" volume="?"/>
|
||||
stir_speed: float = 0.0 测完了能跑✅
|
||||
time: float = 0.0
|
||||
repeats: int = 1
|
||||
|
||||
class AdjustPHProtocol(BaseModel):
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
ph_value: float = Field(..., description="目标pH值") # 改为 ph_value
|
||||
reagent: str = Field(..., description="酸碱试剂名称")
|
||||
# 移除其他可选参数,使用默认值 <新写的,没问题>
|
||||
|
||||
class ResetHandlingProtocol(BaseModel):
|
||||
solvent: str = Field(..., description="溶剂名称") <新写的,没问题>
|
||||
|
||||
class DryProtocol(BaseModel):
|
||||
compound: str = Field(..., description="化合物名称") <新写的,没问题>
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
|
||||
class RecrystallizeProtocol(BaseModel):
|
||||
ratio: str = Field(..., description="溶剂比例(如 '1:1', '3:7')")
|
||||
solvent1: str = Field(..., description="第一种溶剂名称") <新写的,没问题>
|
||||
solvent2: str = Field(..., description="第二种溶剂名称")
|
||||
vessel: str = Field(..., description="目标容器")
|
||||
volume: float = Field(..., description="总体积 (mL)")
|
||||
|
||||
class HydrogenateProtocol(BaseModel):
|
||||
temp: str = Field(..., description="反应温度(如 '45 °C')")
|
||||
time: str = Field(..., description="反应时间(如 '2 h')") <新写的,没问题>
|
||||
vessel: str = Field(..., description="反应容器")
|
||||
|
||||
还差
|
||||
<dissolve>
|
||||
<separate>
|
||||
<CleanVessel vessel="centrifuge"/>
|
||||
|
||||
|
||||
单位修复:
|
||||
evaporate
|
||||
heatchill
|
||||
recrysitallize
|
||||
stir
|
||||
wash solid
|
||||
1752
test/experiments/comprehensive_protocol/comprehensive_slim.json
Normal file
1752
test/experiments/comprehensive_protocol/comprehensive_slim.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,14 +23,20 @@
|
||||
"waste_bottle_2",
|
||||
"solenoid_valve_1",
|
||||
"solenoid_valve_2",
|
||||
"solenoid_valve_3",
|
||||
"vacuum_pump_1",
|
||||
"gas_source_1",
|
||||
"h2_gas_source",
|
||||
"filter_1",
|
||||
"column_1",
|
||||
"separator_1",
|
||||
"collection_bottle_1",
|
||||
"collection_bottle_2",
|
||||
"collection_bottle_3"
|
||||
"collection_bottle_3",
|
||||
"solid_dispenser_1",
|
||||
"solid_reagent_bottle_1",
|
||||
"solid_reagent_bottle_2",
|
||||
"solid_reagent_bottle_3"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
@@ -43,7 +49,6 @@
|
||||
"config": {
|
||||
"protocol_type": [
|
||||
"AddProtocol",
|
||||
"TransferProtocol",
|
||||
"StartStirProtocol",
|
||||
"StopStirProtocol",
|
||||
"StirProtocol",
|
||||
@@ -60,7 +65,12 @@
|
||||
"HeatChillStartProtocol",
|
||||
"HeatChillStopProtocol",
|
||||
"EvacuateAndRefillProtocol",
|
||||
"PumpTransferProtocol"
|
||||
"PumpTransferProtocol",
|
||||
"AdjustPHProtocol",
|
||||
"ResetHandlingProtocol",
|
||||
"DryProtocol",
|
||||
"HydrogenateProtocol",
|
||||
"RecrystallizeProtocol"
|
||||
]
|
||||
},
|
||||
"data": {}
|
||||
@@ -160,12 +170,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "DMF"
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "DMF"
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "DMF",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -181,12 +194,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "ethyl_acetate"
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "ethyl_acetate"
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "ethyl_acetate",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -202,12 +218,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "hexane"
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "hexane"
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "hexane",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -223,12 +242,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "methanol"
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "methanol"
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "methanol",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -244,12 +266,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 1000.0,
|
||||
"reagent": "water"
|
||||
"max_volume": 1000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 1000.0,
|
||||
"reagent_name": "water"
|
||||
"liquids": [
|
||||
{
|
||||
"liquid_type": "water",
|
||||
"liquid_volume": 1000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -309,15 +334,15 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"max_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
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -394,10 +419,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -413,10 +439,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -461,6 +488,28 @@
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solenoid_valve_3",
|
||||
"name": "氢气电磁阀",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solenoid_valve",
|
||||
"position": {
|
||||
"x": 450,
|
||||
"y": 400,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"voltage": 12.0,
|
||||
"response_time": 0.1,
|
||||
"gas_compatible": true
|
||||
},
|
||||
"data": {
|
||||
"valve_state": "Closed",
|
||||
"is_open": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vacuum_pump_1",
|
||||
"name": "真空泵",
|
||||
@@ -500,6 +549,29 @@
|
||||
"max_pressure": 5.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "h2_gas_source",
|
||||
"name": "氢气气源",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_gas_source",
|
||||
"position": {
|
||||
"x": 500,
|
||||
"y": 350,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_pressure": 10.0,
|
||||
"gas_type": "hydrogen"
|
||||
},
|
||||
"data": {
|
||||
"gas_type": "hydrogen",
|
||||
"max_pressure": 10.0,
|
||||
"current_pressure": 0.0,
|
||||
"status": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "filter_1",
|
||||
"name": "过滤器",
|
||||
@@ -577,10 +649,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -596,10 +669,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -615,10 +689,103 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_dispenser_1",
|
||||
"name": "固体粉末加样器",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "device",
|
||||
"class": "virtual_solid_dispenser",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 300,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_capacity": 100.0,
|
||||
"precision": 0.001
|
||||
},
|
||||
"data": {
|
||||
"status": "Ready",
|
||||
"current_reagent": "",
|
||||
"dispensed_amount": 0.0,
|
||||
"total_operations": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_1",
|
||||
"name": "固体试剂瓶1-氯化钠",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 550,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 500.0,
|
||||
"reagent": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_2",
|
||||
"name": "固体试剂瓶2-碳酸钠",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 600,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"reagent": "sodium_carbonate",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "sodium_carbonate",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "solid_reagent_bottle_3",
|
||||
"name": "固体试剂瓶3-氯化镁",
|
||||
"children": [],
|
||||
"parent": "OrganicSynthesisStation",
|
||||
"type": "container",
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 650,
|
||||
"y": 250,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"reagent": "magnesium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
"data": {
|
||||
"current_mass": 500.0,
|
||||
"reagent_name": "magnesium_chloride",
|
||||
"physical_state": "solid"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -680,7 +847,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_1": "5",
|
||||
"rotavap_1": "sample_in"
|
||||
"rotavap_1": "samplein"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -750,7 +917,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "3",
|
||||
"solenoid_valve_2": "in"
|
||||
"solenoid_valve_2": "out"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -760,7 +927,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"gas_source_1": "gassource",
|
||||
"solenoid_valve_2": "out"
|
||||
"solenoid_valve_2": "in"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -770,7 +937,7 @@
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"multiway_valve_2": "4",
|
||||
"filter_1": "filter_in"
|
||||
"filter_1": "filterin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -800,7 +967,7 @@
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"multiway_valve_2": "6",
|
||||
"separator_1": "separator_in"
|
||||
"separator_1": "separatorin"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -809,7 +976,7 @@
|
||||
"target": "collection_bottle_3",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"separator_1": "bottom_phase_out",
|
||||
"separator_1": "bottomphaseout",
|
||||
"collection_bottle_3": "top"
|
||||
}
|
||||
},
|
||||
@@ -859,7 +1026,7 @@
|
||||
"target": "waste_bottle_2",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"separator_1": "top_phase_out",
|
||||
"separator_1": "topphaseout",
|
||||
"waste_bottle_2": "top"
|
||||
}
|
||||
},
|
||||
@@ -874,14 +1041,14 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"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_filtrate_to_collection1",
|
||||
"source": "filter_1",
|
||||
"target": "collection_bottle_1",
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"filter_1": "filtrateout",
|
||||
"collection_bottle_1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_filter_retentate_to_waste1",
|
||||
@@ -889,9 +1056,69 @@
|
||||
"target": "waste_bottle_1",
|
||||
"type": "transport",
|
||||
"port": {
|
||||
"filter_1": "retentate_out",
|
||||
"filter_1": "retentateout",
|
||||
"waste_bottle_1": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_h2_gas_to_valve3",
|
||||
"source": "h2_gas_source",
|
||||
"target": "solenoid_valve_3",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"h2_gas_source": "gassource",
|
||||
"solenoid_valve_3": "in"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_valve3_to_reactor",
|
||||
"source": "solenoid_valve_3",
|
||||
"target": "main_reactor",
|
||||
"type": "fluid",
|
||||
"port": {
|
||||
"solenoid_valve_3": "out",
|
||||
"main_reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_dispenser_to_reactor",
|
||||
"source": "solid_dispenser_1",
|
||||
"target": "main_reactor",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_dispenser_1": "SolidOut",
|
||||
"main_reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle1_to_dispenser",
|
||||
"source": "solid_reagent_bottle_1",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_1": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle2_to_dispenser",
|
||||
"source": "solid_reagent_bottle_2",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_2": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "link_solid_bottle3_to_dispenser",
|
||||
"source": "solid_reagent_bottle_3",
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_3": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
60
test/experiments/dispensing_station_bioyond.json
Normal file
60
test/experiments/dispensing_station_bioyond.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "dispensing_station_bioyond",
|
||||
"name": "dispensing_station_bioyond",
|
||||
"children": [
|
||||
"Bioyond_Dispensing_Deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "dispensing_station.bioyond",
|
||||
"config": {
|
||||
"config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388"
|
||||
},
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "Bioyond_Dispensing_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
||||
}
|
||||
},
|
||||
"station_config": {
|
||||
"station_type": "dispensing_station",
|
||||
"enable_dispensing_station": true,
|
||||
"enable_reaction_station": false,
|
||||
"station_name": "DispensingStation_001",
|
||||
"description": "Bioyond配液工作站"
|
||||
},
|
||||
"protocol_type": []
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "Bioyond_Dispensing_Deck",
|
||||
"name": "Bioyond_Dispensing_Deck",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "dispensing_station_bioyond",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_PolymerPreparationStation_Deck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "BIOYOND_PolymerPreparationStation_Deck",
|
||||
"setup": true,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"id": "liquid_handler",
|
||||
"name": "liquid_handler",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
@@ -37,7 +37,7 @@
|
||||
"tip_rack",
|
||||
"plate_well"
|
||||
],
|
||||
"parent": "PLR_STATION",
|
||||
"parent": "liquid_handler",
|
||||
"type": "deck",
|
||||
"class": "OTDeck",
|
||||
"position": {
|
||||
@@ -9650,7 +9650,7 @@
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "moveit.arm_slider",
|
||||
"class": "robotic_arm.SCARA_with_slider.virtual",
|
||||
"position": {
|
||||
"x": -500,
|
||||
"y": 1000,
|
||||
|
||||
21231
test/experiments/prcxi_9300.json
Normal file
21231
test/experiments/prcxi_9300.json
Normal file
File diff suppressed because it is too large
Load Diff
41190
test/experiments/prcxi_9320.json
Normal file
41190
test/experiments/prcxi_9320.json
Normal file
File diff suppressed because it is too large
Load Diff
13598
test/experiments/prcxi_9320_visual.json
Normal file
13598
test/experiments/prcxi_9320_visual.json
Normal file
File diff suppressed because it is too large
Load Diff
69
test/experiments/reaction_station_bioyond.json
Normal file
69
test/experiments/reaction_station_bioyond.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "reaction_station_bioyond",
|
||||
"name": "reaction_station_bioyond",
|
||||
"parent": null,
|
||||
"children": [
|
||||
"Bioyond_Deck"
|
||||
],
|
||||
"type": "device",
|
||||
"class": "reaction_station.bioyond",
|
||||
"config": {
|
||||
"bioyond_config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44402",
|
||||
"workflow_mappings": {
|
||||
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
|
||||
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
|
||||
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
|
||||
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
|
||||
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||
}
|
||||
},
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "Bioyond_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
||||
}
|
||||
},
|
||||
"protocol_type": []
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "Bioyond_Deck",
|
||||
"name": "Bioyond_Deck",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
],
|
||||
"parent": "reaction_station_bioyond",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "BIOYOND_PolymerReactionStation_Deck",
|
||||
"setup": true,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
69
test/experiments/reaction_station_bioyond_test.json
Normal file
69
test/experiments/reaction_station_bioyond_test.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "reaction_station_bioyond",
|
||||
"name": "reaction_station_bioyond",
|
||||
"parent": null,
|
||||
"children": [
|
||||
"Bioyond_Deck"
|
||||
],
|
||||
"type": "device",
|
||||
"class": "workstation.bioyond",
|
||||
"config": {
|
||||
"bioyond_config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388",
|
||||
"workflow_mappings": {
|
||||
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
|
||||
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
|
||||
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
|
||||
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
|
||||
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||
}
|
||||
},
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "Bioyond_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
||||
}
|
||||
},
|
||||
"protocol_type": []
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": "Bioyond_Deck",
|
||||
"name": "Bioyond_Deck",
|
||||
"sample_id": null,
|
||||
"children": [
|
||||
],
|
||||
"parent": "reaction_station_bioyond",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "BIOYOND_PolymerReactionStation_Deck",
|
||||
"setup": true,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +1,48 @@
|
||||
{
|
||||
"nodes": [
|
||||
|
||||
|
||||
{
|
||||
"id": "benyao",
|
||||
"name": "benyao",
|
||||
"children": [
|
||||
],
|
||||
"id": "arm_slider",
|
||||
"name": "arm_slider",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "moveit.arm_slider",
|
||||
"class": "robotic_arm.SCARA_with_slider.virtual",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
"x": -500,
|
||||
"y": 1000,
|
||||
"z": -100
|
||||
},
|
||||
"config": {
|
||||
"moveit_type": "arm_slider",
|
||||
"joint_poses": {
|
||||
"arm": {
|
||||
"home": [0.0, 0.2, 0.0, 0.0, 0.0],
|
||||
"pick": [1.2, 0.0, 0.0, 0.0, 0.0]
|
||||
"hotel_1": [
|
||||
1.05,
|
||||
0.568,
|
||||
-1.0821,
|
||||
0.0,
|
||||
1.0821
|
||||
],
|
||||
"home": [
|
||||
0.865,
|
||||
0.09,
|
||||
0.8727,
|
||||
0.0,
|
||||
-0.8727
|
||||
]
|
||||
}
|
||||
},
|
||||
"device_config": {
|
||||
}
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": -1.5708,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"device_config": {}
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
|
||||
949
test/experiments/workshop.json
Normal file
949
test/experiments/workshop.json
Normal file
@@ -0,0 +1,949 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "simple_station",
|
||||
"name": "愚公常量合成工作站",
|
||||
"children": [
|
||||
"serial_pump",
|
||||
"pump_reagents",
|
||||
"pump_workup",
|
||||
"flask_CH2Cl2",
|
||||
"waste_workup",
|
||||
"separator_controller",
|
||||
"flask_separator",
|
||||
"flask_air"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"protocol_type": ["PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol"]
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "serial_pump",
|
||||
"name": "serial_pump",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "serial",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "COM7",
|
||||
"baudrate": 9600
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pump_reagents",
|
||||
"name": "pump_reagents",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||
"address": "1",
|
||||
"max_volume": 25.0
|
||||
},
|
||||
"data": {
|
||||
"max_velocity": 1.0,
|
||||
"position": 0.0,
|
||||
"status": "Idle",
|
||||
"valve_position": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_CH2Cl2",
|
||||
"name": "flask_CH2Cl2",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 430.4087301587302,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "CH2Cl2",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_acetone",
|
||||
"name": "flask_acetone",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 295.36944444444447,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "acetone",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_NH4Cl",
|
||||
"name": "flask_NH4Cl",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 165.36944444444444,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "NH4Cl",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_grignard",
|
||||
"name": "flask_grignard",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 165.36944444444444,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "grignard",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_THF",
|
||||
"name": "flask_THF",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 35,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "THF",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reactor",
|
||||
"name": "reactor",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 428,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 5000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "stirrer",
|
||||
"name": "stirrer",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "heaterstirrer.dalong",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "COM43",
|
||||
"temp_warning": 60.0
|
||||
},
|
||||
"data": {
|
||||
"status": "Idle",
|
||||
"temp": 0.0,
|
||||
"stir_speed": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pump_workup",
|
||||
"name": "pump_workup",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"position": {
|
||||
"x": 1195.611507936508,
|
||||
"y": 686,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||
"address": "2",
|
||||
"max_volume": 25.0
|
||||
},
|
||||
"data": {
|
||||
"max_velocity": 1.0,
|
||||
"position": 0.0,
|
||||
"status": "Idle",
|
||||
"valve_position": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "waste_workup",
|
||||
"name": "waste_workup",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1587.703373015873,
|
||||
"y": 1172.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "separator_controller",
|
||||
"name": "separator_controller",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "separator.homemade",
|
||||
"position": {
|
||||
"x": 1624.4027777777778,
|
||||
"y": 665.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port_executor": "/dev/tty.usbserial-11140",
|
||||
"port_sensor": "/dev/tty.usbserial-11130"
|
||||
},
|
||||
"data": {
|
||||
"sensordata": 0.0,
|
||||
"status": "Idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_separator",
|
||||
"name": "flask_separator",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1614.404365079365,
|
||||
"y": 948,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_holding",
|
||||
"name": "flask_holding",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1915.7035714285714,
|
||||
"y": 665.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_H2O",
|
||||
"name": "flask_H2O",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1785.7035714285714,
|
||||
"y": 665.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "H2O",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_NaHCO3",
|
||||
"name": "flask_NaHCO3",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 2054.0650793650793,
|
||||
"y": 665.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
{
|
||||
"liquid_type": "NaHCO3",
|
||||
"liquid_volume": 1500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pump_column",
|
||||
"name": "pump_column",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"position": {
|
||||
"x": 1630.6527777777778,
|
||||
"y": 448.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||
"address": "3",
|
||||
"max_volume": 25.0
|
||||
},
|
||||
"data": {
|
||||
"max_velocity": 1.0,
|
||||
"position": 0.0,
|
||||
"status": "Idle",
|
||||
"valve_position": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rotavap",
|
||||
"name": "rotavap",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "rotavap",
|
||||
"position": {
|
||||
"x": 1339.7031746031746,
|
||||
"y": 968.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "COM15"
|
||||
},
|
||||
"data": {
|
||||
"temperature": 0.0,
|
||||
"rotate_time": 0.0,
|
||||
"status": "Idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_rv",
|
||||
"name": "flask_rv",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1339.7031746031746,
|
||||
"y": 1152,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "column",
|
||||
"name": "column",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 909.722619047619,
|
||||
"y": 948,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 200.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_column",
|
||||
"name": "flask_column",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 867.972619047619,
|
||||
"y": 1152,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_air",
|
||||
"name": "flask_air",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 742.722619047619,
|
||||
"y": 948,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dry_column",
|
||||
"name": "dry_column",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1206.722619047619,
|
||||
"y": 948,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 200.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "flask_dry_column",
|
||||
"name": "flask_dry_column",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"position": {
|
||||
"x": 1148.222619047619,
|
||||
"y": 1152,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"liquid": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pump_ext",
|
||||
"name": "pump_ext",
|
||||
"children": [],
|
||||
"parent": "simple_station",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"position": {
|
||||
"x": 1469.7031746031746,
|
||||
"y": 968.5,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||
"address": "4",
|
||||
"max_volume": 25.0
|
||||
},
|
||||
"data": {
|
||||
"max_velocity": 1.0,
|
||||
"position": 0.0,
|
||||
"status": "Idle",
|
||||
"valve_position": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "AGV",
|
||||
"name": "AGV",
|
||||
"children": ["zhixing_agv", "zhixing_ur_arm"],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"protocol_type": ["AGVTransferProtocol"]
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "zhixing_agv",
|
||||
"name": "zhixing_agv",
|
||||
"children": [],
|
||||
"parent": "AGV",
|
||||
"type": "device",
|
||||
"class": "zhixing_agv",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"host": "192.168.1.42"
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "zhixing_ur_arm",
|
||||
"name": "zhixing_ur_arm",
|
||||
"children": [],
|
||||
"parent": "AGV",
|
||||
"type": "device",
|
||||
"class": "zhixing_ur_arm",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 478,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"host": "192.168.1.178"
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": {
|
||||
"pump_reagents": "port",
|
||||
"serial_pump": "port"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": {
|
||||
"pump_reagents": "port",
|
||||
"serial_pump": "port"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": {
|
||||
"pump_reagents": "port",
|
||||
"serial_pump": "port"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_ext",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": {
|
||||
"pump_reagents": "port",
|
||||
"serial_pump": "port"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "reactor",
|
||||
"target": "pump_reagents",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"reactor": "top",
|
||||
"pump_reagents": "5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "rotavap",
|
||||
"target": "flask_rv",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"rotavap": "bottom",
|
||||
"flask_rv": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "separator_controller",
|
||||
"target": "flask_separator",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"separator_controller": "bottom",
|
||||
"flask_separator": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "column",
|
||||
"target": "flask_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"column": "bottom",
|
||||
"flask_column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "dry_column",
|
||||
"target": "flask_dry_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"dry_column": "bottom",
|
||||
"flask_dry_column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_ext",
|
||||
"target": "pump_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_ext": "8",
|
||||
"pump_column": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_ext",
|
||||
"target": "waste_workup",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_ext": "2",
|
||||
"waste_workup": "-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_THF",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "7",
|
||||
"flask_THF": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_NH4Cl",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "4",
|
||||
"flask_NH4Cl": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_CH2Cl2",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "2",
|
||||
"flask_CH2Cl2": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_acetone",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "3",
|
||||
"flask_acetone": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "pump_workup",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "1",
|
||||
"pump_workup": "8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_grignard",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "6",
|
||||
"flask_grignard": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "reactor",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "5",
|
||||
"reactor": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_reagents",
|
||||
"target": "flask_air",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_reagents": "8",
|
||||
"flask_air": "-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "waste_workup",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "2",
|
||||
"waste_workup": "-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "flask_H2O",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "7",
|
||||
"flask_H2O": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "flask_NaHCO3",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "6",
|
||||
"flask_NaHCO3": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "pump_reagents",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "8",
|
||||
"pump_reagents": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "flask_holding",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "5",
|
||||
"flask_holding": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "separator_controller",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "4",
|
||||
"separator_controller": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "flask_separator",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "3",
|
||||
"flask_separator": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_workup",
|
||||
"target": "pump_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_workup": "1",
|
||||
"pump_column": "8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "4",
|
||||
"column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "flask_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "3",
|
||||
"flask_column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "rotavap",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "2",
|
||||
"rotavap": "-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "pump_workup",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "8",
|
||||
"pump_workup": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "flask_air",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "5",
|
||||
"flask_air": "-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "dry_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "7",
|
||||
"dry_column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "flask_dry_column",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "6",
|
||||
"flask_dry_column": "top"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "pump_column",
|
||||
"target": "pump_ext",
|
||||
"type": "physical",
|
||||
"port": {
|
||||
"pump_column": "1",
|
||||
"pump_ext": "8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
588
test/registry/example_devices.py
Normal file
588
test/registry/example_devices.py
Normal file
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
示例设备类文件,用于测试注册表编辑器
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class SmartPumpController:
|
||||
"""
|
||||
智能泵控制器
|
||||
|
||||
支持多种泵送模式,具有高精度流量控制和自动校准功能。
|
||||
适用于实验室自动化系统中的液体处理任务。
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
||||
"""
|
||||
初始化智能泵控制器
|
||||
|
||||
Args:
|
||||
device_id: 设备唯一标识符
|
||||
port: 通信端口
|
||||
"""
|
||||
self.device_id = device_id
|
||||
self.port = port
|
||||
self.is_connected = False
|
||||
self.current_flow_rate = 0.0
|
||||
self.total_volume_pumped = 0.0
|
||||
self.calibration_factor = 1.0
|
||||
self.pump_mode = "continuous" # continuous, volume, rate
|
||||
|
||||
def connect_device(self, timeout: int = 10) -> bool:
|
||||
"""
|
||||
连接到泵设备
|
||||
|
||||
Args:
|
||||
timeout: 连接超时时间(秒)
|
||||
|
||||
Returns:
|
||||
bool: 连接是否成功
|
||||
"""
|
||||
# 模拟连接过程
|
||||
self.is_connected = True
|
||||
return True
|
||||
|
||||
def disconnect_device(self) -> bool:
|
||||
"""
|
||||
断开设备连接
|
||||
|
||||
Returns:
|
||||
bool: 断开连接是否成功
|
||||
"""
|
||||
self.is_connected = False
|
||||
self.current_flow_rate = 0.0
|
||||
return True
|
||||
|
||||
def set_flow_rate(self, flow_rate: float, units: str = "ml/min") -> bool:
|
||||
"""
|
||||
设置泵流速
|
||||
|
||||
Args:
|
||||
flow_rate: 流速值
|
||||
units: 流速单位
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return False
|
||||
|
||||
self.current_flow_rate = flow_rate
|
||||
return True
|
||||
|
||||
async def pump_volume_async(self, volume: float, flow_rate: float) -> Dict[str, Any]:
|
||||
"""
|
||||
异步泵送指定体积的液体
|
||||
|
||||
Args:
|
||||
volume: 目标体积 (mL)
|
||||
flow_rate: 泵送流速 (mL/min)
|
||||
|
||||
Returns:
|
||||
Dict: 包含操作结果的字典
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return {"success": False, "error": "设备未连接"}
|
||||
|
||||
# 计算泵送时间
|
||||
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
||||
|
||||
self.current_flow_rate = flow_rate
|
||||
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
|
||||
|
||||
self.total_volume_pumped += volume
|
||||
self.current_flow_rate = 0.0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pumped_volume": volume,
|
||||
"actual_time": min(pump_time, 3.0),
|
||||
"total_volume": self.total_volume_pumped,
|
||||
}
|
||||
|
||||
def emergency_stop(self) -> bool:
|
||||
"""
|
||||
紧急停止泵
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
self.current_flow_rate = 0.0
|
||||
return True
|
||||
|
||||
def perform_calibration(self, reference_volume: float, measured_volume: float) -> bool:
|
||||
"""
|
||||
执行泵校准
|
||||
|
||||
Args:
|
||||
reference_volume: 参考体积
|
||||
measured_volume: 实际测量体积
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
if measured_volume > 0:
|
||||
self.calibration_factor = reference_volume / measured_volume
|
||||
return True
|
||||
return False
|
||||
|
||||
# 状态查询方法
|
||||
def get_connection_status(self) -> str:
|
||||
"""获取连接状态"""
|
||||
return "connected" if self.is_connected else "disconnected"
|
||||
|
||||
def get_current_flow_rate(self) -> float:
|
||||
"""获取当前流速 (mL/min)"""
|
||||
return self.current_flow_rate
|
||||
|
||||
def get_total_volume(self) -> float:
|
||||
"""获取累计泵送体积 (mL)"""
|
||||
return self.total_volume_pumped
|
||||
|
||||
def get_calibration_factor(self) -> float:
|
||||
"""获取校准因子"""
|
||||
return self.calibration_factor
|
||||
|
||||
def get_pump_mode(self) -> str:
|
||||
"""获取泵送模式"""
|
||||
return self.pump_mode
|
||||
|
||||
def get_device_status(self) -> Dict[str, Any]:
|
||||
"""获取设备完整状态信息"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"connected": self.is_connected,
|
||||
"flow_rate": self.current_flow_rate,
|
||||
"total_volume": self.total_volume_pumped,
|
||||
"calibration_factor": self.calibration_factor,
|
||||
"mode": self.pump_mode,
|
||||
"running": self.current_flow_rate > 0,
|
||||
}
|
||||
|
||||
|
||||
class AdvancedTemperatureController:
|
||||
"""
|
||||
高级温度控制器
|
||||
|
||||
支持PID控制、多点温度监控和程序化温度曲线。
|
||||
适用于需要精确温度控制的化学反应和材料处理过程。
|
||||
"""
|
||||
|
||||
def __init__(self, controller_id: str = "temp_controller_01"):
|
||||
"""
|
||||
初始化温度控制器
|
||||
|
||||
Args:
|
||||
controller_id: 控制器ID
|
||||
"""
|
||||
self.controller_id = controller_id
|
||||
self.current_temperature = 25.0
|
||||
self.target_temperature = 25.0
|
||||
self.is_heating = False
|
||||
self.is_cooling = False
|
||||
self.pid_enabled = True
|
||||
self.temperature_history: List[Dict] = []
|
||||
|
||||
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
|
||||
"""
|
||||
设置目标温度
|
||||
|
||||
Args:
|
||||
temperature: 目标温度 (°C)
|
||||
rate: 升温/降温速率 (°C/min)
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
self.target_temperature = temperature
|
||||
return True
|
||||
|
||||
async def heat_to_temperature_async(
|
||||
self, temperature: float, tolerance: float = 0.5, timeout: int = 600
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
异步加热到指定温度
|
||||
|
||||
Args:
|
||||
temperature: 目标温度 (°C)
|
||||
tolerance: 温度容差 (°C)
|
||||
timeout: 最大等待时间 (秒)
|
||||
|
||||
Returns:
|
||||
Dict: 操作结果
|
||||
"""
|
||||
self.target_temperature = temperature
|
||||
start_temp = self.current_temperature
|
||||
|
||||
if temperature > start_temp:
|
||||
self.is_heating = True
|
||||
elif temperature < start_temp:
|
||||
self.is_cooling = True
|
||||
|
||||
# 模拟温度变化过程
|
||||
steps = min(abs(temperature - start_temp) * 2, 20) # 计算步数
|
||||
step_time = min(timeout / steps if steps > 0 else 1, 2.0) # 每步最多2秒
|
||||
|
||||
for step in range(int(steps)):
|
||||
progress = (step + 1) / steps
|
||||
self.current_temperature = start_temp + (temperature - start_temp) * progress
|
||||
|
||||
# 记录温度历史
|
||||
self.temperature_history.append(
|
||||
{
|
||||
"timestamp": asyncio.get_event_loop().time(),
|
||||
"temperature": self.current_temperature,
|
||||
"target": self.target_temperature,
|
||||
}
|
||||
)
|
||||
|
||||
await asyncio.sleep(step_time)
|
||||
|
||||
# 保持历史记录不超过100条
|
||||
if len(self.temperature_history) > 100:
|
||||
self.temperature_history.pop(0)
|
||||
|
||||
# 最终设置为目标温度
|
||||
self.current_temperature = temperature
|
||||
self.is_heating = False
|
||||
self.is_cooling = False
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"final_temperature": self.current_temperature,
|
||||
"start_temperature": start_temp,
|
||||
"time_taken": steps * step_time,
|
||||
}
|
||||
|
||||
def enable_pid_control(self, kp: float = 1.0, ki: float = 0.1, kd: float = 0.05) -> bool:
|
||||
"""
|
||||
启用PID控制
|
||||
|
||||
Args:
|
||||
kp: 比例增益
|
||||
ki: 积分增益
|
||||
kd: 微分增益
|
||||
|
||||
Returns:
|
||||
bool: 启用是否成功
|
||||
"""
|
||||
self.pid_enabled = True
|
||||
return True
|
||||
|
||||
def run_temperature_program(self, program: List[Dict]) -> bool:
|
||||
"""
|
||||
运行温度程序
|
||||
|
||||
Args:
|
||||
program: 温度程序列表,每个元素包含温度和持续时间
|
||||
|
||||
Returns:
|
||||
bool: 程序启动是否成功
|
||||
"""
|
||||
# 模拟程序启动
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_current_temperature(self) -> float:
|
||||
"""获取当前温度 (°C)"""
|
||||
return round(self.current_temperature, 2)
|
||||
|
||||
def get_target_temperature(self) -> float:
|
||||
"""获取目标温度 (°C)"""
|
||||
return self.target_temperature
|
||||
|
||||
def get_heating_status(self) -> bool:
|
||||
"""获取加热状态"""
|
||||
return self.is_heating
|
||||
|
||||
def get_cooling_status(self) -> bool:
|
||||
"""获取制冷状态"""
|
||||
return self.is_cooling
|
||||
|
||||
def get_pid_status(self) -> bool:
|
||||
"""获取PID控制状态"""
|
||||
return self.pid_enabled
|
||||
|
||||
def get_temperature_history(self) -> List[Dict]:
|
||||
"""获取温度历史记录"""
|
||||
return self.temperature_history[-10:] # 返回最近10条记录
|
||||
|
||||
def get_controller_status(self) -> Dict[str, Any]:
|
||||
"""获取控制器完整状态"""
|
||||
return {
|
||||
"controller_id": self.controller_id,
|
||||
"current_temp": self.current_temperature,
|
||||
"target_temp": self.target_temperature,
|
||||
"is_heating": self.is_heating,
|
||||
"is_cooling": self.is_cooling,
|
||||
"pid_enabled": self.pid_enabled,
|
||||
"history_count": len(self.temperature_history),
|
||||
}
|
||||
|
||||
|
||||
class MultiChannelAnalyzer:
|
||||
"""
|
||||
多通道分析仪
|
||||
|
||||
支持同时监测多个通道的信号,提供实时数据采集和分析功能。
|
||||
常用于光谱分析、电化学测量等应用场景。
|
||||
"""
|
||||
|
||||
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
||||
"""
|
||||
初始化多通道分析仪
|
||||
|
||||
Args:
|
||||
analyzer_id: 分析仪ID
|
||||
channels: 通道数量
|
||||
"""
|
||||
self.analyzer_id = analyzer_id
|
||||
self.channel_count = channels
|
||||
self.channel_data = {i: {"value": 0.0, "unit": "V", "enabled": True} for i in range(channels)}
|
||||
self.is_measuring = False
|
||||
self.sample_rate = 1000 # Hz
|
||||
|
||||
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
||||
"""
|
||||
配置通道
|
||||
|
||||
Args:
|
||||
channel: 通道编号
|
||||
enabled: 是否启用
|
||||
unit: 测量单位
|
||||
|
||||
Returns:
|
||||
bool: 配置是否成功
|
||||
"""
|
||||
if 0 <= channel < self.channel_count:
|
||||
self.channel_data[channel]["enabled"] = enabled
|
||||
self.channel_data[channel]["unit"] = unit
|
||||
return True
|
||||
return False
|
||||
|
||||
async def start_measurement_async(self, duration: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
开始异步测量
|
||||
|
||||
Args:
|
||||
duration: 测量持续时间(秒)
|
||||
|
||||
Returns:
|
||||
Dict: 测量结果
|
||||
"""
|
||||
self.is_measuring = True
|
||||
|
||||
# 模拟数据采集
|
||||
measurements = []
|
||||
for second in range(duration):
|
||||
timestamp = asyncio.get_event_loop().time()
|
||||
frame_data = {}
|
||||
|
||||
for channel in range(self.channel_count):
|
||||
if self.channel_data[channel]["enabled"]:
|
||||
# 模拟传感器数据
|
||||
import random
|
||||
|
||||
value = random.uniform(-5.0, 5.0)
|
||||
frame_data[f"channel_{channel}"] = value
|
||||
self.channel_data[channel]["value"] = value
|
||||
|
||||
measurements.append({"timestamp": timestamp, "data": frame_data})
|
||||
|
||||
await asyncio.sleep(1.0) # 每秒采集一次
|
||||
|
||||
self.is_measuring = False
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"duration": duration,
|
||||
"samples_count": len(measurements),
|
||||
"measurements": measurements[-5:], # 只返回最后5个样本
|
||||
"channels_active": len([ch for ch in self.channel_data.values() if ch["enabled"]]),
|
||||
}
|
||||
|
||||
def stop_measurement(self) -> bool:
|
||||
"""
|
||||
停止测量
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
self.is_measuring = False
|
||||
return True
|
||||
|
||||
def reset_channels(self) -> bool:
|
||||
"""
|
||||
重置所有通道
|
||||
|
||||
Returns:
|
||||
bool: 重置是否成功
|
||||
"""
|
||||
for channel in self.channel_data:
|
||||
self.channel_data[channel]["value"] = 0.0
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_measurement_status(self) -> bool:
|
||||
"""获取测量状态"""
|
||||
return self.is_measuring
|
||||
|
||||
def get_channel_count(self) -> int:
|
||||
"""获取通道数量"""
|
||||
return self.channel_count
|
||||
|
||||
def get_sample_rate(self) -> float:
|
||||
"""获取采样率 (Hz)"""
|
||||
return self.sample_rate
|
||||
|
||||
def get_channel_values(self) -> Dict[int, float]:
|
||||
"""获取所有通道的当前值"""
|
||||
return {ch: data["value"] for ch, data in self.channel_data.items() if data["enabled"]}
|
||||
|
||||
def get_enabled_channels(self) -> List[int]:
|
||||
"""获取已启用的通道列表"""
|
||||
return [ch for ch, data in self.channel_data.items() if data["enabled"]]
|
||||
|
||||
def get_analyzer_status(self) -> Dict[str, Any]:
|
||||
"""获取分析仪完整状态"""
|
||||
return {
|
||||
"analyzer_id": self.analyzer_id,
|
||||
"channel_count": self.channel_count,
|
||||
"is_measuring": self.is_measuring,
|
||||
"sample_rate": self.sample_rate,
|
||||
"active_channels": len(self.get_enabled_channels()),
|
||||
"channel_data": self.channel_data,
|
||||
}
|
||||
|
||||
|
||||
class AutomatedDispenser:
|
||||
"""
|
||||
自动分配器
|
||||
|
||||
精确控制固体和液体材料的分配,支持多种分配模式和容器管理。
|
||||
集成称重功能,确保分配精度和重现性。
|
||||
"""
|
||||
|
||||
def __init__(self, dispenser_id: str = "dispenser_01"):
|
||||
"""
|
||||
初始化自动分配器
|
||||
|
||||
Args:
|
||||
dispenser_id: 分配器ID
|
||||
"""
|
||||
self.dispenser_id = dispenser_id
|
||||
self.is_ready = True
|
||||
self.current_position = {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
self.dispensed_total = 0.0
|
||||
self.container_capacity = 1000.0 # mL
|
||||
self.precision_mode = True
|
||||
|
||||
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
||||
"""
|
||||
移动到指定位置
|
||||
|
||||
Args:
|
||||
x: X坐标 (mm)
|
||||
y: Y坐标 (mm)
|
||||
z: Z坐标 (mm)
|
||||
|
||||
Returns:
|
||||
bool: 移动是否成功
|
||||
"""
|
||||
self.current_position = {"x": x, "y": y, "z": z}
|
||||
return True
|
||||
|
||||
async def dispense_liquid_async(self, volume: float, container_id: str, viscosity: str = "low") -> Dict[str, Any]:
|
||||
"""
|
||||
异步分配液体
|
||||
|
||||
Args:
|
||||
volume: 分配体积 (mL)
|
||||
container_id: 容器ID
|
||||
viscosity: 液体粘度等级
|
||||
|
||||
Returns:
|
||||
Dict: 分配结果
|
||||
"""
|
||||
if not self.is_ready:
|
||||
return {"success": False, "error": "设备未就绪"}
|
||||
|
||||
if volume <= 0:
|
||||
return {"success": False, "error": "体积必须大于0"}
|
||||
|
||||
# 模拟分配过程
|
||||
dispense_time = volume * 0.1 # 每mL需要0.1秒
|
||||
if viscosity == "high":
|
||||
dispense_time *= 2 # 高粘度液体需要更长时间
|
||||
|
||||
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
|
||||
|
||||
self.dispensed_total += volume
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"dispensed_volume": volume,
|
||||
"container_id": container_id,
|
||||
"actual_time": min(dispense_time, 5.0),
|
||||
"total_dispensed": self.dispensed_total,
|
||||
}
|
||||
|
||||
def clean_dispenser(self, wash_volume: float = 5.0) -> bool:
|
||||
"""
|
||||
清洗分配器
|
||||
|
||||
Args:
|
||||
wash_volume: 清洗液体积 (mL)
|
||||
|
||||
Returns:
|
||||
bool: 清洗是否成功
|
||||
"""
|
||||
# 模拟清洗过程
|
||||
return True
|
||||
|
||||
def calibrate_volume(self, target_volume: float) -> bool:
|
||||
"""
|
||||
校准分配体积
|
||||
|
||||
Args:
|
||||
target_volume: 校准目标体积 (mL)
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
# 模拟校准过程
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_ready_status(self) -> bool:
|
||||
"""获取就绪状态"""
|
||||
return self.is_ready
|
||||
|
||||
def get_current_position(self) -> Dict[str, float]:
|
||||
"""获取当前位置坐标"""
|
||||
return self.current_position.copy()
|
||||
|
||||
def get_dispensed_total(self) -> float:
|
||||
"""获取累计分配体积 (mL)"""
|
||||
return self.dispensed_total
|
||||
|
||||
def get_container_capacity(self) -> float:
|
||||
"""获取容器容量 (mL)"""
|
||||
return self.container_capacity
|
||||
|
||||
def get_precision_mode(self) -> bool:
|
||||
"""获取精密模式状态"""
|
||||
return self.precision_mode
|
||||
|
||||
def get_dispenser_status(self) -> Dict[str, Any]:
|
||||
"""获取分配器完整状态"""
|
||||
return {
|
||||
"dispenser_id": self.dispenser_id,
|
||||
"ready": self.is_ready,
|
||||
"position": self.current_position,
|
||||
"dispensed_total": self.dispensed_total,
|
||||
"capacity": self.container_capacity,
|
||||
"precision_mode": self.precision_mode,
|
||||
}
|
||||
198
test/resources/bioyond_materials.json
Normal file
198
test/resources/bioyond_materials.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00160",
|
||||
"barCode": "",
|
||||
"name": "ODA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 695374.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00161",
|
||||
"barCode": "",
|
||||
"name": "MPDA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 681618.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00041",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 380000.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00042",
|
||||
"barCode": "",
|
||||
"name": "PGME",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 337892.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-ca72-febc-b7c304476c78",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-d748-725e-97a2e549f085",
|
||||
"typeName": "样品板",
|
||||
"code": "0001-00004",
|
||||
"barCode": "",
|
||||
"name": "0917",
|
||||
"quantity": 1.0000000000000000000000000000,
|
||||
"lockQuantity": 4.0000000000000000000000000000,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0009",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1c68c8-0574-69a1-9858-4637e0193451",
|
||||
"detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208",
|
||||
"code": null,
|
||||
"name": "SIDA",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-8d51-3191-a31f5be421e5",
|
||||
"detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53",
|
||||
"code": null,
|
||||
"name": "BTDA-2",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-da80-735b-53ae2197a360",
|
||||
"detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939",
|
||||
"code": null,
|
||||
"name": "BTDA-DD",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "28",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e717-1b1b-99891f875455",
|
||||
"detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2",
|
||||
"code": null,
|
||||
"name": "BTDA-3",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb",
|
||||
"detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1",
|
||||
"code": null,
|
||||
"name": "BTDA-1",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 1758560573511
|
||||
}
|
||||
48
test/resources/test_bottle_carrier.py
Normal file
48
test/resources/test_bottle_carrier.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
||||
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||
|
||||
|
||||
def test_bottle_carrier() -> "BottleCarrier":
|
||||
print("创建载架...")
|
||||
|
||||
# 创建6瓶载架
|
||||
bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01")
|
||||
print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}")
|
||||
|
||||
# 创建1烧杯载架
|
||||
beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01")
|
||||
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
||||
|
||||
# 创建瓶子和烧杯
|
||||
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
|
||||
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
|
||||
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
|
||||
|
||||
print(f"\n创建的物料:")
|
||||
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
||||
print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL")
|
||||
print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL")
|
||||
|
||||
# 测试放置容器
|
||||
print(f"\n测试放置容器...")
|
||||
|
||||
# 通过载架的索引操作来放置容器
|
||||
# bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置
|
||||
print(f"粉末瓶已放置到6瓶载架的位置 0")
|
||||
|
||||
# beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置
|
||||
print(f"溶液烧杯已放置到1烧杯载架的位置 0")
|
||||
|
||||
# 验证放置结果
|
||||
print(f"\n验证放置结果:")
|
||||
bottle_at_0 = bottle_carrier[0].resource
|
||||
beaker_at_0 = beaker_carrier[0].resource
|
||||
|
||||
if bottle_at_0:
|
||||
print(f"位置 0 的瓶子: {bottle_at_0.name}")
|
||||
if beaker_at_0:
|
||||
print(f"位置 0 的烧杯: {beaker_at_0.name}")
|
||||
|
||||
print("\n载架设置完成!")
|
||||
35
test/resources/test_converter_bioyond.py
Normal file
35
test/resources/test_converter_bioyond.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||
|
||||
lab_registry.setup()
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)["data"]
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
def test_bioyond_to_plr(bioyond_materials) -> list[dict]:
|
||||
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||
print("将 BioYond 物料数据转换为 PLR 格式...")
|
||||
output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck)
|
||||
print(deck.summary())
|
||||
print([resource.serialize() for resource in output])
|
||||
print([resource.serialize_all_state() for resource in output])
|
||||
@@ -1,5 +1,6 @@
|
||||
name: unilab
|
||||
channels:
|
||||
- unilab
|
||||
- robostack
|
||||
- robostack-staging
|
||||
- conda-forge
|
||||
@@ -33,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -48,8 +49,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
@@ -60,6 +62,11 @@ dependencies:
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
- git+https://github.com/Xuwznln/pylabrobot
|
||||
@@ -1,5 +1,6 @@
|
||||
name: unilab
|
||||
channels:
|
||||
- unilab
|
||||
- robostack
|
||||
- robostack-staging
|
||||
- conda-forge
|
||||
@@ -33,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -60,6 +61,11 @@ dependencies:
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
- git+https://github.com/Xuwznln/pylabrobot
|
||||
@@ -1,5 +1,6 @@
|
||||
name: unilab
|
||||
channels:
|
||||
- unilab
|
||||
- robostack
|
||||
- robostack-staging
|
||||
- conda-forge
|
||||
@@ -34,8 +35,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- paho-mqtt
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -50,8 +50,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
@@ -62,6 +63,11 @@ dependencies:
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# - ros-humble-unilabos-msgs
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
- git+https://github.com/Xuwznln/pylabrobot
|
||||
@@ -1,17 +1,18 @@
|
||||
name: unilab
|
||||
channels:
|
||||
- unilab
|
||||
- robostack
|
||||
- robostack-staging
|
||||
- conda-forge
|
||||
dependencies:
|
||||
# Basics
|
||||
- python=3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- make
|
||||
- ninja
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
# - compilers
|
||||
# - cmake
|
||||
# - make
|
||||
# - ninja
|
||||
# - sphinx
|
||||
# - sphinx_rtd_theme
|
||||
# Data Visualization
|
||||
- numpy
|
||||
- scipy
|
||||
@@ -23,7 +24,7 @@ dependencies:
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- pymodbus==3.6.9
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
@@ -33,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -48,8 +49,9 @@ dependencies:
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# web
|
||||
# web and visualization
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
# geometry & motion planning
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
@@ -60,6 +62,18 @@ dependencies:
|
||||
- transforms3d
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
# ros-humble-unilabos-msgs
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
# driver
|
||||
#- crcmod
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
- git+https://github.com/Xuwznln/pylabrobot
|
||||
# driver
|
||||
#- ur-rtde # set PYTHONUTF8=1
|
||||
#- pyautogui
|
||||
#- pywinauto
|
||||
#- pywinauto_recorder
|
||||
@@ -15,24 +15,33 @@ def start_backend(
|
||||
without_host: bool = False,
|
||||
visual: str = "None",
|
||||
resources_mesh_config: dict = {},
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
if backend == "ros":
|
||||
# 假设 ros_main, simple_main, automancer_main 是不同 backend 的启动函数
|
||||
from unilabos.ros.main_slave_run import main, slave # 如果选择 'ros' 作为 backend
|
||||
elif backend == 'simple':
|
||||
elif backend == "simple":
|
||||
# 这里假设 simple_backend 和 automancer_backend 是你定义的其他两个后端
|
||||
# from simple_backend import main as simple_main
|
||||
pass
|
||||
elif backend == 'automancer':
|
||||
elif backend == "automancer":
|
||||
# from automancer_backend import main as automancer_main
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Unsupported backend: {backend}")
|
||||
|
||||
|
||||
backend_thread = threading.Thread(
|
||||
target=main if not without_host else slave,
|
||||
args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config),
|
||||
args=(
|
||||
devices_config,
|
||||
resources_config,
|
||||
resources_edge_config,
|
||||
graph,
|
||||
controllers_config,
|
||||
bridges,
|
||||
visual,
|
||||
resources_mesh_config,
|
||||
),
|
||||
name="backend_thread",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
192
unilabos/app/communication.py
Normal file
192
unilabos/app/communication.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""
|
||||
通信模块
|
||||
|
||||
提供WebSocket的统一接口,支持通过配置选择通信协议。
|
||||
包含通信抽象层基类和通信客户端工厂。
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
class BaseCommunicationClient(ABC):
|
||||
"""
|
||||
通信客户端抽象基类
|
||||
|
||||
定义了所有通信客户端(WebSocket等)需要实现的接口。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_disabled = True
|
||||
self.client_id = ""
|
||||
|
||||
@abstractmethod
|
||||
def start(self) -> None:
|
||||
"""
|
||||
启动通信客户端连接
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
停止通信客户端连接
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_device_status(self, device_status: dict, device_id: str, property_name: str) -> None:
|
||||
"""
|
||||
发布设备状态信息
|
||||
|
||||
Args:
|
||||
device_status: 设备状态字典
|
||||
device_id: 设备ID
|
||||
property_name: 属性名称
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_job_status(
|
||||
self, feedback_data: dict, job_id: str, status: str, return_info: Optional[dict] = None
|
||||
) -> None:
|
||||
"""
|
||||
发布作业状态信息
|
||||
|
||||
Args:
|
||||
feedback_data: 反馈数据
|
||||
job_id: 作业ID
|
||||
status: 作业状态
|
||||
return_info: 返回信息
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def send_ping(self, ping_id: str, timestamp: float) -> None:
|
||||
"""
|
||||
发送ping消息
|
||||
|
||||
Args:
|
||||
ping_id: ping ID
|
||||
timestamp: 时间戳
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup_pong_subscription(self) -> None:
|
||||
"""
|
||||
设置pong消息订阅(可选实现)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
检查是否已连接
|
||||
|
||||
Returns:
|
||||
是否已连接
|
||||
"""
|
||||
return not self.is_disabled
|
||||
|
||||
|
||||
class CommunicationClientFactory:
|
||||
"""
|
||||
通信客户端工厂类
|
||||
|
||||
根据配置文件中的通信协议设置创建相应的客户端实例。
|
||||
"""
|
||||
|
||||
_client_cache: Optional[BaseCommunicationClient] = None
|
||||
|
||||
@classmethod
|
||||
def create_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
创建通信客户端实例
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
|
||||
Raises:
|
||||
ValueError: 当协议类型不支持时
|
||||
"""
|
||||
if protocol is None:
|
||||
protocol = BasicConfig.communication_protocol
|
||||
|
||||
protocol = protocol.lower()
|
||||
|
||||
if protocol == "websocket":
|
||||
return cls._create_websocket_client()
|
||||
else:
|
||||
logger.error(f"[CommunicationFactory] Unsupported protocol: {protocol}")
|
||||
logger.warning(f"[CommunicationFactory] Falling back to WebSocket")
|
||||
return cls._create_websocket_client()
|
||||
|
||||
@classmethod
|
||||
def get_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
获取通信客户端实例(单例模式)
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
"""
|
||||
if cls._client_cache is None:
|
||||
cls._client_cache = cls.create_client(protocol)
|
||||
logger.info(f"[CommunicationFactory] Created {type(cls._client_cache).__name__} client")
|
||||
|
||||
return cls._client_cache
|
||||
|
||||
@classmethod
|
||||
def _create_websocket_client(cls) -> BaseCommunicationClient:
|
||||
"""创建WebSocket客户端"""
|
||||
try:
|
||||
from unilabos.app.ws_client import WebSocketClient
|
||||
|
||||
return WebSocketClient()
|
||||
except Exception as e:
|
||||
logger.error(f"[CommunicationFactory] Failed to create WebSocket client: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def reset_client(cls):
|
||||
"""重置客户端缓存(用于测试或重新配置)"""
|
||||
if cls._client_cache:
|
||||
try:
|
||||
cls._client_cache.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"[CommunicationFactory] Error stopping old client: {str(e)}")
|
||||
|
||||
cls._client_cache = None
|
||||
logger.info("[CommunicationFactory] Client cache reset")
|
||||
|
||||
@classmethod
|
||||
def get_supported_protocols(cls) -> list[str]:
|
||||
"""
|
||||
获取支持的协议列表
|
||||
|
||||
Returns:
|
||||
支持的协议列表
|
||||
"""
|
||||
return ["websocket"]
|
||||
|
||||
|
||||
def get_communication_client(protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
获取通信客户端实例的便捷函数
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
"""
|
||||
return CommunicationClientFactory.get_client(protocol)
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from unilabos.app.model import JobAddReq, JobData
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
|
||||
|
||||
def get_resources() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().resources_config
|
||||
|
||||
def devices() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().devices_config
|
||||
|
||||
def job_info(id: str):
|
||||
get_goal_status = HostNode.get_instance().get_goal_status(id)
|
||||
return JobData(jobId=id, status=get_goal_status)
|
||||
|
||||
def job_add(req: JobAddReq) -> JobData:
|
||||
if req.job_id is None:
|
||||
req.job_id = str(uuid.uuid4())
|
||||
action_name = req.data["action"]
|
||||
action_kwargs = req.data["action_kwargs"]
|
||||
req.data['action'] = action_name
|
||||
if action_name == "execute_command_from_outer":
|
||||
action_kwargs = {"command": json.dumps(action_kwargs)}
|
||||
elif "command" in action_kwargs:
|
||||
action_kwargs = action_kwargs["command"]
|
||||
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id, server_info=req.server_info)
|
||||
return JobData(jobId=req.job_id)
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
@@ -10,7 +10,6 @@ from copy import deepcopy
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.resources.graphio import tree_to_list, modify_to_backend_format
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -18,13 +17,14 @@ unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if unilabos_dir not in sys.path:
|
||||
sys.path.append(unilabos_dir)
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
|
||||
|
||||
def load_config_from_file(config_path):
|
||||
if config_path is None:
|
||||
config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", 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")
|
||||
@@ -37,19 +37,34 @@ def load_config_from_file(config_path):
|
||||
load_config(config_path)
|
||||
|
||||
|
||||
def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
# easier for user input, easier for dev search code
|
||||
option_strings = list(args._option_string_actions.keys())
|
||||
for i, arg in enumerate(sys.argv):
|
||||
for option_string in option_strings:
|
||||
if arg.startswith(option_string):
|
||||
new_arg = arg[:2] + arg[2 : len(option_string)].replace("-", "_") + arg[len(option_string) :]
|
||||
sys.argv[i] = new_arg
|
||||
break
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph.")
|
||||
parser.add_argument("-d", "--devices", help="Devices config file.")
|
||||
parser.add_argument("-r", "--resources", help="Resources config file.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
|
||||
parser.add_argument(
|
||||
"--registry_path",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
help="Path to the registry",
|
||||
help="Path to the registry directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--working_dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the working directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
@@ -60,69 +75,179 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--app_bridges",
|
||||
nargs="+",
|
||||
default=["mqtt", "fastapi"],
|
||||
help="Bridges to connect to. Now support 'mqtt' and 'fastapi'.",
|
||||
default=["websocket", "fastapi"],
|
||||
help="Bridges to connect to. Now support 'websocket' and 'fastapi'.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--without_host",
|
||||
"--is_slave",
|
||||
action="store_true",
|
||||
help="Run the backend as slave (without host).",
|
||||
help="Run the backend as slave node (without host privileges).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slave_no_host",
|
||||
action="store_true",
|
||||
help="Slave模式下跳过等待host服务",
|
||||
help="Skip waiting for host service in slave mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upload_registry",
|
||||
action="store_true",
|
||||
help="启动unilab时同时报送注册表信息",
|
||||
help="Upload registry information when starting unilab",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_remote_resource",
|
||||
action="store_true",
|
||||
help="Use remote resources when starting unilab",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="配置文件路径,支持.py格式的Python配置文件",
|
||||
help="Configuration file path, supports .py format Python config files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8002,
|
||||
help="信息页web服务的启动端口",
|
||||
help="Port for web service information page",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable_browser",
|
||||
action='store_true',
|
||||
help="是否在启动时关闭信息页",
|
||||
action="store_true",
|
||||
help="Disable opening information page on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--2d_vis",
|
||||
action='store_true',
|
||||
help="是否在pylabrobot实例启动时,同时启动可视化",
|
||||
action="store_true",
|
||||
help="Enable 2D visualization when starting pylabrobot instance",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--visual",
|
||||
choices=["rviz", "web", "disable"],
|
||||
default="disable",
|
||||
help="选择可视化工具: rviz, web",
|
||||
help="Choose visualization tool: rviz, web, or disable",
|
||||
)
|
||||
return parser.parse_args()
|
||||
parser.add_argument(
|
||||
"--ak",
|
||||
type=str,
|
||||
default="",
|
||||
help="Access key for laboratory requests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sk",
|
||||
type=str,
|
||||
default="",
|
||||
help="Secret key for laboratory requests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--addr",
|
||||
type=str,
|
||||
default="https://uni-lab.bohrium.com/api/v1",
|
||||
help="Laboratory backend address",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip_env_check",
|
||||
action="store_true",
|
||||
help="Skip environment dependency check on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Complete registry information",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 解析命令行参数
|
||||
args = parse_args()
|
||||
args_dict = vars(args)
|
||||
convert_argv_dashes_to_underscores(args)
|
||||
args_dict = vars(args.parse_args())
|
||||
|
||||
# 环境检查 - 检查并自动安装必需的包 (可选)
|
||||
if not args_dict.get("skip_env_check", False):
|
||||
from unilabos.utils.environment_check import check_environment
|
||||
|
||||
print_status("正在进行环境依赖检查...", "info")
|
||||
if not check_environment(auto_install=True):
|
||||
print_status("环境检查失败,程序退出", "error")
|
||||
os._exit(1)
|
||||
else:
|
||||
print_status("跳过环境依赖检查", "warning")
|
||||
|
||||
# 加载配置文件,优先加载config,然后从env读取
|
||||
config_path = args_dict.get("config")
|
||||
if os.getcwd().endswith("unilabos_data"):
|
||||
working_dir = os.path.abspath(os.getcwd())
|
||||
else:
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
if args_dict.get("working_dir"):
|
||||
working_dir = args_dict.get("working_dir", "")
|
||||
if config_path and not os.path.exists(config_path):
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
if not os.path.exists(config_path):
|
||||
print_status(
|
||||
f"当前工作目录 {working_dir} 未找到local_config.py,请通过 --config 传入 local_config.py 文件路径",
|
||||
"error",
|
||||
)
|
||||
os._exit(1)
|
||||
elif config_path and os.path.exists(config_path):
|
||||
working_dir = os.path.dirname(config_path)
|
||||
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
elif not config_path and (
|
||||
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
|
||||
):
|
||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||
if input() != "n":
|
||||
os.makedirs(working_dir, exist_ok=True)
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
||||
)
|
||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||
else:
|
||||
os._exit(1)
|
||||
# 加载配置文件
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
load_config_from_file(config_path)
|
||||
if args_dict["addr"] == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
else:
|
||||
HTTPConfig.remote_addr = args_dict.get("addr", "")
|
||||
|
||||
if args_dict["use_remote_resource"]:
|
||||
print_status("使用远程资源启动", "info")
|
||||
from unilabos.app.web import http_client
|
||||
|
||||
res = http_client.resource_get("host_node", False)
|
||||
if str(res.get("code", 0)) == "0" and len(res.get("data", [])) > 0:
|
||||
print_status("远程资源已存在,使用云端物料!", "info")
|
||||
args_dict["graph"] = None
|
||||
else:
|
||||
print_status("远程资源不存在,本地将进行首次上报!", "info")
|
||||
|
||||
# 设置BasicConfig参数
|
||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||
if args_dict.get("ak", ""):
|
||||
BasicConfig.ak = args_dict.get("ak", "")
|
||||
print_status("传入了ak参数,优先采用传入参数!", "info")
|
||||
if args_dict.get("sk", ""):
|
||||
BasicConfig.sk = args_dict.get("sk", "")
|
||||
print_status("传入了sk参数,优先采用传入参数!", "info")
|
||||
BasicConfig.working_dir = working_dir
|
||||
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||
BasicConfig.communication_protocol = "websocket"
|
||||
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
|
||||
@@ -135,46 +260,101 @@ def main():
|
||||
dict_to_nested_dict,
|
||||
initialize_resources,
|
||||
)
|
||||
from unilabos.app.mq import mqtt_client
|
||||
from unilabos.app.communication import get_communication_client
|
||||
from unilabos.registry.registry import build_registry
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
from unilabos.app.register import register_devices_and_resources
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"])
|
||||
resource_edge_info = []
|
||||
devices_and_resources = None
|
||||
if args_dict["graph"] is not None:
|
||||
import unilabos.resources.graphio as graph_res
|
||||
if args_dict["graph"].endswith(".json"):
|
||||
graph, data = read_node_link_json(args_dict["graph"])
|
||||
lab_registry = build_registry(
|
||||
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
|
||||
)
|
||||
|
||||
if not BasicConfig.ak or not BasicConfig.sk:
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||
os._exit(1)
|
||||
if args_dict["graph"] is None:
|
||||
request_startup_json = http_client.request_startup_json()
|
||||
if not request_startup_json:
|
||||
print_status(
|
||||
"未指定设备加载文件路径,尝试从HTTP获取失败,请检查网络或者使用-g参数指定设备加载文件路径", "error"
|
||||
)
|
||||
os._exit(1)
|
||||
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())
|
||||
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
||||
args_dict["graph"] = graph_res.physical_setup_graph
|
||||
print_status("联网获取设备加载文件成功", "info")
|
||||
graph, data = read_node_link_json(request_startup_json)
|
||||
else:
|
||||
if args_dict["devices"] is None or args_dict["resources"] is None:
|
||||
print_status("Either graph or devices and resources must be provided.", "error")
|
||||
sys.exit(1)
|
||||
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
|
||||
# args_dict["resources_config"] = initialize_resources(
|
||||
# list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||
# )
|
||||
args_dict["resources_config"] = list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
|
||||
file_path = args_dict["graph"]
|
||||
if file_path.endswith(".json"):
|
||||
graph, data = read_node_link_json(file_path)
|
||||
else:
|
||||
graph, data = read_graphml(file_path)
|
||||
import unilabos.resources.graphio as graph_res
|
||||
|
||||
graph_res.physical_setup_graph = graph
|
||||
resource_edge_info = modify_to_backend_format(data["links"])
|
||||
materials = lab_registry.obtain_registry_resource_info()
|
||||
materials.extend(lab_registry.obtain_registry_device_info())
|
||||
materials = {k["id"]: k for k in materials}
|
||||
nodes = {k["id"]: k for k in data["nodes"]}
|
||||
edge_info = len(resource_edge_info)
|
||||
for ind, i in enumerate(resource_edge_info[::-1]):
|
||||
source_node = nodes[i["source"]]
|
||||
target_node = nodes[i["target"]]
|
||||
source_handle = i["sourceHandle"]
|
||||
target_handle = i["targetHandle"]
|
||||
source_handler_keys = [
|
||||
h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == "source"
|
||||
]
|
||||
target_handler_keys = [
|
||||
h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == "target"
|
||||
]
|
||||
if source_handle not in source_handler_keys:
|
||||
print_status(
|
||||
f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}",
|
||||
"error",
|
||||
)
|
||||
resource_edge_info.pop(edge_info - ind - 1)
|
||||
continue
|
||||
if target_handle not in target_handler_keys:
|
||||
print_status(
|
||||
f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}",
|
||||
"error",
|
||||
)
|
||||
resource_edge_info.pop(edge_info - ind - 1)
|
||||
continue
|
||||
|
||||
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())
|
||||
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
|
||||
args_dict["graph"] = graph_res.physical_setup_graph
|
||||
|
||||
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
|
||||
for i in args_dict["resources_config"]:
|
||||
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info")
|
||||
|
||||
if BasicConfig.upload_registry:
|
||||
# 设备注册到服务端 - 需要 ak 和 sk
|
||||
if args_dict.get("ak") and args_dict.get("sk"):
|
||||
print_status("开始注册设备到服务端...", "info")
|
||||
try:
|
||||
register_devices_and_resources(lab_registry)
|
||||
print_status("设备注册完成", "info")
|
||||
except Exception as e:
|
||||
print_status(f"设备注册失败: {e}", "error")
|
||||
else:
|
||||
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
||||
else:
|
||||
print_status(
|
||||
"本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning"
|
||||
)
|
||||
|
||||
if args_dict["controllers"] is not None:
|
||||
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
||||
else:
|
||||
@@ -182,32 +362,44 @@ def main():
|
||||
|
||||
args_dict["bridges"] = []
|
||||
|
||||
if "mqtt" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(mqtt_client)
|
||||
# 获取通信客户端(仅支持WebSocket)
|
||||
comm_client = get_communication_client()
|
||||
|
||||
if "websocket" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(comm_client)
|
||||
if "fastapi" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(http_client)
|
||||
if "mqtt" in args_dict["app_bridges"]:
|
||||
if "websocket" in args_dict["app_bridges"]:
|
||||
|
||||
def _exit(signum, frame):
|
||||
mqtt_client.stop()
|
||||
comm_client.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, _exit)
|
||||
signal.signal(signal.SIGTERM, _exit)
|
||||
mqtt_client.start()
|
||||
comm_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"
|
||||
if devices_and_resources is not None:
|
||||
from unilabos.device_mesh.resource_visalization import ResourceVisualization # 此处开启后,logger会变更为INFO,有需要请调整
|
||||
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
|
||||
from unilabos.device_mesh.resource_visalization import (
|
||||
ResourceVisualization,
|
||||
) # 此处开启后,logger会变更为INFO,有需要请调整
|
||||
|
||||
resource_visualization = ResourceVisualization(
|
||||
devices_and_resources, args_dict["resources_config"], enable_rviz=enable_rviz
|
||||
)
|
||||
args_dict["resources_mesh_config"] = resource_visualization.resource_model
|
||||
start_backend(**args_dict)
|
||||
server_thread = threading.Thread(target=start_server, kwargs=dict(
|
||||
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
|
||||
))
|
||||
server_thread = threading.Thread(
|
||||
target=start_server,
|
||||
kwargs=dict(
|
||||
open_browser=not args_dict["disable_browser"],
|
||||
port=args_dict["port"],
|
||||
),
|
||||
)
|
||||
server_thread.start()
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
resource_visualization.start()
|
||||
@@ -215,10 +407,16 @@ def main():
|
||||
time.sleep(1)
|
||||
else:
|
||||
start_backend(**args_dict)
|
||||
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||
start_server(
|
||||
open_browser=not args_dict["disable_browser"],
|
||||
port=args_dict["port"],
|
||||
)
|
||||
else:
|
||||
start_backend(**args_dict)
|
||||
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
|
||||
start_server(
|
||||
open_browser=not args_dict["disable_browser"],
|
||||
port=args_dict["port"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -50,11 +50,16 @@ class Resp(BaseModel):
|
||||
|
||||
class JobAddReq(BaseModel):
|
||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||
action: str = Field(examples=["_execute_driver_command_async"], description="action name", default="")
|
||||
action_type: str = Field(examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action name", default="")
|
||||
action_args: dict = Field(examples=[{'string': 'string'}], description="action name", default="")
|
||||
task_id: str = Field(examples=["task_id"], description="task uuid")
|
||||
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
||||
node_id: str = Field(examples=["node_id"], description="node uuid")
|
||||
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
||||
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}], default={})
|
||||
|
||||
|
||||
class JobStepFinishReq(BaseModel):
|
||||
token: str = Field(examples=["030944"], description="token")
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import ssl
|
||||
import base64
|
||||
import hmac
|
||||
from hashlib import sha1
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from unilabos.config.config import MQConfig
|
||||
from unilabos.app.controler import job_add
|
||||
from unilabos.app.model import JobAddReq
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
from paho.mqtt.enums import CallbackAPIVersion
|
||||
|
||||
|
||||
class MQTTClient:
|
||||
mqtt_disable = True
|
||||
|
||||
def __init__(self):
|
||||
self.mqtt_disable = not MQConfig.lab_id
|
||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.client.on_log = self._on_log
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_message = self._on_message
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
|
||||
def _on_log(self, client, userdata, level, buf):
|
||||
# logger.info(f"[MQTT] log: {buf}")
|
||||
pass
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
|
||||
|
||||
def _on_message(self, client, userdata, msg) -> None:
|
||||
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
try:
|
||||
payload_str = msg.payload.decode("utf-8")
|
||||
payload_json = json.loads(payload_str)
|
||||
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
||||
if "data" not in payload_json:
|
||||
payload_json["data"] = {}
|
||||
if "action" in payload_json:
|
||||
payload_json["data"]["action"] = payload_json.pop("action")
|
||||
if "action_kwargs" in payload_json:
|
||||
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
|
||||
job_req = JobAddReq.model_validate(payload_json)
|
||||
data = job_add(job_req)
|
||||
return
|
||||
elif msg.topic == f"labs/{MQConfig.lab_id}/pong/":
|
||||
# 处理pong响应,通知HostNode
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
|
||||
host_instance = HostNode.get_instance(0)
|
||||
if host_instance:
|
||||
host_instance.handle_pong_response(payload_json)
|
||||
return
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||||
logger.error(traceback.format_exc())
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||||
if rc != 0:
|
||||
logger.error(f"[MQTT] Unexpected disconnection {rc}")
|
||||
|
||||
def _setup_ssl_context(self):
|
||||
temp_files = []
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
|
||||
ca_temp.write(MQConfig.ca_content)
|
||||
temp_files.append(ca_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
|
||||
cert_temp.write(MQConfig.cert_content)
|
||||
temp_files.append(cert_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
|
||||
key_temp.write(MQConfig.key_content)
|
||||
temp_files.append(key_temp.name)
|
||||
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
context.load_verify_locations(cafile=temp_files[0])
|
||||
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
|
||||
self.client.tls_set_context(context)
|
||||
finally:
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
if self.mqtt_disable:
|
||||
logger.warning("MQTT is disabled, skipping connection.")
|
||||
return
|
||||
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
|
||||
password = base64.b64encode(
|
||||
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
|
||||
).decode()
|
||||
|
||||
self.client.username_pw_set(userName, password)
|
||||
self._setup_ssl_context()
|
||||
|
||||
# 创建连接线程
|
||||
def connect_thread_func():
|
||||
try:
|
||||
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
|
||||
self.client.loop_start()
|
||||
|
||||
# 添加连接超时检测
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
while not self.client.is_connected() and attempt < max_attempts:
|
||||
logger.info(
|
||||
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
|
||||
)
|
||||
time.sleep(3)
|
||||
attempt += 1
|
||||
|
||||
if self.client.is_connected():
|
||||
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
|
||||
else:
|
||||
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
|
||||
self.client.loop_stop()
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 连接失败: {str(e)}")
|
||||
|
||||
connect_thread_func()
|
||||
# connect_thread = threading.Thread(target=connect_thread_func)
|
||||
# connect_thread.daemon = True
|
||||
# connect_thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
self.client.disconnect()
|
||||
self.client.loop_stop()
|
||||
|
||||
def publish_device_status(self, device_status: dict, device_id, property_name):
|
||||
# status = device_status.get(device_id, {})
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
|
||||
address = f"labs/{MQConfig.lab_id}/devices/"
|
||||
self.client.publish(address, json.dumps(status), qos=2)
|
||||
logger.debug(f"Device status published: address: {address}, {status}")
|
||||
|
||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
if return_info is None:
|
||||
return_info = "{}"
|
||||
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, 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)
|
||||
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:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/actions/"
|
||||
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||
|
||||
def send_ping(self, ping_id: str, timestamp: float):
|
||||
"""发送ping消息到服务端"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/ping/"
|
||||
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
|
||||
self.client.publish(address, json.dumps(ping_data), qos=2)
|
||||
|
||||
def setup_pong_subscription(self):
|
||||
"""设置pong消息订阅"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
|
||||
self.client.subscribe(pong_topic, 0)
|
||||
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||
|
||||
def handle_pong(self, pong_data: dict):
|
||||
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
||||
logger.debug(f"Pong received: {pong_data}")
|
||||
# 这里会被HostNode的ping-pong处理逻辑调用
|
||||
pass
|
||||
|
||||
|
||||
mqtt_client = MQTTClient()
|
||||
|
||||
if __name__ == "__main__":
|
||||
mqtt_client.start()
|
||||
@@ -1,67 +1,62 @@
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.registry.registry import build_registry
|
||||
|
||||
from unilabos.app.main import load_config_from_file
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
|
||||
def register_devices_and_resources(mqtt_client, lab_registry):
|
||||
def register_devices_and_resources(lab_registry):
|
||||
"""
|
||||
注册设备和资源到 MQTT
|
||||
注册设备和资源到服务器(仅支持HTTP)
|
||||
"""
|
||||
|
||||
# 注册资源信息 - 使用HTTP方式
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||
|
||||
# 注册设备信息
|
||||
devices_to_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']}")
|
||||
devices_to_register[device_info["id"]] = json.loads(
|
||||
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
|
||||
)
|
||||
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
|
||||
|
||||
# 注册资源信息
|
||||
resources_to_register = {}
|
||||
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']}")
|
||||
resources_to_register[resource_info["id"]] = resource_info
|
||||
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||
|
||||
time.sleep(10)
|
||||
# 注册设备
|
||||
if devices_to_register:
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||
|
||||
# 注册资源
|
||||
if resources_to_register:
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||
|
||||
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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,13 @@ HTTP客户端模块
|
||||
提供与远程服务器通信的客户端功能,只有host需要用
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
import requests
|
||||
from unilabos.utils.log import info
|
||||
from unilabos.config.config import MQConfig, HTTPConfig
|
||||
from unilabos.config.config import HTTPConfig, BasicConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
@@ -23,14 +25,17 @@ class HTTPClient:
|
||||
remote_addr: 远程服务器地址,如果不提供则从配置中获取
|
||||
auth: 授权信息
|
||||
"""
|
||||
self.initialized = False
|
||||
self.remote_addr = remote_addr or HTTPConfig.remote_addr
|
||||
if auth is not None:
|
||||
self.auth = auth
|
||||
else:
|
||||
self.auth = MQConfig.lab_id
|
||||
auth_secret = BasicConfig.auth_secret()
|
||||
self.auth = auth_secret
|
||||
info(f"正在使用ak sk作为授权信息:[{auth_secret}]")
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
@@ -41,31 +46,50 @@ class HTTPClient:
|
||||
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}"},
|
||||
f"{self.remote_addr}/lab/material/edge",
|
||||
json={
|
||||
"edges": resources,
|
||||
},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料关系失败: {response.text}")
|
||||
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:
|
||||
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
Args:
|
||||
resources: 要添加的资源列表
|
||||
database_process_later: 后台处理资源
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
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=100,
|
||||
)
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response
|
||||
@@ -82,9 +106,9 @@ class HTTPClient:
|
||||
Dict: 返回的资源数据
|
||||
"""
|
||||
response = requests.get(
|
||||
f"{self.remote_addr}/lab/resource/?edge_format=1",
|
||||
f"{self.remote_addr}/lab/material",
|
||||
params={"id": id, "with_children": with_children},
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=20,
|
||||
)
|
||||
return response.json()
|
||||
@@ -102,7 +126,7 @@ class HTTPClient:
|
||||
response = requests.delete(
|
||||
f"{self.remote_addr}/lab/resource/batch_delete/",
|
||||
params={"id": id},
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=20,
|
||||
)
|
||||
return response
|
||||
@@ -117,13 +141,29 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.patch(
|
||||
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
return response
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response.json()
|
||||
|
||||
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
|
||||
"""
|
||||
@@ -144,11 +184,61 @@ class HTTPClient:
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||
files=files,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=30, # 上传文件可能需要更长的超时时间
|
||||
)
|
||||
return response
|
||||
|
||||
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
注册资源到服务器
|
||||
|
||||
Args:
|
||||
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/resource",
|
||||
json=registry_data,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=30,
|
||||
)
|
||||
if response.status_code not in [200, 201]:
|
||||
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
||||
return response
|
||||
|
||||
def request_startup_json(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
请求启动配置
|
||||
|
||||
Args:
|
||||
startup_json: 启动配置JSON数据
|
||||
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.get(
|
||||
f"{self.remote_addr}/lab/resource/graph_info/",
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=(3, 30),
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"请求启动配置失败: {response.status_code}, {response.text}")
|
||||
else:
|
||||
try:
|
||||
with open(os.path.join(BasicConfig.working_dir, "startup_config.json"), "w", encoding="utf-8") as f:
|
||||
f.write(response.text)
|
||||
target_dict = json.loads(response.text)
|
||||
if "data" in target_dict:
|
||||
target_dict = target_dict["data"]
|
||||
return target_dict
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"解析启动配置JSON失败: {str(e.args)}\n响应内容: {response.text}")
|
||||
logger.error(f"响应内容: {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
# 创建默认客户端实例
|
||||
http_client = HTTPClient()
|
||||
|
||||
45
unilabos/app/web/controler.py
Normal file
45
unilabos/app/web/controler.py
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import json
|
||||
import traceback
|
||||
import uuid
|
||||
from unilabos.app.model import JobAddReq, JobData
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
|
||||
|
||||
def get_resources() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().resources_config
|
||||
|
||||
def devices() -> tuple:
|
||||
if HostNode.get_instance() is None:
|
||||
return False, "Host node not initialized"
|
||||
|
||||
return True, HostNode.get_instance().devices_config
|
||||
|
||||
def job_info(id: str):
|
||||
get_goal_status = HostNode.get_instance().get_goal_status(id)
|
||||
return JobData(jobId=id, status=get_goal_status)
|
||||
|
||||
def job_add(req: JobAddReq) -> JobData:
|
||||
if req.job_id is None:
|
||||
req.job_id = str(uuid.uuid4())
|
||||
action_name = req.data["action"]
|
||||
action_type = req.data.get("action_type", "LocalUnknown")
|
||||
action_args = req.data.get("action_kwargs", None) # 兼容老版本,后续删除
|
||||
if action_args is None:
|
||||
action_args = req.data.get("action_args")
|
||||
else:
|
||||
if "command" in action_args:
|
||||
action_args = action_args["command"]
|
||||
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
|
||||
try:
|
||||
HostNode.get_instance().send_goal(req.device_id, action_type=action_type, action_name=action_name, action_kwargs=action_args, goal_uuid=req.job_id, server_info=req.server_info)
|
||||
except Exception as e:
|
||||
for bridge in HostNode.get_instance().bridges:
|
||||
traceback.print_exc()
|
||||
if hasattr(bridge, "publish_job_status"):
|
||||
bridge.publish_job_status({}, req.job_id, "failed", serialize_result_info(traceback.format_exc(), False, {}))
|
||||
return JobData(jobId=req.job_id)
|
||||
@@ -7,6 +7,7 @@ Web页面模块
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
@@ -17,7 +18,7 @@ from jinja2 import Environment, FileSystemLoader
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.registry.registry import lab_registry
|
||||
from unilabos.ros.msgs.message_converter import msg_converter_manager
|
||||
from unilabos.utils.log import error
|
||||
from unilabos.utils.log import error, debug
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
from unilabos.app.web.utils.device_utils import get_registry_info
|
||||
from unilabos.app.web.utils.host_utils import get_host_node_info
|
||||
@@ -77,21 +78,23 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
HTMLResponse: 渲染后的HTML页面
|
||||
"""
|
||||
try:
|
||||
# 准备设备数据
|
||||
# 准备初始数据结构(这些数据将通过WebSocket实时更新)
|
||||
devices = []
|
||||
resources = []
|
||||
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
|
||||
|
||||
# 获取在线设备信息
|
||||
# 获取在线设备信息(用于初始渲染)
|
||||
ros_node_info = get_ros_node_info()
|
||||
# 获取主机节点信息
|
||||
# 获取主机节点信息(用于初始渲染)
|
||||
host_node_info = get_host_node_info()
|
||||
# 获取Registry路径信息
|
||||
# 获取Registry路径信息(静态信息,不需要实时更新)
|
||||
registry_info = get_registry_info()
|
||||
|
||||
# 获取已加载的设备
|
||||
# 获取初始数据用于页面渲染(后续将被WebSocket数据覆盖)
|
||||
if lab_registry:
|
||||
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
|
||||
devices = json.loads(
|
||||
json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
|
||||
)
|
||||
# 资源类型
|
||||
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
||||
resources.append(
|
||||
@@ -102,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
# 获取导入的模块
|
||||
# 获取导入的模块(初始数据)
|
||||
if msg_converter_manager:
|
||||
modules["names"] = msg_converter_manager.list_modules()
|
||||
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
|
||||
@@ -123,6 +126,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
|
||||
return html
|
||||
except Exception as e:
|
||||
debug(traceback.format_exc())
|
||||
error(f"生成状态页面时出错: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error generating status page: {str(e)}")
|
||||
|
||||
@@ -169,3 +173,20 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
except Exception as e:
|
||||
error(f"打开文件夹时出错: {str(e)}")
|
||||
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
|
||||
|
||||
@router.get("/registry-editor", response_class=HTMLResponse, summary="Registry Editor")
|
||||
async def registry_editor_page() -> str:
|
||||
"""
|
||||
注册表编辑页面,用于导入Python文件并生成注册表
|
||||
|
||||
Returns:
|
||||
HTMLResponse: 渲染后的HTML页面
|
||||
"""
|
||||
try:
|
||||
# 使用模板渲染页面
|
||||
template = env.get_template("registry_editor.html")
|
||||
html = template.render()
|
||||
return html
|
||||
except Exception as e:
|
||||
error(f"生成注册表编辑页面时出错: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error generating registry editor page: {str(e)}")
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
<body>
|
||||
<h1>{% block header %}UniLab{% endblock %}</h1>
|
||||
{% block nav %}
|
||||
<a href="/unilabos/webtic" class="home-link">Home</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block top_info %}{% endblock %}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}UniLab API{% endblock %}
|
||||
|
||||
{% block header %}UniLab API{% endblock %}
|
||||
|
||||
{% block nav %}
|
||||
<a href="/status" class="status-link">System Status</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Available Endpoints</h2>
|
||||
{% for route in routes %}
|
||||
<div class="endpoint">
|
||||
<span class="method">{{ route.method }}</span>
|
||||
<a href="{{ route.path }}">{{ route.path }}</a>
|
||||
<p>{{ route.summary }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
|
||||
header %}UniLab API{% endblock %} {% block nav %}
|
||||
<div class="nav-tabs">
|
||||
<a
|
||||
href="/"
|
||||
class="nav-tab"
|
||||
style="background-color: #2196f3; color: white"
|
||||
target="_blank"
|
||||
>主页</a
|
||||
>
|
||||
<a href="/status" class="nav-tab">状态</a>
|
||||
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %} {% block content %}
|
||||
<div class="card">
|
||||
<h2>Available Endpoints</h2>
|
||||
{% for route in routes %}
|
||||
<div class="endpoint">
|
||||
<span class="method">{{ route.method }}</span>
|
||||
<a href="{{ route.path }}">{{ route.path }}</a>
|
||||
<p>{{ route.summary }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
1411
unilabos/app/web/templates/registry_editor.html
Normal file
1411
unilabos/app/web/templates/registry_editor.html
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,8 @@ def get_yaml_from_goal_type(goal_type) -> str:
|
||||
Returns:
|
||||
str: 默认Goal参数的YAML格式字符串
|
||||
"""
|
||||
if isinstance(goal_type, str):
|
||||
return "{}"
|
||||
if not goal_type:
|
||||
return "{}"
|
||||
|
||||
|
||||
1166
unilabos/app/ws_client.py
Normal file
1166
unilabos/app/ws_client.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,22 +15,28 @@ from .heatchill_protocol import (
|
||||
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
|
||||
from .adjustph_protocol import generate_adjust_ph_protocol
|
||||
from .reset_handling_protocol import generate_reset_handling_protocol
|
||||
from .dry_protocol import generate_dry_protocol
|
||||
from .recrystallize_protocol import generate_recrystallize_protocol
|
||||
from .hydrogenate_protocol import generate_hydrogenate_protocol
|
||||
|
||||
|
||||
# Define a dictionary of protocol generators.
|
||||
action_protocol_generators = {
|
||||
AddProtocol: generate_add_protocol,
|
||||
AGVTransferProtocol: generate_agv_transfer_protocol,
|
||||
AdjustPHProtocol: generate_adjust_ph_protocol,
|
||||
CentrifugeProtocol: generate_centrifuge_protocol,
|
||||
CleanProtocol: generate_clean_protocol,
|
||||
CleanVesselProtocol: generate_clean_vessel_protocol,
|
||||
DissolveProtocol: generate_dissolve_protocol,
|
||||
DryProtocol: generate_dry_protocol,
|
||||
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
|
||||
EvaporateProtocol: generate_evaporate_protocol,
|
||||
FilterProtocol: generate_filter_protocol,
|
||||
@@ -38,12 +44,15 @@ action_protocol_generators = {
|
||||
HeatChillProtocol: generate_heat_chill_protocol,
|
||||
HeatChillStartProtocol: generate_heat_chill_start_protocol,
|
||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
|
||||
HydrogenateProtocol: generate_hydrogenate_protocol,
|
||||
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
|
||||
TransferProtocol: generate_pump_protocol,
|
||||
RecrystallizeProtocol: generate_recrystallize_protocol,
|
||||
ResetHandlingProtocol: generate_reset_handling_protocol,
|
||||
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,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
632
unilabos/compile/adjustph_protocol.py
Normal file
632
unilabos/compile/adjustph_protocol.py
Normal file
@@ -0,0 +1,632 @@
|
||||
import networkx as nx
|
||||
import logging
|
||||
from typing import List, Dict, Any, Union
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[ADJUST_PH] {message}")
|
||||
|
||||
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||
"""
|
||||
查找酸碱试剂容器,支持多种匹配模式
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
reagent: 试剂名称(如 "hydrochloric acid", "sodium hydroxide")
|
||||
|
||||
Returns:
|
||||
str: 试剂容器ID
|
||||
"""
|
||||
debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...")
|
||||
|
||||
# 常见酸碱试剂的别名映射
|
||||
reagent_aliases = {
|
||||
"hydrochloric acid": ["HCl", "hydrochloric_acid", "hcl", "muriatic_acid"],
|
||||
"sodium hydroxide": ["NaOH", "sodium_hydroxide", "naoh", "caustic_soda"],
|
||||
"sulfuric acid": ["H2SO4", "sulfuric_acid", "h2so4"],
|
||||
"nitric acid": ["HNO3", "nitric_acid", "hno3"],
|
||||
"acetic acid": ["CH3COOH", "acetic_acid", "glacial_acetic_acid"],
|
||||
"ammonia": ["NH3", "ammonium_hydroxide", "nh3"],
|
||||
"potassium hydroxide": ["KOH", "potassium_hydroxide", "koh"]
|
||||
}
|
||||
|
||||
# 构建搜索名称列表
|
||||
search_names = [reagent.lower()]
|
||||
debug_print(f"📋 基础搜索名称: {reagent.lower()}")
|
||||
|
||||
# 添加别名
|
||||
for base_name, aliases in reagent_aliases.items():
|
||||
if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower():
|
||||
search_names.extend([alias.lower() for alias in aliases])
|
||||
debug_print(f"🔗 添加别名: {aliases}")
|
||||
break
|
||||
|
||||
debug_print(f"📝 完整搜索列表: {search_names}")
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = []
|
||||
for name in search_names:
|
||||
name_clean = name.replace(" ", "_").replace("-", "_")
|
||||
possible_names.extend([
|
||||
f"flask_{name_clean}",
|
||||
f"bottle_{name_clean}",
|
||||
f"reagent_{name_clean}",
|
||||
f"acid_{name_clean}" if "acid" in name else f"base_{name_clean}",
|
||||
f"{name_clean}_bottle",
|
||||
f"{name_clean}_flask",
|
||||
name_clean
|
||||
])
|
||||
|
||||
debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)")
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
debug_print(f"📋 方法1: 精确名称匹配...")
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
debug_print(f"✅ 通过名称匹配找到容器: {vessel_name} 🎯")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配
|
||||
debug_print(f"📋 方法2: 模糊名称匹配...")
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
# 检查是否包含任何搜索名称
|
||||
for search_name in search_names:
|
||||
if search_name in node_id.lower() or search_name in node_name:
|
||||
debug_print(f"✅ 通过模糊匹配找到容器: {node_id} 🔍")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过液体类型匹配
|
||||
debug_print(f"📋 方法3: 液体类型匹配...")
|
||||
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', '')).lower()
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
|
||||
for search_name in search_names:
|
||||
if search_name in liquid_type or search_name in reagent_name:
|
||||
debug_print(f"✅ 通过液体类型匹配找到容器: {node_id} 💧")
|
||||
return node_id
|
||||
|
||||
# 列出可用容器帮助调试
|
||||
debug_print(f"📊 列出可用容器帮助调试...")
|
||||
available_containers = []
|
||||
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', [])
|
||||
liquid_types = [liquid.get('liquid_type', '') or liquid.get('name', '')
|
||||
for liquid in liquids if isinstance(liquid, dict)]
|
||||
|
||||
available_containers.append({
|
||||
'id': node_id,
|
||||
'name': G.nodes[node_id].get('name', ''),
|
||||
'liquids': liquid_types,
|
||||
'reagent_name': vessel_data.get('reagent_name', '')
|
||||
})
|
||||
|
||||
debug_print(f"📋 可用容器列表:")
|
||||
for container in available_containers:
|
||||
debug_print(f" - 🧪 {container['id']}: {container['name']}")
|
||||
debug_print(f" 💧 液体: {container['liquids']}")
|
||||
debug_print(f" 🏷️ 试剂: {container['reagent_name']}")
|
||||
|
||||
debug_print(f"❌ 所有匹配方法都失败了")
|
||||
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...")
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找与容器相连的搅拌器"""
|
||||
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
|
||||
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
|
||||
|
||||
debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}")
|
||||
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
|
||||
return stirrer
|
||||
|
||||
if stirrer_nodes:
|
||||
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
|
||||
return stirrer_nodes[0]
|
||||
|
||||
debug_print(f"❌ 未找到任何搅拌器")
|
||||
return None
|
||||
|
||||
def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float:
|
||||
"""
|
||||
估算需要的试剂体积来调节pH
|
||||
|
||||
Args:
|
||||
target_ph_value: 目标pH值
|
||||
reagent: 试剂名称
|
||||
vessel_volume: 容器体积 (mL)
|
||||
|
||||
Returns:
|
||||
float: 估算的试剂体积 (mL)
|
||||
"""
|
||||
debug_print(f"🧮 计算试剂体积...")
|
||||
debug_print(f" 📍 目标pH: {target_ph_value}")
|
||||
debug_print(f" 🧪 试剂: {reagent}")
|
||||
debug_print(f" 📏 容器体积: {vessel_volume}mL")
|
||||
|
||||
# 简化的pH调节体积估算(实际应用中需要更精确的计算)
|
||||
if "acid" in reagent.lower() or "hcl" in reagent.lower():
|
||||
debug_print(f"🍋 检测到酸性试剂")
|
||||
# 酸性试剂:pH越低需要的体积越大
|
||||
if target_ph_value < 3:
|
||||
volume = vessel_volume * 0.05 # 5%
|
||||
debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积")
|
||||
elif target_ph_value < 5:
|
||||
volume = vessel_volume * 0.02 # 2%
|
||||
debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积")
|
||||
else:
|
||||
volume = vessel_volume * 0.01 # 1%
|
||||
debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积")
|
||||
|
||||
elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower():
|
||||
debug_print(f"🧂 检测到碱性试剂")
|
||||
# 碱性试剂:pH越高需要的体积越大
|
||||
if target_ph_value > 11:
|
||||
volume = vessel_volume * 0.05 # 5%
|
||||
debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积")
|
||||
elif target_ph_value > 9:
|
||||
volume = vessel_volume * 0.02 # 2%
|
||||
debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积")
|
||||
else:
|
||||
volume = vessel_volume * 0.01 # 1%
|
||||
debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积")
|
||||
|
||||
else:
|
||||
# 未知试剂,使用默认值
|
||||
volume = vessel_volume * 0.01
|
||||
debug_print(f"❓ 未知试剂类型,使用默认 1% 体积")
|
||||
|
||||
debug_print(f"📊 计算结果: {volume:.2f}mL")
|
||||
return volume
|
||||
|
||||
def generate_adjust_ph_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel:Union[dict,str], # 🔧 修改:从字符串改为字典类型
|
||||
ph_value: float,
|
||||
reagent: str,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成调节pH的协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
vessel: 目标容器字典(需要调节pH的容器)
|
||||
ph_value: 目标pH值(从XDL传入)
|
||||
reagent: 酸碱试剂名称(从XDL传入)
|
||||
**kwargs: 其他可选参数,使用默认值
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
if not vessel_id:
|
||||
debug_print(f"❌ vessel 参数无效,必须包含id字段或直接提供容器ID. vessel: {vessel}")
|
||||
raise ValueError("vessel 参数无效,必须包含id字段或直接提供容器ID")
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("🧪 开始生成pH调节协议")
|
||||
debug_print(f"📋 原始参数:")
|
||||
debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 📊 ph_value: {ph_value}")
|
||||
debug_print(f" 🧪 reagent: '{reagent}'")
|
||||
debug_print(f" 📦 kwargs: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 从kwargs中获取可选参数,如果没有则使用默认值
|
||||
volume = kwargs.get('volume', 0.0) # 自动估算体积
|
||||
stir = kwargs.get('stir', True) # 默认搅拌
|
||||
stir_speed = kwargs.get('stir_speed', 300.0) # 默认搅拌速度
|
||||
stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间
|
||||
settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间
|
||||
|
||||
debug_print(f"🔧 处理后的参数:")
|
||||
debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)")
|
||||
debug_print(f" 🌪️ stir: {stir}")
|
||||
debug_print(f" 🔄 stir_speed: {stir_speed}rpm")
|
||||
debug_print(f" ⏱️ stir_time: {stir_time}s")
|
||||
debug_print(f" ⏳ settling_time: {settling_time}s")
|
||||
|
||||
# 开始处理
|
||||
action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"目标容器: {vessel_id}", "🥼"))
|
||||
action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️"))
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
debug_print(f"🔍 步骤1: 验证目标容器...")
|
||||
if vessel_id not in G.nodes():
|
||||
debug_print(f"❌ 目标容器 '{vessel_id}' 不存在于系统中")
|
||||
raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中")
|
||||
|
||||
debug_print(f"✅ 目标容器验证通过")
|
||||
action_sequence.append(create_action_log("目标容器验证通过", "✅"))
|
||||
|
||||
# 2. 查找酸碱试剂容器
|
||||
debug_print(f"🔍 步骤2: 查找试剂容器...")
|
||||
action_sequence.append(create_action_log("正在查找试剂容器...", "🔍"))
|
||||
|
||||
try:
|
||||
reagent_vessel = find_acid_base_vessel(G, reagent)
|
||||
debug_print(f"✅ 找到试剂容器: {reagent_vessel}")
|
||||
action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪"))
|
||||
except ValueError as e:
|
||||
debug_print(f"❌ 无法找到试剂容器: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", "❌"))
|
||||
raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
|
||||
|
||||
# 3. 体积估算
|
||||
debug_print(f"🔍 步骤3: 体积处理...")
|
||||
if volume <= 0:
|
||||
action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮"))
|
||||
|
||||
# 获取目标容器的体积信息
|
||||
vessel_data = G.nodes[vessel_id].get('data', {})
|
||||
vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL
|
||||
debug_print(f"📏 容器最大体积: {vessel_volume}mL")
|
||||
|
||||
estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume)
|
||||
volume = estimated_volume
|
||||
debug_print(f"✅ 自动估算试剂体积: {volume:.2f} mL")
|
||||
action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊"))
|
||||
else:
|
||||
debug_print(f"📏 使用指定体积: {volume}mL")
|
||||
action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏"))
|
||||
|
||||
# 4. 验证路径存在
|
||||
debug_print(f"🔍 步骤4: 路径验证...")
|
||||
action_sequence.append(create_action_log("验证转移路径...", "🛤️"))
|
||||
|
||||
try:
|
||||
path = nx.shortest_path(G, source=reagent_vessel, target=vessel_id)
|
||||
debug_print(f"✅ 找到路径: {' → '.join(path)}")
|
||||
action_sequence.append(create_action_log(f"找到转移路径: {' → '.join(path)}", "🛤️"))
|
||||
except nx.NetworkXNoPath:
|
||||
debug_print(f"❌ 无法找到转移路径")
|
||||
action_sequence.append(create_action_log("转移路径不存在", "❌"))
|
||||
raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel_id}' 没有可用路径")
|
||||
|
||||
# 5. 搅拌器设置
|
||||
debug_print(f"🔍 步骤5: 搅拌器设置...")
|
||||
stirrer_id = None
|
||||
if stir:
|
||||
action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
|
||||
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id)
|
||||
|
||||
if stirrer_id:
|
||||
debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌")
|
||||
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄"))
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": {"id": vessel_id},
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"pH调节: 启动搅拌,准备添加 {reagent}"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待搅拌稳定
|
||||
action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
else:
|
||||
debug_print(f"⚠️ 未找到搅拌器,继续执行")
|
||||
action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️"))
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 搅拌器配置出错: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", "❌"))
|
||||
else:
|
||||
debug_print(f"📋 跳过搅拌设置")
|
||||
action_sequence.append(create_action_log("跳过搅拌设置", "⏭️"))
|
||||
|
||||
# 6. 试剂添加
|
||||
debug_print(f"🔍 步骤6: 试剂添加...")
|
||||
action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰"))
|
||||
|
||||
# 计算添加时间(pH调节需要缓慢添加)
|
||||
addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒
|
||||
debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)")
|
||||
action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️"))
|
||||
|
||||
try:
|
||||
action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄"))
|
||||
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=reagent_vessel,
|
||||
to_vessel=vessel_id,
|
||||
volume=volume,
|
||||
amount="",
|
||||
time=addition_time,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # pH调节不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=0.5, # 缓慢注入
|
||||
transfer_flowrate=0.3
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions)
|
||||
debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作")
|
||||
action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
|
||||
|
||||
# 🔧 修复体积运算 - 试剂添加成功后更新容器液体体积
|
||||
debug_print(f"🔧 更新容器液体体积...")
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
debug_print(f"📊 添加前容器体积: {current_volume}")
|
||||
|
||||
# 处理不同的体积数据格式
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
# 增加体积(添加试剂)
|
||||
vessel["data"]["liquid_volume"][0] += volume
|
||||
debug_print(f"📊 添加后容器体积: {vessel['data']['liquid_volume'][0]:.2f}mL (+{volume:.2f}mL)")
|
||||
else:
|
||||
# 如果列表为空,创建新的体积记录
|
||||
vessel["data"]["liquid_volume"] = [volume]
|
||||
debug_print(f"📊 初始化容器体积: {volume:.2f}mL")
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
# 直接数值类型
|
||||
vessel["data"]["liquid_volume"] += volume
|
||||
debug_print(f"📊 添加后容器体积: {vessel['data']['liquid_volume']:.2f}mL (+{volume:.2f}mL)")
|
||||
else:
|
||||
debug_print(f"⚠️ 未知的体积数据格式: {type(current_volume)}")
|
||||
# 创建新的体积记录
|
||||
vessel["data"]["liquid_volume"] = volume
|
||||
else:
|
||||
debug_print(f"📊 容器无液体体积数据,创建新记录: {volume:.2f}mL")
|
||||
# 确保vessel有data字段
|
||||
if "data" not in vessel:
|
||||
vessel["data"] = {}
|
||||
vessel["data"]["liquid_volume"] = volume
|
||||
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
vessel_node_data = G.nodes[vessel_id].get('data', {})
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] += volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = current_node_volume + volume
|
||||
|
||||
debug_print(f"✅ 图节点体积数据已更新")
|
||||
|
||||
action_sequence.append(create_action_log(f"容器体积已更新 (+{volume:.2f}mL)", "📊"))
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 生成泵协议时出错: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌"))
|
||||
raise ValueError(f"生成泵协议时出错: {str(e)}")
|
||||
|
||||
# 7. 混合搅拌
|
||||
if stir and stirrer_id:
|
||||
debug_print(f"🔍 步骤7: 混合搅拌...")
|
||||
action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀"))
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time,
|
||||
"purpose": f"pH调节: 混合试剂,目标pH={ph_value}"
|
||||
}
|
||||
})
|
||||
|
||||
debug_print(f"✅ 混合搅拌设置完成")
|
||||
else:
|
||||
debug_print(f"⏭️ 跳过混合搅拌")
|
||||
action_sequence.append(create_action_log("跳过混合搅拌", "⏭️"))
|
||||
|
||||
# 8. 等待平衡
|
||||
debug_print(f"🔍 步骤8: 反应平衡...")
|
||||
action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️"))
|
||||
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": settling_time,
|
||||
"description": f"等待pH平衡到目标值 {ph_value}"
|
||||
}
|
||||
})
|
||||
|
||||
# 9. 完成总结
|
||||
total_time = addition_time + stir_time + settling_time
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"🎉 pH调节协议生成完成")
|
||||
debug_print(f"📊 协议统计:")
|
||||
debug_print(f" 📋 总动作数: {len(action_sequence)}")
|
||||
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)")
|
||||
debug_print(f" 🧪 试剂: {reagent}")
|
||||
debug_print(f" 📏 体积: {volume:.2f}mL")
|
||||
debug_print(f" 📊 目标pH: {ph_value}")
|
||||
debug_print(f" 🥼 目标容器: {vessel_id}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
# 添加完成日志
|
||||
summary_msg = f"pH调节协议完成: {vessel_id} → pH {ph_value} (使用 {volume:.2f}mL {reagent})"
|
||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||
|
||||
return action_sequence
|
||||
|
||||
def generate_adjust_ph_protocol_stepwise(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
ph_value: float,
|
||||
reagent: str,
|
||||
max_volume: float = 10.0,
|
||||
steps: int = 3
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
分步调节pH的协议(更安全,避免过度调节)
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 目标容器字典
|
||||
ph_value: 目标pH值
|
||||
reagent: 酸碱试剂
|
||||
max_volume: 最大试剂体积
|
||||
steps: 分步数量
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id = vessel["id"]
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"🔄 开始分步pH调节")
|
||||
debug_print(f"📋 分步参数:")
|
||||
debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 📊 ph_value: {ph_value}")
|
||||
debug_print(f" 🧪 reagent: {reagent}")
|
||||
debug_print(f" 📏 max_volume: {max_volume}mL")
|
||||
debug_print(f" 🔢 steps: {steps}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 每步添加的体积
|
||||
step_volume = max_volume / steps
|
||||
debug_print(f"📊 每步体积: {step_volume:.2f}mL")
|
||||
|
||||
action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄"))
|
||||
action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏"))
|
||||
|
||||
for i in range(steps):
|
||||
debug_print(f"🔄 执行第 {i+1}/{steps} 步,添加 {step_volume:.2f}mL")
|
||||
action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步开始", "🚀"))
|
||||
|
||||
# 生成单步协议
|
||||
step_actions = generate_adjust_ph_protocol(
|
||||
G=G,
|
||||
vessel=vessel, # 🔧 直接传递vessel字典
|
||||
ph_value=ph_value,
|
||||
reagent=reagent,
|
||||
volume=step_volume,
|
||||
stir=True,
|
||||
stir_speed=300.0,
|
||||
stir_time=30.0,
|
||||
settling_time=20.0
|
||||
)
|
||||
|
||||
action_sequence.extend(step_actions)
|
||||
debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作")
|
||||
action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步完成", "✅"))
|
||||
|
||||
# 步骤间等待
|
||||
if i < steps - 1:
|
||||
debug_print(f"⏳ 步骤间等待30s")
|
||||
action_sequence.append(create_action_log("步骤间等待...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 30,
|
||||
"description": f"pH调节第{i+1}步完成,等待下一步"
|
||||
}
|
||||
})
|
||||
|
||||
debug_print(f"🎉 分步pH调节完成,共 {len(action_sequence)} 个动作")
|
||||
action_sequence.append(create_action_log("分步pH调节全部完成", "🎉"))
|
||||
|
||||
return action_sequence
|
||||
|
||||
# 便捷函数:常用pH调节
|
||||
def generate_acidify_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
target_ph: float = 2.0,
|
||||
acid: str = "hydrochloric acid"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""酸化协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"🍋 生成酸化协议: {vessel_id} → pH {target_ph} (使用 {acid})")
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, target_ph, acid
|
||||
)
|
||||
|
||||
def generate_basify_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
target_ph: float = 12.0,
|
||||
base: str = "sodium hydroxide"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""碱化协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"🧂 生成碱化协议: {vessel_id} → pH {target_ph} (使用 {base})")
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, target_ph, base
|
||||
)
|
||||
|
||||
def generate_neutralize_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
reagent: str = "sodium hydroxide"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""中和协议(pH=7)"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"⚖️ 生成中和协议: {vessel_id} → pH 7.0 (使用 {reagent})")
|
||||
return generate_adjust_ph_protocol(
|
||||
G, vessel, 7.0, reagent
|
||||
)
|
||||
|
||||
# 测试函数
|
||||
def test_adjust_ph_protocol():
|
||||
"""测试pH调节协议"""
|
||||
debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===")
|
||||
|
||||
# 测试体积计算
|
||||
debug_print("🧮 测试体积计算...")
|
||||
test_cases = [
|
||||
(2.0, "hydrochloric acid", 100.0),
|
||||
(4.0, "hydrochloric acid", 100.0),
|
||||
(12.0, "sodium hydroxide", 100.0),
|
||||
(10.0, "sodium hydroxide", 100.0),
|
||||
(7.0, "unknown reagent", 100.0)
|
||||
]
|
||||
|
||||
for ph, reagent, volume in test_cases:
|
||||
result = calculate_reagent_volume(ph, reagent, volume)
|
||||
debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL")
|
||||
|
||||
debug_print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_adjust_ph_protocol()
|
||||
@@ -156,7 +156,7 @@ def generate_centrifuge_protocol(
|
||||
"device_id": centrifuge_id,
|
||||
"action_name": "centrifuge",
|
||||
"action_kwargs": {
|
||||
"vessel": centrifuge_vessel,
|
||||
"vessel": {"id": centrifuge_vessel},
|
||||
"speed": speed,
|
||||
"time": time,
|
||||
"temp": temp
|
||||
|
||||
@@ -1,101 +1,9 @@
|
||||
from typing import List, Dict, Any
|
||||
import networkx as nx
|
||||
from .utils.vessel_parser import get_vessel, find_solvent_vessel
|
||||
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:
|
||||
"""
|
||||
增强版溶剂容器查找,支持各种匹配方式的别名函数
|
||||
@@ -145,7 +53,7 @@ def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
|
||||
|
||||
def generate_clean_vessel_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
solvent: str,
|
||||
volume: float,
|
||||
temp: float,
|
||||
@@ -165,7 +73,7 @@ def generate_clean_vessel_protocol(
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
vessel: 要清洗的容器名称
|
||||
vessel: 要清洗的容器字典(包含id字段)
|
||||
solvent: 用于清洗的溶剂名称
|
||||
volume: 每次清洗使用的溶剂体积
|
||||
temp: 清洗时的温度
|
||||
@@ -178,20 +86,23 @@ def generate_clean_vessel_protocol(
|
||||
ValueError: 当找不到必要的容器或设备时抛出异常
|
||||
|
||||
Examples:
|
||||
clean_protocol = generate_clean_vessel_protocol(G, "main_reactor", "water", 100.0, 60.0, 2)
|
||||
clean_protocol = generate_clean_vessel_protocol(G, {"id": "main_reactor"}, "water", 100.0, 60.0, 2)
|
||||
"""
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
print(f"CLEAN_VESSEL: 开始生成容器清洗协议")
|
||||
print(f" - 目标容器: {vessel}")
|
||||
print(f" - 目标容器: {vessel} (ID: {vessel_id})")
|
||||
print(f" - 清洗溶剂: {solvent}")
|
||||
print(f" - 清洗体积: {volume} mL")
|
||||
print(f" - 清洗温度: {temp}°C")
|
||||
print(f" - 重复次数: {repeats}")
|
||||
|
||||
# 验证目标容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
|
||||
if vessel_id not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中")
|
||||
|
||||
# 查找溶剂容器
|
||||
try:
|
||||
@@ -208,12 +119,23 @@ def generate_clean_vessel_protocol(
|
||||
raise ValueError(f"无法找到废液容器: {str(e)}")
|
||||
|
||||
# 查找加热设备(可选)
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id
|
||||
if heatchill_id:
|
||||
print(f"CLEAN_VESSEL: 找到加热设备: {heatchill_id}")
|
||||
else:
|
||||
print(f"CLEAN_VESSEL: 未找到加热设备,将在室温下清洗")
|
||||
|
||||
# 🔧 新增:记录清洗前的容器状态
|
||||
print(f"CLEAN_VESSEL: 记录清洗前容器状态...")
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
print(f"CLEAN_VESSEL: 清洗前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# 第一步:如果需要加热且有加热设备,启动加热
|
||||
if temp > 25.0 and heatchill_id:
|
||||
print(f"CLEAN_VESSEL: 启动加热至 {temp}°C")
|
||||
@@ -221,7 +143,7 @@ def generate_clean_vessel_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": temp,
|
||||
"purpose": f"cleaning with {solvent}"
|
||||
}
|
||||
@@ -240,18 +162,61 @@ def generate_clean_vessel_protocol(
|
||||
print(f"CLEAN_VESSEL: 执行第 {repeat + 1} 次清洗")
|
||||
|
||||
# 2a. 使用 pump_protocol 将溶剂转移到目标容器
|
||||
print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel}")
|
||||
print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel_id}")
|
||||
try:
|
||||
# 调用成熟的 pump_protocol 算法
|
||||
add_solvent_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=vessel,
|
||||
to_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
volume=volume,
|
||||
flowrate=2.5, # 适中的流速,避免飞溅
|
||||
transfer_flowrate=2.5
|
||||
)
|
||||
action_sequence.extend(add_solvent_actions)
|
||||
|
||||
# 🔧 新增:更新容器体积(添加清洗溶剂)
|
||||
print(f"CLEAN_VESSEL: 更新容器体积 - 添加清洗溶剂 {volume:.2f}mL")
|
||||
if "data" not in vessel:
|
||||
vessel["data"] = {}
|
||||
|
||||
if "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] += volume
|
||||
print(f"CLEAN_VESSEL: 添加溶剂后体积: {vessel['data']['liquid_volume'][0]:.2f}mL (+{volume:.2f}mL)")
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [volume]
|
||||
print(f"CLEAN_VESSEL: 初始化清洗体积: {volume:.2f}mL")
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
vessel["data"]["liquid_volume"] += volume
|
||||
print(f"CLEAN_VESSEL: 添加溶剂后体积: {vessel['data']['liquid_volume']:.2f}mL (+{volume:.2f}mL)")
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = volume
|
||||
print(f"CLEAN_VESSEL: 重置体积为: {volume:.2f}mL")
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = volume
|
||||
print(f"CLEAN_VESSEL: 创建新体积记录: {volume:.2f}mL")
|
||||
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] += volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = current_node_volume + volume
|
||||
|
||||
print(f"CLEAN_VESSEL: 图节点体积数据已更新")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"无法将溶剂转移到容器: {str(e)}")
|
||||
|
||||
@@ -265,18 +230,52 @@ def generate_clean_vessel_protocol(
|
||||
action_sequence.append(wait_action)
|
||||
|
||||
# 2c. 使用 pump_protocol 将清洗液转移到废液容器
|
||||
print(f"CLEAN_VESSEL: 将清洗液从 {vessel} 转移到废液容器")
|
||||
print(f"CLEAN_VESSEL: 将清洗液从 {vessel_id} 转移到废液容器")
|
||||
try:
|
||||
# 调用成熟的 pump_protocol 算法
|
||||
remove_waste_actions = generate_pump_protocol(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
from_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
to_vessel=waste_vessel,
|
||||
volume=volume,
|
||||
flowrate=2.5, # 适中的流速
|
||||
transfer_flowrate=2.5
|
||||
)
|
||||
action_sequence.extend(remove_waste_actions)
|
||||
|
||||
# 🔧 新增:更新容器体积(移除清洗液)
|
||||
print(f"CLEAN_VESSEL: 更新容器体积 - 移除清洗液 {volume:.2f}mL")
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = max(0.0, vessel["data"]["liquid_volume"][0] - volume)
|
||||
print(f"CLEAN_VESSEL: 移除清洗液后体积: {vessel['data']['liquid_volume'][0]:.2f}mL (-{volume:.2f}mL)")
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [0.0]
|
||||
print(f"CLEAN_VESSEL: 重置体积为0mL")
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
vessel["data"]["liquid_volume"] = max(0.0, current_volume - volume)
|
||||
print(f"CLEAN_VESSEL: 移除清洗液后体积: {vessel['data']['liquid_volume']:.2f}mL (-{volume:.2f}mL)")
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = 0.0
|
||||
print(f"CLEAN_VESSEL: 重置体积为0mL")
|
||||
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
vessel_node_data = G.nodes[vessel_id].get('data', {})
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = max(0.0, current_node_volume[0] - volume)
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [0.0]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = max(0.0, current_node_volume - volume)
|
||||
|
||||
print(f"CLEAN_VESSEL: 图节点体积数据已更新")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"无法将清洗液转移到废液容器: {str(e)}")
|
||||
|
||||
@@ -296,13 +295,24 @@ def generate_clean_vessel_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
}
|
||||
}
|
||||
action_sequence.append(heatchill_stop_action)
|
||||
|
||||
print(f"CLEAN_VESSEL: 生成了 {len(action_sequence)} 个动作")
|
||||
print(f"CLEAN_VESSEL: 清洗协议生成完成")
|
||||
# 🔧 新增:清洗完成后的状态报告
|
||||
final_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
final_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
final_liquid_volume = current_volume
|
||||
|
||||
print(f"CLEAN_VESSEL: 清洗完成")
|
||||
print(f" - 清洗前体积: {original_liquid_volume:.2f}mL")
|
||||
print(f" - 清洗后体积: {final_liquid_volume:.2f}mL")
|
||||
print(f" - 生成了 {len(action_sequence)} 个动作")
|
||||
|
||||
return action_sequence
|
||||
|
||||
@@ -310,7 +320,7 @@ def generate_clean_vessel_protocol(
|
||||
# 便捷函数:常用清洗方案
|
||||
def generate_quick_clean_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
solvent: str = "water",
|
||||
volume: float = 100.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
@@ -320,7 +330,7 @@ def generate_quick_clean_protocol(
|
||||
|
||||
def generate_thorough_clean_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
solvent: str = "water",
|
||||
volume: float = 150.0,
|
||||
temp: float = 60.0
|
||||
@@ -331,7 +341,7 @@ def generate_thorough_clean_protocol(
|
||||
|
||||
def generate_organic_clean_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
volume: float = 100.0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""有机清洗:先用有机溶剂,再用水清洗"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
335
unilabos/compile/dry_protocol.py
Normal file
335
unilabos/compile/dry_protocol.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||
|
||||
|
||||
def find_connected_heater(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""
|
||||
查找与容器相连的加热器
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 容器名称
|
||||
|
||||
Returns:
|
||||
str: 加热器ID,如果没有则返回None
|
||||
"""
|
||||
print(f"DRY: 正在查找与容器 '{vessel}' 相连的加热器...")
|
||||
|
||||
# 查找所有加热器节点
|
||||
heater_nodes = [node for node in G.nodes()
|
||||
if ('heater' in node.lower() or
|
||||
'heat' in node.lower() or
|
||||
G.nodes[node].get('class') == 'virtual_heatchill' or
|
||||
G.nodes[node].get('type') == 'heater')]
|
||||
|
||||
print(f"DRY: 找到的加热器节点: {heater_nodes}")
|
||||
|
||||
# 检查是否有加热器与目标容器相连
|
||||
for heater in heater_nodes:
|
||||
if G.has_edge(heater, vessel) or G.has_edge(vessel, heater):
|
||||
print(f"DRY: 找到与容器 '{vessel}' 相连的加热器: {heater}")
|
||||
return heater
|
||||
|
||||
# 如果没有直接连接,查找距离最近的加热器
|
||||
for heater in heater_nodes:
|
||||
try:
|
||||
path = nx.shortest_path(G, source=heater, target=vessel)
|
||||
if len(path) <= 3: # 最多2个中间节点
|
||||
print(f"DRY: 找到距离较近的加热器: {heater}, 路径: {' → '.join(path)}")
|
||||
return heater
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
|
||||
print(f"DRY: 未找到与容器 '{vessel}' 相连的加热器")
|
||||
return None
|
||||
|
||||
|
||||
def generate_dry_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
compound: str = "", # 🔧 修改:参数顺序调整,并设置默认值
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成干燥协议序列
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
vessel: 目标容器字典(从XDL传入)
|
||||
compound: 化合物名称(从XDL传入,可选)
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 默认参数
|
||||
dry_temp = 60.0 # 默认干燥温度 60°C
|
||||
dry_time = 3600.0 # 默认干燥时间 1小时(3600秒)
|
||||
simulation_time = 60.0 # 模拟时间 1分钟
|
||||
|
||||
print(f"🌡️ DRY: 开始生成干燥协议 ✨")
|
||||
print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
print(f" 🧪 化合物: {compound or '未指定'}")
|
||||
print(f" 🔥 干燥温度: {dry_temp}°C")
|
||||
print(f" ⏰ 干燥时间: {dry_time/60:.0f} 分钟")
|
||||
|
||||
# 🔧 新增:记录干燥前的容器状态
|
||||
print(f"🔍 记录干燥前容器状态...")
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
print(f"📊 干燥前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
print(f"\n📋 步骤1: 验证目标容器 '{vessel_id}' 是否存在...")
|
||||
if vessel_id not in G.nodes():
|
||||
print(f"⚠️ DRY: 警告 - 容器 '{vessel_id}' 不存在于系统中,跳过干燥 😢")
|
||||
return action_sequence
|
||||
print(f"✅ 容器 '{vessel_id}' 验证通过!")
|
||||
|
||||
# 2. 查找相连的加热器
|
||||
print(f"\n🔍 步骤2: 查找与容器相连的加热器...")
|
||||
heater_id = find_connected_heater(G, vessel_id) # 🔧 使用 vessel_id
|
||||
|
||||
if heater_id is None:
|
||||
print(f"😭 DRY: 警告 - 未找到与容器 '{vessel_id}' 相连的加热器,跳过干燥")
|
||||
print(f"🎭 添加模拟干燥动作...")
|
||||
# 添加一个等待动作,表示干燥过程(模拟)
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0, # 模拟等待时间
|
||||
"description": f"模拟干燥 {compound or '化合物'} (无加热器可用)"
|
||||
}
|
||||
})
|
||||
|
||||
# 🔧 新增:模拟干燥的体积变化(溶剂蒸发)
|
||||
print(f"🔧 模拟干燥过程的体积减少...")
|
||||
if original_liquid_volume > 0:
|
||||
# 假设干燥过程中损失10%的体积(溶剂蒸发)
|
||||
volume_loss = original_liquid_volume * 0.1
|
||||
new_volume = max(0.0, original_liquid_volume - volume_loss)
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [new_volume]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = new_volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = new_volume
|
||||
|
||||
print(f"📊 模拟干燥体积变化: {original_liquid_volume:.2f}mL → {new_volume:.2f}mL (-{volume_loss:.2f}mL)")
|
||||
|
||||
print(f"📄 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯")
|
||||
return action_sequence
|
||||
|
||||
print(f"🎉 找到加热器: {heater_id}!")
|
||||
|
||||
# 3. 启动加热器进行干燥
|
||||
print(f"\n🚀 步骤3: 开始执行干燥流程...")
|
||||
print(f"🔥 启动加热器 {heater_id} 进行干燥")
|
||||
|
||||
# 3.1 启动加热
|
||||
print(f" ⚡ 动作1: 启动加热到 {dry_temp}°C...")
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": dry_temp,
|
||||
"purpose": f"干燥 {compound or '化合物'}"
|
||||
}
|
||||
})
|
||||
print(f" ✅ 加热器启动命令已添加 🔥")
|
||||
|
||||
# 3.2 等待温度稳定
|
||||
print(f" ⏳ 动作2: 等待温度稳定...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0,
|
||||
"description": f"等待温度稳定到 {dry_temp}°C"
|
||||
}
|
||||
})
|
||||
print(f" ✅ 温度稳定等待命令已添加 🌡️")
|
||||
|
||||
# 3.3 保持干燥温度
|
||||
print(f" 🔄 动作3: 保持干燥温度 {simulation_time/60:.0f} 分钟...")
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": dry_temp,
|
||||
"time": simulation_time,
|
||||
"purpose": f"干燥 {compound or '化合物'},保持温度 {dry_temp}°C"
|
||||
}
|
||||
})
|
||||
print(f" ✅ 温度保持命令已添加 🌡️⏰")
|
||||
|
||||
# 🔧 新增:干燥过程中的体积变化计算
|
||||
print(f"🔧 计算干燥过程中的体积变化...")
|
||||
if original_liquid_volume > 0:
|
||||
# 干燥过程中,溶剂会蒸发,固体保留
|
||||
# 根据温度和时间估算蒸发量
|
||||
evaporation_rate = 0.001 * dry_temp # 每秒每°C蒸发0.001mL
|
||||
total_evaporation = min(original_liquid_volume * 0.8,
|
||||
evaporation_rate * simulation_time) # 最多蒸发80%
|
||||
|
||||
new_volume = max(0.0, original_liquid_volume - total_evaporation)
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [new_volume]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = new_volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = new_volume
|
||||
|
||||
print(f"📊 干燥体积变化计算:")
|
||||
print(f" - 初始体积: {original_liquid_volume:.2f}mL")
|
||||
print(f" - 蒸发量: {total_evaporation:.2f}mL")
|
||||
print(f" - 剩余体积: {new_volume:.2f}mL")
|
||||
print(f" - 蒸发率: {(total_evaporation/original_liquid_volume*100):.1f}%")
|
||||
|
||||
# 3.4 停止加热
|
||||
print(f" ⏹️ 动作4: 停止加热...")
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"purpose": f"干燥完成,停止加热"
|
||||
}
|
||||
})
|
||||
print(f" ✅ 停止加热命令已添加 🛑")
|
||||
|
||||
# 3.5 等待冷却
|
||||
print(f" ❄️ 动作5: 等待冷却...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 10.0, # 等待10秒冷却
|
||||
"description": f"等待 {compound or '化合物'} 冷却"
|
||||
}
|
||||
})
|
||||
print(f" ✅ 冷却等待命令已添加 🧊")
|
||||
|
||||
# 🔧 新增:干燥完成后的状态报告
|
||||
final_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
final_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
final_liquid_volume = current_volume
|
||||
|
||||
print(f"\n🎊 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯")
|
||||
print(f"⏱️ DRY: 预计总时间: {(simulation_time + 30)/60:.0f} 分钟 ⌛")
|
||||
print(f"📊 干燥结果:")
|
||||
print(f" - 容器: {vessel_id}")
|
||||
print(f" - 化合物: {compound or '未指定'}")
|
||||
print(f" - 干燥前体积: {original_liquid_volume:.2f}mL")
|
||||
print(f" - 干燥后体积: {final_liquid_volume:.2f}mL")
|
||||
print(f" - 蒸发体积: {(original_liquid_volume - final_liquid_volume):.2f}mL")
|
||||
print(f"🏁 所有动作序列准备就绪! ✨")
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 🔧 新增:便捷函数
|
||||
def generate_quick_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "",
|
||||
temp: float = 40.0, time: float = 30.0) -> List[Dict[str, Any]]:
|
||||
"""快速干燥:低温短时间"""
|
||||
vessel_id = vessel["id"]
|
||||
print(f"🌡️ 快速干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)")
|
||||
|
||||
# 临时修改默认参数
|
||||
import types
|
||||
temp_func = types.FunctionType(
|
||||
generate_dry_protocol.__code__,
|
||||
generate_dry_protocol.__globals__
|
||||
)
|
||||
|
||||
# 直接调用原函数,但修改内部参数
|
||||
return generate_dry_protocol(G, vessel, compound)
|
||||
|
||||
|
||||
def generate_thorough_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "",
|
||||
temp: float = 80.0, time: float = 120.0) -> List[Dict[str, Any]]:
|
||||
"""深度干燥:高温长时间"""
|
||||
vessel_id = vessel["id"]
|
||||
print(f"🔥 深度干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)")
|
||||
return generate_dry_protocol(G, vessel, compound)
|
||||
|
||||
|
||||
def generate_gentle_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "",
|
||||
temp: float = 30.0, time: float = 180.0) -> List[Dict[str, Any]]:
|
||||
"""温和干燥:低温长时间"""
|
||||
vessel_id = vessel["id"]
|
||||
print(f"🌡️ 温和干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)")
|
||||
return generate_dry_protocol(G, vessel, compound)
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_dry_protocol():
|
||||
"""测试干燥协议"""
|
||||
print("=== DRY PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dry_protocol()
|
||||
@@ -1,191 +1,488 @@
|
||||
import numpy as np
|
||||
from functools import partial
|
||||
|
||||
import networkx as nx
|
||||
import logging
|
||||
import uuid
|
||||
import sys
|
||||
from typing import List, Dict, Any, Optional
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .utils.logger_util import action_log
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 确保输出编码为UTF-8
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
except:
|
||||
pass
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出函数 - 支持中文"""
|
||||
try:
|
||||
# 确保消息是字符串格式
|
||||
safe_message = str(message)
|
||||
logger.info(f"[抽真空充气] {safe_message}")
|
||||
except UnicodeEncodeError:
|
||||
# 如果编码失败,尝试替换不支持的字符
|
||||
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
||||
logger.info(f"[抽真空充气] {safe_message}")
|
||||
except Exception as e:
|
||||
# 最后的安全措施
|
||||
fallback_message = f"日志输出错误: {repr(message)}"
|
||||
logger.info(f"[抽真空充气] {fallback_message}")
|
||||
|
||||
create_action_log = partial(action_log, prefix="[抽真空充气]")
|
||||
|
||||
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
|
||||
"""根据气体名称查找对应的气源"""
|
||||
# 按照命名规则查找气源
|
||||
"""
|
||||
根据气体名称查找对应的气源,支持多种匹配模式:
|
||||
1. 容器名称匹配
|
||||
2. 气体类型匹配(data.gas_type)
|
||||
3. 默认气源
|
||||
"""
|
||||
debug_print(f"🔍 正在查找气体 '{gas}' 的气源...")
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
debug_print(f"📋 方法1: 容器名称匹配...")
|
||||
gas_source_patterns = [
|
||||
f"gas_source_{gas}",
|
||||
f"gas_{gas}",
|
||||
f"flask_{gas}",
|
||||
f"{gas}_source"
|
||||
f"{gas}_source",
|
||||
f"source_{gas}",
|
||||
f"reagent_bottle_{gas}",
|
||||
f"bottle_{gas}"
|
||||
]
|
||||
|
||||
debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}")
|
||||
|
||||
for pattern in gas_source_patterns:
|
||||
if pattern in G.nodes():
|
||||
debug_print(f"✅ 通过名称找到气源: {pattern}")
|
||||
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
|
||||
# 第二步:通过气体类型匹配 (data.gas_type)
|
||||
debug_print(f"📋 方法2: 气体类型匹配...")
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
# 检查是否是气源设备
|
||||
if ('gas_source' in node_class or
|
||||
'gas' in node_id.lower() or
|
||||
node_id.startswith('flask_')):
|
||||
|
||||
# 检查 data.gas_type
|
||||
data = node_data.get('data', {})
|
||||
gas_type = data.get('gas_type', '')
|
||||
|
||||
if gas_type.lower() == gas.lower():
|
||||
debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})")
|
||||
return node_id
|
||||
|
||||
# 检查 config.gas_type
|
||||
config = node_data.get('config', {})
|
||||
config_gas_type = config.get('gas_type', '')
|
||||
|
||||
if config_gas_type.lower() == gas.lower():
|
||||
debug_print(f"✅ 通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})")
|
||||
return node_id
|
||||
|
||||
# 查找所有可用的气源
|
||||
available_gas_sources = [
|
||||
# 第三步:查找所有可用的气源设备
|
||||
debug_print(f"📋 方法3: 查找可用气源...")
|
||||
available_gas_sources = []
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('gas_source' in node_class or
|
||||
'gas' in node_id.lower() or
|
||||
(node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))):
|
||||
|
||||
data = node_data.get('data', {})
|
||||
gas_type = data.get('gas_type', '未知')
|
||||
available_gas_sources.append(f"{node_id} (气体类型: {gas_type})")
|
||||
|
||||
debug_print(f"📊 可用气源: {available_gas_sources}")
|
||||
|
||||
# 第四步:如果找不到特定气体,使用默认的第一个气源
|
||||
debug_print(f"📋 方法4: 查找默认气源...")
|
||||
default_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'])))
|
||||
if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1
|
||||
or 'gas_source' in node)
|
||||
]
|
||||
|
||||
raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
|
||||
|
||||
if default_gas_sources:
|
||||
default_source = default_gas_sources[0]
|
||||
debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
|
||||
return default_source
|
||||
|
||||
debug_print(f"❌ 所有方法都失败了!")
|
||||
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 ''))
|
||||
]
|
||||
debug_print("🔍 正在查找真空泵...")
|
||||
|
||||
vacuum_pumps = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('virtual_vacuum_pump' in node_class or
|
||||
'vacuum_pump' in node.lower() or
|
||||
'vacuum' in node_class.lower()):
|
||||
vacuum_pumps.append(node)
|
||||
debug_print(f"📋 发现真空泵: {node}")
|
||||
|
||||
if not vacuum_pumps:
|
||||
raise ValueError("系统中未找到真空泵设备")
|
||||
debug_print(f"❌ 系统中未找到真空泵")
|
||||
raise ValueError("系统中未找到真空泵")
|
||||
|
||||
debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}")
|
||||
return vacuum_pumps[0]
|
||||
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
|
||||
"""查找与指定容器相连的搅拌器"""
|
||||
stirrer_nodes = [node for node in G.nodes()
|
||||
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
|
||||
debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...")
|
||||
|
||||
stirrer_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'virtual_stirrer' in node_class or 'stirrer' in node.lower():
|
||||
stirrer_nodes.append(node)
|
||||
debug_print(f"📋 发现搅拌器: {node}")
|
||||
|
||||
debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}")
|
||||
|
||||
# 检查哪个搅拌器与目标容器相连
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"✅ 找到连接的搅拌器: {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
|
||||
# 如果没有连接的搅拌器,返回第一个可用的
|
||||
if stirrer_nodes:
|
||||
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
|
||||
return stirrer_nodes[0]
|
||||
|
||||
debug_print("❌ 未找到搅拌器")
|
||||
return None
|
||||
|
||||
def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
|
||||
"""查找真空泵相关的电磁阀"""
|
||||
debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...")
|
||||
|
||||
# 查找所有电磁阀
|
||||
solenoid_valves = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
|
||||
solenoid_valves.append(node)
|
||||
debug_print(f"📋 发现电磁阀: {node}")
|
||||
|
||||
debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
|
||||
|
||||
# 检查连接关系
|
||||
debug_print(f"📋 方法1: 检查连接关系...")
|
||||
for solenoid in solenoid_valves:
|
||||
if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid):
|
||||
debug_print(f"✅ 找到连接的真空电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找
|
||||
debug_print(f"📋 方法2: 检查命名规则...")
|
||||
for solenoid in solenoid_valves:
|
||||
if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1':
|
||||
debug_print(f"✅ 通过命名找到真空电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
debug_print("⚠️ 未找到真空电磁阀")
|
||||
return None
|
||||
|
||||
def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
|
||||
"""查找气源相关的电磁阀"""
|
||||
debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...")
|
||||
|
||||
# 查找所有电磁阀
|
||||
solenoid_valves = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
|
||||
solenoid_valves.append(node)
|
||||
|
||||
debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
|
||||
|
||||
# 检查连接关系
|
||||
debug_print(f"📋 方法1: 检查连接关系...")
|
||||
for solenoid in solenoid_valves:
|
||||
if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source):
|
||||
debug_print(f"✅ 找到连接的气源电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
# 通过命名规则查找
|
||||
debug_print(f"📋 方法2: 检查命名规则...")
|
||||
for solenoid in solenoid_valves:
|
||||
if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2':
|
||||
debug_print(f"✅ 通过命名找到气源电磁阀: {solenoid}")
|
||||
return solenoid
|
||||
|
||||
debug_print("⚠️ 未找到气源电磁阀")
|
||||
return None
|
||||
|
||||
def generate_evacuateandrefill_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
gas: str,
|
||||
repeats: int = 1
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成抽真空和充气操作的动作序列
|
||||
生成抽真空和充气操作的动作序列 - 中文版
|
||||
|
||||
**修复版本**: 正确调用 pump_protocol 并处理异常
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 目标容器字典(必需)
|
||||
gas: 气体名称(必需)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
# 硬编码重复次数为 3
|
||||
repeats = 3
|
||||
|
||||
# 生成协议ID
|
||||
protocol_id = str(uuid.uuid4())
|
||||
debug_print(f"🆔 生成协议ID: {protocol_id}")
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("🧪 开始生成抽真空充气协议")
|
||||
debug_print(f"📋 原始参数:")
|
||||
debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 💨 气体: '{gas}'")
|
||||
debug_print(f" 🔄 循环次数: {repeats} (硬编码)")
|
||||
debug_print(f" 📦 其他参数: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 参数设置 - 关键修复:减小体积避免超出泵容量
|
||||
VACUUM_VOLUME = 20.0 # 减小抽真空体积
|
||||
REFILL_VOLUME = 20.0 # 减小充气体积
|
||||
PUMP_FLOW_RATE = 2.5 # 降低流速
|
||||
STIR_SPEED = 300.0
|
||||
# === 参数验证和修正 ===
|
||||
debug_print("🔍 步骤1: 参数验证和修正...")
|
||||
action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel_id}", "🎬"))
|
||||
action_sequence.append(create_action_log(f"目标气体: {gas}", "💨"))
|
||||
action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄"))
|
||||
|
||||
print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}")
|
||||
# 验证必需参数
|
||||
if not vessel_id:
|
||||
debug_print("❌ 容器参数不能为空")
|
||||
raise ValueError("容器参数不能为空")
|
||||
|
||||
# 1. 验证设备存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
|
||||
if not gas:
|
||||
debug_print("❌ 气体参数不能为空")
|
||||
raise ValueError("气体参数不能为空")
|
||||
|
||||
if vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print(f"❌ 容器 '{vessel_id}' 在系统中不存在")
|
||||
raise ValueError(f"容器 '{vessel_id}' 在系统中不存在")
|
||||
|
||||
debug_print("✅ 基本参数验证通过")
|
||||
action_sequence.append(create_action_log("参数验证通过", "✅"))
|
||||
|
||||
# 标准化气体名称
|
||||
debug_print("🔧 标准化气体名称...")
|
||||
gas_aliases = {
|
||||
'n2': 'nitrogen',
|
||||
'ar': 'argon',
|
||||
'air': 'air',
|
||||
'o2': 'oxygen',
|
||||
'co2': 'carbon_dioxide',
|
||||
'h2': 'hydrogen',
|
||||
'氮气': 'nitrogen',
|
||||
'氩气': 'argon',
|
||||
'空气': 'air',
|
||||
'氧气': 'oxygen',
|
||||
'二氧化碳': 'carbon_dioxide',
|
||||
'氢气': 'hydrogen'
|
||||
}
|
||||
|
||||
original_gas = gas
|
||||
gas_lower = gas.lower().strip()
|
||||
if gas_lower in gas_aliases:
|
||||
gas = gas_aliases[gas_lower]
|
||||
debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}")
|
||||
action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄"))
|
||||
|
||||
debug_print(f"📋 最终参数: 容器={vessel_id}, 气体={gas}, 重复={repeats}")
|
||||
|
||||
# === 查找设备 ===
|
||||
debug_print("🔍 步骤2: 查找设备...")
|
||||
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
|
||||
|
||||
# 2. 查找设备
|
||||
try:
|
||||
vacuum_pump = find_vacuum_pump(G)
|
||||
vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
|
||||
action_sequence.append(create_action_log(f"找到真空泵: {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)
|
||||
action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨"))
|
||||
|
||||
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_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
|
||||
if vacuum_solenoid:
|
||||
action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪"))
|
||||
else:
|
||||
action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️"))
|
||||
|
||||
except ValueError as e:
|
||||
gas_solenoid = find_gas_solenoid_valve(G, gas_source)
|
||||
if gas_solenoid:
|
||||
action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪"))
|
||||
else:
|
||||
action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️"))
|
||||
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id
|
||||
if stirrer_id:
|
||||
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
|
||||
else:
|
||||
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
|
||||
|
||||
debug_print(f"📊 设备配置:")
|
||||
debug_print(f" 🌪️ 真空泵: {vacuum_pump}")
|
||||
debug_print(f" 💨 气源: {gas_source}")
|
||||
debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}")
|
||||
debug_print(f" 🚪 气源电磁阀: {gas_solenoid}")
|
||||
debug_print(f" 🌪️ 搅拌器: {stirrer_id}")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌"))
|
||||
raise ValueError(f"设备查找失败: {str(e)}")
|
||||
|
||||
# 3. **关键修复**: 验证路径存在性
|
||||
# === 参数设置 ===
|
||||
debug_print("🔍 步骤3: 参数设置...")
|
||||
action_sequence.append(create_action_log("设置操作参数...", "⚙️"))
|
||||
|
||||
# 根据气体类型调整参数
|
||||
if gas.lower() in ['nitrogen', 'argon']:
|
||||
VACUUM_VOLUME = 25.0
|
||||
REFILL_VOLUME = 25.0
|
||||
PUMP_FLOW_RATE = 2.0
|
||||
VACUUM_TIME = 30.0
|
||||
REFILL_TIME = 20.0
|
||||
debug_print("💨 惰性气体: 使用标准参数")
|
||||
action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨"))
|
||||
elif gas.lower() in ['air', 'oxygen']:
|
||||
VACUUM_VOLUME = 20.0
|
||||
REFILL_VOLUME = 20.0
|
||||
PUMP_FLOW_RATE = 1.5
|
||||
VACUUM_TIME = 45.0
|
||||
REFILL_TIME = 25.0
|
||||
debug_print("🔥 活性气体: 使用保守参数")
|
||||
action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥"))
|
||||
else:
|
||||
VACUUM_VOLUME = 15.0
|
||||
REFILL_VOLUME = 15.0
|
||||
PUMP_FLOW_RATE = 1.0
|
||||
VACUUM_TIME = 60.0
|
||||
REFILL_TIME = 30.0
|
||||
debug_print("❓ 未知气体: 使用安全参数")
|
||||
action_sequence.append(create_action_log("未知气体类型,使用安全参数", "❓"))
|
||||
|
||||
STIR_SPEED = 200.0
|
||||
|
||||
debug_print(f"⚙️ 操作参数:")
|
||||
debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL")
|
||||
debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL")
|
||||
debug_print(f" ⚡ 泵流速: {PUMP_FLOW_RATE}mL/s")
|
||||
debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s")
|
||||
debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s")
|
||||
debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM")
|
||||
|
||||
action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏"))
|
||||
action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏"))
|
||||
action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", "⚡"))
|
||||
|
||||
# === 路径验证 ===
|
||||
debug_print("🔍 步骤4: 路径验证...")
|
||||
action_sequence.append(create_action_log("验证传输路径...", "🛤️"))
|
||||
|
||||
try:
|
||||
# 验证抽真空路径
|
||||
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
|
||||
print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}")
|
||||
if nx.has_path(G, vessel_id, vacuum_pump): # 🔧 使用 vessel_id
|
||||
vacuum_path = nx.shortest_path(G, source=vessel_id, target=vacuum_pump)
|
||||
debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}")
|
||||
action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️"))
|
||||
else:
|
||||
debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题")
|
||||
action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️"))
|
||||
|
||||
# 验证充气路径
|
||||
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
|
||||
print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}")
|
||||
if nx.has_path(G, gas_source, vessel_id): # 🔧 使用 vessel_id
|
||||
gas_path = nx.shortest_path(G, source=gas_source, target=vessel_id)
|
||||
debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}")
|
||||
action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️"))
|
||||
else:
|
||||
debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题")
|
||||
action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️"))
|
||||
|
||||
# **新增**: 检查路径中的边数据
|
||||
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)}")
|
||||
debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
|
||||
action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️"))
|
||||
|
||||
# === 启动搅拌器 ===
|
||||
debug_print("🔍 步骤5: 启动搅拌器...")
|
||||
|
||||
# 4. 启动搅拌器
|
||||
if stirrer_id:
|
||||
debug_print(f"🌪️ 启动搅拌器: {stirrer_id}")
|
||||
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️"))
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"stir_speed": STIR_SPEED,
|
||||
"purpose": "抽真空充气操作前启动搅拌"
|
||||
"purpose": "抽真空充气前预搅拌"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待搅拌稳定
|
||||
action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
else:
|
||||
debug_print("⚠️ 未找到搅拌器,跳过搅拌器启动")
|
||||
action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️"))
|
||||
|
||||
# === 执行循环 ===
|
||||
debug_print("🔍 步骤6: 执行抽真空-充气循环...")
|
||||
action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄"))
|
||||
|
||||
# 5. 执行多次抽真空-充气循环
|
||||
for cycle in range(repeats):
|
||||
print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===")
|
||||
debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===")
|
||||
action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀"))
|
||||
|
||||
# ============ 抽真空阶段 ============
|
||||
print(f"EVACUATE_REFILL: 抽真空阶段开始")
|
||||
debug_print(f"🌪️ 抽真空阶段开始")
|
||||
action_sequence.append(create_action_log("开始抽真空阶段", "🌪️"))
|
||||
|
||||
# 启动真空泵
|
||||
debug_print(f"🔛 启动真空泵: {vacuum_pump}")
|
||||
action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛"))
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_pump,
|
||||
"action_name": "set_status",
|
||||
@@ -194,27 +491,28 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
# 开启真空电磁阀
|
||||
if vacuum_solenoid:
|
||||
debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}")
|
||||
action_sequence.append(create_action_log(f"打开真空电磁阀: {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")
|
||||
# 抽真空操作
|
||||
debug_print(f"🌪️ 抽真空操作: {vessel_id} -> {vacuum_pump}")
|
||||
action_sequence.append(create_action_log(f"开始抽真空: {vessel_id} -> {vacuum_pump}", "🌪️"))
|
||||
|
||||
try:
|
||||
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=vessel,
|
||||
from_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
to_vessel=vacuum_pump,
|
||||
volume=VACUUM_VOLUME,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # **修复**: 明确不使用清洗
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
@@ -224,52 +522,36 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
if vacuum_transfer_actions:
|
||||
action_sequence.extend(vacuum_transfer_actions)
|
||||
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作")
|
||||
debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作")
|
||||
action_sequence.append(create_action_log(f"抽真空协议完成 ({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: 使用备选手动泵动作")
|
||||
debug_print("⚠️ 抽真空协议返回空序列,添加手动动作")
|
||||
action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print(f"❌ 抽真空失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", "❌"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
# 抽真空后等待
|
||||
wait_minutes = VACUUM_TIME / 60
|
||||
action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": VACUUM_TIME}
|
||||
})
|
||||
|
||||
# 关闭真空电磁阀
|
||||
if vacuum_solenoid:
|
||||
debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}")
|
||||
action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪"))
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
@@ -277,43 +559,58 @@ def generate_evacuateandrefill_protocol(
|
||||
})
|
||||
|
||||
# 关闭真空泵
|
||||
debug_print(f"🔴 停止真空泵: {vacuum_pump}")
|
||||
action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴"))
|
||||
action_sequence.append({
|
||||
"device_id": vacuum_pump,
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {"string": "OFF"}
|
||||
})
|
||||
|
||||
# 阶段间等待
|
||||
action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
|
||||
# ============ 充气阶段 ============
|
||||
print(f"EVACUATE_REFILL: 充气阶段开始")
|
||||
debug_print(f"💨 充气阶段开始")
|
||||
action_sequence.append(create_action_log("开始气体充气阶段", "💨"))
|
||||
|
||||
# 启动气源
|
||||
debug_print(f"🔛 启动气源: {gas_source}")
|
||||
action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛"))
|
||||
action_sequence.append({
|
||||
"device_id": gas_source,
|
||||
"action_name": "set_status",
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {"string": "ON"}
|
||||
})
|
||||
|
||||
# 开启气源电磁阀
|
||||
if gas_solenoid:
|
||||
debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}")
|
||||
action_sequence.append(create_action_log(f"打开气源电磁阀: {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}")
|
||||
# 充气操作
|
||||
debug_print(f"💨 充气操作: {gas_source} -> {vessel_id}")
|
||||
action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel_id}", "💨"))
|
||||
|
||||
try:
|
||||
gas_transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=gas_source,
|
||||
to_vessel=vessel,
|
||||
to_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
volume=REFILL_VOLUME,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # **修复**: 明确不使用清洗
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
@@ -323,77 +620,36 @@ def generate_evacuateandrefill_protocol(
|
||||
|
||||
if gas_transfer_actions:
|
||||
action_sequence.extend(gas_transfer_actions)
|
||||
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作")
|
||||
debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作")
|
||||
action_sequence.append(create_action_log(f"气体充气协议完成 ({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
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print("⚠️ 充气协议返回空序列,添加手动动作")
|
||||
action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
])
|
||||
debug_print(f"❌ 气体充气失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", "❌"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
# 充气后等待
|
||||
refill_wait_minutes = REFILL_TIME / 60
|
||||
action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": REFILL_TIME}
|
||||
})
|
||||
|
||||
# 关闭气源电磁阀
|
||||
if gas_solenoid:
|
||||
debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}")
|
||||
action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪"))
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
@@ -401,37 +657,96 @@ def generate_evacuateandrefill_protocol(
|
||||
})
|
||||
|
||||
# 关闭气源
|
||||
debug_print(f"🔴 停止气源: {gas_source}")
|
||||
action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴"))
|
||||
action_sequence.append({
|
||||
"device_id": gas_source,
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {"string": "OFF"}
|
||||
})
|
||||
|
||||
# 等待下一次循环
|
||||
# 循环间等待
|
||||
if cycle < repeats - 1:
|
||||
debug_print(f"⏳ 等待下一个循环...")
|
||||
action_sequence.append(create_action_log("等待下一个循环...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 2.0}
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅"))
|
||||
|
||||
# === 停止搅拌器 ===
|
||||
debug_print("🔍 步骤7: 停止搅拌器...")
|
||||
|
||||
# 停止搅拌器
|
||||
if stirrer_id:
|
||||
debug_print(f"🛑 停止搅拌器: {stirrer_id}")
|
||||
action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑"))
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {"vessel": vessel}
|
||||
"action_kwargs": {"vessel": {"id": vessel_id},} # 🔧 使用 vessel_id
|
||||
})
|
||||
else:
|
||||
action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️"))
|
||||
|
||||
# === 最终等待 ===
|
||||
action_sequence.append(create_action_log("最终稳定等待...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# === 总结 ===
|
||||
total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"🎉 抽真空充气协议生成完成")
|
||||
debug_print(f"📊 协议统计:")
|
||||
debug_print(f" 📋 总动作数: {len(action_sequence)}")
|
||||
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
|
||||
debug_print(f" 🥼 处理容器: {vessel_id}")
|
||||
debug_print(f" 💨 使用气体: {gas}")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
# 添加完成日志
|
||||
summary_msg = f"抽真空充气协议完成: {vessel_id} (使用 {gas},{repeats} 次循环)"
|
||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||
|
||||
print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作")
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型
|
||||
"""生成氮气置换协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"💨 生成氮气置换协议: {vessel_id}")
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs)
|
||||
|
||||
def generate_argon_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型
|
||||
"""生成氩气置换协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"💨 生成氩气置换协议: {vessel_id}")
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs)
|
||||
|
||||
def generate_air_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型
|
||||
"""生成空气置换协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"💨 生成空气置换协议: {vessel_id}")
|
||||
return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs)
|
||||
|
||||
def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: dict, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型
|
||||
"""生成惰性气氛协议"""
|
||||
vessel_id = vessel["id"]
|
||||
debug_print(f"🛡️ 生成惰性气氛协议: {vessel_id} (使用 {gas})")
|
||||
return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs)
|
||||
|
||||
# 测试函数
|
||||
def test_evacuateandrefill_protocol():
|
||||
"""测试抽真空充气协议"""
|
||||
print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
|
||||
print("测试完成")
|
||||
|
||||
debug_print("=== 抽真空充气协议增强中文版测试 ===")
|
||||
debug_print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_evacuateandrefill_protocol()
|
||||
@@ -1,326 +1,410 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
import networkx as nx
|
||||
from .pump_protocol import generate_pump_protocol
|
||||
import logging
|
||||
import re
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .utils.unit_parser import parse_time_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[EVAPORATE] {message}")
|
||||
|
||||
|
||||
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
|
||||
"""
|
||||
获取容器中的液体体积
|
||||
在组态图中查找旋转蒸发仪设备
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 指定的设备名称(可选)
|
||||
|
||||
Returns:
|
||||
str: 找到的旋转蒸发仪设备ID,如果没找到返回None
|
||||
"""
|
||||
if vessel not in G.nodes():
|
||||
return 0.0
|
||||
debug_print("🔍 开始查找旋转蒸发仪设备... 🌪️")
|
||||
|
||||
vessel_data = G.nodes[vessel].get('data', {})
|
||||
liquids = vessel_data.get('liquid', [])
|
||||
# 如果指定了vessel,先检查是否存在且是旋转蒸发仪
|
||||
if vessel:
|
||||
debug_print(f"🎯 检查指定设备: {vessel} 🔧")
|
||||
if vessel in G.nodes():
|
||||
node_data = G.nodes[vessel]
|
||||
node_class = node_data.get('class', '')
|
||||
node_type = node_data.get('type', '')
|
||||
|
||||
debug_print(f"📋 设备信息 {vessel}: class={node_class}, type={node_type}")
|
||||
|
||||
# 检查是否为旋转蒸发仪
|
||||
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
debug_print(f"🎉 找到指定的旋转蒸发仪: {vessel} ✨")
|
||||
return vessel
|
||||
elif node_type == 'device':
|
||||
debug_print(f"✅ 指定设备存在,尝试直接使用: {vessel} 🔧")
|
||||
return vessel
|
||||
else:
|
||||
debug_print(f"❌ 指定的设备 {vessel} 不存在 😞")
|
||||
|
||||
total_volume = 0.0
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
|
||||
total_volume += liquid['liquid_volume']
|
||||
# 在所有设备中查找旋转蒸发仪
|
||||
debug_print("🔎 在所有设备中搜索旋转蒸发仪... 🕵️♀️")
|
||||
rotavap_candidates = []
|
||||
|
||||
return total_volume
|
||||
for node_id, node_data in G.nodes(data=True):
|
||||
node_class = node_data.get('class', '')
|
||||
node_type = node_data.get('type', '')
|
||||
|
||||
# 跳过非设备节点
|
||||
if node_type != 'device':
|
||||
continue
|
||||
|
||||
# 检查设备类型
|
||||
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
rotavap_candidates.append(node_id)
|
||||
debug_print(f"🌟 找到旋转蒸发仪候选: {node_id} (class: {node_class}) 🌪️")
|
||||
elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
|
||||
rotavap_candidates.append(node_id)
|
||||
debug_print(f"🌟 找到旋转蒸发仪候选 (按名称): {node_id} 🌪️")
|
||||
|
||||
if rotavap_candidates:
|
||||
selected = rotavap_candidates[0] # 选择第一个找到的
|
||||
debug_print(f"🎯 选择旋转蒸发仪: {selected} 🏆")
|
||||
return selected
|
||||
|
||||
debug_print("😭 未找到旋转蒸发仪设备 💔")
|
||||
return None
|
||||
|
||||
|
||||
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']
|
||||
def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
|
||||
"""
|
||||
查找与旋转蒸发仪连接的容器
|
||||
|
||||
if rotavap_nodes:
|
||||
return rotavap_nodes[0]
|
||||
Args:
|
||||
G: 设备图
|
||||
rotavap_device: 旋转蒸发仪设备ID
|
||||
|
||||
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"
|
||||
]
|
||||
Returns:
|
||||
str: 连接的容器ID,如果没找到返回None
|
||||
"""
|
||||
debug_print(f"🔗 查找与 {rotavap_device} 连接的容器... 🥽")
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
# 查看旋转蒸发仪的子设备
|
||||
rotavap_data = G.nodes[rotavap_device]
|
||||
children = rotavap_data.get('children', [])
|
||||
|
||||
# 如果找不到专门的回收容器,使用废液容器
|
||||
waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"]
|
||||
for vessel_name in waste_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
debug_print(f"👶 检查子设备: {children}")
|
||||
for child_id in children:
|
||||
if child_id in G.nodes():
|
||||
child_data = G.nodes[child_id]
|
||||
child_type = child_data.get('type', '')
|
||||
|
||||
if child_type == 'container':
|
||||
debug_print(f"🎉 找到连接的容器: {child_id} 🥽✨")
|
||||
return child_id
|
||||
|
||||
raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}")
|
||||
|
||||
# 查看邻接的容器
|
||||
debug_print("🤝 检查邻接设备...")
|
||||
for neighbor in G.neighbors(rotavap_device):
|
||||
neighbor_data = G.nodes[neighbor]
|
||||
neighbor_type = neighbor_data.get('type', '')
|
||||
|
||||
if neighbor_type == 'container':
|
||||
debug_print(f"🎉 找到邻接的容器: {neighbor} 🥽✨")
|
||||
return neighbor
|
||||
|
||||
debug_print("😞 未找到连接的容器 💔")
|
||||
return None
|
||||
|
||||
def generate_evaporate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
pressure: float = 0.1,
|
||||
temp: float = 60.0,
|
||||
time: float = 1800.0,
|
||||
stir_speed: float = 100.0
|
||||
time: Union[str, float] = "180", # 🔧 修改:支持字符串时间
|
||||
stir_speed: float = 100.0,
|
||||
solvent: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成蒸发操作的协议序列
|
||||
|
||||
蒸发流程:
|
||||
1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪
|
||||
2. 蒸发操作:执行旋转蒸发
|
||||
3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器
|
||||
4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器
|
||||
生成蒸发操作的协议序列 - 支持单位和体积运算
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
vessel: 包含待蒸发溶液的容器名称
|
||||
pressure: 蒸发时的真空度 (bar),默认0.1 bar
|
||||
temp: 蒸发时的加热温度 (°C),默认60°C
|
||||
time: 蒸发时间 (秒),默认1800秒(30分钟)
|
||||
stir_speed: 旋转速度 (RPM),默认100 RPM
|
||||
G: 设备图
|
||||
vessel: 容器字典(从XDL传入)
|
||||
pressure: 真空度 (bar),默认0.1
|
||||
temp: 加热温度 (°C),默认60
|
||||
time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等)
|
||||
stir_speed: 旋转速度 (RPM),默认100
|
||||
solvent: 溶剂名称(用于参数优化)
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 蒸发操作的动作序列
|
||||
|
||||
Raises:
|
||||
ValueError: 当找不到必要的设备时抛出异常
|
||||
|
||||
Examples:
|
||||
evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0)
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
debug_print("🌟" * 20)
|
||||
debug_print("🌪️ 开始生成蒸发协议(支持单位和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 💨 pressure: {pressure} bar")
|
||||
debug_print(f" 🌡️ temp: {temp}°C")
|
||||
debug_print(f" ⏰ time: {time} (类型: {type(time)})")
|
||||
debug_print(f" 🌪️ stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" 🧪 solvent: '{solvent}'")
|
||||
debug_print("🌟" * 20)
|
||||
|
||||
# 🔧 新增:记录蒸发前的容器状态
|
||||
debug_print("🔍 记录蒸发前容器状态...")
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
debug_print(f"📊 蒸发前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# === 步骤1: 查找旋转蒸发仪设备 ===
|
||||
debug_print("📍 步骤1: 查找旋转蒸发仪设备... 🔍")
|
||||
|
||||
# 验证vessel参数
|
||||
if not vessel_id:
|
||||
debug_print("❌ vessel 参数不能为空! 😱")
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
# 查找旋转蒸发仪设备
|
||||
rotavap_device = find_rotavap_device(G, vessel_id)
|
||||
if not rotavap_device:
|
||||
debug_print("💥 未找到旋转蒸发仪设备! 😭")
|
||||
raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备")
|
||||
|
||||
debug_print(f"🎉 成功找到旋转蒸发仪: {rotavap_device} ✨")
|
||||
|
||||
# === 步骤2: 确定目标容器 ===
|
||||
debug_print("📍 步骤2: 确定目标容器... 🥽")
|
||||
|
||||
target_vessel = vessel_id
|
||||
|
||||
# 如果vessel就是旋转蒸发仪设备,查找连接的容器
|
||||
if vessel_id == rotavap_device:
|
||||
debug_print("🔄 vessel就是旋转蒸发仪,查找连接的容器...")
|
||||
connected_vessel = find_connected_vessel(G, rotavap_device)
|
||||
if connected_vessel:
|
||||
target_vessel = connected_vessel
|
||||
debug_print(f"✅ 使用连接的容器: {target_vessel} 🥽✨")
|
||||
else:
|
||||
debug_print(f"⚠️ 未找到连接的容器,使用设备本身: {rotavap_device} 🔧")
|
||||
target_vessel = rotavap_device
|
||||
elif vessel_id in G.nodes() and G.nodes[vessel_id].get('type') == 'container':
|
||||
debug_print(f"✅ 使用指定的容器: {vessel_id} 🥽✨")
|
||||
target_vessel = vessel_id
|
||||
else:
|
||||
debug_print(f"⚠️ 容器 '{vessel_id}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧")
|
||||
target_vessel = rotavap_device
|
||||
|
||||
# === 🔧 新增:步骤3:单位解析处理 ===
|
||||
debug_print("📍 步骤3: 单位解析处理... ⚡")
|
||||
|
||||
# 解析时间
|
||||
final_time = parse_time_input(time)
|
||||
debug_print(f"🎯 时间解析完成: {time} → {final_time}s ({final_time/60:.1f}分钟) ⏰✨")
|
||||
|
||||
# === 步骤4: 参数验证和修正 ===
|
||||
debug_print("📍 步骤4: 参数验证和修正... 🔧")
|
||||
|
||||
# 修正参数范围
|
||||
if pressure <= 0 or pressure > 1.0:
|
||||
debug_print(f"⚠️ 真空度 {pressure} bar 超出范围,修正为 0.1 bar 💨")
|
||||
pressure = 0.1
|
||||
else:
|
||||
debug_print(f"✅ 真空度 {pressure} bar 在正常范围内 💨")
|
||||
|
||||
if temp < 10.0 or temp > 200.0:
|
||||
debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 60°C 🌡️")
|
||||
temp = 60.0
|
||||
else:
|
||||
debug_print(f"✅ 温度 {temp}°C 在正常范围内 🌡️")
|
||||
|
||||
if final_time <= 0:
|
||||
debug_print(f"⚠️ 时间 {final_time}s 无效,修正为 180s (3分钟) ⏰")
|
||||
final_time = 180.0
|
||||
else:
|
||||
debug_print(f"✅ 时间 {final_time}s ({final_time/60:.1f}分钟) 有效 ⏰")
|
||||
|
||||
if stir_speed < 10.0 or stir_speed > 300.0:
|
||||
debug_print(f"⚠️ 旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM 🌪️")
|
||||
stir_speed = 100.0
|
||||
else:
|
||||
debug_print(f"✅ 旋转速度 {stir_speed} RPM 在正常范围内 🌪️")
|
||||
|
||||
# 根据溶剂优化参数
|
||||
if solvent:
|
||||
debug_print(f"🧪 根据溶剂 '{solvent}' 优化参数... 🔬")
|
||||
solvent_lower = solvent.lower()
|
||||
|
||||
if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
|
||||
temp = max(temp, 80.0)
|
||||
pressure = max(pressure, 0.2)
|
||||
debug_print("💧 水系溶剂:提高温度和真空度 🌡️💨")
|
||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
||||
temp = min(temp, 50.0)
|
||||
pressure = min(pressure, 0.05)
|
||||
debug_print("🍺 易挥发溶剂:降低温度和真空度 🌡️💨")
|
||||
elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
|
||||
temp = max(temp, 100.0)
|
||||
pressure = min(pressure, 0.01)
|
||||
debug_print("🔥 高沸点溶剂:提高温度,降低真空度 🌡️💨")
|
||||
else:
|
||||
debug_print("🧪 通用溶剂,使用标准参数 ✨")
|
||||
else:
|
||||
debug_print("🤷♀️ 未指定溶剂,使用默认参数 ✨")
|
||||
|
||||
debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️")
|
||||
|
||||
# === 🔧 新增:步骤5:蒸发体积计算 ===
|
||||
debug_print("📍 步骤5: 蒸发体积计算... 📊")
|
||||
|
||||
# 根据温度、真空度、时间和溶剂类型估算蒸发量
|
||||
evaporation_volume = 0.0
|
||||
if original_liquid_volume > 0:
|
||||
# 基础蒸发速率(mL/min)
|
||||
base_evap_rate = 0.5 # 基础速率
|
||||
|
||||
# 温度系数(高温蒸发更快)
|
||||
temp_factor = 1.0 + (temp - 25.0) / 100.0
|
||||
|
||||
# 真空系数(真空度越高蒸发越快)
|
||||
vacuum_factor = 1.0 + (1.0 - pressure) * 2.0
|
||||
|
||||
# 溶剂系数
|
||||
solvent_factor = 1.0
|
||||
if solvent:
|
||||
solvent_lower = solvent.lower()
|
||||
if any(s in solvent_lower for s in ['water', 'h2o']):
|
||||
solvent_factor = 0.8 # 水蒸发较慢
|
||||
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
|
||||
solvent_factor = 1.5 # 易挥发溶剂蒸发快
|
||||
elif any(s in solvent_lower for s in ['dmso', 'dmi']):
|
||||
solvent_factor = 0.3 # 高沸点溶剂蒸发慢
|
||||
|
||||
# 计算总蒸发量
|
||||
total_evap_rate = base_evap_rate * temp_factor * vacuum_factor * solvent_factor
|
||||
evaporation_volume = min(
|
||||
original_liquid_volume * 0.95, # 最多蒸发95%
|
||||
total_evap_rate * (final_time / 60.0) # 时间相关的蒸发量
|
||||
)
|
||||
|
||||
debug_print(f"📊 蒸发量计算:")
|
||||
debug_print(f" - 基础蒸发速率: {base_evap_rate} mL/min")
|
||||
debug_print(f" - 温度系数: {temp_factor:.2f} (基于 {temp}°C)")
|
||||
debug_print(f" - 真空系数: {vacuum_factor:.2f} (基于 {pressure} bar)")
|
||||
debug_print(f" - 溶剂系数: {solvent_factor:.2f} ({solvent or '通用'})")
|
||||
debug_print(f" - 总蒸发速率: {total_evap_rate:.2f} mL/min")
|
||||
debug_print(f" - 预计蒸发量: {evaporation_volume:.2f}mL ({evaporation_volume/original_liquid_volume*100:.1f}%)")
|
||||
|
||||
# === 步骤6: 生成动作序列 ===
|
||||
debug_print("📍 步骤6: 生成动作序列... 🎬")
|
||||
|
||||
action_sequence = []
|
||||
|
||||
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")
|
||||
|
||||
# 验证源容器存在
|
||||
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 = {
|
||||
# 1. 等待稳定
|
||||
debug_print(" 🔄 动作1: 添加初始等待稳定... ⏳")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10}
|
||||
}
|
||||
action_sequence.append(wait_action)
|
||||
})
|
||||
debug_print(" ✅ 初始等待动作已添加 ⏳✨")
|
||||
|
||||
# 2. 执行蒸发
|
||||
debug_print(f" 🌪️ 动作2: 执行蒸发操作...")
|
||||
debug_print(f" 🔧 设备: {rotavap_device}")
|
||||
debug_print(f" 🥽 容器: {target_vessel}")
|
||||
debug_print(f" 💨 真空度: {pressure} bar")
|
||||
debug_print(f" 🌡️ 温度: {temp}°C")
|
||||
debug_print(f" ⏰ 时间: {final_time}s ({final_time/60:.1f}分钟)")
|
||||
debug_print(f" 🌪️ 旋转速度: {stir_speed} RPM")
|
||||
|
||||
# === 第二步:执行旋转蒸发 ===
|
||||
print(f"EVAPORATE: 执行旋转蒸发操作")
|
||||
evaporate_action = {
|
||||
"device_id": rotavap_id,
|
||||
"device_id": rotavap_device,
|
||||
"action_name": "evaporate",
|
||||
"action_kwargs": {
|
||||
"vessel": rotavap_vessel,
|
||||
"pressure": pressure,
|
||||
"temp": temp,
|
||||
"time": time,
|
||||
"stir_speed": stir_speed
|
||||
"vessel": {"id": target_vessel},
|
||||
"pressure": float(pressure),
|
||||
"temp": float(temp),
|
||||
"time": float(final_time), # 🔧 强制转换为float类型
|
||||
"stir_speed": float(stir_speed),
|
||||
"solvent": str(solvent)
|
||||
}
|
||||
}
|
||||
action_sequence.append(evaporate_action)
|
||||
debug_print(" ✅ 蒸发动作已添加 🌪️✨")
|
||||
|
||||
# 蒸发后等待系统稳定
|
||||
wait_action = {
|
||||
"action_name": "wait",
|
||||
"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)
|
||||
# 🔧 新增:蒸发过程中的体积变化
|
||||
debug_print(" 🔧 更新容器体积 - 蒸发过程...")
|
||||
if evaporation_volume > 0:
|
||||
new_volume = max(0.0, original_liquid_volume - evaporation_volume)
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [new_volume]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
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")
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
|
||||
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} 清洗")
|
||||
# 🔧 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
# 清洗溶剂加入
|
||||
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)
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
# 将清洗液转移到废液/回收容器
|
||||
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)
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = new_volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = new_volume
|
||||
|
||||
except Exception as e:
|
||||
print(f"EVAPORATE: 清洗步骤失败: {str(e)}")
|
||||
debug_print(f" 📊 蒸发体积变化: {original_liquid_volume:.2f}mL → {new_volume:.2f}mL (-{evaporation_volume:.2f}mL)")
|
||||
|
||||
print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作")
|
||||
print(f"EVAPORATE: 蒸发协议生成完成")
|
||||
print(f"EVAPORATE: 总处理体积: {transfer_volume} mL")
|
||||
# 3. 蒸发后等待
|
||||
debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10}
|
||||
})
|
||||
debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨")
|
||||
|
||||
# 🔧 新增:蒸发完成后的状态报告
|
||||
final_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
final_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
final_liquid_volume = current_volume
|
||||
|
||||
# === 总结 ===
|
||||
debug_print("🎊" * 20)
|
||||
debug_print(f"🎉 蒸发协议生成完成! ✨")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
|
||||
debug_print(f"🌪️ 旋转蒸发仪: {rotavap_device} 🔧")
|
||||
debug_print(f"🥽 目标容器: {target_vessel} 🧪")
|
||||
debug_print(f"⚙️ 蒸发参数: {pressure} bar 💨, {temp}°C 🌡️, {final_time}s ⏰, {stir_speed} RPM 🌪️")
|
||||
debug_print(f"⏱️ 预计总时间: {(final_time + 20)/60:.1f} 分钟 ⌛")
|
||||
debug_print(f"📊 体积变化:")
|
||||
debug_print(f" - 蒸发前: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 蒸发后: {final_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 蒸发量: {evaporation_volume:.2f}mL ({evaporation_volume/max(original_liquid_volume, 0.01)*100:.1f}%)")
|
||||
debug_print("🎊" * 20)
|
||||
|
||||
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 # 适中旋转速度
|
||||
)
|
||||
|
||||
@@ -1,304 +1,366 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Optional
|
||||
import networkx as nx
|
||||
from .pump_protocol import generate_pump_protocol
|
||||
import logging
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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 debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[FILTER] {message}")
|
||||
|
||||
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']
|
||||
debug_print("🔍 查找过滤器设备... 🌊")
|
||||
|
||||
if filter_nodes:
|
||||
return filter_nodes[0]
|
||||
# 查找过滤器设备
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'filter' in node_class.lower() or 'filter' in node.lower():
|
||||
debug_print(f"🎉 找到过滤器设备: {node} ✨")
|
||||
return node
|
||||
|
||||
raise ValueError("系统中未找到过滤器设备")
|
||||
# 如果没找到,寻找可能的过滤器名称
|
||||
debug_print("🔎 在预定义名称中搜索过滤器... 📋")
|
||||
possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"]
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
debug_print(f"🎉 找到过滤器设备: {name} ✨")
|
||||
return name
|
||||
|
||||
debug_print("😭 未找到过滤器设备 💔")
|
||||
raise ValueError("未找到过滤器设备")
|
||||
|
||||
|
||||
def find_filter_vessel(G: nx.DiGraph) -> str:
|
||||
"""查找过滤器专用容器"""
|
||||
possible_names = [
|
||||
"filter_vessel", # 标准过滤器容器
|
||||
"filtration_vessel", # 备选名称
|
||||
"vessel_filter", # 备选名称
|
||||
"filter_unit", # 备选名称
|
||||
"filter" # 简单名称
|
||||
]
|
||||
def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None:
|
||||
"""验证容器是否存在"""
|
||||
debug_print(f"🔍 验证{vessel_type}: '{vessel}' 🧪")
|
||||
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
return vessel_name
|
||||
if not vessel:
|
||||
debug_print(f"❌ {vessel_type}不能为空! 😱")
|
||||
raise ValueError(f"{vessel_type}不能为空")
|
||||
|
||||
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
|
||||
if vessel not in G.nodes():
|
||||
debug_print(f"❌ {vessel_type} '{vessel}' 不存在于系统中! 😞")
|
||||
raise ValueError(f"{vessel_type} '{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} 相连的加热搅拌器")
|
||||
|
||||
debug_print(f"✅ {vessel_type} '{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
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
filtrate_vessel: dict = {"id": "waste"},
|
||||
**kwargs
|
||||
) -> 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表示全部过滤
|
||||
G: 设备图
|
||||
vessel: 过滤容器字典(必需)- 包含需要过滤的混合物
|
||||
filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 过滤操作的动作序列
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
filtrate_vessel_id, filtrate_vessel_data = get_vessel(filtrate_vessel)
|
||||
|
||||
debug_print("🌊" * 20)
|
||||
debug_print("🚀 开始生成过滤协议(支持体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 🧪 filtrate_vessel: {filtrate_vessel}")
|
||||
debug_print(f" ⚙️ 其他参数: {kwargs}")
|
||||
debug_print("🌊" * 20)
|
||||
|
||||
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}")
|
||||
# 🔧 新增:记录过滤前的容器状态
|
||||
debug_print("🔍 记录过滤前容器状态...")
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
debug_print(f"📊 过滤前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# 验证源容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
|
||||
# === 参数验证 ===
|
||||
debug_print("📍 步骤1: 参数验证... 🔧")
|
||||
|
||||
# 获取源容器中的液体体积
|
||||
source_volume = get_vessel_liquid_volume(G, vessel)
|
||||
print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体")
|
||||
# 验证必需参数
|
||||
debug_print(" 🔍 验证必需参数...")
|
||||
validate_vessel(G, vessel_id, "过滤容器") # 🔧 使用 vessel_id
|
||||
debug_print(" ✅ 必需参数验证完成 🎯")
|
||||
|
||||
# 查找过滤器设备
|
||||
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")
|
||||
# 验证可选参数
|
||||
debug_print(" 🔍 验证可选参数...")
|
||||
if filtrate_vessel:
|
||||
validate_vessel(G, filtrate_vessel_id, "滤液容器")
|
||||
debug_print(" 🌊 模式: 过滤并收集滤液 💧")
|
||||
else:
|
||||
transfer_volume = 50.0 # 默认过滤量
|
||||
print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL")
|
||||
debug_print(" 🧱 模式: 过滤并收集固体 🔬")
|
||||
debug_print(" ✅ 可选参数验证完成 🎯")
|
||||
|
||||
# === 第一步:启动加热搅拌器(在转移前预热) ===
|
||||
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}
|
||||
})
|
||||
# === 查找设备 ===
|
||||
debug_print("📍 步骤2: 查找设备... 🔍")
|
||||
|
||||
# === 第二步:将待过滤溶液转移到过滤器 ===
|
||||
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)
|
||||
debug_print(" 🔎 搜索过滤器设备...")
|
||||
filter_device = find_filter_device(G)
|
||||
debug_print(f" 🎉 使用过滤器设备: {filter_device} 🌊✨")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"无法将溶液转移到过滤器: {str(e)}")
|
||||
debug_print(f" ❌ 设备查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"设备查找失败: {str(e)}")
|
||||
|
||||
# 转移后等待
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
# 🔧 新增:过滤效率和体积分配估算
|
||||
debug_print("📍 步骤2.5: 过滤体积分配估算... 📊")
|
||||
|
||||
# === 第三步:执行过滤操作(完全按照 Filter.action 参数) ===
|
||||
print(f"FILTER: 执行过滤操作")
|
||||
# 估算过滤分离比例(基于经验数据)
|
||||
solid_ratio = 0.1 # 假设10%是固体(保留在过滤器上)
|
||||
liquid_ratio = 0.9 # 假设90%是液体(通过过滤器)
|
||||
volume_loss_ratio = 0.05 # 假设5%体积损失(残留在过滤器等)
|
||||
|
||||
# 从kwargs中获取过滤参数进行优化
|
||||
if "solid_content" in kwargs:
|
||||
try:
|
||||
solid_ratio = float(kwargs["solid_content"])
|
||||
liquid_ratio = 1.0 - solid_ratio
|
||||
debug_print(f"📋 使用指定的固体含量: {solid_ratio*100:.1f}%")
|
||||
except:
|
||||
debug_print("⚠️ 固体含量参数无效,使用默认值")
|
||||
|
||||
if original_liquid_volume > 0:
|
||||
expected_filtrate_volume = original_liquid_volume * liquid_ratio * (1.0 - volume_loss_ratio)
|
||||
expected_solid_volume = original_liquid_volume * solid_ratio
|
||||
volume_loss = original_liquid_volume * volume_loss_ratio
|
||||
|
||||
debug_print(f"📊 过滤体积分配估算:")
|
||||
debug_print(f" - 原始体积: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 预计滤液体积: {expected_filtrate_volume:.2f}mL ({liquid_ratio*100:.1f}%)")
|
||||
debug_print(f" - 预计固体体积: {expected_solid_volume:.2f}mL ({solid_ratio*100:.1f}%)")
|
||||
debug_print(f" - 预计损失体积: {volume_loss:.2f}mL ({volume_loss_ratio*100:.1f}%)")
|
||||
|
||||
# === 转移到过滤器(如果需要)===
|
||||
debug_print("📍 步骤3: 转移到过滤器... 🚚")
|
||||
|
||||
if vessel_id != filter_device: # 🔧 使用 vessel_id
|
||||
debug_print(f" 🚛 需要转移: {vessel_id} → {filter_device} 📦")
|
||||
|
||||
try:
|
||||
debug_print(" 🔄 开始执行转移操作...")
|
||||
# 使用pump protocol转移液体到过滤器
|
||||
transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel={"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
to_vessel={"id": filter_device},
|
||||
volume=0.0, # 转移所有液体
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
|
||||
if transfer_actions:
|
||||
action_sequence.extend(transfer_actions)
|
||||
debug_print(f" ✅ 添加了 {len(transfer_actions)} 个转移动作 🚚✨")
|
||||
|
||||
# 🔧 新增:转移后更新容器体积
|
||||
debug_print(" 🔧 更新转移后的容器体积...")
|
||||
|
||||
# 原容器体积变为0(所有液体已转移)
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
vessel["data"]["liquid_volume"] = [0.0] if len(current_volume) > 0 else [0.0]
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = 0.0
|
||||
|
||||
# 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = 0.0
|
||||
|
||||
debug_print(f" 📊 转移完成,{vessel_id} 体积更新为 0.0mL")
|
||||
|
||||
else:
|
||||
debug_print(" ⚠️ 转移协议返回空序列 🤔")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f" ❌ 转移失败: {str(e)} 😞")
|
||||
debug_print(" 🔄 继续执行,可能是直接连接的过滤器 🤞")
|
||||
else:
|
||||
debug_print(" ✅ 过滤容器就是过滤器,无需转移 🎯")
|
||||
|
||||
# === 执行过滤操作 ===
|
||||
debug_print("📍 步骤4: 执行过滤操作... 🌊")
|
||||
|
||||
# 构建过滤动作参数
|
||||
debug_print(" ⚙️ 构建过滤参数...")
|
||||
filter_kwargs = {
|
||||
"vessel": {"id": filter_device}, # 过滤器设备
|
||||
"filtrate_vessel": {"id": filtrate_vessel_id}, # 滤液容器(可能为空)
|
||||
"stir": kwargs.get("stir", False),
|
||||
"stir_speed": kwargs.get("stir_speed", 0.0),
|
||||
"temp": kwargs.get("temp", 25.0),
|
||||
"continue_heatchill": kwargs.get("continue_heatchill", False),
|
||||
"volume": kwargs.get("volume", 0.0) # 0表示过滤所有
|
||||
}
|
||||
|
||||
debug_print(f" 📋 过滤参数: {filter_kwargs}")
|
||||
debug_print(" 🌊 开始过滤操作...")
|
||||
|
||||
# 过滤动作
|
||||
filter_action = {
|
||||
"device_id": filter_id,
|
||||
"device_id": filter_device,
|
||||
"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_kwargs": filter_kwargs
|
||||
}
|
||||
action_sequence.append(filter_action)
|
||||
debug_print(" ✅ 过滤动作已添加 🌊✨")
|
||||
|
||||
# 过滤后等待
|
||||
debug_print(" ⏳ 添加过滤后等待...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10}
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
debug_print(" ✅ 过滤后等待动作已添加 ⏰✨")
|
||||
|
||||
# === 第四步:如果不继续加热搅拌,停止加热器 ===
|
||||
if heatchill_id and not continue_heatchill and (temp != 25.0 or stir):
|
||||
print(f"FILTER: 停止加热搅拌器")
|
||||
# === 收集滤液(如果需要)===
|
||||
debug_print("📍 步骤5: 收集滤液... 💧")
|
||||
|
||||
if filtrate_vessel_id and filtrate_vessel_id not in G.neighbors(filter_device):
|
||||
debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel_id} 💧")
|
||||
|
||||
stop_action = {
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": filter_vessel_id
|
||||
}
|
||||
}
|
||||
action_sequence.append(stop_action)
|
||||
try:
|
||||
debug_print(" 🔄 开始执行收集操作...")
|
||||
# 使用pump protocol收集滤液
|
||||
collect_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=filter_device,
|
||||
to_vessel=filtrate_vessel,
|
||||
volume=0.0, # 收集所有滤液
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0,
|
||||
transfer_flowrate=2.0
|
||||
)
|
||||
|
||||
if collect_actions:
|
||||
action_sequence.extend(collect_actions)
|
||||
debug_print(f" ✅ 添加了 {len(collect_actions)} 个收集动作 🧪✨")
|
||||
|
||||
# 🔧 新增:收集滤液后的体积更新
|
||||
debug_print(" 🔧 更新滤液容器体积...")
|
||||
|
||||
# 更新filtrate_vessel在图中的体积(如果它是节点)
|
||||
if filtrate_vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[filtrate_vessel_id]:
|
||||
G.nodes[filtrate_vessel_id]['data'] = {}
|
||||
|
||||
current_filtrate_volume = G.nodes[filtrate_vessel_id]['data'].get('liquid_volume', 0.0)
|
||||
if isinstance(current_filtrate_volume, list):
|
||||
if len(current_filtrate_volume) > 0:
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'][0] += expected_filtrate_volume
|
||||
else:
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = [expected_filtrate_volume]
|
||||
else:
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
|
||||
|
||||
debug_print(f" 📊 滤液容器 {filtrate_vessel_id} 体积增加 {expected_filtrate_volume:.2f}mL")
|
||||
|
||||
else:
|
||||
debug_print(" ⚠️ 收集协议返回空序列 🤔")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f" ❌ 收集滤液失败: {str(e)} 😞")
|
||||
debug_print(" 🔄 继续执行,可能滤液直接流入指定容器 🤞")
|
||||
else:
|
||||
debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬")
|
||||
|
||||
print(f"FILTER: 生成了 {len(action_sequence)} 个动作")
|
||||
print(f"FILTER: 过滤协议生成完成")
|
||||
# 🔧 新增:过滤完成后的容器状态更新
|
||||
debug_print("📍 步骤5.5: 过滤完成后状态更新... 📊")
|
||||
|
||||
if vessel_id == filter_device:
|
||||
# 如果过滤容器就是过滤器,需要更新其体积状态
|
||||
if original_liquid_volume > 0:
|
||||
if filtrate_vessel:
|
||||
# 收集滤液模式:过滤器中主要保留固体
|
||||
remaining_volume = expected_solid_volume
|
||||
debug_print(f" 🧱 过滤器中保留固体: {remaining_volume:.2f}mL")
|
||||
else:
|
||||
# 保留固体模式:过滤器中保留所有物质
|
||||
remaining_volume = original_liquid_volume * (1.0 - volume_loss_ratio)
|
||||
debug_print(f" 🔬 过滤器中保留所有物质: {remaining_volume:.2f}mL")
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
vessel["data"]["liquid_volume"] = [remaining_volume] if len(current_volume) > 0 else [remaining_volume]
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = remaining_volume
|
||||
|
||||
# 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = remaining_volume
|
||||
|
||||
debug_print(f" 📊 过滤器 {vessel_id} 体积更新为: {remaining_volume:.2f}mL")
|
||||
|
||||
# === 最终等待 ===
|
||||
debug_print("📍 步骤6: 最终等待... ⏰")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5.0}
|
||||
})
|
||||
debug_print(" ✅ 最终等待动作已添加 ⏰✨")
|
||||
|
||||
# 🔧 新增:过滤完成后的状态报告
|
||||
final_vessel_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
final_vessel_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
final_vessel_volume = current_volume
|
||||
|
||||
# === 总结 ===
|
||||
debug_print("🎊" * 20)
|
||||
debug_print(f"🎉 过滤协议生成完成! ✨")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
|
||||
debug_print(f"🥽 过滤容器: {vessel_id} 🧪")
|
||||
debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
|
||||
debug_print(f"💧 滤液容器: {filtrate_vessel_id or '无(保留固体)'} 🧱")
|
||||
debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
|
||||
if original_liquid_volume > 0:
|
||||
debug_print(f"📊 体积变化统计:")
|
||||
debug_print(f" - 过滤前体积: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 过滤后容器体积: {final_vessel_volume:.2f}mL")
|
||||
if filtrate_vessel:
|
||||
debug_print(f" - 预计滤液体积: {expected_filtrate_volume:.2f}mL")
|
||||
debug_print(f" - 预计损失体积: {volume_loss:.2f}mL")
|
||||
debug_print("🎊" * 20)
|
||||
|
||||
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)
|
||||
@@ -1,373 +1,328 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .utils.unit_parser import parse_time_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[HEATCHILL] {message}")
|
||||
|
||||
|
||||
def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
|
||||
"""
|
||||
解析温度输入(统一函数)
|
||||
|
||||
Args:
|
||||
temp_input: 温度输入
|
||||
default_temp: 默认温度
|
||||
|
||||
Returns:
|
||||
float: 温度(°C)
|
||||
"""
|
||||
if not temp_input:
|
||||
return default_temp
|
||||
|
||||
# 🔢 数值输入
|
||||
if isinstance(temp_input, (int, float)):
|
||||
result = float(temp_input)
|
||||
debug_print(f"🌡️ 数值温度: {temp_input} → {result}°C")
|
||||
return result
|
||||
|
||||
# 📝 字符串输入
|
||||
temp_str = str(temp_input).lower().strip()
|
||||
debug_print(f"🔍 解析温度: '{temp_str}'")
|
||||
|
||||
# 🎯 特殊温度
|
||||
special_temps = {
|
||||
"room temperature": 25.0, "reflux": 78.0, "ice bath": 0.0,
|
||||
"boiling": 100.0, "hot": 60.0, "warm": 40.0, "cold": 10.0
|
||||
}
|
||||
|
||||
if temp_str in special_temps:
|
||||
result = special_temps[temp_str]
|
||||
debug_print(f"🎯 特殊温度: '{temp_str}' → {result}°C")
|
||||
return result
|
||||
|
||||
# 📐 正则解析(如 "256 °C")
|
||||
temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
|
||||
match = re.search(temp_pattern, temp_str)
|
||||
|
||||
if match:
|
||||
result = float(match.group(1))
|
||||
debug_print(f"✅ 温度解析: '{temp_str}' → {result}°C")
|
||||
return result
|
||||
|
||||
debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值: {default_temp}°C")
|
||||
return default_temp
|
||||
|
||||
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']
|
||||
"""查找与指定容器相连的加热/冷却设备"""
|
||||
debug_print(f"🔍 查找加热设备,目标容器: {vessel}")
|
||||
|
||||
# 检查哪个加热/冷却设备与目标容器相连(机械连接)
|
||||
for heatchill in heatchill_nodes:
|
||||
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
|
||||
return heatchill
|
||||
# 🔧 查找所有加热设备
|
||||
heatchill_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class:
|
||||
heatchill_nodes.append(node)
|
||||
debug_print(f"🎉 找到加热设备: {node}")
|
||||
|
||||
# 如果没有直接连接,返回第一个可用的加热/冷却设备
|
||||
# 🔗 检查连接
|
||||
if vessel and heatchill_nodes:
|
||||
for heatchill in heatchill_nodes:
|
||||
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
|
||||
debug_print(f"✅ 加热设备 '{heatchill}' 与容器 '{vessel}' 相连")
|
||||
return heatchill
|
||||
|
||||
# 🎯 使用第一个可用设备
|
||||
if heatchill_nodes:
|
||||
return heatchill_nodes[0]
|
||||
selected = heatchill_nodes[0]
|
||||
debug_print(f"🔧 使用第一个加热设备: {selected}")
|
||||
return selected
|
||||
|
||||
raise ValueError("系统中未找到可用的加热/冷却设备")
|
||||
# 🆘 默认设备
|
||||
debug_print("⚠️ 未找到加热设备,使用默认设备")
|
||||
return "heatchill_1"
|
||||
|
||||
def validate_and_fix_params(temp: float, time: float, stir_speed: float) -> tuple:
|
||||
"""验证和修正参数"""
|
||||
# 🌡️ 温度范围验证
|
||||
if temp < -50.0 or temp > 300.0:
|
||||
debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 25°C")
|
||||
temp = 25.0
|
||||
else:
|
||||
debug_print(f"✅ 温度 {temp}°C 在正常范围内")
|
||||
|
||||
# ⏰ 时间验证
|
||||
if time < 0:
|
||||
debug_print(f"⚠️ 时间 {time}s 无效,修正为 300s")
|
||||
time = 300.0
|
||||
else:
|
||||
debug_print(f"✅ 时间 {time}s ({time/60:.1f}分钟) 有效")
|
||||
|
||||
# 🌪️ 搅拌速度验证
|
||||
if stir_speed < 0 or stir_speed > 1500.0:
|
||||
debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
|
||||
stir_speed = 300.0
|
||||
else:
|
||||
debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内")
|
||||
|
||||
return temp, time, stir_speed
|
||||
|
||||
def generate_heat_chill_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
temp: float,
|
||||
time: float,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
temp: float = 25.0,
|
||||
time: Union[str, float] = "300",
|
||||
temp_spec: str = "",
|
||||
time_spec: str = "",
|
||||
pressure: str = "",
|
||||
reflux_solvent: str = "",
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
purpose: str = "加热/冷却操作"
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成加热/冷却操作的协议序列 - 带时间限制的完整操作
|
||||
生成加热/冷却操作的协议序列 - 支持vessel字典
|
||||
|
||||
Args:
|
||||
G: 设备图
|
||||
vessel: 容器字典(从XDL传入)
|
||||
temp: 目标温度 (°C)
|
||||
time: 加热时间(支持字符串如 "30 min")
|
||||
temp_spec: 温度规格说明(优先级高于temp)
|
||||
time_spec: 时间规格说明(优先级高于time)
|
||||
pressure: 压力设置
|
||||
reflux_solvent: 回流溶剂
|
||||
stir: 是否搅拌
|
||||
stir_speed: 搅拌速度 (RPM)
|
||||
purpose: 操作目的说明
|
||||
**kwargs: 其他参数(兼容性)
|
||||
|
||||
Returns:
|
||||
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}")
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
debug_print("🌡️" * 20)
|
||||
debug_print("🚀 开始生成加热冷却协议(支持vessel字典)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 🌡️ temp: {temp}°C")
|
||||
debug_print(f" ⏰ time: {time}")
|
||||
debug_print(f" 🎯 temp_spec: {temp_spec}")
|
||||
debug_print(f" ⏱️ time_spec: {time_spec}")
|
||||
debug_print(f" 🌪️ stir: {stir} ({stir_speed} RPM)")
|
||||
debug_print(f" 🎭 purpose: '{purpose}'")
|
||||
debug_print("🌡️" * 20)
|
||||
|
||||
# 2. 查找加热/冷却设备
|
||||
# 📋 参数验证
|
||||
debug_print("📍 步骤1: 参数验证... 🔧")
|
||||
if not vessel_id: # 🔧 使用 vessel_id
|
||||
debug_print("❌ vessel 参数不能为空! 😱")
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中! 😞")
|
||||
raise ValueError(f"容器 '{vessel_id}' 不存在于系统中")
|
||||
|
||||
debug_print("✅ 基础参数验证通过 🎯")
|
||||
|
||||
# 🔄 参数解析
|
||||
debug_print("📍 步骤2: 参数解析... ⚡")
|
||||
|
||||
#温度解析:优先使用 temp_spec
|
||||
final_temp = parse_temp_input(temp_spec, temp) if temp_spec else temp
|
||||
|
||||
# 时间解析:优先使用 time_spec
|
||||
final_time = parse_time_input(time_spec) if time_spec else parse_time_input(time)
|
||||
|
||||
# 参数修正
|
||||
final_temp, final_time, stir_speed = validate_and_fix_params(final_temp, final_time, stir_speed)
|
||||
|
||||
debug_print(f"🎯 最终参数: temp={final_temp}°C, time={final_time}s, stir_speed={stir_speed} RPM")
|
||||
|
||||
# 🔍 查找设备
|
||||
debug_print("📍 步骤3: 查找加热设备... 🔍")
|
||||
try:
|
||||
heatchill_id = find_connected_heatchill(G, vessel)
|
||||
print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
|
||||
heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id
|
||||
debug_print(f"🎉 使用加热设备: {heatchill_id} ✨")
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"无法找到加热设备: {str(e)}")
|
||||
|
||||
# 3. 执行加热/冷却操作
|
||||
# 🚀 生成动作
|
||||
debug_print("📍 步骤4: 生成加热动作... 🔥")
|
||||
|
||||
# 🕐 模拟运行时间优化
|
||||
debug_print(" ⏱️ 检查模拟运行时间限制...")
|
||||
original_time = final_time
|
||||
simulation_time_limit = 100.0 # 模拟运行时间限制:100秒
|
||||
|
||||
if final_time > simulation_time_limit:
|
||||
final_time = simulation_time_limit
|
||||
debug_print(f" 🎮 模拟运行优化: {original_time}s → {final_time}s (限制为{simulation_time_limit}s) ⚡")
|
||||
debug_print(f" 📊 时间缩短: {original_time/60:.1f}分钟 → {final_time/60:.1f}分钟 🚀")
|
||||
else:
|
||||
debug_print(f" ✅ 时间在限制内: {final_time}s ({final_time/60:.1f}分钟) 保持不变 🎯")
|
||||
|
||||
action_sequence = []
|
||||
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"
|
||||
"vessel": {"id": vessel},
|
||||
"temp": float(final_temp),
|
||||
"time": float(final_time),
|
||||
"stir": bool(stir),
|
||||
"stir_speed": float(stir_speed),
|
||||
"purpose": str(purpose or f"加热到 {final_temp}°C") + (f" (模拟时间: {final_time}s)" if original_time != final_time else "")
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(heatchill_action)
|
||||
debug_print("✅ 加热动作已添加 🔥✨")
|
||||
|
||||
# 显示时间调整信息
|
||||
if original_time != final_time:
|
||||
debug_print(f" 🎭 模拟优化说明: 原计划 {original_time/60:.1f}分钟,实际模拟 {final_time/60:.1f}分钟 ⚡")
|
||||
|
||||
# 🎊 总结
|
||||
debug_print("🎊" * 20)
|
||||
debug_print(f"🎉 加热冷却协议生成完成! ✨")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个")
|
||||
debug_print(f"🥽 加热容器: {vessel_id}")
|
||||
debug_print(f"🌡️ 目标温度: {final_temp}°C")
|
||||
debug_print(f"⏰ 加热时间: {final_time}s ({final_time/60:.1f}分钟)")
|
||||
debug_print("🎊" * 20)
|
||||
|
||||
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
|
||||
vessel: dict, # 🔧 修改参数类型
|
||||
temp: float = 25.0,
|
||||
time: Union[str, float] = 100.0,
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成加热/冷却到指定温度的协议序列 - 智能温控协议
|
||||
"""生成加热到指定温度的协议(简化版)"""
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
debug_print(f"🌡️ 生成加热到温度协议: {vessel_id} → {temp}°C")
|
||||
return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
|
||||
|
||||
def generate_heat_chill_start_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改参数类型
|
||||
temp: float = 25.0,
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""生成开始加热操作的协议序列"""
|
||||
|
||||
**关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件
|
||||
"""
|
||||
action_sequence = []
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
|
||||
# 设置默认值
|
||||
if stir_speed is None:
|
||||
stir_speed = 300.0
|
||||
if purpose is None:
|
||||
purpose = f"智能温控到 {temp}°C"
|
||||
debug_print("🔥 开始生成启动加热协议 ✨")
|
||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {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}")
|
||||
# 基础验证
|
||||
if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print("❌ 容器验证失败!")
|
||||
raise ValueError("vessel 参数无效")
|
||||
|
||||
# 1. 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
# 查找设备
|
||||
heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id
|
||||
|
||||
# 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)}")
|
||||
# 生成动作
|
||||
action_sequence = [{
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"temp": temp,
|
||||
"purpose": purpose or f"开始加热到 {temp}°C",
|
||||
"vessel": {"id": vessel_id},
|
||||
}
|
||||
}]
|
||||
|
||||
# 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)} 个动作")
|
||||
debug_print(f"✅ 启动加热协议生成完成 🎯")
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 扩展版本的加热/冷却协议,集成智能温控功能
|
||||
def generate_smart_heat_chill_protocol(
|
||||
def generate_heat_chill_stop_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 = "智能加热/冷却"
|
||||
vessel: dict, # 🔧 修改参数类型
|
||||
**kwargs
|
||||
) -> 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()
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
|
||||
debug_print("🛑 开始生成停止加热协议 ✨")
|
||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
|
||||
# 基础验证
|
||||
if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print("❌ 容器验证失败!")
|
||||
raise ValueError("vessel 参数无效")
|
||||
|
||||
# 查找设备
|
||||
heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id
|
||||
|
||||
# 生成动作
|
||||
action_sequence = [{
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
}
|
||||
}]
|
||||
|
||||
debug_print(f"✅ 停止加热协议生成完成 🎯")
|
||||
return action_sequence
|
||||
|
||||
458
unilabos/compile/hydrogenate_protocol.py
Normal file
458
unilabos/compile/hydrogenate_protocol.py
Normal file
@@ -0,0 +1,458 @@
|
||||
import networkx as nx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from .utils.vessel_parser import get_vessel
|
||||
|
||||
|
||||
def parse_temperature(temp_str: str) -> float:
|
||||
"""
|
||||
解析温度字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
temp_str: 温度字符串(如 "45 °C", "45°C", "45")
|
||||
|
||||
Returns:
|
||||
float: 温度值(摄氏度)
|
||||
"""
|
||||
try:
|
||||
# 移除常见的温度单位和符号
|
||||
temp_clean = temp_str.replace("°C", "").replace("°", "").replace("C", "").strip()
|
||||
return float(temp_clean)
|
||||
except ValueError:
|
||||
print(f"HYDROGENATE: 无法解析温度 '{temp_str}',使用默认温度 25°C")
|
||||
return 25.0
|
||||
|
||||
|
||||
def parse_time(time_str: str) -> float:
|
||||
"""
|
||||
解析时间字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串(如 "2 h", "120 min", "7200 s")
|
||||
|
||||
Returns:
|
||||
float: 时间值(秒)
|
||||
"""
|
||||
try:
|
||||
time_clean = time_str.lower().strip()
|
||||
|
||||
# 处理小时
|
||||
if "h" in time_clean:
|
||||
hours = float(time_clean.replace("h", "").strip())
|
||||
return hours * 3600.0
|
||||
|
||||
# 处理分钟
|
||||
if "min" in time_clean:
|
||||
minutes = float(time_clean.replace("min", "").strip())
|
||||
return minutes * 60.0
|
||||
|
||||
# 处理秒
|
||||
if "s" in time_clean:
|
||||
seconds = float(time_clean.replace("s", "").strip())
|
||||
return seconds
|
||||
|
||||
# 默认按小时处理
|
||||
return float(time_clean) * 3600.0
|
||||
|
||||
except ValueError:
|
||||
print(f"HYDROGENATE: 无法解析时间 '{time_str}',使用默认时间 2小时")
|
||||
return 7200.0 # 2小时
|
||||
|
||||
|
||||
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 'gas' in device_id.lower():
|
||||
device_type = "gas"
|
||||
elif 'h2' in device_id.lower() or 'hydrogen' 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 find_connected_device(G: nx.DiGraph, vessel: str, device_type: str) -> str:
|
||||
"""
|
||||
查找与容器相连的指定类型设备
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel: 容器名称
|
||||
device_type: 设备类型 ('heater', 'stirrer', 'gas_source')
|
||||
|
||||
Returns:
|
||||
str: 设备ID,如果没有则返回None
|
||||
"""
|
||||
print(f"HYDROGENATE: 正在查找与容器 '{vessel}' 相连的 {device_type}...")
|
||||
|
||||
# 根据设备类型定义搜索关键词
|
||||
if device_type == 'heater':
|
||||
keywords = ['heater', 'heat', 'heatchill']
|
||||
device_class = 'virtual_heatchill'
|
||||
elif device_type == 'stirrer':
|
||||
keywords = ['stirrer', 'stir']
|
||||
device_class = 'virtual_stirrer'
|
||||
elif device_type == 'gas_source':
|
||||
keywords = ['gas', 'h2', 'hydrogen']
|
||||
device_class = 'virtual_gas_source'
|
||||
else:
|
||||
return None
|
||||
|
||||
# 查找设备节点
|
||||
device_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_name = node.lower()
|
||||
node_class = node_data.get('class', '').lower()
|
||||
|
||||
# 通过名称匹配
|
||||
if any(keyword in node_name for keyword in keywords):
|
||||
device_nodes.append(node)
|
||||
# 通过类型匹配
|
||||
elif device_class in node_class:
|
||||
device_nodes.append(node)
|
||||
|
||||
print(f"HYDROGENATE: 找到的{device_type}节点: {device_nodes}")
|
||||
|
||||
# 检查是否有设备与目标容器相连
|
||||
for device in device_nodes:
|
||||
if G.has_edge(device, vessel) or G.has_edge(vessel, device):
|
||||
print(f"HYDROGENATE: 找到与容器 '{vessel}' 相连的{device_type}: {device}")
|
||||
return device
|
||||
|
||||
# 如果没有直接连接,查找距离最近的设备
|
||||
for device in device_nodes:
|
||||
try:
|
||||
path = nx.shortest_path(G, source=device, target=vessel)
|
||||
if len(path) <= 3: # 最多2个中间节点
|
||||
print(f"HYDROGENATE: 找到距离较近的{device_type}: {device}")
|
||||
return device
|
||||
except nx.NetworkXNoPath:
|
||||
continue
|
||||
|
||||
print(f"HYDROGENATE: 未找到与容器 '{vessel}' 相连的{device_type}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_hydrogenate_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
temp: str,
|
||||
time: str,
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成氢化反应协议序列 - 支持vessel字典
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
vessel: 反应容器字典(从XDL传入)
|
||||
temp: 反应温度(如 "45 °C")
|
||||
time: 反应时间(如 "2 h")
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 解析参数
|
||||
temperature = parse_temperature(temp)
|
||||
reaction_time = parse_time(time)
|
||||
|
||||
print("🧪" * 20)
|
||||
print(f"HYDROGENATE: 开始生成氢化反应协议(支持vessel字典)✨")
|
||||
print(f"📝 输入参数:")
|
||||
print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
print(f" 🌡️ 反应温度: {temperature}°C")
|
||||
print(f" ⏰ 反应时间: {reaction_time/3600:.1f} 小时")
|
||||
print("🧪" * 20)
|
||||
|
||||
# 🔧 新增:记录氢化前的容器状态(可选,氢化反应通常不改变体积)
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
print(f"📊 氢化前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
print("📍 步骤1: 验证目标容器...")
|
||||
if vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
print(f"⚠️ HYDROGENATE: 警告 - 容器 '{vessel_id}' 不存在于系统中,跳过氢化反应")
|
||||
return action_sequence
|
||||
print(f"✅ 容器 '{vessel_id}' 验证通过")
|
||||
|
||||
# 2. 查找相连的设备
|
||||
print("📍 步骤2: 查找相连设备...")
|
||||
heater_id = find_connected_device(G, vessel_id, 'heater') # 🔧 使用 vessel_id
|
||||
stirrer_id = find_connected_device(G, vessel_id, 'stirrer') # 🔧 使用 vessel_id
|
||||
gas_source_id = find_connected_device(G, vessel_id, 'gas_source') # 🔧 使用 vessel_id
|
||||
|
||||
print(f"🔧 设备配置:")
|
||||
print(f" 🔥 加热器: {heater_id or '未找到'}")
|
||||
print(f" 🌪️ 搅拌器: {stirrer_id or '未找到'}")
|
||||
print(f" 💨 气源: {gas_source_id or '未找到'}")
|
||||
|
||||
# 3. 启动搅拌器
|
||||
print("📍 步骤3: 启动搅拌器...")
|
||||
if stirrer_id:
|
||||
print(f"🌪️ 启动搅拌器 {stirrer_id}")
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"stir_speed": 300.0,
|
||||
"purpose": "氢化反应: 开始搅拌"
|
||||
}
|
||||
})
|
||||
print("✅ 搅拌器启动动作已添加")
|
||||
else:
|
||||
print(f"⚠️ HYDROGENATE: 警告 - 未找到搅拌器,继续执行")
|
||||
|
||||
# 4. 启动气源(氢气)
|
||||
print("📍 步骤4: 启动氢气源...")
|
||||
if gas_source_id:
|
||||
print(f"💨 启动气源 {gas_source_id} (氢气)")
|
||||
action_sequence.append({
|
||||
"device_id": gas_source_id,
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {
|
||||
"string": "ON"
|
||||
}
|
||||
})
|
||||
|
||||
# 查找相关的电磁阀
|
||||
gas_solenoid = find_associated_solenoid_valve(G, gas_source_id)
|
||||
if gas_solenoid:
|
||||
print(f"🚪 开启气源电磁阀 {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": "OPEN"
|
||||
}
|
||||
})
|
||||
print("✅ 氢气源启动动作已添加")
|
||||
else:
|
||||
print(f"⚠️ HYDROGENATE: 警告 - 未找到气源,继续执行")
|
||||
|
||||
# 5. 等待气体稳定
|
||||
print("📍 步骤5: 等待气体环境稳定...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 30.0,
|
||||
"description": "等待氢气环境稳定"
|
||||
}
|
||||
})
|
||||
print("✅ 气体稳定等待动作已添加")
|
||||
|
||||
# 6. 启动加热器
|
||||
print("📍 步骤6: 启动加热反应...")
|
||||
if heater_id:
|
||||
print(f"🔥 启动加热器 {heater_id} 到 {temperature}°C")
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"temp": temperature,
|
||||
"purpose": f"氢化反应: 加热到 {temperature}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 等待温度稳定
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 20.0,
|
||||
"description": f"等待温度稳定到 {temperature}°C"
|
||||
}
|
||||
})
|
||||
|
||||
# 🕐 模拟运行时间优化
|
||||
print(" ⏰ 检查模拟运行时间限制...")
|
||||
original_reaction_time = reaction_time
|
||||
simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
|
||||
|
||||
if reaction_time > simulation_time_limit:
|
||||
reaction_time = simulation_time_limit
|
||||
print(f" 🎮 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)")
|
||||
print(f" 📊 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟")
|
||||
else:
|
||||
print(f" ✅ 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变")
|
||||
|
||||
# 保持反应温度
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"temp": temperature,
|
||||
"time": reaction_time,
|
||||
"purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "")
|
||||
}
|
||||
})
|
||||
|
||||
# 显示时间调整信息
|
||||
if original_reaction_time != reaction_time:
|
||||
print(f" 🎭 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟")
|
||||
|
||||
print("✅ 加热反应动作已添加")
|
||||
|
||||
else:
|
||||
print(f"⚠️ HYDROGENATE: 警告 - 未找到加热器,使用室温反应")
|
||||
|
||||
# 🕐 室温反应也需要时间优化
|
||||
print(" ⏰ 检查室温反应模拟时间限制...")
|
||||
original_reaction_time = reaction_time
|
||||
simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
|
||||
|
||||
if reaction_time > simulation_time_limit:
|
||||
reaction_time = simulation_time_limit
|
||||
print(f" 🎮 室温反应时间优化: {original_reaction_time}s → {reaction_time}s")
|
||||
print(f" 📊 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟")
|
||||
else:
|
||||
print(f" ✅ 室温反应时间在限制内: {reaction_time}s 保持不变")
|
||||
|
||||
# 室温反应,只等待时间
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": reaction_time,
|
||||
"description": f"室温氢化反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "")
|
||||
}
|
||||
})
|
||||
|
||||
# 显示时间调整信息
|
||||
if original_reaction_time != reaction_time:
|
||||
print(f" 🎭 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟")
|
||||
|
||||
print("✅ 室温反应等待动作已添加")
|
||||
|
||||
# 7. 停止加热
|
||||
print("📍 步骤7: 停止加热...")
|
||||
if heater_id:
|
||||
action_sequence.append({
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"purpose": "氢化反应完成,停止加热"
|
||||
}
|
||||
})
|
||||
print("✅ 停止加热动作已添加")
|
||||
|
||||
# 8. 等待冷却
|
||||
print("📍 步骤8: 等待冷却...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 300.0,
|
||||
"description": "等待反应混合物冷却"
|
||||
}
|
||||
})
|
||||
print("✅ 冷却等待动作已添加")
|
||||
|
||||
# 9. 停止气源
|
||||
print("📍 步骤9: 停止氢气源...")
|
||||
if gas_source_id:
|
||||
# 先关闭电磁阀
|
||||
gas_solenoid = find_associated_solenoid_valve(G, gas_source_id)
|
||||
if gas_solenoid:
|
||||
print(f"🚪 关闭气源电磁阀 {gas_solenoid}")
|
||||
action_sequence.append({
|
||||
"device_id": gas_solenoid,
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {
|
||||
"command": "CLOSED"
|
||||
}
|
||||
})
|
||||
|
||||
# 再关闭气源
|
||||
action_sequence.append({
|
||||
"device_id": gas_source_id,
|
||||
"action_name": "set_status",
|
||||
"action_kwargs": {
|
||||
"string": "OFF"
|
||||
}
|
||||
})
|
||||
print("✅ 氢气源停止动作已添加")
|
||||
|
||||
# 10. 停止搅拌
|
||||
print("📍 步骤10: 停止搅拌...")
|
||||
if stirrer_id:
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"purpose": "氢化反应完成,停止搅拌"
|
||||
}
|
||||
})
|
||||
print("✅ 停止搅拌动作已添加")
|
||||
|
||||
# 🔧 新增:氢化完成后的状态(氢化反应通常不改变体积)
|
||||
final_liquid_volume = original_liquid_volume # 氢化反应体积基本不变
|
||||
|
||||
# 总结
|
||||
print("🎊" * 20)
|
||||
print(f"🎉 氢化反应协议生成完成! ✨")
|
||||
print(f"📊 总动作数: {len(action_sequence)} 个")
|
||||
print(f"🥽 反应容器: {vessel_id}")
|
||||
print(f"🌡️ 反应温度: {temperature}°C")
|
||||
print(f"⏰ 反应时间: {reaction_time/60:.1f}分钟")
|
||||
print(f"⏱️ 预计总时间: {(reaction_time + 450)/3600:.1f} 小时")
|
||||
print(f"📊 体积状态:")
|
||||
print(f" - 反应前体积: {original_liquid_volume:.2f}mL")
|
||||
print(f" - 反应后体积: {final_liquid_volume:.2f}mL (氢化反应体积基本不变)")
|
||||
print("🎊" * 20)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_hydrogenate_protocol():
|
||||
"""测试氢化反应协议"""
|
||||
print("🧪 === HYDROGENATE PROTOCOL 测试 === ✨")
|
||||
|
||||
# 测试温度解析
|
||||
test_temps = ["45 °C", "45°C", "45", "25 C", "invalid"]
|
||||
for temp in test_temps:
|
||||
parsed = parse_temperature(temp)
|
||||
print(f"温度 '{temp}' -> {parsed}°C")
|
||||
|
||||
# 测试时间解析
|
||||
test_times = ["2 h", "120 min", "7200 s", "2", "invalid"]
|
||||
for time in test_times:
|
||||
parsed = parse_time(time)
|
||||
print(f"时间 '{time}' -> {parsed/3600:.1f} 小时")
|
||||
|
||||
print("✅ 测试完成 🎉")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_hydrogenate_protocol()
|
||||
File diff suppressed because it is too large
Load Diff
390
unilabos/compile/recrystallize_protocol.py
Normal file
390
unilabos/compile/recrystallize_protocol.py
Normal file
@@ -0,0 +1,390 @@
|
||||
import networkx as nx
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Dict, Any, Tuple, Union
|
||||
from .utils.vessel_parser import get_vessel, find_solvent_vessel
|
||||
from .utils.unit_parser import parse_volume_input
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[RECRYSTALLIZE] {message}")
|
||||
|
||||
|
||||
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
||||
"""
|
||||
解析比例字符串,支持多种格式
|
||||
|
||||
Args:
|
||||
ratio_str: 比例字符串(如 "1:1", "3:7", "50:50")
|
||||
|
||||
Returns:
|
||||
Tuple[float, float]: 比例元组 (ratio1, ratio2)
|
||||
"""
|
||||
debug_print(f"⚖️ 开始解析比例: '{ratio_str}' 📊")
|
||||
|
||||
try:
|
||||
# 处理 "1:1", "3:7", "50:50" 等格式
|
||||
if ":" in ratio_str:
|
||||
parts = ratio_str.split(":")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
debug_print(f"✅ 冒号格式解析成功: {ratio1}:{ratio2} 🎯")
|
||||
return ratio1, ratio2
|
||||
|
||||
# 处理 "1-1", "3-7" 等格式
|
||||
if "-" in ratio_str:
|
||||
parts = ratio_str.split("-")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
debug_print(f"✅ 横线格式解析成功: {ratio1}:{ratio2} 🎯")
|
||||
return ratio1, ratio2
|
||||
|
||||
# 处理 "1,1", "3,7" 等格式
|
||||
if "," in ratio_str:
|
||||
parts = ratio_str.split(",")
|
||||
if len(parts) == 2:
|
||||
ratio1 = float(parts[0])
|
||||
ratio2 = float(parts[1])
|
||||
debug_print(f"✅ 逗号格式解析成功: {ratio1}:{ratio2} 🎯")
|
||||
return ratio1, ratio2
|
||||
|
||||
# 默认 1:1
|
||||
debug_print(f"⚠️ 无法解析比例 '{ratio_str}',使用默认比例 1:1 🎭")
|
||||
return 1.0, 1.0
|
||||
|
||||
except ValueError:
|
||||
debug_print(f"❌ 比例解析错误 '{ratio_str}',使用默认比例 1:1 🎭")
|
||||
return 1.0, 1.0
|
||||
|
||||
|
||||
def generate_recrystallize_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
ratio: str,
|
||||
solvent1: str,
|
||||
solvent2: str,
|
||||
volume: Union[str, float], # 支持字符串和数值
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成重结晶协议序列 - 支持vessel字典和体积运算
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
vessel: 目标容器字典(从XDL传入)
|
||||
ratio: 溶剂比例(如 "1:1", "3:7")
|
||||
solvent1: 第一种溶剂名称
|
||||
solvent2: 第二种溶剂名称
|
||||
volume: 总体积(支持 "100 mL", "50", "2.5 L" 等)
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
debug_print("💎" * 20)
|
||||
debug_print("🚀 开始生成重结晶协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" ⚖️ 比例: {ratio}")
|
||||
debug_print(f" 🧪 溶剂1: {solvent1}")
|
||||
debug_print(f" 🧪 溶剂2: {solvent2}")
|
||||
debug_print(f" 💧 总体积: {volume} (类型: {type(volume)})")
|
||||
debug_print("💎" * 20)
|
||||
|
||||
# 🔧 新增:记录重结晶前的容器状态
|
||||
debug_print("🔍 记录重结晶前容器状态...")
|
||||
original_liquid_volume = 0.0
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list) and len(current_volume) > 0:
|
||||
original_liquid_volume = current_volume[0]
|
||||
elif isinstance(current_volume, (int, float)):
|
||||
original_liquid_volume = current_volume
|
||||
debug_print(f"📊 重结晶前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
debug_print("📍 步骤1: 验证目标容器... 🔧")
|
||||
if vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print(f"❌ 目标容器 '{vessel_id}' 不存在于系统中! 😱")
|
||||
raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中")
|
||||
debug_print(f"✅ 目标容器 '{vessel_id}' 验证通过 🎯")
|
||||
|
||||
# 2. 解析体积(支持单位)
|
||||
debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
|
||||
final_volume = parse_volume_input(volume, "mL")
|
||||
debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨")
|
||||
|
||||
# 3. 解析比例
|
||||
debug_print("📍 步骤3: 解析比例... ⚖️")
|
||||
ratio1, ratio2 = parse_ratio(ratio)
|
||||
total_ratio = ratio1 + ratio2
|
||||
debug_print(f"🎯 比例解析完成: {ratio1}:{ratio2} (总比例: {total_ratio}) ✨")
|
||||
|
||||
# 4. 计算各溶剂体积
|
||||
debug_print("📍 步骤4: 计算各溶剂体积... 🧮")
|
||||
volume1 = final_volume * (ratio1 / total_ratio)
|
||||
volume2 = final_volume * (ratio2 / total_ratio)
|
||||
|
||||
debug_print(f"🧪 {solvent1} 体积: {volume1:.2f} mL ({ratio1}/{total_ratio} × {final_volume})")
|
||||
debug_print(f"🧪 {solvent2} 体积: {volume2:.2f} mL ({ratio2}/{total_ratio} × {final_volume})")
|
||||
debug_print(f"✅ 体积计算完成: 总计 {volume1 + volume2:.2f} mL 🎯")
|
||||
|
||||
# 5. 查找溶剂容器
|
||||
debug_print("📍 步骤5: 查找溶剂容器... 🔍")
|
||||
try:
|
||||
debug_print(f" 🔍 查找溶剂1容器...")
|
||||
solvent1_vessel = find_solvent_vessel(G, solvent1)
|
||||
debug_print(f" 🎉 找到溶剂1容器: {solvent1_vessel} ✨")
|
||||
except ValueError as e:
|
||||
debug_print(f" ❌ 溶剂1容器查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"无法找到溶剂1 '{solvent1}': {str(e)}")
|
||||
|
||||
try:
|
||||
debug_print(f" 🔍 查找溶剂2容器...")
|
||||
solvent2_vessel = find_solvent_vessel(G, solvent2)
|
||||
debug_print(f" 🎉 找到溶剂2容器: {solvent2_vessel} ✨")
|
||||
except ValueError as e:
|
||||
debug_print(f" ❌ 溶剂2容器查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}")
|
||||
|
||||
# 6. 验证路径存在
|
||||
debug_print("📍 步骤6: 验证传输路径... 🛤️")
|
||||
try:
|
||||
path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel_id) # 🔧 使用 vessel_id
|
||||
debug_print(f" 🛤️ 溶剂1路径: {' → '.join(path1)} ✅")
|
||||
except nx.NetworkXNoPath:
|
||||
debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel_id} 😞")
|
||||
raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel_id}' 没有可用路径")
|
||||
|
||||
try:
|
||||
path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel_id) # 🔧 使用 vessel_id
|
||||
debug_print(f" 🛤️ 溶剂2路径: {' → '.join(path2)} ✅")
|
||||
except nx.NetworkXNoPath:
|
||||
debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel_id} 😞")
|
||||
raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel_id}' 没有可用路径")
|
||||
|
||||
# 7. 添加第一种溶剂
|
||||
debug_print("📍 步骤7: 添加第一种溶剂... 🧪")
|
||||
debug_print(f" 🚰 开始添加溶剂1: {solvent1} ({volume1:.2f} mL)")
|
||||
|
||||
try:
|
||||
pump_actions1 = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent1_vessel,
|
||||
to_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
volume=volume1, # 使用解析后的体积
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重结晶不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0, # 正常流速
|
||||
transfer_flowrate=0.5
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions1)
|
||||
debug_print(f" ✅ 溶剂1泵送动作已添加: {len(pump_actions1)} 个动作 🚰✨")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f" ❌ 溶剂1泵协议生成失败: {str(e)} 😭")
|
||||
raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}")
|
||||
|
||||
# 🔧 新增:更新容器体积 - 添加溶剂1后
|
||||
debug_print(" 🔧 更新容器体积 - 添加溶剂1后...")
|
||||
new_volume_after_solvent1 = original_liquid_volume + volume1
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = new_volume_after_solvent1
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [new_volume_after_solvent1]
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = new_volume_after_solvent1
|
||||
|
||||
# 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = new_volume_after_solvent1
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume_after_solvent1]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = new_volume_after_solvent1
|
||||
|
||||
debug_print(f" 📊 体积更新: {original_liquid_volume:.2f}mL + {volume1:.2f}mL = {new_volume_after_solvent1:.2f}mL")
|
||||
|
||||
# 8. 等待溶剂1稳定
|
||||
debug_print(" ⏳ 添加溶剂1稳定等待...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 5.0, # 缩短等待时间
|
||||
"description": f"等待溶剂1 {solvent1} 稳定"
|
||||
}
|
||||
})
|
||||
debug_print(" ✅ 溶剂1稳定等待已添加 ⏰✨")
|
||||
|
||||
# 9. 添加第二种溶剂
|
||||
debug_print("📍 步骤8: 添加第二种溶剂... 🧪")
|
||||
debug_print(f" 🚰 开始添加溶剂2: {solvent2} ({volume2:.2f} mL)")
|
||||
|
||||
try:
|
||||
pump_actions2 = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent2_vessel,
|
||||
to_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
volume=volume2, # 使用解析后的体积
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重结晶不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.0, # 正常流速
|
||||
transfer_flowrate=0.5
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions2)
|
||||
debug_print(f" ✅ 溶剂2泵送动作已添加: {len(pump_actions2)} 个动作 🚰✨")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f" ❌ 溶剂2泵协议生成失败: {str(e)} 😭")
|
||||
raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}")
|
||||
|
||||
# 🔧 新增:更新容器体积 - 添加溶剂2后
|
||||
debug_print(" 🔧 更新容器体积 - 添加溶剂2后...")
|
||||
final_liquid_volume = new_volume_after_solvent1 + volume2
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = final_liquid_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [final_liquid_volume]
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = final_liquid_volume
|
||||
|
||||
# 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = final_liquid_volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [final_liquid_volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = final_liquid_volume
|
||||
|
||||
debug_print(f" 📊 最终体积: {new_volume_after_solvent1:.2f}mL + {volume2:.2f}mL = {final_liquid_volume:.2f}mL")
|
||||
|
||||
# 10. 等待溶剂2稳定
|
||||
debug_print(" ⏳ 添加溶剂2稳定等待...")
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 5.0, # 缩短等待时间
|
||||
"description": f"等待溶剂2 {solvent2} 稳定"
|
||||
}
|
||||
})
|
||||
debug_print(" ✅ 溶剂2稳定等待已添加 ⏰✨")
|
||||
|
||||
# 11. 等待重结晶完成
|
||||
debug_print("📍 步骤9: 等待重结晶完成... 💎")
|
||||
|
||||
# 模拟运行时间优化
|
||||
debug_print(" ⏱️ 检查模拟运行时间限制...")
|
||||
original_crystallize_time = 600.0 # 原始重结晶时间
|
||||
simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
|
||||
|
||||
final_crystallize_time = min(original_crystallize_time, simulation_time_limit)
|
||||
|
||||
if original_crystallize_time > simulation_time_limit:
|
||||
debug_print(f" 🎮 模拟运行优化: {original_crystallize_time}s → {final_crystallize_time}s ⚡")
|
||||
debug_print(f" 📊 时间缩短: {original_crystallize_time/60:.1f}分钟 → {final_crystallize_time/60:.1f}分钟 🚀")
|
||||
else:
|
||||
debug_print(f" ✅ 时间在限制内: {final_crystallize_time}s 保持不变 🎯")
|
||||
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": final_crystallize_time,
|
||||
"description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + (f" (模拟时间)" if original_crystallize_time != final_crystallize_time else "")
|
||||
}
|
||||
})
|
||||
debug_print(f" ✅ 重结晶等待已添加: {final_crystallize_time}s 💎✨")
|
||||
|
||||
# 显示时间调整信息
|
||||
if original_crystallize_time != final_crystallize_time:
|
||||
debug_print(f" 🎭 模拟优化说明: 原计划 {original_crystallize_time/60:.1f}分钟,实际模拟 {final_crystallize_time/60:.1f}分钟 ⚡")
|
||||
|
||||
# 总结
|
||||
debug_print("💎" * 20)
|
||||
debug_print(f"🎉 重结晶协议生成完成! ✨")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个")
|
||||
debug_print(f"🥽 目标容器: {vessel_id}")
|
||||
debug_print(f"💧 总体积变化:")
|
||||
debug_print(f" - 原始体积: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 添加溶剂: {final_volume:.2f}mL")
|
||||
debug_print(f" - 最终体积: {final_liquid_volume:.2f}mL")
|
||||
debug_print(f"⚖️ 溶剂比例: {solvent1}:{solvent2} = {ratio1}:{ratio2}")
|
||||
debug_print(f"🧪 溶剂1: {solvent1} ({volume1:.2f}mL)")
|
||||
debug_print(f"🧪 溶剂2: {solvent2} ({volume2:.2f}mL)")
|
||||
debug_print(f"⏱️ 预计总时间: {(final_crystallize_time + 10)/60:.1f} 分钟 ⌛")
|
||||
debug_print("💎" * 20)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_recrystallize_protocol():
|
||||
"""测试重结晶协议"""
|
||||
debug_print("🧪 === RECRYSTALLIZE PROTOCOL 测试 === ✨")
|
||||
|
||||
# 测试体积解析
|
||||
debug_print("💧 测试体积解析...")
|
||||
test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"]
|
||||
for vol in test_volumes:
|
||||
parsed = parse_volume_input(vol)
|
||||
debug_print(f" 📊 体积 '{vol}' -> {parsed}mL")
|
||||
|
||||
# 测试比例解析
|
||||
debug_print("⚖️ 测试比例解析...")
|
||||
test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"]
|
||||
for ratio in test_ratios:
|
||||
r1, r2 = parse_ratio(ratio)
|
||||
debug_print(f" 📊 比例 '{ratio}' -> {r1}:{r2}")
|
||||
|
||||
debug_print("✅ 测试完成 🎉")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_recrystallize_protocol()
|
||||
387
unilabos/compile/reset_handling_protocol.py
Normal file
387
unilabos/compile/reset_handling_protocol.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import networkx as nx
|
||||
import logging
|
||||
import sys
|
||||
from typing import List, Dict, Any, Optional
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 确保输出编码为UTF-8
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
except:
|
||||
pass
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出函数 - 支持中文"""
|
||||
try:
|
||||
# 确保消息是字符串格式
|
||||
safe_message = str(message)
|
||||
print(f"[重置处理] {safe_message}", flush=True)
|
||||
logger.info(f"[重置处理] {safe_message}")
|
||||
except UnicodeEncodeError:
|
||||
# 如果编码失败,尝试替换不支持的字符
|
||||
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
||||
print(f"[重置处理] {safe_message}", flush=True)
|
||||
logger.info(f"[重置处理] {safe_message}")
|
||||
except Exception as e:
|
||||
# 最后的安全措施
|
||||
fallback_message = f"日志输出错误: {repr(message)}"
|
||||
print(f"[重置处理] {fallback_message}", flush=True)
|
||||
logger.info(f"[重置处理] {fallback_message}")
|
||||
|
||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
||||
"""创建一个动作日志 - 支持中文和emoji"""
|
||||
try:
|
||||
full_message = f"{emoji} {message}"
|
||||
debug_print(full_message)
|
||||
logger.info(full_message)
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": full_message,
|
||||
"progress_message": full_message
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
# 如果emoji有问题,使用纯文本
|
||||
safe_message = f"[日志] {message}"
|
||||
debug_print(safe_message)
|
||||
logger.info(safe_message)
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": safe_message,
|
||||
"progress_message": safe_message
|
||||
}
|
||||
}
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""
|
||||
查找溶剂容器,支持多种匹配模式
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
solvent: 溶剂名称(如 "methanol", "ethanol", "water")
|
||||
|
||||
Returns:
|
||||
str: 溶剂容器ID
|
||||
"""
|
||||
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...")
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = [
|
||||
f"flask_{solvent}", # flask_methanol
|
||||
f"bottle_{solvent}", # bottle_methanol
|
||||
f"reagent_{solvent}", # reagent_methanol
|
||||
f"reagent_bottle_{solvent}", # reagent_bottle_methanol
|
||||
f"{solvent}_flask", # methanol_flask
|
||||
f"{solvent}_bottle", # methanol_bottle
|
||||
f"{solvent}", # methanol
|
||||
f"vessel_{solvent}", # vessel_methanol
|
||||
]
|
||||
|
||||
debug_print(f"🎯 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个)")
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
debug_print("📋 方法1: 精确名称匹配...")
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
debug_print(f"✅ 通过名称匹配找到容器: {vessel_name}")
|
||||
return vessel_name
|
||||
debug_print("⚠️ 精确名称匹配失败,尝试模糊匹配...")
|
||||
|
||||
# 第二步:通过模糊匹配
|
||||
debug_print("📋 方法2: 模糊名称匹配...")
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
# 检查是否包含溶剂名称
|
||||
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
||||
debug_print(f"✅ 通过模糊匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
debug_print("⚠️ 模糊匹配失败,尝试液体类型匹配...")
|
||||
|
||||
# 第三步:通过液体类型匹配
|
||||
debug_print("📋 方法3: 液体类型匹配...")
|
||||
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', '')).lower()
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
|
||||
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
|
||||
debug_print(f"✅ 通过液体类型匹配找到容器: {node_id}")
|
||||
return node_id
|
||||
|
||||
# 列出可用容器帮助调试
|
||||
debug_print("📊 显示可用容器信息...")
|
||||
available_containers = []
|
||||
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', [])
|
||||
liquid_types = [liquid.get('liquid_type', '') or liquid.get('name', '')
|
||||
for liquid in liquids if isinstance(liquid, dict)]
|
||||
|
||||
available_containers.append({
|
||||
'id': node_id,
|
||||
'name': G.nodes[node_id].get('name', ''),
|
||||
'liquids': liquid_types,
|
||||
'reagent_name': vessel_data.get('reagent_name', '')
|
||||
})
|
||||
|
||||
debug_print(f"📋 可用容器列表 (共{len(available_containers)}个):")
|
||||
for i, container in enumerate(available_containers[:5]): # 只显示前5个
|
||||
debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}")
|
||||
debug_print(f" 💧 液体: {container['liquids']}")
|
||||
debug_print(f" 🧪 试剂: {container['reagent_name']}")
|
||||
|
||||
if len(available_containers) > 5:
|
||||
debug_print(f" ... 还有 {len(available_containers)-5} 个容器")
|
||||
|
||||
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器")
|
||||
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names[:3]}...")
|
||||
|
||||
def generate_reset_handling_protocol(
|
||||
G: nx.DiGraph,
|
||||
solvent: str,
|
||||
vessel: Optional[str] = None, # 🆕 新增可选vessel参数
|
||||
**kwargs # 接收其他可能的参数但不使用
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成重置处理协议序列 - 支持自定义容器
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为容器和设备
|
||||
solvent: 溶剂名称(从XDL传入)
|
||||
vessel: 目标容器名称(可选,默认为 "main_reactor")
|
||||
**kwargs: 其他可选参数,但不使用
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
action_sequence = []
|
||||
|
||||
# 🔧 修改:支持自定义vessel参数
|
||||
target_vessel = vessel if vessel is not None else "main_reactor" # 默认目标容器
|
||||
volume = 50.0 # 默认体积 50 mL
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("🚀 开始生成重置处理协议")
|
||||
debug_print(f"📋 输入参数:")
|
||||
debug_print(f" 🧪 溶剂: {solvent}")
|
||||
debug_print(f" 🥽 目标容器: {target_vessel} {'(默认)' if vessel is None else '(指定)'}")
|
||||
debug_print(f" 💧 体积: {volume} mL")
|
||||
debug_print(f" ⚙️ 其他参数: {kwargs}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
# 添加初始日志
|
||||
action_sequence.append(create_action_log(f"开始重置处理操作 - 容器: {target_vessel}", "🎬"))
|
||||
action_sequence.append(create_action_log(f"使用溶剂: {solvent}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"重置体积: {volume}mL", "💧"))
|
||||
|
||||
if vessel is None:
|
||||
action_sequence.append(create_action_log("使用默认目标容器: main_reactor", "⚙️"))
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"使用指定目标容器: {vessel}", "🎯"))
|
||||
|
||||
# 1. 验证目标容器存在
|
||||
debug_print("🔍 步骤1: 验证目标容器...")
|
||||
action_sequence.append(create_action_log("正在验证目标容器...", "🔍"))
|
||||
|
||||
if target_vessel not in G.nodes():
|
||||
debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中!")
|
||||
action_sequence.append(create_action_log(f"目标容器 '{target_vessel}' 不存在", "❌"))
|
||||
raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中")
|
||||
|
||||
debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过")
|
||||
action_sequence.append(create_action_log(f"目标容器验证通过: {target_vessel}", "✅"))
|
||||
|
||||
# 2. 查找溶剂容器
|
||||
debug_print("🔍 步骤2: 查找溶剂容器...")
|
||||
action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍"))
|
||||
|
||||
try:
|
||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||
debug_print(f"✅ 找到溶剂容器: {solvent_vessel}")
|
||||
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "✅"))
|
||||
except ValueError as e:
|
||||
debug_print(f"❌ 溶剂容器查找失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌"))
|
||||
raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}")
|
||||
|
||||
# 3. 验证路径存在
|
||||
debug_print("🔍 步骤3: 验证传输路径...")
|
||||
action_sequence.append(create_action_log("正在验证传输路径...", "🛤️"))
|
||||
|
||||
try:
|
||||
path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel)
|
||||
debug_print(f"✅ 找到路径: {' → '.join(path)}")
|
||||
action_sequence.append(create_action_log(f"传输路径: {' → '.join(path)}", "🛤️"))
|
||||
except nx.NetworkXNoPath:
|
||||
debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel}")
|
||||
action_sequence.append(create_action_log(f"路径不可达: {solvent_vessel} → {target_vessel}", "❌"))
|
||||
raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径")
|
||||
|
||||
# 4. 使用pump_protocol转移溶剂
|
||||
debug_print("🔍 步骤4: 转移溶剂...")
|
||||
action_sequence.append(create_action_log("开始溶剂转移操作...", "🚰"))
|
||||
|
||||
debug_print(f"🚛 开始转移: {solvent_vessel} → {target_vessel}")
|
||||
debug_print(f"💧 转移体积: {volume} mL")
|
||||
action_sequence.append(create_action_log(f"转移: {solvent_vessel} → {target_vessel} ({volume}mL)", "🚛"))
|
||||
|
||||
try:
|
||||
debug_print("🔄 生成泵送协议...")
|
||||
action_sequence.append(create_action_log("正在生成泵送协议...", "🔄"))
|
||||
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=target_vessel,
|
||||
volume=volume,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="", # 重置处理不需要清洗
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.5, # 正常流速
|
||||
transfer_flowrate=0.5 # 正常转移流速
|
||||
)
|
||||
|
||||
action_sequence.extend(pump_actions)
|
||||
debug_print(f"✅ 泵送协议已添加: {len(pump_actions)} 个动作")
|
||||
action_sequence.append(create_action_log(f"泵送协议完成 ({len(pump_actions)} 个操作)", "✅"))
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 泵送协议生成失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"泵送协议生成失败: {str(e)}", "❌"))
|
||||
raise ValueError(f"生成泵协议时出错: {str(e)}")
|
||||
|
||||
# 5. 等待溶剂稳定
|
||||
debug_print("🔍 步骤5: 等待溶剂稳定...")
|
||||
action_sequence.append(create_action_log("等待溶剂稳定...", "⏳"))
|
||||
|
||||
# 模拟运行时间优化
|
||||
debug_print("⏱️ 检查模拟运行时间限制...")
|
||||
original_wait_time = 10.0 # 原始等待时间
|
||||
simulation_time_limit = 5.0 # 模拟运行时间限制:5秒
|
||||
|
||||
final_wait_time = min(original_wait_time, simulation_time_limit)
|
||||
|
||||
if original_wait_time > simulation_time_limit:
|
||||
debug_print(f"🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s")
|
||||
action_sequence.append(create_action_log(f"时间优化: {original_wait_time}s → {final_wait_time}s", "⚡"))
|
||||
else:
|
||||
debug_print(f"✅ 时间在限制内: {final_wait_time}s 保持不变")
|
||||
action_sequence.append(create_action_log(f"等待时间: {final_wait_time}s", "⏰"))
|
||||
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": final_wait_time,
|
||||
"description": f"等待溶剂 {solvent} 在容器 {target_vessel} 中稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "")
|
||||
}
|
||||
})
|
||||
debug_print(f"✅ 稳定等待已添加: {final_wait_time}s")
|
||||
|
||||
# 显示时间调整信息
|
||||
if original_wait_time != final_wait_time:
|
||||
debug_print(f"🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s")
|
||||
action_sequence.append(create_action_log("应用模拟时间优化", "🎭"))
|
||||
|
||||
# 总结
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"🎉 重置处理协议生成完成!")
|
||||
debug_print(f"📊 总结信息:")
|
||||
debug_print(f" 📋 总动作数: {len(action_sequence)} 个")
|
||||
debug_print(f" 🧪 溶剂: {solvent}")
|
||||
debug_print(f" 🥽 源容器: {solvent_vessel}")
|
||||
debug_print(f" 🥽 目标容器: {target_vessel} {'(默认)' if vessel is None else '(指定)'}")
|
||||
debug_print(f" 💧 转移体积: {volume} mL")
|
||||
debug_print(f" ⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒")
|
||||
debug_print(f" 🎯 操作结果: 已添加 {volume} mL {solvent} 到 {target_vessel}")
|
||||
debug_print("=" * 60)
|
||||
|
||||
# 添加完成日志
|
||||
summary_msg = f"重置处理完成: {target_vessel} (使用 {volume}mL {solvent})"
|
||||
if vessel is None:
|
||||
summary_msg += " [默认容器]"
|
||||
else:
|
||||
summary_msg += " [指定容器]"
|
||||
|
||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||
|
||||
return action_sequence
|
||||
|
||||
# === 便捷函数 ===
|
||||
|
||||
def reset_main_reactor(G: nx.DiGraph, solvent: str = "methanol", **kwargs) -> List[Dict[str, Any]]:
|
||||
"""重置主反应器 (默认行为)"""
|
||||
debug_print(f"🔄 重置主反应器,使用溶剂: {solvent}")
|
||||
return generate_reset_handling_protocol(G, solvent=solvent, vessel=None, **kwargs)
|
||||
|
||||
def reset_custom_vessel(G: nx.DiGraph, vessel: str, solvent: str = "methanol", **kwargs) -> List[Dict[str, Any]]:
|
||||
"""重置指定容器"""
|
||||
debug_print(f"🔄 重置指定容器: {vessel},使用溶剂: {solvent}")
|
||||
return generate_reset_handling_protocol(G, solvent=solvent, vessel=vessel, **kwargs)
|
||||
|
||||
def reset_with_water(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""使用水重置容器"""
|
||||
target = vessel or "main_reactor"
|
||||
debug_print(f"💧 使用水重置容器: {target}")
|
||||
return generate_reset_handling_protocol(G, solvent="water", vessel=vessel, **kwargs)
|
||||
|
||||
def reset_with_methanol(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""使用甲醇重置容器"""
|
||||
target = vessel or "main_reactor"
|
||||
debug_print(f"🧪 使用甲醇重置容器: {target}")
|
||||
return generate_reset_handling_protocol(G, solvent="methanol", vessel=vessel, **kwargs)
|
||||
|
||||
def reset_with_ethanol(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""使用乙醇重置容器"""
|
||||
target = vessel or "main_reactor"
|
||||
debug_print(f"🧪 使用乙醇重置容器: {target}")
|
||||
return generate_reset_handling_protocol(G, solvent="ethanol", vessel=vessel, **kwargs)
|
||||
|
||||
# 测试函数
|
||||
def test_reset_handling_protocol():
|
||||
"""测试重置处理协议"""
|
||||
debug_print("=== 重置处理协议增强中文版测试 ===")
|
||||
|
||||
# 测试溶剂名称
|
||||
debug_print("🧪 测试常用溶剂名称...")
|
||||
test_solvents = ["methanol", "ethanol", "water", "acetone", "dmso"]
|
||||
for solvent in test_solvents:
|
||||
debug_print(f" 🔍 测试溶剂: {solvent}")
|
||||
|
||||
# 测试容器参数
|
||||
debug_print("🥽 测试容器参数...")
|
||||
test_cases = [
|
||||
{"solvent": "methanol", "vessel": None, "desc": "默认容器"},
|
||||
{"solvent": "ethanol", "vessel": "reactor_2", "desc": "指定容器"},
|
||||
{"solvent": "water", "vessel": "flask_1", "desc": "自定义容器"}
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
debug_print(f" 🧪 测试案例: {case['desc']} - {case['solvent']} -> {case['vessel'] or 'main_reactor'}")
|
||||
|
||||
debug_print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_reset_handling_protocol()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,166 +1,478 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .utils.unit_parser import parse_time_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[STIR] {message}")
|
||||
|
||||
|
||||
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']
|
||||
"""查找与指定容器相连的搅拌设备"""
|
||||
debug_print(f"🔍 查找搅拌设备,目标容器: {vessel} 🥽")
|
||||
|
||||
if vessel:
|
||||
# 检查哪个搅拌设备与目标容器相连(机械连接)
|
||||
# 🔧 查找所有搅拌设备
|
||||
stirrer_nodes = []
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class:
|
||||
stirrer_nodes.append(node)
|
||||
debug_print(f"🎉 找到搅拌设备: {node} 🌪️")
|
||||
|
||||
# 🔗 检查连接
|
||||
if vessel and stirrer_nodes:
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"✅ 搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连 🔗")
|
||||
return stirrer
|
||||
|
||||
# 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
|
||||
# 🎯 使用第一个可用设备
|
||||
if stirrer_nodes:
|
||||
return stirrer_nodes[0]
|
||||
selected = stirrer_nodes[0]
|
||||
debug_print(f"🔧 使用第一个搅拌设备: {selected} 🌪️")
|
||||
return selected
|
||||
|
||||
raise ValueError("系统中未找到可用的搅拌设备")
|
||||
# 🆘 默认设备
|
||||
debug_print("⚠️ 未找到搅拌设备,使用默认设备 🌪️")
|
||||
return "stirrer_1"
|
||||
|
||||
def validate_and_fix_params(stir_time: float, stir_speed: float, settling_time: float) -> tuple:
|
||||
"""验证和修正参数"""
|
||||
# ⏰ 搅拌时间验证
|
||||
if stir_time < 0:
|
||||
debug_print(f"⚠️ 搅拌时间 {stir_time}s 无效,修正为 100s 🕐")
|
||||
stir_time = 100.0
|
||||
elif stir_time > 100: # 限制为100s
|
||||
debug_print(f"⚠️ 搅拌时间 {stir_time}s 过长,仿真运行时,修正为 100s 🕐")
|
||||
stir_time = 100.0
|
||||
else:
|
||||
debug_print(f"✅ 搅拌时间 {stir_time}s ({stir_time/60:.1f}分钟) 有效 ⏰")
|
||||
|
||||
# 🌪️ 搅拌速度验证
|
||||
if stir_speed < 10.0 or stir_speed > 1500.0:
|
||||
debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM 🌪️")
|
||||
stir_speed = 300.0
|
||||
else:
|
||||
debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内 🌪️")
|
||||
|
||||
# ⏱️ 沉降时间验证
|
||||
if settling_time < 0 or settling_time > 600: # 限制为10分钟
|
||||
debug_print(f"⚠️ 沉降时间 {settling_time}s 超出范围,修正为 60s ⏱️")
|
||||
settling_time = 60.0
|
||||
else:
|
||||
debug_print(f"✅ 沉降时间 {settling_time}s 在正常范围内 ⏱️")
|
||||
|
||||
return stir_time, stir_speed, settling_time
|
||||
|
||||
def extract_vessel_id(vessel: Union[str, dict]) -> str:
|
||||
"""
|
||||
从vessel参数中提取vessel_id
|
||||
|
||||
Args:
|
||||
vessel: vessel字典或vessel_id字符串
|
||||
|
||||
Returns:
|
||||
str: vessel_id
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
vessel_id = list(vessel.values())[0].get("id", "")
|
||||
debug_print(f"🔧 从vessel字典提取ID: {vessel_id}")
|
||||
return vessel_id
|
||||
elif isinstance(vessel, str):
|
||||
debug_print(f"🔧 vessel参数为字符串: {vessel}")
|
||||
return vessel
|
||||
else:
|
||||
debug_print(f"⚠️ 无效的vessel参数类型: {type(vessel)}")
|
||||
return ""
|
||||
|
||||
def get_vessel_display_info(vessel: Union[str, dict]) -> str:
|
||||
"""
|
||||
获取容器的显示信息(用于日志)
|
||||
|
||||
Args:
|
||||
vessel: vessel字典或vessel_id字符串
|
||||
|
||||
Returns:
|
||||
str: 显示信息
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
vessel_id = vessel.get("id", "unknown")
|
||||
vessel_name = vessel.get("name", "")
|
||||
if vessel_name:
|
||||
return f"{vessel_id} ({vessel_name})"
|
||||
else:
|
||||
return vessel_id
|
||||
else:
|
||||
return str(vessel)
|
||||
|
||||
def generate_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
stir_time: float,
|
||||
stir_speed: float,
|
||||
settling_time: float
|
||||
vessel: Union[str, dict], # 支持vessel字典或字符串
|
||||
time: Union[str, float, int] = "300",
|
||||
stir_time: Union[str, float, int] = "0",
|
||||
time_spec: str = "",
|
||||
event: str = "",
|
||||
stir_speed: float = 300.0,
|
||||
settling_time: Union[str, float] = "60",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成搅拌操作的协议序列 - 定时搅拌 + 沉降
|
||||
"""
|
||||
action_sequence = []
|
||||
"""生成搅拌操作的协议序列 - 修复vessel参数传递"""
|
||||
|
||||
print(f"STIR: 开始生成搅拌协议")
|
||||
print(f" - 搅拌时间: {stir_time}秒")
|
||||
print(f" - 搅拌速度: {stir_speed} RPM")
|
||||
print(f" - 沉降时间: {settling_time}秒")
|
||||
# 🔧 核心修改:正确处理vessel参数
|
||||
vessel_id = extract_vessel_id(vessel)
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
|
||||
# 查找搅拌设备
|
||||
# 🔧 关键修复:确保vessel_resource是完整的Resource对象
|
||||
if isinstance(vessel, dict):
|
||||
vessel_resource = vessel # 已经是完整的Resource字典
|
||||
debug_print(f"✅ 使用传入的vessel Resource对象")
|
||||
else:
|
||||
# 如果只是字符串,构建一个基本的Resource对象
|
||||
vessel_resource = {
|
||||
"id": vessel,
|
||||
"name": "",
|
||||
"category": "",
|
||||
"children": [],
|
||||
"config": "",
|
||||
"data": "",
|
||||
"parent": "",
|
||||
"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": "",
|
||||
"type": ""
|
||||
}
|
||||
debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}")
|
||||
|
||||
debug_print("🌪️" * 20)
|
||||
debug_print("🚀 开始生成搅拌协议(支持vessel字典)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel_display} (ID: {vessel_id})")
|
||||
debug_print(f" ⏰ time: {time}")
|
||||
debug_print(f" 🕐 stir_time: {stir_time}")
|
||||
debug_print(f" 🎯 time_spec: {time_spec}")
|
||||
debug_print(f" 🌪️ stir_speed: {stir_speed} RPM")
|
||||
debug_print(f" ⏱️ settling_time: {settling_time}")
|
||||
debug_print("🌪️" * 20)
|
||||
|
||||
# 📋 参数验证
|
||||
debug_print("📍 步骤1: 参数验证... 🔧")
|
||||
if not vessel_id: # 🔧 使用 vessel_id
|
||||
debug_print("❌ vessel 参数不能为空! 😱")
|
||||
raise ValueError("vessel 参数不能为空")
|
||||
|
||||
if vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中! 😞")
|
||||
raise ValueError(f"容器 '{vessel_id}' 不存在于系统中")
|
||||
|
||||
debug_print("✅ 基础参数验证通过 🎯")
|
||||
|
||||
# 🔄 参数解析
|
||||
debug_print("📍 步骤2: 参数解析... ⚡")
|
||||
|
||||
# 确定实际时间(优先级:time_spec > stir_time > time)
|
||||
if time_spec:
|
||||
parsed_time = parse_time_input(time_spec)
|
||||
debug_print(f"🎯 使用time_spec: '{time_spec}' → {parsed_time}s")
|
||||
elif stir_time not in ["0", 0, 0.0]:
|
||||
parsed_time = parse_time_input(stir_time)
|
||||
debug_print(f"🎯 使用stir_time: {stir_time} → {parsed_time}s")
|
||||
else:
|
||||
parsed_time = parse_time_input(time)
|
||||
debug_print(f"🎯 使用time: {time} → {parsed_time}s")
|
||||
|
||||
# 解析沉降时间
|
||||
parsed_settling_time = parse_time_input(settling_time)
|
||||
|
||||
# 🕐 模拟运行时间优化
|
||||
debug_print(" ⏱️ 检查模拟运行时间限制...")
|
||||
original_stir_time = parsed_time
|
||||
original_settling_time = parsed_settling_time
|
||||
|
||||
# 搅拌时间限制为60秒
|
||||
stir_time_limit = 60.0
|
||||
if parsed_time > stir_time_limit:
|
||||
parsed_time = stir_time_limit
|
||||
debug_print(f" 🎮 搅拌时间优化: {original_stir_time}s → {parsed_time}s ⚡")
|
||||
|
||||
# 沉降时间限制为30秒
|
||||
settling_time_limit = 30.0
|
||||
if parsed_settling_time > settling_time_limit:
|
||||
parsed_settling_time = settling_time_limit
|
||||
debug_print(f" 🎮 沉降时间优化: {original_settling_time}s → {parsed_settling_time}s ⚡")
|
||||
|
||||
# 参数修正
|
||||
parsed_time, stir_speed, parsed_settling_time = validate_and_fix_params(
|
||||
parsed_time, stir_speed, parsed_settling_time
|
||||
)
|
||||
|
||||
debug_print(f"🎯 最终参数: time={parsed_time}s, speed={stir_speed}RPM, settling={parsed_settling_time}s")
|
||||
|
||||
# 🔍 查找设备
|
||||
debug_print("📍 步骤3: 查找搅拌设备... 🔍")
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G)
|
||||
print(f"STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id
|
||||
debug_print(f"🎉 使用搅拌设备: {stirrer_id} ✨")
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
|
||||
# 执行搅拌操作
|
||||
# 🚀 生成动作
|
||||
debug_print("📍 步骤4: 生成搅拌动作... 🌪️")
|
||||
|
||||
action_sequence = []
|
||||
stir_action = {
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"stir_time": stir_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": settling_time
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
"time": str(time),
|
||||
"event": event,
|
||||
"time_spec": time_spec,
|
||||
"stir_time": float(parsed_time),
|
||||
"stir_speed": float(stir_speed),
|
||||
"settling_time": float(parsed_settling_time)
|
||||
}
|
||||
}
|
||||
|
||||
action_sequence.append(stir_action)
|
||||
debug_print("✅ 搅拌动作已添加 🌪️✨")
|
||||
|
||||
# 显示时间优化信息
|
||||
if original_stir_time != parsed_time or original_settling_time != parsed_settling_time:
|
||||
debug_print(f" 🎭 模拟优化说明:")
|
||||
debug_print(f" 搅拌时间: {original_stir_time/60:.1f}分钟 → {parsed_time/60:.1f}分钟")
|
||||
debug_print(f" 沉降时间: {original_settling_time/60:.1f}分钟 → {parsed_settling_time/60:.1f}分钟")
|
||||
|
||||
# 🎊 总结
|
||||
debug_print("🎊" * 20)
|
||||
debug_print(f"🎉 搅拌协议生成完成! ✨")
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个")
|
||||
debug_print(f"🥽 搅拌容器: {vessel_display}")
|
||||
debug_print(f"🌪️ 搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s")
|
||||
debug_print(f"⏱️ 预计总时间: {(parsed_time + parsed_settling_time)/60:.1f} 分钟 ⌛")
|
||||
debug_print("🎊" * 20)
|
||||
|
||||
print(f"STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_start_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
stir_speed: float,
|
||||
purpose: str
|
||||
vessel: Union[str, dict],
|
||||
stir_speed: float = 300.0,
|
||||
purpose: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成开始搅拌操作的协议序列 - 持续搅拌
|
||||
"""
|
||||
action_sequence = []
|
||||
"""生成开始搅拌操作的协议序列 - 修复vessel参数传递"""
|
||||
|
||||
print(f"START_STIR: 开始生成启动搅拌协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
print(f" - 搅拌速度: {stir_speed} RPM")
|
||||
print(f" - 目的: {purpose}")
|
||||
# 🔧 核心修改:正确处理vessel参数
|
||||
vessel_id = extract_vessel_id(vessel)
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
|
||||
# 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
# 🔧 关键修复:确保vessel_resource是完整的Resource对象
|
||||
if isinstance(vessel, dict):
|
||||
vessel_resource = vessel # 已经是完整的Resource字典
|
||||
debug_print(f"✅ 使用传入的vessel Resource对象")
|
||||
else:
|
||||
# 如果只是字符串,构建一个基本的Resource对象
|
||||
vessel_resource = {
|
||||
"id": vessel,
|
||||
"name": "",
|
||||
"category": "",
|
||||
"children": [],
|
||||
"config": "",
|
||||
"data": "",
|
||||
"parent": "",
|
||||
"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": "",
|
||||
"type": ""
|
||||
}
|
||||
debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}")
|
||||
|
||||
# 查找搅拌设备
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
print(f"START_STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
debug_print("🔄 开始生成启动搅拌协议(修复vessel参数)✨")
|
||||
debug_print(f"🥽 vessel: {vessel_display} (ID: {vessel_id})")
|
||||
debug_print(f"🌪️ speed: {stir_speed} RPM")
|
||||
debug_print(f"🎯 purpose: {purpose}")
|
||||
|
||||
# 执行开始搅拌操作
|
||||
start_stir_action = {
|
||||
# 基础验证
|
||||
if not vessel_id or vessel_id not in G.nodes():
|
||||
debug_print("❌ 容器验证失败!")
|
||||
raise ValueError("vessel 参数无效")
|
||||
|
||||
# 参数修正
|
||||
if stir_speed < 10.0 or stir_speed > 1500.0:
|
||||
debug_print(f"⚠️ 搅拌速度修正: {stir_speed} → 300 RPM 🌪️")
|
||||
stir_speed = 300.0
|
||||
|
||||
# 查找设备
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id)
|
||||
|
||||
# 🔧 关键修复:传递vessel_id字符串
|
||||
action_sequence = [{
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": purpose
|
||||
"purpose": purpose or f"启动搅拌 {stir_speed} RPM"
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
action_sequence.append(start_stir_action)
|
||||
|
||||
print(f"START_STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
debug_print(f"✅ 启动搅拌协议生成完成 🎯")
|
||||
return action_sequence
|
||||
|
||||
|
||||
def generate_stop_stir_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str
|
||||
vessel: Union[str, dict],
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成停止搅拌操作的协议序列
|
||||
"""
|
||||
action_sequence = []
|
||||
"""生成停止搅拌操作的协议序列 - 修复vessel参数传递"""
|
||||
|
||||
print(f"STOP_STIR: 开始生成停止搅拌协议")
|
||||
print(f" - 容器: {vessel}")
|
||||
# 🔧 核心修改:正确处理vessel参数
|
||||
vessel_id = extract_vessel_id(vessel)
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
|
||||
# 验证容器存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
|
||||
# 🔧 关键修复:确保vessel_resource是完整的Resource对象
|
||||
if isinstance(vessel, dict):
|
||||
vessel_resource = vessel # 已经是完整的Resource字典
|
||||
debug_print(f"✅ 使用传入的vessel Resource对象")
|
||||
else:
|
||||
# 如果只是字符串,构建一个基本的Resource对象
|
||||
vessel_resource = {
|
||||
"id": vessel,
|
||||
"name": "",
|
||||
"category": "",
|
||||
"children": [],
|
||||
"config": "",
|
||||
"data": "",
|
||||
"parent": "",
|
||||
"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": "",
|
||||
"type": ""
|
||||
}
|
||||
debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}")
|
||||
|
||||
# 查找搅拌设备
|
||||
try:
|
||||
stirrer_id = find_connected_stirrer(G, vessel)
|
||||
print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}")
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法找到搅拌设备: {str(e)}")
|
||||
debug_print("🛑 开始生成停止搅拌协议(修复vessel参数)✨")
|
||||
debug_print(f"🥽 vessel: {vessel_display} (ID: {vessel_id})")
|
||||
|
||||
# 执行停止搅拌操作
|
||||
stop_stir_action = {
|
||||
# 基础验证
|
||||
if not vessel_id or vessel_id not in G.nodes():
|
||||
debug_print("❌ 容器验证失败!")
|
||||
raise ValueError("vessel 参数无效")
|
||||
|
||||
# 查找设备
|
||||
stirrer_id = find_connected_stirrer(G, vessel_id)
|
||||
|
||||
# 🔧 关键修复:传递vessel_id字符串
|
||||
action_sequence = [{
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
action_sequence.append(stop_stir_action)
|
||||
|
||||
print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作")
|
||||
debug_print(f"✅ 停止搅拌协议生成完成 🎯")
|
||||
return action_sequence
|
||||
|
||||
# 🔧 新增:便捷函数
|
||||
def stir_briefly(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
speed: float = 300.0) -> List[Dict[str, Any]]:
|
||||
"""短时间搅拌(30秒)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"⚡ 短时间搅拌: {vessel_display} @ {speed}RPM (30s)")
|
||||
return generate_stir_protocol(G, vessel, time="30", stir_speed=speed)
|
||||
|
||||
# 便捷函数
|
||||
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 stir_slowly(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
|
||||
"""慢速搅拌"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🐌 慢速搅拌: {vessel_display} @ 150RPM")
|
||||
return generate_stir_protocol(G, vessel, time=time, stir_speed=150.0)
|
||||
|
||||
def stir_vigorously(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
|
||||
"""剧烈搅拌"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"💨 剧烈搅拌: {vessel_display} @ 800RPM")
|
||||
return generate_stir_protocol(G, vessel, time=time, stir_speed=800.0)
|
||||
|
||||
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)
|
||||
def stir_for_reaction(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
time: Union[str, float] = "1 h") -> List[Dict[str, Any]]:
|
||||
"""反应搅拌(标准速度,长时间)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🧪 反应搅拌: {vessel_display} @ 400RPM")
|
||||
return generate_stir_protocol(G, vessel, time=time, stir_speed=400.0)
|
||||
|
||||
def stir_for_dissolution(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
|
||||
"""溶解搅拌(中等速度)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"💧 溶解搅拌: {vessel_display} @ 500RPM")
|
||||
return generate_stir_protocol(G, vessel, time=time, stir_speed=500.0)
|
||||
|
||||
def stir_gently(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
time: Union[str, float] = "30 min") -> List[Dict[str, Any]]:
|
||||
"""温和搅拌"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🍃 温和搅拌: {vessel_display} @ 200RPM")
|
||||
return generate_stir_protocol(G, vessel, time=time, stir_speed=200.0)
|
||||
|
||||
def stir_overnight(G: nx.DiGraph, vessel: Union[str, dict]) -> List[Dict[str, Any]]:
|
||||
"""过夜搅拌(模拟时缩短为2小时)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🌙 过夜搅拌(模拟2小时): {vessel_display} @ 300RPM")
|
||||
return generate_stir_protocol(G, vessel, time="2 h", stir_speed=300.0)
|
||||
|
||||
def start_continuous_stirring(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
speed: float = 300.0, purpose: str = "continuous stirring") -> List[Dict[str, Any]]:
|
||||
"""开始连续搅拌"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🔄 开始连续搅拌: {vessel_display} @ {speed}RPM")
|
||||
return generate_start_stir_protocol(G, vessel, stir_speed=speed, purpose=purpose)
|
||||
|
||||
def stop_all_stirring(G: nx.DiGraph, vessel: Union[str, dict]) -> List[Dict[str, Any]]:
|
||||
"""停止所有搅拌"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🛑 停止搅拌: {vessel_display}")
|
||||
return generate_stop_stir_protocol(G, vessel)
|
||||
|
||||
# 测试函数
|
||||
def test_stir_protocol():
|
||||
"""测试搅拌协议"""
|
||||
debug_print("🧪 === STIR PROTOCOL 测试 === ✨")
|
||||
|
||||
# 测试vessel参数处理
|
||||
debug_print("🔧 测试vessel参数处理...")
|
||||
|
||||
# 测试字典格式
|
||||
vessel_dict = {"id": "flask_1", "name": "反应瓶1"}
|
||||
vessel_id = extract_vessel_id(vessel_dict)
|
||||
vessel_display = get_vessel_display_info(vessel_dict)
|
||||
debug_print(f" 字典格式: {vessel_dict} → ID: {vessel_id}, 显示: {vessel_display}")
|
||||
|
||||
# 测试字符串格式
|
||||
vessel_str = "flask_2"
|
||||
vessel_id = extract_vessel_id(vessel_str)
|
||||
vessel_display = get_vessel_display_info(vessel_str)
|
||||
debug_print(f" 字符串格式: {vessel_str} → ID: {vessel_id}, 显示: {vessel_display}")
|
||||
|
||||
debug_print("✅ 测试完成 🎉")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_stir_protocol()
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
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
|
||||
36
unilabos/compile/utils/logger_util.py
Normal file
36
unilabos/compile/utils/logger_util.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# 🆕 创建进度日志动作
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message, prefix="[UNIT_PARSER]"):
|
||||
"""调试输出"""
|
||||
logger.info(f"{prefix} {message}")
|
||||
|
||||
|
||||
def action_log(message: str, emoji: str = "📝", prefix="[HIGH-LEVEL OPERATION]") -> Dict[str, Any]:
|
||||
"""创建一个动作日志 - 支持中文和emoji"""
|
||||
try:
|
||||
full_message = f"{prefix} {emoji} {message}"
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": full_message,
|
||||
"progress_message": full_message
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
# 如果emoji有问题,使用纯文本
|
||||
safe_message = f"{prefix} {message}"
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": safe_message,
|
||||
"progress_message": safe_message
|
||||
}
|
||||
}
|
||||
215
unilabos/compile/utils/unit_parser.py
Normal file
215
unilabos/compile/utils/unit_parser.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
统一的单位解析工具模块
|
||||
支持时间、体积、质量等各种单位的解析
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
from .logger_util import debug_print
|
||||
|
||||
|
||||
def parse_volume_input(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||
"""
|
||||
解析带单位的体积输入
|
||||
|
||||
Args:
|
||||
volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
|
||||
default_unit: 默认单位(默认为毫升)
|
||||
|
||||
Returns:
|
||||
float: 体积(毫升)
|
||||
"""
|
||||
if not volume_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(volume_input, (int, float)):
|
||||
result = float(volume_input)
|
||||
debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
volume_str = str(volume_input).lower().strip()
|
||||
debug_print(f"解析体积字符串: '{volume_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_volume = 50.0 # 50mL默认值
|
||||
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
|
||||
return default_volume
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(volume_str)
|
||||
if default_unit.lower() in ["ml", "milliliter"]:
|
||||
result = value
|
||||
elif default_unit.lower() in ["l", "liter"]:
|
||||
result = value * 1000.0
|
||||
elif default_unit.lower() in ["μl", "ul", "microliter"]:
|
||||
result = value / 1000.0
|
||||
else:
|
||||
result = value # 默认mL
|
||||
debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
|
||||
return 50.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit.lower()
|
||||
|
||||
# 转换为毫升
|
||||
if unit in ['l', 'liter']:
|
||||
volume = value * 1000.0 # L -> mL
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
|
||||
debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
|
||||
return volume
|
||||
|
||||
|
||||
def parse_mass_input(mass_input: Union[str, float]) -> float:
|
||||
"""
|
||||
解析质量输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5)
|
||||
|
||||
Returns:
|
||||
float: 质量(克)
|
||||
"""
|
||||
if isinstance(mass_input, (int, float)):
|
||||
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
|
||||
return float(mass_input)
|
||||
|
||||
if not mass_input or not str(mass_input).strip():
|
||||
debug_print(f"⚠️ 质量输入为空,返回0.0g")
|
||||
return 0.0
|
||||
|
||||
mass_str = str(mass_input).lower().strip()
|
||||
debug_print(f"🔍 解析质量输入: '{mass_str}'")
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
mass_clean = re.sub(r'\s+', '', mass_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
|
||||
|
||||
if not match:
|
||||
debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g")
|
||||
return 0.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 'g' # 默认单位为克
|
||||
|
||||
# 转换为克
|
||||
if unit in ['mg', 'milligram']:
|
||||
mass = value / 1000.0 # mg -> g
|
||||
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
|
||||
elif unit in ['kg', 'kilogram']:
|
||||
mass = value * 1000.0 # kg -> g
|
||||
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
|
||||
else: # g, gram 或默认
|
||||
mass = value # 已经是g
|
||||
debug_print(f"✅ 质量已为g: {mass}g")
|
||||
|
||||
return mass
|
||||
|
||||
|
||||
def parse_time_input(time_input: Union[str, float]) -> float:
|
||||
"""
|
||||
解析时间输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if isinstance(time_input, (int, float)):
|
||||
debug_print(f"⏱️ 时间输入为数值: {time_input}秒")
|
||||
return float(time_input)
|
||||
|
||||
if not time_input or not str(time_input).strip():
|
||||
debug_print(f"⚠️ 时间输入为空,返回0秒")
|
||||
return 0.0
|
||||
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"🔍 解析时间输入: '{time_str}'")
|
||||
|
||||
# 处理未知时间
|
||||
if time_str in ['?', 'unknown', 'tbd']:
|
||||
default_time = 60.0 # 默认1分钟
|
||||
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
|
||||
return default_time
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
time_clean = re.sub(r'\s+', '', time_str)
|
||||
|
||||
# 匹配数字和单位的正则表达式
|
||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
|
||||
|
||||
if not match:
|
||||
debug_print(f"❌ 无法解析时间: '{time_str}',返回0s")
|
||||
return 0.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 's' # 默认单位为秒
|
||||
|
||||
# 转换为秒
|
||||
if unit in ['m', 'min', 'minute', 'mins', 'minutes']:
|
||||
time_sec = value * 60.0 # min -> s
|
||||
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
|
||||
elif unit in ['h', 'hr', 'hour', 'hrs', 'hours']:
|
||||
time_sec = value * 3600.0 # h -> s
|
||||
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
|
||||
elif unit in ['d', 'day', 'days']:
|
||||
time_sec = value * 86400.0 # d -> s
|
||||
debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒")
|
||||
else: # s, sec, second 或默认
|
||||
time_sec = value # 已经是s
|
||||
debug_print(f"✅ 时间已为秒: {time_sec}秒")
|
||||
|
||||
return time_sec
|
||||
|
||||
# 测试函数
|
||||
def test_unit_parser():
|
||||
"""测试单位解析功能"""
|
||||
print("=== 单位解析器测试 ===")
|
||||
|
||||
# 测试时间解析
|
||||
time_tests = [
|
||||
"30 min", "1 h", "300", "5.5 h", "?", 60.0, "2 hours", "30 s"
|
||||
]
|
||||
|
||||
print("\n时间解析测试:")
|
||||
for time_input in time_tests:
|
||||
result = parse_time_input(time_input)
|
||||
print(f" {time_input} → {result}s ({result/60:.1f}min)")
|
||||
|
||||
# 测试体积解析
|
||||
volume_tests = [
|
||||
"100 mL", "2.5 L", "500", "?", 100.0, "500 μL", "1 liter"
|
||||
]
|
||||
|
||||
print("\n体积解析测试:")
|
||||
for volume_input in volume_tests:
|
||||
result = parse_volume_input(volume_input)
|
||||
print(f" {volume_input} → {result}mL")
|
||||
|
||||
print("\n✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_unit_parser()
|
||||
281
unilabos/compile/utils/vessel_parser.py
Normal file
281
unilabos/compile/utils/vessel_parser.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import networkx as nx
|
||||
|
||||
from .logger_util import debug_print
|
||||
|
||||
|
||||
def get_vessel(vessel):
|
||||
"""
|
||||
统一处理vessel参数,返回vessel_id和vessel_data。
|
||||
|
||||
Args:
|
||||
vessel: 可以是一个字典或字符串,表示vessel的ID或数据。
|
||||
|
||||
Returns:
|
||||
tuple: 包含vessel_id和vessel_data。
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
if "id" not in vessel:
|
||||
vessel_id = list(vessel.values())[0].get("id", "")
|
||||
else:
|
||||
vessel_id = vessel.get("id", "")
|
||||
vessel_data = vessel.get("data", {})
|
||||
else:
|
||||
vessel_id = str(vessel)
|
||||
vessel_data = {}
|
||||
return vessel_id, vessel_data
|
||||
|
||||
|
||||
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||
"""增强版试剂容器查找,支持固体和液体"""
|
||||
debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...")
|
||||
|
||||
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
|
||||
debug_print(f"📋 方法1: 搜索reagent字段...")
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node].get('data', {})
|
||||
node_type = G.nodes[node].get('type', '')
|
||||
config_data = G.nodes[node].get('config', {})
|
||||
|
||||
# 只搜索容器类型的节点
|
||||
if node_type == 'container':
|
||||
reagent_name = node_data.get('reagent_name', '').lower()
|
||||
config_reagent = config_data.get('reagent', '').lower()
|
||||
|
||||
# 精确匹配
|
||||
if reagent_name == reagent.lower() or config_reagent == reagent.lower():
|
||||
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
|
||||
return node
|
||||
|
||||
# 模糊匹配
|
||||
if (reagent.lower() in reagent_name and reagent_name) or \
|
||||
(reagent.lower() in config_reagent and config_reagent):
|
||||
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
|
||||
return node
|
||||
|
||||
# 🔧 方法2:常见的容器命名规则
|
||||
debug_print(f"📋 方法2: 使用命名规则查找...")
|
||||
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
|
||||
possible_names = [
|
||||
reagent_clean,
|
||||
f"flask_{reagent_clean}",
|
||||
f"bottle_{reagent_clean}",
|
||||
f"vessel_{reagent_clean}",
|
||||
f"{reagent_clean}_flask",
|
||||
f"{reagent_clean}_bottle",
|
||||
f"reagent_{reagent_clean}",
|
||||
f"reagent_bottle_{reagent_clean}",
|
||||
f"solid_reagent_bottle_{reagent_clean}",
|
||||
f"reagent_bottle_1", # 通用试剂瓶
|
||||
f"reagent_bottle_2",
|
||||
f"reagent_bottle_3"
|
||||
]
|
||||
|
||||
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
|
||||
|
||||
for name in possible_names:
|
||||
if name in G.nodes():
|
||||
node_type = G.nodes[name].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
|
||||
return name
|
||||
|
||||
# 🔧 方法3:节点名称模糊匹配
|
||||
debug_print(f"📋 方法3: 节点名称模糊匹配...")
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
if node_data.get('type') == 'container':
|
||||
# 检查节点名称是否包含试剂名称
|
||||
if reagent_clean in node_id.lower():
|
||||
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
|
||||
return node_id
|
||||
|
||||
# 检查液体类型匹配
|
||||
vessel_data = node_data.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', '')
|
||||
if liquid_type.lower() == reagent.lower():
|
||||
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
|
||||
return node_id
|
||||
|
||||
# 🔧 方法4:使用第一个试剂瓶作为备选
|
||||
debug_print(f"📋 方法4: 查找备选试剂瓶...")
|
||||
for node_id in G.nodes():
|
||||
node_data = G.nodes[node_id]
|
||||
if (node_data.get('type') == 'container' and
|
||||
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
|
||||
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
|
||||
return node_id
|
||||
|
||||
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
|
||||
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
|
||||
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""
|
||||
查找溶剂容器
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
solvent: 溶剂名称
|
||||
|
||||
Returns:
|
||||
str: 溶剂容器ID
|
||||
"""
|
||||
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
|
||||
|
||||
# 第四步:通过数据中的试剂信息匹配
|
||||
debug_print(" 🧪 步骤1: 数据试剂信息匹配...")
|
||||
for node_id in G.nodes():
|
||||
debug_print(f"查找 id {node_id}, type={G.nodes[node_id].get('type')}, data={G.nodes[node_id].get('data', {})} 的容器...")
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
vessel_data = G.nodes[node_id].get('data', {})
|
||||
|
||||
# 检查 data 中的 reagent_name 字段
|
||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||
if reagent_name and solvent.lower() == reagent_name:
|
||||
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
|
||||
return node_id
|
||||
|
||||
# 检查 data 中的液体信息
|
||||
liquids = vessel_data.get('liquid', []) or vessel_data.get('liquids', [])
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||
|
||||
if solvent.lower() == liquid_type or solvent.lower() in liquid_type:
|
||||
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
|
||||
return node_id
|
||||
|
||||
# 构建可能的容器名称
|
||||
possible_names = [
|
||||
f"flask_{solvent}",
|
||||
f"bottle_{solvent}",
|
||||
f"reagent_{solvent}",
|
||||
f"reagent_bottle_{solvent}",
|
||||
f"{solvent}_flask",
|
||||
f"{solvent}_bottle",
|
||||
f"{solvent}",
|
||||
f"vessel_{solvent}",
|
||||
]
|
||||
|
||||
debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝")
|
||||
|
||||
# 第一步:通过容器名称匹配
|
||||
debug_print(" 🎯 步骤2: 精确名称匹配...")
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配(节点ID和名称)
|
||||
debug_print(" 🔍 步骤3: 模糊名称匹配...")
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
||||
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过配置中的试剂信息匹配
|
||||
debug_print(" 🧪 步骤4: 配置试剂信息匹配...")
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
# 检查 config 中的 reagent 字段
|
||||
node_config = G.nodes[node_id].get('config', {})
|
||||
config_reagent = node_config.get('reagent', '').lower()
|
||||
|
||||
if config_reagent and solvent.lower() == config_reagent:
|
||||
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
|
||||
return node_id
|
||||
|
||||
# 第五步:部分匹配(如果前面都没找到)
|
||||
debug_print(" 🔍 步骤5: 部分匹配...")
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_config = G.nodes[node_id].get('config', {})
|
||||
node_data = G.nodes[node_id].get('data', {})
|
||||
node_name = G.nodes[node_id].get('name', '').lower()
|
||||
|
||||
config_reagent = node_config.get('reagent', '').lower()
|
||||
data_reagent = node_data.get('reagent_name', '').lower()
|
||||
|
||||
# 检查是否包含溶剂名称
|
||||
if (solvent.lower() in config_reagent or
|
||||
solvent.lower() in data_reagent or
|
||||
solvent.lower() in node_name or
|
||||
solvent.lower() in node_id.lower()):
|
||||
debug_print(f" 🎉 通过部分匹配找到容器: {node_id} ✨")
|
||||
debug_print(f" - 节点名称: {node_name}")
|
||||
debug_print(f" - 配置试剂: {config_reagent}")
|
||||
debug_print(f" - 数据试剂: {data_reagent}")
|
||||
return node_id
|
||||
|
||||
# 调试信息:列出所有容器
|
||||
debug_print(" 🔎 调试信息:列出所有容器...")
|
||||
container_list = []
|
||||
for node_id in G.nodes():
|
||||
if G.nodes[node_id].get('type') == 'container':
|
||||
node_config = G.nodes[node_id].get('config', {})
|
||||
node_data = G.nodes[node_id].get('data', {})
|
||||
node_name = G.nodes[node_id].get('name', '')
|
||||
|
||||
container_info = {
|
||||
'id': node_id,
|
||||
'name': node_name,
|
||||
'config_reagent': node_config.get('reagent', ''),
|
||||
'data_reagent': node_data.get('reagent_name', '')
|
||||
}
|
||||
container_list.append(container_info)
|
||||
debug_print(
|
||||
f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
|
||||
|
||||
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
|
||||
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
|
||||
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
|
||||
|
||||
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
|
||||
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||
"""查找连接到指定容器的搅拌器"""
|
||||
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
|
||||
|
||||
stirrer_nodes = []
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class', '').lower()
|
||||
if 'stirrer' in node_class:
|
||||
stirrer_nodes.append(node)
|
||||
debug_print(f"📋 发现搅拌器: {node}")
|
||||
|
||||
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
|
||||
|
||||
# 查找连接到容器的搅拌器
|
||||
for stirrer in stirrer_nodes:
|
||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
|
||||
return stirrer
|
||||
|
||||
# 返回第一个搅拌器
|
||||
if stirrer_nodes:
|
||||
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
|
||||
return stirrer_nodes[0]
|
||||
|
||||
debug_print(f"❌ 未找到任何搅拌器")
|
||||
return ""
|
||||
|
||||
|
||||
def find_solid_dispenser(G: nx.DiGraph) -> str:
|
||||
"""查找固体加样器"""
|
||||
debug_print(f"🔍 查找固体加样器...")
|
||||
|
||||
for node in G.nodes():
|
||||
node_class = G.nodes[node].get('class', '').lower()
|
||||
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
|
||||
debug_print(f"✅ 找到固体加样器: {node} 🥄")
|
||||
return node
|
||||
|
||||
debug_print(f"❌ 未找到固体加样器")
|
||||
return ""
|
||||
@@ -1,216 +1,548 @@
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .utils.unit_parser import parse_time_input, parse_volume_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
logger.info(f"[WASH_SOLID] {message}")
|
||||
|
||||
|
||||
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""查找溶剂源(精简版)"""
|
||||
debug_print(f"🔍 查找溶剂源: {solvent}")
|
||||
|
||||
# 简化搜索列表
|
||||
search_patterns = [
|
||||
f"flask_{solvent}", f"bottle_{solvent}", f"reagent_{solvent}",
|
||||
"liquid_reagent_bottle_1", "flask_1", "solvent_bottle"
|
||||
]
|
||||
|
||||
for pattern in search_patterns:
|
||||
if pattern in G.nodes():
|
||||
debug_print(f"🎉 找到溶剂源: {pattern}")
|
||||
return pattern
|
||||
|
||||
debug_print(f"⚠️ 使用默认溶剂源: flask_{solvent}")
|
||||
return f"flask_{solvent}"
|
||||
|
||||
def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
|
||||
"""查找滤液容器(精简版)"""
|
||||
debug_print(f"🔍 查找滤液容器: {filtrate_vessel}")
|
||||
|
||||
# 如果指定了且存在,直接使用
|
||||
if filtrate_vessel and filtrate_vessel in G.nodes():
|
||||
debug_print(f"✅ 使用指定容器: {filtrate_vessel}")
|
||||
return filtrate_vessel
|
||||
|
||||
# 简化搜索列表
|
||||
default_vessels = ["waste_workup", "filtrate_vessel", "flask_1", "collection_bottle_1"]
|
||||
|
||||
for vessel in default_vessels:
|
||||
if vessel in G.nodes():
|
||||
debug_print(f"🎉 找到滤液容器: {vessel}")
|
||||
return vessel
|
||||
|
||||
debug_print(f"⚠️ 使用默认滤液容器: waste_workup")
|
||||
return "waste_workup"
|
||||
|
||||
def extract_vessel_id(vessel: Union[str, dict]) -> str:
|
||||
"""
|
||||
从vessel参数中提取vessel_id
|
||||
|
||||
Args:
|
||||
vessel: vessel字典或vessel_id字符串
|
||||
|
||||
Returns:
|
||||
str: vessel_id
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
vessel_id = list(vessel.values())[0].get("id", "")
|
||||
debug_print(f"🔧 从vessel字典提取ID: {vessel_id}")
|
||||
return vessel_id
|
||||
elif isinstance(vessel, str):
|
||||
debug_print(f"🔧 vessel参数为字符串: {vessel}")
|
||||
return vessel
|
||||
else:
|
||||
debug_print(f"⚠️ 无效的vessel参数类型: {type(vessel)}")
|
||||
return ""
|
||||
|
||||
def get_vessel_display_info(vessel: Union[str, dict]) -> str:
|
||||
"""
|
||||
获取容器的显示信息(用于日志)
|
||||
|
||||
Args:
|
||||
vessel: vessel字典或vessel_id字符串
|
||||
|
||||
Returns:
|
||||
str: 显示信息
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
vessel_id = vessel.get("id", "unknown")
|
||||
vessel_name = vessel.get("name", "")
|
||||
if vessel_name:
|
||||
return f"{vessel_id} ({vessel_name})"
|
||||
else:
|
||||
return vessel_id
|
||||
else:
|
||||
return str(vessel)
|
||||
|
||||
def get_vessel_liquid_volume(vessel: dict) -> float:
|
||||
"""
|
||||
获取容器中的液体体积 - 支持vessel字典
|
||||
|
||||
Args:
|
||||
vessel: 容器字典
|
||||
|
||||
Returns:
|
||||
float: 液体体积(mL)
|
||||
"""
|
||||
if not vessel or "data" not in vessel:
|
||||
debug_print(f"⚠️ 容器数据为空,返回 0.0mL")
|
||||
return 0.0
|
||||
|
||||
vessel_data = vessel["data"]
|
||||
vessel_id = vessel.get("id", "unknown")
|
||||
|
||||
debug_print(f"🔍 读取容器 '{vessel_id}' 体积数据: {vessel_data}")
|
||||
|
||||
# 检查liquid_volume字段
|
||||
if "liquid_volume" in vessel_data:
|
||||
liquid_volume = vessel_data["liquid_volume"]
|
||||
|
||||
# 处理列表格式
|
||||
if isinstance(liquid_volume, list):
|
||||
if len(liquid_volume) > 0:
|
||||
volume = liquid_volume[0]
|
||||
if isinstance(volume, (int, float)):
|
||||
debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (列表格式)")
|
||||
return float(volume)
|
||||
|
||||
# 处理直接数值格式
|
||||
elif isinstance(liquid_volume, (int, float)):
|
||||
debug_print(f"✅ 容器 '{vessel_id}' 体积: {liquid_volume}mL (数值格式)")
|
||||
return float(liquid_volume)
|
||||
|
||||
# 检查其他可能的体积字段
|
||||
volume_keys = ['current_volume', 'total_volume', 'volume']
|
||||
for key in volume_keys:
|
||||
if key in vessel_data:
|
||||
try:
|
||||
volume = float(vessel_data[key])
|
||||
if volume > 0:
|
||||
debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (字段: {key})")
|
||||
return volume
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
debug_print(f"⚠️ 无法获取容器 '{vessel_id}' 的体积,返回默认值 0.0mL")
|
||||
return 0.0
|
||||
|
||||
def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, description: str = "") -> None:
|
||||
"""
|
||||
更新容器体积(同时更新vessel字典和图节点)
|
||||
|
||||
Args:
|
||||
vessel: 容器字典
|
||||
G: 网络图
|
||||
new_volume: 新体积
|
||||
description: 更新描述
|
||||
"""
|
||||
vessel_id = vessel.get("id", "unknown")
|
||||
|
||||
if description:
|
||||
debug_print(f"🔧 更新容器体积 - {description}")
|
||||
|
||||
# 更新vessel字典中的体积
|
||||
if "data" in vessel:
|
||||
if "liquid_volume" in vessel["data"]:
|
||||
current_volume = vessel["data"]["liquid_volume"]
|
||||
if isinstance(current_volume, list):
|
||||
if len(current_volume) > 0:
|
||||
vessel["data"]["liquid_volume"][0] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = [new_volume]
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
else:
|
||||
vessel["data"]["liquid_volume"] = new_volume
|
||||
else:
|
||||
vessel["data"] = {"liquid_volume": new_volume}
|
||||
|
||||
# 同时更新图中的容器数据
|
||||
if vessel_id in G.nodes():
|
||||
if 'data' not in G.nodes[vessel_id]:
|
||||
G.nodes[vessel_id]['data'] = {}
|
||||
|
||||
vessel_node_data = G.nodes[vessel_id]['data']
|
||||
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
|
||||
|
||||
if isinstance(current_node_volume, list):
|
||||
if len(current_node_volume) > 0:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'][0] = new_volume
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume]
|
||||
else:
|
||||
G.nodes[vessel_id]['data']['liquid_volume'] = new_volume
|
||||
|
||||
debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
|
||||
|
||||
def generate_wash_solid_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: str,
|
||||
vessel: Union[str, dict], # 🔧 修改:支持vessel字典
|
||||
solvent: str,
|
||||
volume: float,
|
||||
filtrate_vessel: str = "",
|
||||
volume: Union[float, str] = "50",
|
||||
filtrate_vessel: Union[str, dict] = "", # 🔧 修改:支持vessel字典
|
||||
temp: float = 25.0,
|
||||
stir: bool = False,
|
||||
stir_speed: float = 0.0,
|
||||
time: float = 0.0,
|
||||
repeats: int = 1
|
||||
time: Union[str, float] = "0",
|
||||
repeats: int = 1,
|
||||
volume_spec: str = "",
|
||||
repeats_spec: str = "",
|
||||
mass: str = "",
|
||||
event: str = "",
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成固体清洗的协议序列
|
||||
生成固体清洗协议 - 支持vessel字典和体积运算
|
||||
|
||||
Args:
|
||||
G: 有向图,节点为设备和容器
|
||||
vessel: 装有固体物质的容器名称
|
||||
solvent: 用于清洗固体的溶剂名称
|
||||
volume: 清洗溶剂的体积
|
||||
filtrate_vessel: 滤液要收集到的容器名称,可选参数
|
||||
temp: 清洗时的温度,可选参数
|
||||
stir: 是否在清洗过程中搅拌,默认为 False
|
||||
stir_speed: 搅拌速度,可选参数
|
||||
time: 清洗的时间,可选参数
|
||||
repeats: 清洗操作的重复次数,默认为 1
|
||||
G: 有向图,节点为设备和容器,边为流体管道
|
||||
vessel: 清洗容器字典(从XDL传入)或容器ID字符串
|
||||
solvent: 清洗溶剂名称
|
||||
volume: 溶剂体积(每次清洗)
|
||||
filtrate_vessel: 滤液收集容器字典或容器ID字符串
|
||||
temp: 清洗温度(°C)
|
||||
stir: 是否搅拌
|
||||
stir_speed: 搅拌速度(RPM)
|
||||
time: 搅拌时间
|
||||
repeats: 清洗重复次数
|
||||
volume_spec: 体积规格(small/medium/large)
|
||||
repeats_spec: 重复次数规格(few/several/many)
|
||||
mass: 固体质量(用于计算溶剂用量)
|
||||
event: 事件描述
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
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
|
||||
)
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从vessel参数中提取vessel_id
|
||||
vessel_id = extract_vessel_id(vessel)
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
|
||||
# 🔧 处理filtrate_vessel参数
|
||||
filtrate_vessel_id = extract_vessel_id(filtrate_vessel) if filtrate_vessel else ""
|
||||
|
||||
debug_print("🧼" * 20)
|
||||
debug_print("🚀 开始生成固体清洗协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel_display} (ID: {vessel_id})")
|
||||
debug_print(f" 🧪 solvent: {solvent}")
|
||||
debug_print(f" 💧 volume: {volume}")
|
||||
debug_print(f" 🗑️ filtrate_vessel: {filtrate_vessel_id}")
|
||||
debug_print(f" ⏰ time: {time}")
|
||||
debug_print(f" 🔄 repeats: {repeats}")
|
||||
debug_print("🧼" * 20)
|
||||
|
||||
# 🔧 新增:记录清洗前的容器状态
|
||||
debug_print("🔍 记录清洗前容器状态...")
|
||||
if isinstance(vessel, dict):
|
||||
original_volume = get_vessel_liquid_volume(vessel)
|
||||
debug_print(f"📊 清洗前液体体积: {original_volume:.2f}mL")
|
||||
else:
|
||||
original_volume = 0.0
|
||||
debug_print(f"📊 vessel为字符串格式,无法获取体积信息")
|
||||
|
||||
# 📋 快速验证
|
||||
if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id
|
||||
debug_print("❌ 容器验证失败! 😱")
|
||||
raise ValueError("vessel 参数无效")
|
||||
|
||||
if not solvent:
|
||||
debug_print("❌ 溶剂不能为空! 😱")
|
||||
raise ValueError("solvent 参数不能为空")
|
||||
|
||||
debug_print("✅ 基础验证通过 🎯")
|
||||
|
||||
# 🔄 参数解析
|
||||
debug_print("📍 步骤1: 参数解析... ⚡")
|
||||
final_volume = parse_volume_input(volume, volume_spec, mass)
|
||||
final_time = parse_time_input(time)
|
||||
|
||||
# 重复次数处理(简化)
|
||||
if repeats_spec:
|
||||
spec_map = {'few': 2, 'several': 3, 'many': 4, 'thorough': 5}
|
||||
final_repeats = next((v for k, v in spec_map.items() if k in repeats_spec.lower()), repeats)
|
||||
else:
|
||||
final_repeats = max(1, min(repeats, 5)) # 限制1-5次
|
||||
|
||||
# 🕐 模拟时间优化
|
||||
debug_print(" ⏱️ 模拟时间优化...")
|
||||
original_time = final_time
|
||||
if final_time > 60.0:
|
||||
final_time = 60.0 # 限制最长60秒
|
||||
debug_print(f" 🎮 时间优化: {original_time}s → {final_time}s ⚡")
|
||||
|
||||
# 参数修正
|
||||
temp = max(25.0, min(temp, 80.0)) # 温度范围25-80°C
|
||||
stir_speed = max(0.0, min(stir_speed, 300.0)) if stir else 0.0 # 速度范围0-300
|
||||
|
||||
debug_print(f"🎯 最终参数: 体积={final_volume}mL, 时间={final_time}s, 重复={final_repeats}次")
|
||||
|
||||
# 🔍 查找设备
|
||||
debug_print("📍 步骤2: 查找设备... 🔍")
|
||||
try:
|
||||
solvent_source = find_solvent_source(G, solvent)
|
||||
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel_id)
|
||||
debug_print(f"🎉 设备配置完成 ✨")
|
||||
debug_print(f" 🧪 溶剂源: {solvent_source}")
|
||||
debug_print(f" 🗑️ 滤液容器: {actual_filtrate_vessel}")
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 设备查找失败: {str(e)} 😭")
|
||||
raise ValueError(f"设备查找失败: {str(e)}")
|
||||
|
||||
# 🚀 生成动作序列
|
||||
debug_print("📍 步骤3: 生成清洗动作... 🧼")
|
||||
action_sequence = []
|
||||
|
||||
# 验证容器是否存在
|
||||
if vessel not in G.nodes():
|
||||
raise ValueError(f"固体容器 {vessel} 不存在于图中")
|
||||
# 🔧 新增:体积变化跟踪变量
|
||||
current_volume = original_volume
|
||||
total_solvent_used = 0.0
|
||||
|
||||
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
|
||||
for cycle in range(final_repeats):
|
||||
debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...")
|
||||
|
||||
# 步骤1:如果需要加热,先设置温度
|
||||
if temp > 25.0 and heatchill_id:
|
||||
action_sequence.append({
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
# 1. 转移溶剂
|
||||
try:
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
debug_print(f" 💧 添加溶剂: {final_volume}mL {solvent}")
|
||||
transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_source,
|
||||
to_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
volume=final_volume,
|
||||
amount="",
|
||||
time=0.0,
|
||||
viscous=False,
|
||||
rinsing_solvent="",
|
||||
rinsing_volume=0.0,
|
||||
rinsing_repeats=0,
|
||||
solid=False,
|
||||
flowrate=2.5,
|
||||
transfer_flowrate=0.5
|
||||
)
|
||||
|
||||
if transfer_actions:
|
||||
action_sequence.extend(transfer_actions)
|
||||
debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚")
|
||||
|
||||
# 🔧 新增:更新体积 - 添加溶剂后
|
||||
current_volume += final_volume
|
||||
total_solvent_used += final_volume
|
||||
|
||||
if isinstance(vessel, dict):
|
||||
update_vessel_volume(vessel, G, current_volume,
|
||||
f"第{cycle+1}次清洗添加{final_volume}mL溶剂后")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f" ❌ 转移失败: {str(e)} 😞")
|
||||
|
||||
# 2. 搅拌(如果需要)
|
||||
if stir and final_time > 0:
|
||||
debug_print(f" 🌪️ 搅拌: {final_time}s @ {stir_speed}RPM")
|
||||
stir_action = {
|
||||
"device_id": "stirrer_1",
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"temp": temp,
|
||||
"purpose": f"固体清洗 - 第 {repeat_num} 次"
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"time": str(time),
|
||||
"stir_time": final_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": 10.0 # 🕐 缩短沉降时间
|
||||
}
|
||||
})
|
||||
|
||||
# 步骤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
|
||||
}
|
||||
action_sequence.append(stir_action)
|
||||
debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️")
|
||||
|
||||
# 3. 过滤
|
||||
debug_print(f" 🌊 过滤到: {actual_filtrate_vessel}")
|
||||
filter_action = {
|
||||
"device_id": "filter_1",
|
||||
"action_name": "filter",
|
||||
"action_kwargs": {
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"filtrate_vessel": actual_filtrate_vessel,
|
||||
"temp": temp,
|
||||
"volume": final_volume
|
||||
}
|
||||
}
|
||||
action_sequence.append(filter_action)
|
||||
debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊")
|
||||
|
||||
# 🔧 新增:更新体积 - 过滤后(液体被滤除)
|
||||
# 假设滤液完全被移除,固体残留在容器中
|
||||
filtered_volume = current_volume * 0.9 # 假设90%的液体被过滤掉
|
||||
current_volume = current_volume - filtered_volume
|
||||
|
||||
if isinstance(vessel, dict):
|
||||
update_vessel_volume(vessel, G, current_volume,
|
||||
f"第{cycle+1}次清洗过滤后")
|
||||
|
||||
# 4. 等待(缩短时间)
|
||||
wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": wait_time}
|
||||
})
|
||||
|
||||
# 步骤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
|
||||
}
|
||||
})
|
||||
debug_print(f" ✅ 等待: {wait_time}s ⏰")
|
||||
|
||||
return action_sequence
|
||||
# 🔧 新增:清洗完成后的最终状态报告
|
||||
if isinstance(vessel, dict):
|
||||
final_volume_vessel = get_vessel_liquid_volume(vessel)
|
||||
else:
|
||||
final_volume_vessel = current_volume
|
||||
|
||||
# 🎊 总结
|
||||
debug_print("🧼" * 20)
|
||||
debug_print(f"🎉 固体清洗协议生成完成! ✨")
|
||||
debug_print(f"📊 协议统计:")
|
||||
debug_print(f" 📋 总动作数: {len(action_sequence)} 个")
|
||||
debug_print(f" 🥽 清洗容器: {vessel_display}")
|
||||
debug_print(f" 🧪 使用溶剂: {solvent}")
|
||||
debug_print(f" 💧 单次体积: {final_volume}mL")
|
||||
debug_print(f" 🔄 清洗次数: {final_repeats}次")
|
||||
debug_print(f" 💧 总溶剂用量: {total_solvent_used:.2f}mL")
|
||||
debug_print(f"📊 体积变化统计:")
|
||||
debug_print(f" - 清洗前体积: {original_volume:.2f}mL")
|
||||
debug_print(f" - 清洗后体积: {final_volume_vessel:.2f}mL")
|
||||
debug_print(f" - 溶剂总用量: {total_solvent_used:.2f}mL")
|
||||
debug_print(f"⏱️ 预计总时间: {(final_time + 5) * final_repeats / 60:.1f} 分钟")
|
||||
debug_print("🧼" * 20)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# 🔧 新增:便捷函数
|
||||
def wash_with_water(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
volume: Union[float, str] = "50",
|
||||
repeats: int = 2) -> List[Dict[str, Any]]:
|
||||
"""用水清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"💧 水洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, "water", volume=volume, repeats=repeats)
|
||||
|
||||
def wash_with_ethanol(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
volume: Union[float, str] = "30",
|
||||
repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""用乙醇清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🍺 乙醇洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, "ethanol", volume=volume, repeats=repeats)
|
||||
|
||||
def wash_with_acetone(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
volume: Union[float, str] = "25",
|
||||
repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""用丙酮清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"💨 丙酮洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, "acetone", volume=volume, repeats=repeats)
|
||||
|
||||
def wash_with_ether(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
volume: Union[float, str] = "40",
|
||||
repeats: int = 2) -> List[Dict[str, Any]]:
|
||||
"""用乙醚清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🌬️ 乙醚洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, "diethyl_ether", volume=volume, repeats=repeats)
|
||||
|
||||
def wash_with_cold_solvent(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvent: str, volume: Union[float, str] = "30",
|
||||
repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""用冷溶剂清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"❄️ 冷{solvent}洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, solvent, volume=volume,
|
||||
temp=5.0, repeats=repeats)
|
||||
|
||||
def wash_with_hot_solvent(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvent: str, volume: Union[float, str] = "50",
|
||||
repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""用热溶剂清洗固体"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🔥 热{solvent}洗固体: {vessel_display} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, solvent, volume=volume,
|
||||
temp=60.0, repeats=repeats)
|
||||
|
||||
def wash_with_stirring(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvent: str, volume: Union[float, str] = "50",
|
||||
stir_time: Union[str, float] = "5 min",
|
||||
repeats: int = 1) -> List[Dict[str, Any]]:
|
||||
"""带搅拌的溶剂清洗"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🌪️ 搅拌清洗: {vessel_display} with {solvent} ({repeats} 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, solvent, volume=volume,
|
||||
stir=True, stir_speed=200.0,
|
||||
time=stir_time, repeats=repeats)
|
||||
|
||||
def thorough_wash(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvent: str, volume: Union[float, str] = "50") -> List[Dict[str, Any]]:
|
||||
"""彻底清洗(多次重复)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"🔄 彻底清洗: {vessel_display} with {solvent} (5 次)")
|
||||
return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, repeats=5)
|
||||
|
||||
def quick_rinse(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvent: str, volume: Union[float, str] = "20") -> List[Dict[str, Any]]:
|
||||
"""快速冲洗(单次,小体积)"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"⚡ 快速冲洗: {vessel_display} with {solvent}")
|
||||
return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, repeats=1)
|
||||
|
||||
def sequential_wash(G: nx.DiGraph, vessel: Union[str, dict],
|
||||
solvents: list, volume: Union[float, str] = "40") -> List[Dict[str, Any]]:
|
||||
"""连续多溶剂清洗"""
|
||||
vessel_display = get_vessel_display_info(vessel)
|
||||
debug_print(f"📝 连续清洗: {vessel_display} with {' → '.join(solvents)}")
|
||||
|
||||
action_sequence = []
|
||||
for solvent in solvents:
|
||||
wash_actions = generate_wash_solid_protocol(G, vessel, solvent,
|
||||
volume=volume, repeats=1)
|
||||
action_sequence.extend(wash_actions)
|
||||
|
||||
return action_sequence
|
||||
|
||||
# 测试函数
|
||||
def test_wash_solid_protocol():
|
||||
"""测试固体清洗协议"""
|
||||
debug_print("🧪 === WASH SOLID PROTOCOL 测试 === ✨")
|
||||
|
||||
# 测试vessel参数处理
|
||||
debug_print("🔧 测试vessel参数处理...")
|
||||
|
||||
# 测试字典格式
|
||||
vessel_dict = {"id": "filter_flask_1", "name": "过滤瓶1",
|
||||
"data": {"liquid_volume": 25.0}}
|
||||
vessel_id = extract_vessel_id(vessel_dict)
|
||||
vessel_display = get_vessel_display_info(vessel_dict)
|
||||
volume = get_vessel_liquid_volume(vessel_dict)
|
||||
debug_print(f" 字典格式: {vessel_dict}")
|
||||
debug_print(f" → ID: {vessel_id}, 显示: {vessel_display}, 体积: {volume}mL")
|
||||
|
||||
# 测试字符串格式
|
||||
vessel_str = "filter_flask_2"
|
||||
vessel_id = extract_vessel_id(vessel_str)
|
||||
vessel_display = get_vessel_display_info(vessel_str)
|
||||
debug_print(f" 字符串格式: {vessel_str}")
|
||||
debug_print(f" → ID: {vessel_id}, 显示: {vessel_display}")
|
||||
|
||||
debug_print("✅ 测试完成 🎉")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_wash_solid_protocol()
|
||||
@@ -1,39 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
# 定义配置变量和加载函数
|
||||
import base64
|
||||
import traceback
|
||||
import os
|
||||
import importlib.util
|
||||
from typing import Optional
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
class BasicConfig:
|
||||
ENV = "pro" # 'test'
|
||||
ak = ""
|
||||
sk = ""
|
||||
working_dir = ""
|
||||
config_path = ""
|
||||
is_host_mode = True
|
||||
slave_no_host = False # 是否跳过rclient.wait_for_service()
|
||||
upload_registry = False
|
||||
machine_name = "undefined"
|
||||
vis_2d_enable = False
|
||||
enable_resource_load = True
|
||||
communication_protocol = "websocket"
|
||||
|
||||
@classmethod
|
||||
def auth_secret(cls):
|
||||
if not cls.ak or not cls.sk:
|
||||
return ""
|
||||
target = f"{cls.ak}:{cls.sk}"
|
||||
base64_target = base64.b64encode(target.encode("utf-8")).decode("utf-8")
|
||||
return base64_target
|
||||
|
||||
|
||||
# MQTT配置
|
||||
class MQConfig:
|
||||
lab_id = ""
|
||||
instance_id = ""
|
||||
access_key = ""
|
||||
secret_key = ""
|
||||
group_id = ""
|
||||
broker_url = ""
|
||||
port = 1883
|
||||
ca_content = ""
|
||||
cert_content = ""
|
||||
key_content = ""
|
||||
|
||||
# 指定
|
||||
ca_file = "" # 相对config.py所在目录的路径
|
||||
cert_file = "" # 相对config.py所在目录的路径
|
||||
key_file = "" # 相对config.py所在目录的路径
|
||||
# WebSocket配置
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
|
||||
# OSS上传配置
|
||||
@@ -70,47 +69,15 @@ def _update_config_from_module(module):
|
||||
for attr in dir(getattr(module, name)):
|
||||
if not attr.startswith("_"):
|
||||
setattr(obj, attr, getattr(getattr(module, name), attr))
|
||||
# 更新OSS认证
|
||||
if len(OSSUploadConfig.authorization) == 0:
|
||||
OSSUploadConfig.authorization = f"lab {MQConfig.lab_id}"
|
||||
# 对 ca_file cert_file key_file 进行初始化
|
||||
if len(MQConfig.ca_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.ca_file.startswith("."):
|
||||
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.ca_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping CA file loading, ca_file is empty")
|
||||
if len(MQConfig.cert_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.cert_file.startswith("."):
|
||||
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.cert_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping cert file loading, cert_file is empty")
|
||||
if len(MQConfig.key_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.key_file.startswith("."):
|
||||
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.key_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping key file loading, key_file is empty")
|
||||
|
||||
|
||||
def _update_config_from_env():
|
||||
prefix = "UNILABOS."
|
||||
prefix = "UNILABOS_"
|
||||
for env_key, env_value in os.environ.items():
|
||||
if not env_key.startswith(prefix):
|
||||
continue
|
||||
try:
|
||||
key_path = env_key[len(prefix):] # Remove UNILAB_ prefix
|
||||
class_field = key_path.upper().split(".", 1)
|
||||
class_field = key_path.upper().split("_", 1)
|
||||
if len(class_field) != 2:
|
||||
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")
|
||||
continue
|
||||
@@ -154,16 +121,15 @@ def _update_config_from_env():
|
||||
logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}")
|
||||
|
||||
|
||||
|
||||
def load_config(config_path=None):
|
||||
# 如果提供了配置文件路径,从该文件导入配置
|
||||
if config_path:
|
||||
_update_config_from_env() # 允许config_path被env设定后读取
|
||||
env_config_path = os.environ.get("UNILABOS_BASICCONFIG_CONFIG_PATH")
|
||||
config_path = env_config_path if env_config_path else config_path
|
||||
BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path))
|
||||
if not os.path.exists(config_path):
|
||||
logger.error(f"[ENV] 配置文件 {config_path} 不存在")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
module_name = "lab_" + os.path.basename(config_path).replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, config_path)
|
||||
@@ -174,6 +140,7 @@ def load_config(config_path=None):
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
_update_config_from_module(module)
|
||||
logger.info(f"[ENV] 配置文件 {config_path} 加载成功")
|
||||
_update_config_from_env()
|
||||
except Exception as e:
|
||||
logger.error(f"[ENV] 加载配置文件 {config_path} 失败")
|
||||
traceback.print_exc()
|
||||
|
||||
12
unilabos/config/example_config.py
Normal file
12
unilabos/config/example_config.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# unilabos的配置文件
|
||||
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
|
||||
sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
|
||||
|
||||
|
||||
# WebSocket配置,一般无需调整
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
@@ -1 +0,0 @@
|
||||
from .eis_model import EISModelBasedController
|
||||
@@ -1,5 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def EISModelBasedController(eis: np.array) -> float:
|
||||
return 0.0
|
||||
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
纽扣电池组装工作站
|
||||
Coin Cell Assembly Workstation
|
||||
|
||||
继承工作站基类,实现纽扣电池特定功能
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
|
||||
from unilabos.device_comms.workstation_communication import (
|
||||
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
|
||||
)
|
||||
from unilabos.device_comms.workstation_material_management import (
|
||||
MaterialManagementBase, CoinCellMaterialManagement
|
||||
)
|
||||
from unilabos.utils.log import logger
|
||||
|
||||
|
||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"""纽扣电池组装工作站
|
||||
|
||||
基于工作站基类,实现纽扣电池制造的特定功能:
|
||||
1. 纽扣电池特定的通信协议
|
||||
2. 纽扣电池物料管理(料板、极片、电池等)
|
||||
3. 电池制造工作流
|
||||
4. 质量检查工作流
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
children: Dict[str, Dict[str, Any]],
|
||||
protocol_type: Union[str, List[str]] = "BatteryManufacturingProtocol",
|
||||
resource_tracker: Optional[DeviceNodeResourceTracker] = None,
|
||||
modbus_config: Optional[Dict[str, Any]] = None,
|
||||
deck_config: Optional[Dict[str, Any]] = None,
|
||||
csv_path: str = "./coin_cell_assembly.csv",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# 设置通信配置
|
||||
modbus_config = modbus_config or {"host": "127.0.0.1", "port": 5021}
|
||||
self.communication_config = CommunicationConfig(
|
||||
protocol=CommunicationProtocol.MODBUS_TCP,
|
||||
host=modbus_config["host"],
|
||||
port=modbus_config["port"],
|
||||
timeout=modbus_config.get("timeout", 5.0),
|
||||
retry_count=modbus_config.get("retry_count", 3)
|
||||
)
|
||||
|
||||
# 设置台面配置
|
||||
self.deck_config = deck_config or {
|
||||
"size_x": 1620.0,
|
||||
"size_y": 1270.0,
|
||||
"size_z": 500.0
|
||||
}
|
||||
|
||||
# CSV地址映射文件路径
|
||||
self.csv_path = csv_path
|
||||
|
||||
# 创建资源跟踪器(如果没有提供)
|
||||
if resource_tracker is None:
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
resource_tracker = DeviceNodeResourceTracker()
|
||||
|
||||
# 初始化基类
|
||||
super().__init__(
|
||||
device_id=device_id,
|
||||
children=children,
|
||||
protocol_type=protocol_type,
|
||||
resource_tracker=resource_tracker,
|
||||
communication_config=self.communication_config,
|
||||
deck_config=self.deck_config,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
logger.info(f"纽扣电池组装工作站 {device_id} 初始化完成")
|
||||
|
||||
def _create_communication_module(self) -> WorkstationCommunicationBase:
|
||||
"""创建纽扣电池通信模块"""
|
||||
return CoinCellCommunication(
|
||||
communication_config=self.communication_config,
|
||||
csv_path=self.csv_path
|
||||
)
|
||||
|
||||
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||
"""创建纽扣电池物料管理模块"""
|
||||
return CoinCellMaterialManagement(
|
||||
device_id=self.device_id,
|
||||
deck_config=self.deck_config,
|
||||
resource_tracker=self.resource_tracker,
|
||||
children_config=self.children
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
"""注册纽扣电池工作流"""
|
||||
# 电池制造工作流
|
||||
self.supported_workflows["battery_manufacturing"] = WorkflowInfo(
|
||||
name="battery_manufacturing",
|
||||
description="纽扣电池制造工作流",
|
||||
estimated_duration=300.0, # 5分钟
|
||||
required_materials=["cathode_sheet", "anode_sheet", "separator", "electrolyte"],
|
||||
output_product="coin_cell_battery",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"electrolyte_num": {
|
||||
"type": "integer",
|
||||
"description": "电解液瓶数",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"electrolyte_volume": {
|
||||
"type": "number",
|
||||
"description": "电解液体积 (μL)",
|
||||
"minimum": 0.1,
|
||||
"maximum": 100.0
|
||||
},
|
||||
"assembly_pressure": {
|
||||
"type": "number",
|
||||
"description": "组装压力 (N)",
|
||||
"minimum": 100.0,
|
||||
"maximum": 5000.0
|
||||
},
|
||||
"cathode_material": {
|
||||
"type": "string",
|
||||
"description": "正极材料类型",
|
||||
"enum": ["LiFePO4", "LiCoO2", "NCM", "LMO"]
|
||||
},
|
||||
"anode_material": {
|
||||
"type": "string",
|
||||
"description": "负极材料类型",
|
||||
"enum": ["Graphite", "LTO", "Silicon"]
|
||||
}
|
||||
},
|
||||
"required": ["electrolyte_num", "electrolyte_volume", "assembly_pressure"]
|
||||
}
|
||||
)
|
||||
|
||||
# 质量检查工作流
|
||||
self.supported_workflows["quality_inspection"] = WorkflowInfo(
|
||||
name="quality_inspection",
|
||||
description="产品质量检查工作流",
|
||||
estimated_duration=60.0, # 1分钟
|
||||
required_materials=["finished_battery"],
|
||||
output_product="quality_report",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"test_voltage": {
|
||||
"type": "boolean",
|
||||
"description": "是否测试电压",
|
||||
"default": True
|
||||
},
|
||||
"test_capacity": {
|
||||
"type": "boolean",
|
||||
"description": "是否测试容量",
|
||||
"default": False
|
||||
},
|
||||
"voltage_threshold": {
|
||||
"type": "number",
|
||||
"description": "电压阈值 (V)",
|
||||
"minimum": 2.0,
|
||||
"maximum": 4.5,
|
||||
"default": 3.0
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# 设备初始化工作流
|
||||
self.supported_workflows["device_initialization"] = WorkflowInfo(
|
||||
name="device_initialization",
|
||||
description="设备初始化工作流",
|
||||
estimated_duration=30.0, # 30秒
|
||||
required_materials=[],
|
||||
output_product="ready_status",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_mode": {
|
||||
"type": "boolean",
|
||||
"description": "是否启用自动模式",
|
||||
"default": True
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# ============ 纽扣电池特定方法 ============
|
||||
|
||||
def get_electrode_sheet_inventory(self) -> Dict[str, int]:
|
||||
"""获取极片库存统计"""
|
||||
try:
|
||||
sheets = self.material_management.find_electrode_sheets()
|
||||
inventory = {}
|
||||
|
||||
for sheet in sheets:
|
||||
material_type = getattr(sheet, 'material_type', 'unknown')
|
||||
inventory[material_type] = inventory.get(material_type, 0) + 1
|
||||
|
||||
return inventory
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取极片库存失败: {e}")
|
||||
return {}
|
||||
|
||||
def get_battery_production_statistics(self) -> Dict[str, Any]:
|
||||
"""获取电池生产统计"""
|
||||
try:
|
||||
production_data = self.communication.get_production_data()
|
||||
|
||||
# 添加物料统计
|
||||
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||
battery_count = len(self.material_management.find_batteries())
|
||||
|
||||
return {
|
||||
**production_data,
|
||||
"electrode_inventory": electrode_inventory,
|
||||
"finished_battery_count": battery_count,
|
||||
"material_plates": len(self.material_management.find_material_plates()),
|
||||
"press_slots": len(self.material_management.find_press_slots())
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取生产统计失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def create_new_battery(self, battery_spec: Dict[str, Any]) -> Optional[str]:
|
||||
"""创建新电池资源"""
|
||||
try:
|
||||
from unilabos.device_comms.button_battery_station import Battery
|
||||
import uuid
|
||||
|
||||
battery_id = f"battery_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
battery = Battery(
|
||||
name=battery_id,
|
||||
diameter=battery_spec.get("diameter", 20.0),
|
||||
height=battery_spec.get("height", 3.2),
|
||||
max_volume=battery_spec.get("max_volume", 100.0),
|
||||
barcode=battery_spec.get("barcode", "")
|
||||
)
|
||||
|
||||
# 添加到物料管理系统
|
||||
self.material_management.plr_resources[battery_id] = battery
|
||||
self.material_management.resource_tracker.add_resource(battery)
|
||||
|
||||
logger.info(f"创建新电池资源: {battery_id}")
|
||||
return battery_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建电池资源失败: {e}")
|
||||
return None
|
||||
|
||||
def find_available_press_slot(self) -> Optional[str]:
|
||||
"""查找可用的压制槽"""
|
||||
try:
|
||||
press_slots = self.material_management.find_press_slots()
|
||||
|
||||
for slot in press_slots:
|
||||
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||
return slot.name
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查找可用压制槽失败: {e}")
|
||||
return None
|
||||
|
||||
def get_glove_box_environment(self) -> Dict[str, Any]:
|
||||
"""获取手套箱环境数据"""
|
||||
try:
|
||||
device_status = self.communication.get_device_status()
|
||||
environment = device_status.get("environment", {})
|
||||
|
||||
return {
|
||||
"pressure": environment.get("glove_box_pressure", 0.0),
|
||||
"o2_content": environment.get("o2_content", 0.0),
|
||||
"water_content": environment.get("water_content", 0.0),
|
||||
"is_safe": (
|
||||
environment.get("o2_content", 0.0) < 10.0 and # 氧气含量 < 10ppm
|
||||
environment.get("water_content", 0.0) < 1.0 # 水分含量 < 1ppm
|
||||
)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取手套箱环境失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def start_data_export(self, file_path: str) -> bool:
|
||||
"""开始生产数据导出"""
|
||||
try:
|
||||
return self.communication.start_data_export(file_path, export_interval=5.0)
|
||||
except Exception as e:
|
||||
logger.error(f"启动数据导出失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_data_export(self) -> bool:
|
||||
"""停止生产数据导出"""
|
||||
try:
|
||||
return self.communication.stop_data_export()
|
||||
except Exception as e:
|
||||
logger.error(f"停止数据导出失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 重写基类方法以支持纽扣电池特定功能 ============
|
||||
|
||||
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||
"""启动工作流(重写以支持纽扣电池特定预处理)"""
|
||||
try:
|
||||
# 进行纽扣电池特定的预检查
|
||||
if workflow_type == "battery_manufacturing":
|
||||
# 检查手套箱环境
|
||||
env = self.get_glove_box_environment()
|
||||
if not env.get("is_safe", False):
|
||||
logger.error("手套箱环境不安全,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 检查是否有可用的压制槽
|
||||
available_slot = self.find_available_press_slot()
|
||||
if not available_slot:
|
||||
logger.error("没有可用的压制槽,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 检查极片库存
|
||||
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||
if not electrode_inventory.get("cathode", 0) > 0 or not electrode_inventory.get("anode", 0) > 0:
|
||||
logger.error("极片库存不足,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 调用基类方法
|
||||
return super().start_workflow(workflow_type, parameters)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动纽扣电池工作流失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 纽扣电池特定状态属性 ============
|
||||
|
||||
@property
|
||||
def electrode_sheet_count(self) -> int:
|
||||
"""极片总数"""
|
||||
try:
|
||||
return len(self.material_management.find_electrode_sheets())
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def battery_count(self) -> int:
|
||||
"""电池总数"""
|
||||
try:
|
||||
return len(self.material_management.find_batteries())
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def available_press_slots(self) -> int:
|
||||
"""可用压制槽数"""
|
||||
try:
|
||||
press_slots = self.material_management.find_press_slots()
|
||||
available = 0
|
||||
for slot in press_slots:
|
||||
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||
available += 1
|
||||
return available
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def environment_status(self) -> Dict[str, Any]:
|
||||
"""环境状态"""
|
||||
return self.get_glove_box_environment()
|
||||
|
||||
|
||||
# ============ 工厂函数 ============
|
||||
|
||||
def create_coin_cell_workstation(
|
||||
device_id: str,
|
||||
config_file: str,
|
||||
modbus_host: str = "127.0.0.1",
|
||||
modbus_port: int = 5021,
|
||||
csv_path: str = "./coin_cell_assembly.csv"
|
||||
) -> CoinCellAssemblyWorkstation:
|
||||
"""工厂函数:创建纽扣电池组装工作站
|
||||
|
||||
Args:
|
||||
device_id: 设备ID
|
||||
config_file: 配置文件路径(JSON格式)
|
||||
modbus_host: Modbus主机地址
|
||||
modbus_port: Modbus端口
|
||||
csv_path: 地址映射CSV文件路径
|
||||
|
||||
Returns:
|
||||
CoinCellAssemblyWorkstation: 工作站实例
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
# 加载配置文件
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 提取配置
|
||||
children = config.get("children", {})
|
||||
deck_config = config.get("deck_config", {})
|
||||
|
||||
# 创建工作站
|
||||
workstation = CoinCellAssemblyWorkstation(
|
||||
device_id=device_id,
|
||||
children=children,
|
||||
modbus_config={
|
||||
"host": modbus_host,
|
||||
"port": modbus_port
|
||||
},
|
||||
deck_config=deck_config,
|
||||
csv_path=csv_path
|
||||
)
|
||||
|
||||
logger.info(f"纽扣电池工作站创建成功: {device_id}")
|
||||
return workstation
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建纽扣电池工作站失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例用法
|
||||
workstation = create_coin_cell_workstation(
|
||||
device_id="coin_cell_station_01",
|
||||
config_file="./button_battery_workstation.json",
|
||||
modbus_host="127.0.0.1",
|
||||
modbus_port=5021
|
||||
)
|
||||
|
||||
# 启动电池制造工作流
|
||||
success = workstation.start_workflow(
|
||||
"battery_manufacturing",
|
||||
{
|
||||
"electrolyte_num": 16,
|
||||
"electrolyte_volume": 50.0,
|
||||
"assembly_pressure": 2000.0,
|
||||
"cathode_material": "LiFePO4",
|
||||
"anode_material": "Graphite"
|
||||
}
|
||||
)
|
||||
|
||||
if success:
|
||||
print("电池制造工作流启动成功")
|
||||
else:
|
||||
print("电池制造工作流启动失败")
|
||||
@@ -8,8 +8,8 @@ from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
||||
from pymodbus.framer import FramerType
|
||||
from typing import TypedDict
|
||||
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.modbus_plc.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||
from unilabos.utils.log import logger
|
||||
import pandas as pd
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil, HoldRegister
|
||||
from unilabos.device_comms.modbus_plc.modbus import Coil, HoldRegister
|
||||
from pymodbus.payload import BinaryPayloadDecoder
|
||||
from pymodbus.constants import Endian
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# coding=utf-8
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil
|
||||
from unilabos.device_comms.modbus_plc.modbus import Coil
|
||||
import time
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusWorkflow, WorkflowAction, load_csv
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||
|
||||
############ 第一种写法 ##############
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Default initial positions for full_dev's ros2_control fake system
|
||||
|
||||
initial_positions:
|
||||
arm_base_joint: 0
|
||||
arm_link_1_joint: 0
|
||||
arm_link_2_joint: 0
|
||||
arm_link_3_joint: 0
|
||||
gripper_base_joint: 0
|
||||
gripper_right_joint: 0.03
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user