mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-09 17:25:09 +00:00
Compare commits
18 Commits
v0.10.1
...
e5aa4d940a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5aa4d940a | ||
|
|
4771ff2347 | ||
|
|
8bcc92a394 | ||
|
|
49354fcf39 | ||
|
|
a8973ea92b | ||
|
|
0bfb52df00 | ||
|
|
a555c59dc2 | ||
|
|
9ac0ad49cb | ||
|
|
daa46aaf50 | ||
|
|
bbd9629f98 | ||
|
|
2d560a8182 | ||
|
|
8beb80f0e7 | ||
|
|
09c1e8ca73 | ||
|
|
e7b6b8190a | ||
|
|
933e84bf13 | ||
|
|
0b56378287 | ||
|
|
51b47596ce | ||
|
|
42e8befec4 |
@@ -1,69 +1,90 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.1
|
||||
version: 0.10.4
|
||||
|
||||
source:
|
||||
path: ../unilabos
|
||||
target_directory: unilabos
|
||||
|
||||
build:
|
||||
noarch: python
|
||||
number: 0
|
||||
python:
|
||||
entry_points:
|
||||
- unilab = unilabos.app.main:main
|
||||
- unilab-register = unilabos.app.register:main
|
||||
script:
|
||||
- python -m pip install paho-mqtt opentrons_shared_data
|
||||
- python -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
- 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
|
||||
- pip
|
||||
- setuptools
|
||||
- python ==3.11.11
|
||||
- pip
|
||||
- setuptools
|
||||
run:
|
||||
- conda-forge::python =3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- conda-forge::python ==3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- zstd
|
||||
- ninja
|
||||
- if: unix
|
||||
then:
|
||||
- make
|
||||
- ninja
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
- numpy
|
||||
- scipy
|
||||
- pandas
|
||||
- networkx
|
||||
- matplotlib
|
||||
- pint
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
- pydantic
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- 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
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
- numpy
|
||||
- scipy
|
||||
- pandas
|
||||
- networkx
|
||||
- matplotlib
|
||||
- pint
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
- pydantic
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- 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
|
||||
license: GPL-3.0-only
|
||||
description: "Uni-Lab-OS"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.10.1"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
build:
|
||||
noarch: python
|
||||
script: |
|
||||
{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv
|
||||
# {{ PYTHON }} clean_build_dir.py
|
||||
|
||||
requirements:
|
||||
host:
|
||||
- python
|
||||
- pip
|
||||
run:
|
||||
- python
|
||||
|
||||
test:
|
||||
imports:
|
||||
- unilabos
|
||||
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
|
||||
98
.github/workflows/deploy-docs.yml
vendored
Normal file
98
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: '要部署文档的分支'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
deploy_to_pages:
|
||||
description: '是否部署到 GitHub Pages'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
# 设置 GITHUB_TOKEN 权限以部署到 GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# 只允许一个并发部署,跳过正在进行和最新排队之间的运行
|
||||
# 但是不取消正在进行的运行,因为我们希望允许这些生产部署完成
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build documentation
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
|
||||
- name: Setup Python environment
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pandoc
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
# Install package in development mode to get version info
|
||||
pip install -e .
|
||||
# Install documentation dependencies
|
||||
pip install -r docs/requirements.txt
|
||||
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v4
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
|
||||
- name: Build Sphinx documentation
|
||||
run: |
|
||||
cd docs
|
||||
# Clean previous builds
|
||||
rm -rf _build
|
||||
# Build HTML documentation
|
||||
python -m sphinx -b html . _build/html -v
|
||||
|
||||
- name: Check build results
|
||||
run: |
|
||||
echo "Documentation build completed, checking output directory:"
|
||||
ls -la docs/_build/html/
|
||||
echo "Checking for index.html:"
|
||||
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
with:
|
||||
path: docs/_build/html
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/static *
|
||||
recursive-include unilabos/app/templates *
|
||||
recursive-include unilabos/app/web/static *
|
||||
recursive-include unilabos/app/web/templates *
|
||||
recursive-include unilabos/device_mesh/devices *
|
||||
recursive-include unilabos/device_mesh/resources *
|
||||
|
||||
17
README.md
17
README.md
@@ -5,6 +5,7 @@
|
||||
# Uni-Lab-OS
|
||||
|
||||
<!-- Language switcher -->
|
||||
|
||||
**English** | [中文](README_zh.md)
|
||||
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||
@@ -30,24 +31,18 @@ Join the [Intelligent Organic Chemistry Synthesis Competition](https://bohrium.d
|
||||
|
||||
Detailed documentation can be found at:
|
||||
|
||||
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
- [Online Documentation](https://dptech-corp.github.io/Uni-Lab-OS/)
|
||||
|
||||
## 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 create -n unilab unilab -c unilab -c robostack -c robostack-staging -c conda-forge
|
||||
|
||||
# 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
|
||||
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
|
||||
@@ -60,7 +55,7 @@ pip install .
|
||||
|
||||
3. Start Uni-Lab System:
|
||||
|
||||
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
Please refer to [Documentation - Boot Examples](https://dptech-corp.github.io/Uni-Lab-OS/boot_examples/index.html)
|
||||
|
||||
## Message Format
|
||||
|
||||
@@ -80,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)
|
||||
|
||||
19
README_zh.md
19
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 是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
|
||||
## 🏆 比赛
|
||||
|
||||
@@ -30,24 +31,20 @@ Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控
|
||||
|
||||
详细文档可在以下位置找到:
|
||||
|
||||
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
- [在线文档](https://dptech-corp.github.io/Uni-Lab-OS/)
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 配置Conda环境
|
||||
1. 配置 Conda 环境
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||
|
||||
```bash
|
||||
# 创建新环境
|
||||
mamba create -n unilab unilab -c unilab -c robostack -c robostack-staging -c conda-forge
|
||||
|
||||
# 或更新现有环境
|
||||
# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。
|
||||
conda env update --file unilabos-[YOUR_OS].yml -n 环境名
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. 安装 Uni-Lab-OS:
|
||||
2. 安装开发版 Uni-Lab-OS:
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
@@ -60,7 +57,7 @@ pip install .
|
||||
|
||||
3. 启动 Uni-Lab 系统:
|
||||
|
||||
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
请见[文档-启动样例](https://dptech-corp.github.io/Uni-Lab-OS/boot_examples/index.html)
|
||||
|
||||
## 消息格式
|
||||
|
||||
@@ -80,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
docs/requirements.txt
Normal file
13
docs/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Sphinx文档构建依赖
|
||||
sphinx>=7.0.0
|
||||
sphinx-rtd-theme>=2.0.0
|
||||
myst-parser>=2.0.0
|
||||
|
||||
# 用于支持Jupyter notebook文档
|
||||
myst-nb>=1.0.0
|
||||
|
||||
# 用于代码复制按钮
|
||||
sphinx-copybutton>=0.5.0
|
||||
|
||||
# 用于自动摘要生成
|
||||
sphinx-autobuild>=2024.2.4
|
||||
@@ -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.4
|
||||
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.10.1
|
||||
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.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.*
|
||||
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.10.1"
|
||||
version: "0.10.4"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.10.1',
|
||||
version='0.10.4',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"config": {
|
||||
"protocol_type": [
|
||||
"AddProtocol",
|
||||
"TransferProtocol",
|
||||
"StartStirProtocol",
|
||||
"StopStirProtocol",
|
||||
"StirProtocol",
|
||||
@@ -171,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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -192,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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -213,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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -234,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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -255,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
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -320,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": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -405,10 +419,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -424,10 +439,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 2000.0
|
||||
"max_volume": 2000.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -633,10 +649,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -652,10 +669,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -671,10 +689,11 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 250.0
|
||||
"max_volume": 250.0
|
||||
},
|
||||
"data": {
|
||||
"current_volume": 0.0
|
||||
"liquids": [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -713,7 +732,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"volume": 500.0,
|
||||
"max_volume": 500.0,
|
||||
"reagent": "sodium_chloride",
|
||||
"physical_state": "solid"
|
||||
},
|
||||
@@ -1077,7 +1096,7 @@
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_1": "top",
|
||||
"solid_reagent_bottle_1": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
@@ -1087,7 +1106,7 @@
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_2": "top",
|
||||
"solid_reagent_bottle_2": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
},
|
||||
@@ -1097,7 +1116,7 @@
|
||||
"target": "solid_dispenser_1",
|
||||
"type": "resource",
|
||||
"port": {
|
||||
"solid_reagent_bottle_3": "top",
|
||||
"solid_reagent_bottle_3": "bottom",
|
||||
"solid_dispenser_1": "SolidIn"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "moveit.arm_slider",
|
||||
"class": "robotic_arm.SCARA_with_slider.virtual",
|
||||
"position": {
|
||||
"x": -500,
|
||||
"y": 1000,
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,7 +24,7 @@ from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
|
||||
def load_config_from_file(config_path, override_labid=None):
|
||||
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")
|
||||
@@ -43,10 +43,11 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
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):]
|
||||
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.")
|
||||
@@ -94,6 +95,11 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="启动unilab时同时报送注册表信息",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_remote_resource",
|
||||
action="store_true",
|
||||
help="启动unilab时使用远程资源启动",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
@@ -126,7 +132,17 @@ def parse_args():
|
||||
"--labid",
|
||||
type=str,
|
||||
default="",
|
||||
help="实验室唯一ID,也可通过环境变量 UNILABOS.MQCONFIG.LABID 设置或传入--config设置",
|
||||
help="实验室唯一ID,也可通过环境变量 UNILABOS_MQCONFIG_LABID 设置或传入--config设置",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip_env_check",
|
||||
action="store_true",
|
||||
help="跳过启动时的环境依赖检查",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--direct_end",
|
||||
action="store_true",
|
||||
help="直接结束任务",
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -138,35 +154,70 @@ def main():
|
||||
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")
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
if 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 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)
|
||||
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")
|
||||
print_status(f"请在文件夹中配置lab_id,放入下载的CA.crt、lab.crt、lab.key重新启动本程序", "info")
|
||||
os._exit(1)
|
||||
else:
|
||||
os._exit(1)
|
||||
else:
|
||||
working_dir = args_dict.get("working_dir") or os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
if 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)
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
# 加载配置文件
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
load_config_from_file(config_path, args_dict["labid"])
|
||||
|
||||
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.working_dir = working_dir
|
||||
BasicConfig.direct_end = args_dict.get("direct_end", False)
|
||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||
@@ -192,7 +243,7 @@ def main():
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"])
|
||||
build_registry(args_dict["registry_path"], False, args_dict["upload_registry"])
|
||||
if args_dict["graph"] is None:
|
||||
request_startup_json = http_client.request_startup_json()
|
||||
if not request_startup_json:
|
||||
@@ -204,10 +255,11 @@ def main():
|
||||
print_status("联网获取设备加载文件成功", "info")
|
||||
graph, data = read_node_link_json(request_startup_json)
|
||||
else:
|
||||
if args_dict["graph"].endswith(".json"):
|
||||
graph, data = read_node_link_json(args_dict["graph"])
|
||||
file_path = args_dict["graph"]
|
||||
if file_path.endswith(".json"):
|
||||
graph, data = read_node_link_json(file_path)
|
||||
else:
|
||||
graph, data = read_graphml(args_dict["graph"])
|
||||
graph, data = read_graphml(file_path)
|
||||
import unilabos.resources.graphio as graph_res
|
||||
|
||||
graph_res.physical_setup_graph = graph
|
||||
|
||||
@@ -166,7 +166,7 @@ class MQTTClient:
|
||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
|
||||
address = f"labs/{MQConfig.lab_id}/devices/"
|
||||
self.client.publish(address, json.dumps(status), qos=2)
|
||||
logger.info(f"Device {device_id} status published: address: {address}, {status}")
|
||||
# logger.info(f"Device {device_id} 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:
|
||||
|
||||
@@ -18,6 +18,11 @@ def register_devices_and_resources(mqtt_client, lab_registry):
|
||||
mqtt_client.publish_registry(device_info["id"], device_info, False)
|
||||
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
|
||||
|
||||
# # 注册资源信息
|
||||
# for resource_info in lab_registry.obtain_registry_resource_info():
|
||||
# mqtt_client.publish_registry(resource_info["id"], resource_info, False)
|
||||
# logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
|
||||
|
||||
# 注册资源信息 - 使用HTTP方式
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
@@ -64,7 +69,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
load_config_from_file(args.config)
|
||||
# 构建注册表
|
||||
build_registry(args.registry, args.complete_registry)
|
||||
build_registry(args.registry, args.complete_registry, True)
|
||||
from unilabos.app.mq import mqtt_client
|
||||
|
||||
# 连接mqtt
|
||||
|
||||
@@ -4,11 +4,12 @@ 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 MQConfig, HTTPConfig, BasicConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
@@ -189,7 +190,7 @@ class HTTPClient:
|
||||
logger.error(f"请求启动配置失败: {response.status_code}, {response.text}")
|
||||
else:
|
||||
try:
|
||||
with open("startup_config.json", "w", encoding="utf-8") as f:
|
||||
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:
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
@@ -54,6 +53,5 @@ action_protocol_generators = {
|
||||
StartStirProtocol: generate_start_stir_protocol,
|
||||
StirProtocol: generate_stir_protocol,
|
||||
StopStirProtocol: generate_stop_stir_protocol,
|
||||
TransferProtocol: generate_transfer_protocol,
|
||||
WashSolidProtocol: generate_wash_solid_protocol,
|
||||
}
|
||||
@@ -1,313 +1,24 @@
|
||||
from functools import partial
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Dict, Any, Union
|
||||
|
||||
from .utils.unit_parser import parse_volume_input, parse_mass_input, parse_time_input
|
||||
from .utils.vessel_parser import get_vessel, find_solid_dispenser, find_connected_stirrer, find_reagent_vessel
|
||||
from .utils.logger_util import action_log
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[ADD] {message}", flush=True)
|
||||
logger.info(f"[ADD] {message}")
|
||||
|
||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||
"""
|
||||
解析体积输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0)
|
||||
|
||||
Returns:
|
||||
float: 体积(毫升)
|
||||
"""
|
||||
if isinstance(volume_input, (int, float)):
|
||||
debug_print(f"📏 体积输入为数值: {volume_input}")
|
||||
return float(volume_input)
|
||||
|
||||
if not volume_input or not str(volume_input).strip():
|
||||
debug_print(f"⚠️ 体积输入为空,返回0.0mL")
|
||||
return 0.0
|
||||
|
||||
volume_str = str(volume_input).lower().strip()
|
||||
debug_print(f"🔍 解析体积输入: '{volume_str}'")
|
||||
|
||||
# 处理未知体积
|
||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_volume = 10.0 # 默认10mL
|
||||
debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
|
||||
return default_volume
|
||||
|
||||
# 移除空格并提取数字和单位
|
||||
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}',使用默认值10mL")
|
||||
return 10.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 'ml' # 默认单位为毫升
|
||||
|
||||
# 转换为毫升
|
||||
if unit in ['l', 'liter']:
|
||||
volume = value * 1000.0 # L -> mL
|
||||
debug_print(f"🔄 体积转换: {value}L → {volume}mL")
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
debug_print(f"✅ 体积已为mL: {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 ['min', 'minute']:
|
||||
time_sec = value * 60.0 # min -> s
|
||||
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
|
||||
elif unit in ['h', 'hr', 'hour']:
|
||||
time_sec = value * 3600.0 # h -> s
|
||||
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
|
||||
elif unit in ['d', 'day']:
|
||||
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 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_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 ""
|
||||
|
||||
# 🆕 创建进度日志动作
|
||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
||||
"""创建一个动作日志"""
|
||||
full_message = f"{emoji} {message}"
|
||||
debug_print(full_message)
|
||||
logger.info(full_message)
|
||||
print(f"[ACTION] {full_message}", flush=True)
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": full_message
|
||||
}
|
||||
}
|
||||
create_action_log = partial(action_log, prefix="[ADD]")
|
||||
|
||||
def generate_add_protocol(
|
||||
G: nx.DiGraph,
|
||||
@@ -346,16 +57,7 @@ def generate_add_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
# 🔧 修改:更新容器的液体体积(假设有 liquid_volume 字段)
|
||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||
@@ -406,12 +108,7 @@ def generate_add_protocol(
|
||||
final_time = parse_time_input(time)
|
||||
|
||||
debug_print(f"📊 解析结果:")
|
||||
debug_print(f" 📏 体积: {final_volume}mL")
|
||||
debug_print(f" ⚖️ 质量: {final_mass}g")
|
||||
debug_print(f" ⏱️ 时间: {final_time}s")
|
||||
debug_print(f" 🧬 摩尔: '{mol}'")
|
||||
debug_print(f" 🎯 事件: '{event}'")
|
||||
debug_print(f" ⚡ 速率: '{rate_spec}'")
|
||||
debug_print(f" 体积: {final_volume}mL, 质量: {final_mass}g, 时间: {final_time}s, 摩尔: '{mol}', 事件: '{event}', 速率: '{rate_spec}'")
|
||||
|
||||
# === 判断添加类型 ===
|
||||
debug_print("🔍 步骤3: 判断添加类型...")
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
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):
|
||||
"""调试输出"""
|
||||
print(f"[ADJUST_PH] {message}", flush=True)
|
||||
logger.info(f"[ADJUST_PH] {message}")
|
||||
|
||||
# 🆕 创建进度日志动作
|
||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
||||
"""创建一个动作日志"""
|
||||
full_message = f"{emoji} {message}"
|
||||
debug_print(full_message)
|
||||
logger.info(full_message)
|
||||
print(f"[ACTION] {full_message}", flush=True)
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": full_message
|
||||
}
|
||||
}
|
||||
|
||||
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||
"""
|
||||
查找酸碱试剂容器,支持多种匹配模式
|
||||
@@ -235,16 +219,7 @@ def generate_adjust_ph_protocol(
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
if not vessel_id:
|
||||
debug_print(f"❌ vessel 参数无效,必须包含id字段或直接提供容器ID. vessel: {vessel}")
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
增强版溶剂容器查找,支持各种匹配方式的别名函数
|
||||
@@ -181,16 +89,7 @@ def generate_clean_vessel_protocol(
|
||||
clean_protocol = generate_clean_vessel_protocol(G, {"id": "main_reactor"}, "water", 100.0, 60.0, 2)
|
||||
"""
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
from functools import partial
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
import logging
|
||||
from typing import List, Dict, Any, Union
|
||||
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .utils.logger_util import action_log
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"[DISSOLVE] {message}", flush=True)
|
||||
logger.info(f"[DISSOLVE] {message}")
|
||||
|
||||
# 🆕 创建进度日志动作
|
||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
||||
"""创建一个动作日志"""
|
||||
full_message = f"{emoji} {message}"
|
||||
debug_print(full_message)
|
||||
logger.info(full_message)
|
||||
print(f"[ACTION] {full_message}", flush=True)
|
||||
|
||||
return {
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {
|
||||
"time": 0.1,
|
||||
"log_message": full_message
|
||||
}
|
||||
}
|
||||
create_action_log = partial(action_log, prefix="[DISSOLVE]")
|
||||
|
||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||
"""
|
||||
@@ -446,7 +437,7 @@ def generate_dissolve_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id = vessel["id"]
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
debug_print("=" * 60)
|
||||
debug_print("🧪 开始生成溶解协议")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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:
|
||||
"""
|
||||
@@ -63,7 +65,7 @@ def generate_dry_protocol(
|
||||
List[Dict[str, Any]]: 动作序列
|
||||
"""
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id = vessel["id"]
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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
|
||||
|
||||
# 设置日志
|
||||
@@ -21,48 +25,17 @@ 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
|
||||
}
|
||||
}
|
||||
create_action_log = partial(action_log, prefix="[抽真空充气]")
|
||||
|
||||
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
|
||||
"""
|
||||
@@ -288,16 +261,7 @@ def generate_evacuateandrefill_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
# 硬编码重复次数为 3
|
||||
repeats = 3
|
||||
|
||||
@@ -2,75 +2,15 @@ from typing import List, Dict, Any, Optional, 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):
|
||||
"""调试输出"""
|
||||
print(f"🧪 [EVAPORATE] {message}", flush=True)
|
||||
logger.info(f"[EVAPORATE] {message}")
|
||||
|
||||
def parse_time_input(time_input: Union[str, float]) -> float:
|
||||
"""
|
||||
解析时间输入,支持带单位的字符串
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "3 min", "180", "0.5 h" 等)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if isinstance(time_input, (int, float)):
|
||||
debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
|
||||
return float(time_input) # 🔧 确保返回float
|
||||
|
||||
if not time_input or not str(time_input).strip():
|
||||
debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
|
||||
return 180.0 # 默认3分钟
|
||||
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"🔍 解析时间输入: '{time_str}' 📝")
|
||||
|
||||
# 处理未知时间
|
||||
if time_str in ['?', 'unknown', 'tbd']:
|
||||
default_time = 180.0 # 默认3分钟
|
||||
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷♀️")
|
||||
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:
|
||||
# 如果无法解析,尝试直接转换为数字(默认秒)
|
||||
try:
|
||||
value = float(time_str)
|
||||
debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰")
|
||||
return float(value) # 🔧 确保返回float
|
||||
except ValueError:
|
||||
debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅")
|
||||
return 180.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 's' # 默认单位为秒
|
||||
|
||||
# 转换为秒
|
||||
if unit in ['min', 'minute']:
|
||||
time_sec = value * 60.0 # min -> s
|
||||
debug_print(f"🕐 时间转换: {value} 分钟 → {time_sec}s ⏰")
|
||||
elif unit in ['h', 'hr', 'hour']:
|
||||
time_sec = value * 3600.0 # h -> s
|
||||
debug_print(f"🕐 时间转换: {value} 小时 → {time_sec}s ({time_sec/60:.1f}分钟) ⏰")
|
||||
elif unit in ['d', 'day']:
|
||||
time_sec = value * 86400.0 # d -> s
|
||||
debug_print(f"🕐 时间转换: {value} 天 → {time_sec}s ({time_sec/3600:.1f}小时) ⏰")
|
||||
else: # s, sec, second 或默认
|
||||
time_sec = value # 已经是s
|
||||
debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰")
|
||||
|
||||
return float(time_sec) # 🔧 确保返回float
|
||||
|
||||
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
|
||||
"""
|
||||
@@ -201,16 +141,7 @@ def generate_evaporate_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
debug_print("🌟" * 20)
|
||||
debug_print("🌪️ 开始生成蒸发协议(支持单位和体积运算)✨")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
import networkx as nx
|
||||
import logging
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"🧪 [FILTER] {message}", flush=True)
|
||||
logger.info(f"[FILTER] {message}")
|
||||
|
||||
def find_filter_device(G: nx.DiGraph) -> str:
|
||||
@@ -51,7 +51,7 @@ def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") ->
|
||||
def generate_filter_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
filtrate_vessel: str = "",
|
||||
filtrate_vessel: dict = {"id": "waste"},
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -68,16 +68,8 @@ def generate_filter_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
filtrate_vessel_id, filtrate_vessel_data = get_vessel(filtrate_vessel)
|
||||
|
||||
debug_print("🌊" * 20)
|
||||
debug_print("🚀 开始生成过滤协议(支持体积运算)✨")
|
||||
@@ -111,7 +103,7 @@ def generate_filter_protocol(
|
||||
# 验证可选参数
|
||||
debug_print(" 🔍 验证可选参数...")
|
||||
if filtrate_vessel:
|
||||
validate_vessel(G, filtrate_vessel, "滤液容器")
|
||||
validate_vessel(G, filtrate_vessel_id, "滤液容器")
|
||||
debug_print(" 🌊 模式: 过滤并收集滤液 💧")
|
||||
else:
|
||||
debug_print(" 🧱 模式: 过滤并收集固体 🔬")
|
||||
@@ -168,8 +160,8 @@ def generate_filter_protocol(
|
||||
# 使用pump protocol转移液体到过滤器
|
||||
transfer_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=vessel_id, # 🔧 使用 vessel_id
|
||||
to_vessel=filter_device,
|
||||
from_vessel={"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
to_vessel={"id": filter_device},
|
||||
volume=0.0, # 转移所有液体
|
||||
amount="",
|
||||
time=0.0,
|
||||
@@ -220,8 +212,8 @@ def generate_filter_protocol(
|
||||
# 构建过滤动作参数
|
||||
debug_print(" ⚙️ 构建过滤参数...")
|
||||
filter_kwargs = {
|
||||
"vessel": filter_device, # 过滤器设备
|
||||
"filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
|
||||
"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),
|
||||
@@ -252,8 +244,8 @@ def generate_filter_protocol(
|
||||
# === 收集滤液(如果需要)===
|
||||
debug_print("📍 步骤5: 收集滤液... 💧")
|
||||
|
||||
if filtrate_vessel:
|
||||
debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧")
|
||||
if filtrate_vessel_id and filtrate_vessel_id not in G.neighbors(filter_device):
|
||||
debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel_id} 💧")
|
||||
|
||||
try:
|
||||
debug_print(" 🔄 开始执行收集操作...")
|
||||
@@ -282,20 +274,20 @@ def generate_filter_protocol(
|
||||
debug_print(" 🔧 更新滤液容器体积...")
|
||||
|
||||
# 更新filtrate_vessel在图中的体积(如果它是节点)
|
||||
if filtrate_vessel in G.nodes():
|
||||
if 'data' not in G.nodes[filtrate_vessel]:
|
||||
G.nodes[filtrate_vessel]['data'] = {}
|
||||
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]['data'].get('liquid_volume', 0.0)
|
||||
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]['data']['liquid_volume'][0] += expected_filtrate_volume
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'][0] += expected_filtrate_volume
|
||||
else:
|
||||
G.nodes[filtrate_vessel]['data']['liquid_volume'] = [expected_filtrate_volume]
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = [expected_filtrate_volume]
|
||||
else:
|
||||
G.nodes[filtrate_vessel]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
|
||||
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
|
||||
|
||||
debug_print(f" 📊 滤液容器 {filtrate_vessel} 体积增加 {expected_filtrate_volume:.2f}mL")
|
||||
debug_print(f" 📊 滤液容器 {filtrate_vessel_id} 体积增加 {expected_filtrate_volume:.2f}mL")
|
||||
|
||||
else:
|
||||
debug_print(" ⚠️ 收集协议返回空序列 🤔")
|
||||
@@ -360,7 +352,7 @@ def generate_filter_protocol(
|
||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
|
||||
debug_print(f"🥽 过滤容器: {vessel_id} 🧪")
|
||||
debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
|
||||
debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱")
|
||||
debug_print(f"💧 滤液容器: {filtrate_vessel_id or '无(保留固体)'} 🧱")
|
||||
debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
|
||||
if original_liquid_volume > 0:
|
||||
debug_print(f"📊 体积变化统计:")
|
||||
@@ -372,4 +364,3 @@ def generate_filter_protocol(
|
||||
debug_print("🎊" * 20)
|
||||
|
||||
return action_sequence
|
||||
|
||||
|
||||
@@ -2,81 +2,15 @@ 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):
|
||||
"""调试输出"""
|
||||
print(f"🌡️ [HEATCHILL] {message}", flush=True)
|
||||
logger.info(f"[HEATCHILL] {message}")
|
||||
|
||||
def parse_time_input(time_input: Union[str, float, int]) -> float:
|
||||
"""
|
||||
解析时间输入(统一函数)
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 300.0
|
||||
|
||||
# 🔢 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
||||
return result
|
||||
|
||||
# 📝 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"🔍 解析时间: '{time_str}'")
|
||||
|
||||
# ❓ 特殊值处理
|
||||
special_times = {
|
||||
'?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
|
||||
'overnight': 43200.0, 'several hours': 10800.0,
|
||||
'few hours': 7200.0, 'long time': 3600.0, 'short time': 300.0
|
||||
}
|
||||
|
||||
if time_str in special_times:
|
||||
result = special_times[time_str]
|
||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
|
||||
return result
|
||||
|
||||
# 🔢 纯数字处理
|
||||
try:
|
||||
result = float(time_str)
|
||||
debug_print(f"⏰ 纯数字: {time_str} → {result}s")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 📐 正则表达式解析
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 300s")
|
||||
return 300.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or 's'
|
||||
|
||||
# 📏 单位转换
|
||||
unit_multipliers = {
|
||||
's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
|
||||
'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
|
||||
'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
|
||||
'd': 86400.0, 'day': 86400.0, 'days': 86400.0
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
|
||||
return result
|
||||
|
||||
def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
|
||||
"""
|
||||
@@ -217,16 +151,7 @@ def generate_heat_chill_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
debug_print("🌡️" * 20)
|
||||
debug_print("🚀 开始生成加热冷却协议(支持vessel字典)✨")
|
||||
@@ -295,7 +220,7 @@ def generate_heat_chill_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": vessel,
|
||||
"temp": float(final_temp),
|
||||
"time": float(final_time),
|
||||
"stir": bool(stir),
|
||||
@@ -329,7 +254,7 @@ def generate_heat_chill_to_temp_protocol(
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""生成加热到指定温度的协议(简化版)"""
|
||||
vessel_id = vessel["id"]
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
debug_print(f"🌡️ 生成加热到温度协议: {vessel_id} → {temp}°C")
|
||||
return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
|
||||
|
||||
@@ -343,7 +268,7 @@ def generate_heat_chill_start_protocol(
|
||||
"""生成开始加热操作的协议序列"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id = vessel["id"]
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
|
||||
debug_print("🔥 开始生成启动加热协议 ✨")
|
||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {temp}°C")
|
||||
@@ -361,7 +286,6 @@ def generate_heat_chill_start_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"temp": temp,
|
||||
"purpose": purpose or f"开始加热到 {temp}°C"
|
||||
}
|
||||
@@ -378,7 +302,7 @@ def generate_heat_chill_stop_protocol(
|
||||
"""生成停止加热操作的协议序列"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id = vessel["id"]
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
|
||||
debug_print("🛑 开始生成停止加热协议 ✨")
|
||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
@@ -396,10 +320,8 @@ def generate_heat_chill_stop_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id # 🔧 使用 vessel_id
|
||||
}
|
||||
}]
|
||||
|
||||
debug_print(f"✅ 停止加热协议生成完成 🎯")
|
||||
return action_sequence
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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:
|
||||
@@ -170,16 +171,7 @@ def generate_hydrogenate_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,91 +2,17 @@ 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):
|
||||
"""调试输出"""
|
||||
print(f"💎 [RECRYSTALLIZE] {message}", flush=True)
|
||||
logger.info(f"[RECRYSTALLIZE] {message}")
|
||||
|
||||
|
||||
def parse_volume_with_units(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:
|
||||
debug_print("⚠️ 体积输入为空,返回 0.0mL 📦")
|
||||
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
|
||||
debug_print(f"📏 升转毫升: {value}L → {volume}mL 💧")
|
||||
elif unit in ['μl', 'ul', 'microliter']:
|
||||
volume = value / 1000.0 # μL -> mL
|
||||
debug_print(f"📏 微升转毫升: {value}μL → {volume}mL 💧")
|
||||
else: # ml, milliliter 或默认
|
||||
volume = value # 已经是mL
|
||||
debug_print(f"📏 毫升单位: {value}mL → {volume}mL 💧")
|
||||
|
||||
debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨")
|
||||
return volume
|
||||
|
||||
|
||||
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
||||
"""
|
||||
解析比例字符串,支持多种格式
|
||||
@@ -136,131 +62,6 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
||||
return 1.0, 1.0
|
||||
|
||||
|
||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""
|
||||
查找溶剂容器
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
solvent: 溶剂名称
|
||||
|
||||
Returns:
|
||||
str: 溶剂容器ID
|
||||
"""
|
||||
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
|
||||
|
||||
# 构建可能的容器名称
|
||||
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(" 🎯 步骤1: 精确名称匹配...")
|
||||
for vessel_name in possible_names:
|
||||
if vessel_name in G.nodes():
|
||||
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
|
||||
return vessel_name
|
||||
|
||||
# 第二步:通过模糊匹配(节点ID和名称)
|
||||
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} (名称: {node_name}) ✨")
|
||||
return node_id
|
||||
|
||||
# 第三步:通过配置中的试剂信息匹配
|
||||
debug_print(" 🧪 步骤3: 配置试剂信息匹配...")
|
||||
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(" 🧪 步骤4: 数据试剂信息匹配...")
|
||||
for node_id in G.nodes():
|
||||
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', [])
|
||||
for liquid in liquids:
|
||||
if isinstance(liquid, dict):
|
||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||
|
||||
if solvent.lower() in liquid_type:
|
||||
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
|
||||
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 generate_recrystallize_protocol(
|
||||
G: nx.DiGraph,
|
||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||
@@ -287,16 +88,7 @@ def generate_recrystallize_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
@@ -330,7 +122,7 @@ def generate_recrystallize_protocol(
|
||||
|
||||
# 2. 解析体积(支持单位)
|
||||
debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
|
||||
final_volume = parse_volume_with_units(volume, "mL")
|
||||
final_volume = parse_volume_input(volume, "mL")
|
||||
debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨")
|
||||
|
||||
# 3. 解析比例
|
||||
@@ -582,7 +374,7 @@ def test_recrystallize_protocol():
|
||||
debug_print("💧 测试体积解析...")
|
||||
test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"]
|
||||
for vol in test_volumes:
|
||||
parsed = parse_volume_with_units(vol)
|
||||
parsed = parse_volume_input(vol)
|
||||
debug_print(f" 📊 体积 '{vol}' -> {parsed}mL")
|
||||
|
||||
# 测试比例解析
|
||||
|
||||
@@ -2,13 +2,13 @@ from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"🏛️ [RUN_COLUMN] {message}", flush=True)
|
||||
logger.info(f"[RUN_COLUMN] {message}")
|
||||
|
||||
def parse_percentage(pct_str: str) -> float:
|
||||
@@ -404,9 +404,9 @@ def generate_run_column_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
|
||||
debug_print("🏛️" * 20)
|
||||
debug_print("🚀 开始生成柱层析协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
@@ -773,8 +773,8 @@ def generate_gradient_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vesse
|
||||
column: str, start_ratio: str = "10:90",
|
||||
end_ratio: str = "50:50") -> List[Dict[str, Any]]:
|
||||
"""梯度洗脱柱层析(中等比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"📈 梯度柱层析: {from_vessel_id} → {to_vessel_id} ({start_ratio} → {end_ratio})")
|
||||
# 使用中间比例作为近似
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column, ratio="30:70")
|
||||
@@ -782,8 +782,8 @@ def generate_gradient_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vesse
|
||||
def generate_polar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict,
|
||||
column: str) -> List[Dict[str, Any]]:
|
||||
"""极性化合物柱层析(高极性溶剂比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"⚡ 极性化合物柱层析: {from_vessel_id} → {to_vessel_id}")
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column,
|
||||
solvent1="ethyl_acetate", solvent2="hexane", ratio="70:30")
|
||||
@@ -791,8 +791,8 @@ def generate_polar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel:
|
||||
def generate_nonpolar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict,
|
||||
column: str) -> List[Dict[str, Any]]:
|
||||
"""非极性化合物柱层析(低极性溶剂比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"🛢️ 非极性化合物柱层析: {from_vessel_id} → {to_vessel_id}")
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column,
|
||||
solvent1="ethyl_acetate", solvent2="hexane", ratio="5:95")
|
||||
@@ -805,4 +805,3 @@ def test_run_column_protocol():
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_run_column_protocol()
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from functools import partial
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
import logging
|
||||
import sys
|
||||
from typing import List, Dict, Any, Union
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .utils.logger_util import action_log
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,48 +24,472 @@ def debug_print(message):
|
||||
try:
|
||||
# 确保消息是字符串格式
|
||||
safe_message = str(message)
|
||||
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
|
||||
logger.info(f"[SEPARATE] {safe_message}")
|
||||
except UnicodeEncodeError:
|
||||
# 如果编码失败,尝试替换不支持的字符
|
||||
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
||||
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
|
||||
logger.info(f"[SEPARATE] {safe_message}")
|
||||
except Exception as e:
|
||||
# 最后的安全措施
|
||||
fallback_message = f"日志输出错误: {repr(message)}"
|
||||
print(f"🌀 [SEPARATE] {fallback_message}", flush=True)
|
||||
logger.info(f"[SEPARATE] {fallback_message}")
|
||||
|
||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
||||
"""创建一个动作日志 - 支持中文和emoji"""
|
||||
create_action_log = partial(action_log, prefix="[SEPARATE]")
|
||||
|
||||
|
||||
def generate_separate_protocol(
|
||||
G: nx.DiGraph,
|
||||
# 🔧 基础参数,支持XDL的vessel参数
|
||||
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
|
||||
purpose: str = "separate", # 分离目的
|
||||
product_phase: str = "top", # 产物相
|
||||
# 🔧 可选的详细参数
|
||||
from_vessel: Union[str, dict] = "", # 源容器(通常在separate前已经transfer了)
|
||||
separation_vessel: Union[str, dict] = "", # 分离容器(与vessel同义)
|
||||
to_vessel: Union[str, dict] = "", # 目标容器(可选)
|
||||
waste_phase_to_vessel: Union[str, dict] = "", # 废相目标容器
|
||||
product_vessel: Union[str, dict] = "", # XDL: 产物容器(与to_vessel同义)
|
||||
waste_vessel: Union[str, dict] = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
|
||||
# 🔧 溶剂相关参数
|
||||
solvent: str = "", # 溶剂名称
|
||||
solvent_volume: Union[str, float] = 0.0, # 溶剂体积
|
||||
volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
|
||||
# 🔧 操作参数
|
||||
through: str = "", # 通过材料
|
||||
repeats: int = 1, # 重复次数
|
||||
stir_time: float = 30.0, # 搅拌时间(秒)
|
||||
stir_speed: float = 300.0, # 搅拌速度
|
||||
settling_time: float = 300.0, # 沉降时间(秒)
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成分离操作的协议序列 - 支持vessel字典和体积运算
|
||||
|
||||
支持XDL参数格式:
|
||||
- vessel: 分离容器字典(必需)
|
||||
- purpose: "wash", "extract", "separate"
|
||||
- product_phase: "top", "bottom"
|
||||
- product_vessel: 产物收集容器
|
||||
- waste_vessel: 废液收集容器
|
||||
- solvent: 溶剂名称
|
||||
- volume: "200 mL", "?" 或数值
|
||||
- repeats: 重复次数
|
||||
|
||||
分离流程:
|
||||
1. (可选)添加溶剂到分离容器
|
||||
2. 搅拌混合
|
||||
3. 静置分层
|
||||
4. 收集指定相到目标容器
|
||||
5. 重复指定次数
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:vessel参数兼容处理
|
||||
if vessel is None:
|
||||
if isinstance(separation_vessel, dict):
|
||||
vessel = separation_vessel
|
||||
else:
|
||||
raise ValueError("必须提供vessel字典参数")
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
vessel_id, vessel_data = get_vessel(vessel)
|
||||
|
||||
debug_print("🌀" * 20)
|
||||
debug_print("🚀 开始生成分离协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 🎯 分离目的: '{purpose}'")
|
||||
debug_print(f" 📊 产物相: '{product_phase}'")
|
||||
debug_print(f" 💧 溶剂: '{solvent}'")
|
||||
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
debug_print(f" 🎯 产物容器: '{product_vessel}'")
|
||||
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
|
||||
debug_print(f" 📦 其他参数: {kwargs}")
|
||||
debug_print("🌀" * 20)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 🔧 新增:记录分离前的容器状态
|
||||
debug_print("🔍 记录分离前容器状态...")
|
||||
original_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||
debug_print(f"📊 分离前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# === 参数验证和标准化 ===
|
||||
debug_print("🔍 步骤1: 参数验证和标准化...")
|
||||
action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel_id}", "🎬"))
|
||||
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
|
||||
|
||||
# 统一容器参数 - 支持字典和字符串
|
||||
def extract_vessel_id(vessel_param):
|
||||
if isinstance(vessel_param, dict):
|
||||
return vessel_param.get("id", "")
|
||||
elif isinstance(vessel_param, str):
|
||||
return vessel_param
|
||||
else:
|
||||
return ""
|
||||
|
||||
final_vessel_id, _ = vessel_id
|
||||
final_to_vessel_id, _ = get_vessel(to_vessel) or get_vessel(product_vessel)
|
||||
final_waste_vessel_id, _ = get_vessel(waste_phase_to_vessel) or get_vessel(waste_vessel)
|
||||
|
||||
# 统一体积参数
|
||||
final_volume = parse_volume_input(volume or solvent_volume)
|
||||
|
||||
# 🔧 修复:确保repeats至少为1
|
||||
if repeats <= 0:
|
||||
repeats = 1
|
||||
debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1")
|
||||
|
||||
debug_print(f"🔧 标准化后的参数:")
|
||||
debug_print(f" 🥼 分离容器: '{final_vessel_id}'")
|
||||
debug_print(f" 🎯 产物容器: '{final_to_vessel_id}'")
|
||||
debug_print(f" 🗑️ 废液容器: '{final_waste_vessel_id}'")
|
||||
debug_print(f" 📏 溶剂体积: {final_volume}mL")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
|
||||
action_sequence.append(create_action_log(f"分离容器: {final_vessel_id}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
|
||||
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
|
||||
|
||||
# 验证必需参数
|
||||
if not purpose:
|
||||
purpose = "separate"
|
||||
if not product_phase:
|
||||
product_phase = "top"
|
||||
if purpose not in ["wash", "extract", "separate"]:
|
||||
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
|
||||
purpose = "separate"
|
||||
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
|
||||
product_phase = "top"
|
||||
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
|
||||
|
||||
debug_print("✅ 参数验证通过")
|
||||
action_sequence.append(create_action_log("参数验证通过", "✅"))
|
||||
|
||||
# === 查找设备 ===
|
||||
debug_print("🔍 步骤2: 查找设备...")
|
||||
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
|
||||
|
||||
# 查找分离器设备
|
||||
separator_device = find_separator_device(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||
if separator_device:
|
||||
action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
|
||||
else:
|
||||
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
|
||||
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
|
||||
|
||||
# 查找搅拌器
|
||||
stirrer_device = find_connected_stirrer(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||
if stirrer_device:
|
||||
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
|
||||
else:
|
||||
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
|
||||
|
||||
# 查找溶剂容器(如果需要)
|
||||
solvent_vessel = ""
|
||||
if solvent and solvent.strip():
|
||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||
if solvent_vessel:
|
||||
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
|
||||
|
||||
debug_print(f"📊 设备配置:")
|
||||
debug_print(f" 🧪 分离器设备: '{separator_device}'")
|
||||
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
|
||||
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
|
||||
|
||||
# === 执行分离流程 ===
|
||||
debug_print("🔍 步骤3: 执行分离流程...")
|
||||
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
|
||||
|
||||
# 🔧 新增:体积变化跟踪变量
|
||||
current_volume = original_liquid_volume
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
for repeat_idx in range(repeats):
|
||||
cycle_num = repeat_idx + 1
|
||||
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
|
||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
|
||||
|
||||
# 步骤3.1: 添加溶剂(如果需要)
|
||||
if solvent_vessel and final_volume > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
|
||||
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
|
||||
|
||||
try:
|
||||
# 使用pump protocol添加溶剂
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=final_vessel_id, # 🔧 使用 final_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,
|
||||
rate_spec="",
|
||||
event="",
|
||||
through="",
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
|
||||
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
|
||||
|
||||
# 🔧 新增:更新体积 - 添加溶剂后
|
||||
current_volume += final_volume
|
||||
update_vessel_volume(vessel, G, current_volume, f"添加{final_volume}mL {solvent}后")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 溶剂添加失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌"))
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
|
||||
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
|
||||
|
||||
# 步骤3.2: 启动搅拌(如果有搅拌器)
|
||||
if stirrer_device and stir_time > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)")
|
||||
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️"))
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_device,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"分离混合 - {purpose}"
|
||||
}
|
||||
})
|
||||
|
||||
# 搅拌等待
|
||||
stir_minutes = stir_time / 60
|
||||
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": stir_time}
|
||||
})
|
||||
|
||||
# 停止搅拌
|
||||
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_device,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
|
||||
})
|
||||
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
|
||||
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
|
||||
|
||||
# 步骤3.3: 静置分层
|
||||
if settling_time > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
|
||||
settling_minutes = settling_time / 60
|
||||
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": settling_time}
|
||||
})
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
|
||||
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
|
||||
|
||||
# 步骤3.4: 执行分离操作
|
||||
if separator_device:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
|
||||
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
|
||||
|
||||
# 🔧 替换为具体的分离操作逻辑(基于old版本)
|
||||
|
||||
# 首先进行分液判断(电导突跃)
|
||||
action_sequence.append({
|
||||
"device_id": separator_device,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "delta > 0.05"
|
||||
}
|
||||
})
|
||||
|
||||
# 估算每相的体积(假设大致平分)
|
||||
phase_volume = current_volume / 2
|
||||
|
||||
# 智能查找分离容器底部
|
||||
separation_vessel_bottom = find_separation_vessel_bottom(G, final_vessel_id) # ✅
|
||||
|
||||
if product_phase == "bottom":
|
||||
debug_print(f"🔄 收集底相产物到 {final_to_vessel_id}")
|
||||
action_sequence.append(create_action_log("收集底相产物", "📦"))
|
||||
|
||||
# 产物转移到目标瓶
|
||||
if final_to_vessel_id:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=separation_vessel_bottom,
|
||||
to_vessel=final_to_vessel_id,
|
||||
volume=current_volume,
|
||||
flowrate=2.5,
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
# 放出上面那一相,60秒后关阀门
|
||||
action_sequence.append({
|
||||
"device_id": separator_device,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "time > 60"
|
||||
}
|
||||
})
|
||||
|
||||
# 弃去上面那一相进废液
|
||||
if final_waste_vessel_id:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=separation_vessel_bottom,
|
||||
to_vessel=final_waste_vessel_id,
|
||||
volume=current_volume,
|
||||
flowrate=2.5,
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
elif product_phase == "top":
|
||||
debug_print(f"🔄 收集上相产物到 {final_to_vessel_id}")
|
||||
action_sequence.append(create_action_log("收集上相产物", "📦"))
|
||||
|
||||
# 弃去下面那一相进废液
|
||||
if final_waste_vessel_id:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=separation_vessel_bottom,
|
||||
to_vessel=final_waste_vessel_id,
|
||||
volume=phase_volume,
|
||||
flowrate=2.5,
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
# 放出上面那一相,60秒后关阀门
|
||||
action_sequence.append({
|
||||
"device_id": separator_device,
|
||||
"action_name": "valve_open",
|
||||
"action_kwargs": {
|
||||
"command": "time > 60"
|
||||
}
|
||||
})
|
||||
|
||||
# 产物转移到目标瓶
|
||||
if final_to_vessel_id:
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=separation_vessel_bottom,
|
||||
to_vessel=final_to_vessel_id,
|
||||
volume=phase_volume,
|
||||
flowrate=2.5,
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
debug_print(f"✅ 分离操作已完成")
|
||||
action_sequence.append(create_action_log("分离操作完成", "✅"))
|
||||
|
||||
# 🔧 新增:分离后体积估算
|
||||
separated_volume = phase_volume * 0.95 # 假设5%损失,只保留产物相体积
|
||||
update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)")
|
||||
current_volume = separated_volume
|
||||
|
||||
# 收集结果
|
||||
if final_to_vessel_id:
|
||||
action_sequence.append(
|
||||
create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel_id}", "📦"))
|
||||
if final_waste_vessel_id:
|
||||
action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel_id}", "🗑️"))
|
||||
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
|
||||
action_sequence.append(create_action_log("无分离器设备可用", "❌"))
|
||||
# 添加等待时间模拟分离
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# 🔧 新增:如果不是最后一次,从中转瓶转移回分液漏斗(基于old版本逻辑)
|
||||
if repeat_idx < repeats - 1 and final_to_vessel_id and final_to_vessel_id != final_vessel_id:
|
||||
debug_print(f"🔄 第{cycle_num}轮: 产物转移回分离容器准备下一轮")
|
||||
action_sequence.append(create_action_log("产物转回分离容器,准备下一轮", "🔄"))
|
||||
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=final_to_vessel_id,
|
||||
to_vessel=final_vessel_id,
|
||||
volume=current_volume,
|
||||
flowrate=2.5,
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
|
||||
# 更新体积回到分离容器
|
||||
update_vessel_volume(vessel, G, current_volume, f"产物转回分离容器(第{cycle_num}轮后)")
|
||||
|
||||
# 循环间等待(除了最后一次)
|
||||
if repeat_idx < repeats - 1:
|
||||
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
|
||||
action_sequence.append(create_action_log("等待下一次循环...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
|
||||
|
||||
# 🔧 新增:分离完成后的最终状态报告
|
||||
final_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||
|
||||
# === 最终结果 ===
|
||||
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
|
||||
|
||||
debug_print("🌀" * 20)
|
||||
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" 🥼 分离容器: {final_vessel_id}")
|
||||
debug_print(f" 🎯 分离目的: {purpose}")
|
||||
debug_print(f" 📊 产物相: {product_phase}")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
debug_print(f"💧 体积变化统计:")
|
||||
debug_print(f" - 分离前体积: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 分离后体积: {final_liquid_volume:.2f}mL")
|
||||
if solvent:
|
||||
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL × {repeats}轮 = {final_volume * repeats:.2f}mL)")
|
||||
if final_to_vessel_id:
|
||||
debug_print(f" 🎯 产物容器: {final_to_vessel_id}")
|
||||
if final_waste_vessel_id:
|
||||
debug_print(f" 🗑️ 废液容器: {final_waste_vessel_id}")
|
||||
debug_print("🌀" * 20)
|
||||
|
||||
# 添加完成日志
|
||||
summary_msg = f"分离协议完成: {final_vessel_id} ({purpose},{repeats} 次循环)"
|
||||
if solvent:
|
||||
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
|
||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||
|
||||
return action_sequence
|
||||
|
||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||
"""
|
||||
@@ -364,386 +792,54 @@ def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, descrip
|
||||
|
||||
debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
|
||||
|
||||
def generate_separate_protocol(
|
||||
G: nx.DiGraph,
|
||||
# 🔧 基础参数,支持XDL的vessel参数
|
||||
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
|
||||
purpose: str = "separate", # 分离目的
|
||||
product_phase: str = "top", # 产物相
|
||||
# 🔧 可选的详细参数
|
||||
from_vessel: Union[str, dict] = "", # 源容器(通常在separate前已经transfer了)
|
||||
separation_vessel: Union[str, dict] = "", # 分离容器(与vessel同义)
|
||||
to_vessel: Union[str, dict] = "", # 目标容器(可选)
|
||||
waste_phase_to_vessel: Union[str, dict] = "", # 废相目标容器
|
||||
product_vessel: Union[str, dict] = "", # XDL: 产物容器(与to_vessel同义)
|
||||
waste_vessel: Union[str, dict] = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
|
||||
# 🔧 溶剂相关参数
|
||||
solvent: str = "", # 溶剂名称
|
||||
solvent_volume: Union[str, float] = 0.0, # 溶剂体积
|
||||
volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
|
||||
# 🔧 操作参数
|
||||
through: str = "", # 通过材料
|
||||
repeats: int = 1, # 重复次数
|
||||
stir_time: float = 30.0, # 搅拌时间(秒)
|
||||
stir_speed: float = 300.0, # 搅拌速度
|
||||
settling_time: float = 300.0, # 沉降时间(秒)
|
||||
**kwargs
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成分离操作的协议序列 - 支持vessel字典和体积运算
|
||||
|
||||
支持XDL参数格式:
|
||||
- vessel: 分离容器字典(必需)
|
||||
- purpose: "wash", "extract", "separate"
|
||||
- product_phase: "top", "bottom"
|
||||
- product_vessel: 产物收集容器
|
||||
- waste_vessel: 废液收集容器
|
||||
- solvent: 溶剂名称
|
||||
- volume: "200 mL", "?" 或数值
|
||||
- repeats: 重复次数
|
||||
|
||||
分离流程:
|
||||
1. (可选)添加溶剂到分离容器
|
||||
2. 搅拌混合
|
||||
3. 静置分层
|
||||
4. 收集指定相到目标容器
|
||||
5. 重复指定次数
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:vessel参数兼容处理
|
||||
if vessel is None:
|
||||
if isinstance(separation_vessel, dict):
|
||||
vessel = separation_vessel
|
||||
else:
|
||||
raise ValueError("必须提供vessel字典参数")
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
# 统一处理vessel参数
|
||||
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 = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
||||
|
||||
debug_print("🌀" * 20)
|
||||
debug_print("🚀 开始生成分离协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||
debug_print(f" 🎯 分离目的: '{purpose}'")
|
||||
debug_print(f" 📊 产物相: '{product_phase}'")
|
||||
debug_print(f" 💧 溶剂: '{solvent}'")
|
||||
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
debug_print(f" 🎯 产物容器: '{product_vessel}'")
|
||||
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
|
||||
debug_print(f" 📦 其他参数: {kwargs}")
|
||||
debug_print("🌀" * 20)
|
||||
|
||||
action_sequence = []
|
||||
|
||||
# 🔧 新增:记录分离前的容器状态
|
||||
debug_print("🔍 记录分离前容器状态...")
|
||||
original_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||
debug_print(f"📊 分离前液体体积: {original_liquid_volume:.2f}mL")
|
||||
|
||||
# === 参数验证和标准化 ===
|
||||
debug_print("🔍 步骤1: 参数验证和标准化...")
|
||||
action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel_id}", "🎬"))
|
||||
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
|
||||
|
||||
# 统一容器参数 - 支持字典和字符串
|
||||
def extract_vessel_id(vessel_param):
|
||||
if isinstance(vessel_param, dict):
|
||||
return vessel_param.get("id", "")
|
||||
elif isinstance(vessel_param, str):
|
||||
return vessel_param
|
||||
else:
|
||||
return ""
|
||||
|
||||
final_vessel_id = vessel_id
|
||||
final_to_vessel_id = extract_vessel_id(to_vessel) or extract_vessel_id(product_vessel)
|
||||
final_waste_vessel_id = extract_vessel_id(waste_phase_to_vessel) or extract_vessel_id(waste_vessel)
|
||||
|
||||
# 统一体积参数
|
||||
final_volume = parse_volume_input(volume or solvent_volume)
|
||||
|
||||
# 🔧 修复:确保repeats至少为1
|
||||
if repeats <= 0:
|
||||
repeats = 1
|
||||
debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1")
|
||||
|
||||
debug_print(f"🔧 标准化后的参数:")
|
||||
debug_print(f" 🥼 分离容器: '{final_vessel_id}'")
|
||||
debug_print(f" 🎯 产物容器: '{final_to_vessel_id}'")
|
||||
debug_print(f" 🗑️ 废液容器: '{final_waste_vessel_id}'")
|
||||
debug_print(f" 📏 溶剂体积: {final_volume}mL")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
|
||||
action_sequence.append(create_action_log(f"分离容器: {final_vessel_id}", "🧪"))
|
||||
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
|
||||
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
|
||||
|
||||
# 验证必需参数
|
||||
if not purpose:
|
||||
purpose = "separate"
|
||||
if not product_phase:
|
||||
product_phase = "top"
|
||||
if purpose not in ["wash", "extract", "separate"]:
|
||||
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
|
||||
purpose = "separate"
|
||||
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
|
||||
product_phase = "top"
|
||||
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
|
||||
|
||||
debug_print("✅ 参数验证通过")
|
||||
action_sequence.append(create_action_log("参数验证通过", "✅"))
|
||||
|
||||
# === 查找设备 ===
|
||||
debug_print("🔍 步骤2: 查找设备...")
|
||||
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
|
||||
|
||||
# 查找分离器设备
|
||||
separator_device = find_separator_device(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||
if separator_device:
|
||||
action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
|
||||
else:
|
||||
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
|
||||
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
|
||||
|
||||
# 查找搅拌器
|
||||
stirrer_device = find_connected_stirrer(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||
if stirrer_device:
|
||||
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
|
||||
else:
|
||||
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
|
||||
|
||||
# 查找溶剂容器(如果需要)
|
||||
solvent_vessel = ""
|
||||
if solvent and solvent.strip():
|
||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||
if solvent_vessel:
|
||||
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
|
||||
|
||||
debug_print(f"📊 设备配置:")
|
||||
debug_print(f" 🧪 分离器设备: '{separator_device}'")
|
||||
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
|
||||
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
|
||||
|
||||
# === 执行分离流程 ===
|
||||
debug_print("🔍 步骤3: 执行分离流程...")
|
||||
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
|
||||
|
||||
# 🔧 新增:体积变化跟踪变量
|
||||
current_volume = original_liquid_volume
|
||||
|
||||
try:
|
||||
for repeat_idx in range(repeats):
|
||||
cycle_num = repeat_idx + 1
|
||||
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
|
||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
|
||||
|
||||
# 步骤3.1: 添加溶剂(如果需要)
|
||||
if solvent_vessel and final_volume > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
|
||||
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
|
||||
|
||||
try:
|
||||
# 使用pump protocol添加溶剂
|
||||
pump_actions = generate_pump_protocol_with_rinsing(
|
||||
G=G,
|
||||
from_vessel=solvent_vessel,
|
||||
to_vessel=final_vessel_id, # 🔧 使用 final_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,
|
||||
rate_spec="",
|
||||
event="",
|
||||
through="",
|
||||
**kwargs
|
||||
)
|
||||
action_sequence.extend(pump_actions)
|
||||
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
|
||||
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
|
||||
|
||||
# 🔧 新增:更新体积 - 添加溶剂后
|
||||
current_volume += final_volume
|
||||
update_vessel_volume(vessel, G, current_volume, f"添加{final_volume}mL {solvent}后")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 溶剂添加失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌"))
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
|
||||
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
|
||||
|
||||
# 步骤3.2: 启动搅拌(如果有搅拌器)
|
||||
if stirrer_device and stir_time > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)")
|
||||
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️"))
|
||||
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_device,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"分离混合 - {purpose}"
|
||||
}
|
||||
})
|
||||
|
||||
# 搅拌等待
|
||||
stir_minutes = stir_time / 60
|
||||
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": stir_time}
|
||||
})
|
||||
|
||||
# 停止搅拌
|
||||
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_device,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
|
||||
})
|
||||
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
|
||||
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
|
||||
|
||||
# 步骤3.3: 静置分层
|
||||
if settling_time > 0:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
|
||||
settling_minutes = settling_time / 60
|
||||
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": settling_time}
|
||||
})
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
|
||||
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
|
||||
|
||||
# 步骤3.4: 执行分离操作
|
||||
if separator_device:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
|
||||
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
|
||||
|
||||
# 调用分离器设备的separate方法
|
||||
separate_action = {
|
||||
"device_id": separator_device,
|
||||
"action_name": "separate",
|
||||
"action_kwargs": {
|
||||
"purpose": purpose,
|
||||
"product_phase": product_phase,
|
||||
"from_vessel": extract_vessel_id(from_vessel) or final_vessel_id, # 🔧 使用vessel_id
|
||||
"separation_vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
||||
"to_vessel": final_to_vessel_id or final_vessel_id, # 🔧 使用vessel_id
|
||||
"waste_phase_to_vessel": final_waste_vessel_id or final_vessel_id, # 🔧 使用vessel_id
|
||||
"solvent": solvent,
|
||||
"solvent_volume": final_volume,
|
||||
"through": through,
|
||||
"repeats": 1, # 每次调用只做一次分离
|
||||
"stir_time": 0, # 已经在上面完成
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": 0 # 已经在上面完成
|
||||
}
|
||||
}
|
||||
action_sequence.append(separate_action)
|
||||
debug_print(f"✅ 分离操作已添加")
|
||||
action_sequence.append(create_action_log("分离操作完成", "✅"))
|
||||
|
||||
# 🔧 新增:分离后体积估算(分离通常不改变总体积,但会重新分配)
|
||||
# 假设分离后保持体积(实际情况可能有少量损失)
|
||||
separated_volume = current_volume * 0.95 # 假设5%损失
|
||||
update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)")
|
||||
current_volume = separated_volume
|
||||
|
||||
# 收集结果
|
||||
if final_to_vessel_id:
|
||||
action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel_id}", "📦"))
|
||||
if final_waste_vessel_id:
|
||||
action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel_id}", "🗑️"))
|
||||
|
||||
else:
|
||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
|
||||
action_sequence.append(create_action_log("无分离器设备可用", "❌"))
|
||||
# 添加等待时间模拟分离
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 10.0}
|
||||
})
|
||||
|
||||
# 循环间等待(除了最后一次)
|
||||
if repeat_idx < repeats - 1:
|
||||
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
|
||||
action_sequence.append(create_action_log("等待下一次循环...", "⏳"))
|
||||
action_sequence.append({
|
||||
"action_name": "wait",
|
||||
"action_kwargs": {"time": 5}
|
||||
})
|
||||
else:
|
||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
|
||||
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
|
||||
# 添加错误日志
|
||||
action_sequence.append({
|
||||
"device_id": "system",
|
||||
"action_name": "log_message",
|
||||
"action_kwargs": {
|
||||
"message": f"分离操作失败: {str(e)}"
|
||||
}
|
||||
})
|
||||
|
||||
# 🔧 新增:分离完成后的最终状态报告
|
||||
final_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||
|
||||
# === 最终结果 ===
|
||||
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
|
||||
|
||||
debug_print("🌀" * 20)
|
||||
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" 🥼 分离容器: {final_vessel_id}")
|
||||
debug_print(f" 🎯 分离目的: {purpose}")
|
||||
debug_print(f" 📊 产物相: {product_phase}")
|
||||
debug_print(f" 🔄 重复次数: {repeats}")
|
||||
debug_print(f"💧 体积变化统计:")
|
||||
debug_print(f" - 分离前体积: {original_liquid_volume:.2f}mL")
|
||||
debug_print(f" - 分离后体积: {final_liquid_volume:.2f}mL")
|
||||
if solvent:
|
||||
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL × {repeats}轮 = {final_volume * repeats:.2f}mL)")
|
||||
if final_to_vessel_id:
|
||||
debug_print(f" 🎯 产物容器: {final_to_vessel_id}")
|
||||
if final_waste_vessel_id:
|
||||
debug_print(f" 🗑️ 废液容器: {final_waste_vessel_id}")
|
||||
debug_print("🌀" * 20)
|
||||
|
||||
# 添加完成日志
|
||||
summary_msg = f"分离协议完成: {final_vessel_id} ({purpose},{repeats} 次循环)"
|
||||
if solvent:
|
||||
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
|
||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||
|
||||
return action_sequence
|
||||
|
||||
def find_separation_vessel_bottom(G: nx.DiGraph, vessel_id: str) -> str:
|
||||
"""
|
||||
智能查找分离容器的底部容器(假设为flask或vessel类型)
|
||||
|
||||
Args:
|
||||
G: 网络图
|
||||
vessel_id: 分离容器ID
|
||||
|
||||
Returns:
|
||||
str: 底部容器ID
|
||||
"""
|
||||
debug_print(f"🔍 查找分离容器 {vessel_id} 的底部容器...")
|
||||
|
||||
# 方法1:根据命名规则推测
|
||||
possible_bottoms = [
|
||||
f"{vessel_id}_bottom",
|
||||
f"flask_{vessel_id}",
|
||||
f"vessel_{vessel_id}",
|
||||
f"{vessel_id}_flask",
|
||||
f"{vessel_id}_vessel"
|
||||
]
|
||||
|
||||
debug_print(f"📋 尝试的底部容器名称: {possible_bottoms}")
|
||||
|
||||
for bottom_id in possible_bottoms:
|
||||
if bottom_id in G.nodes():
|
||||
node_type = G.nodes[bottom_id].get('type', '')
|
||||
if node_type == 'container':
|
||||
debug_print(f"✅ 通过命名规则找到底部容器: {bottom_id}")
|
||||
return bottom_id
|
||||
|
||||
# 方法2:查找与分离器相连的容器(假设底部容器会与分离器相连)
|
||||
debug_print(f"📋 方法2: 查找连接的容器...")
|
||||
for node in G.nodes():
|
||||
node_data = G.nodes[node]
|
||||
node_class = node_data.get('class', '') or ''
|
||||
|
||||
if 'separator' in node_class.lower():
|
||||
# 检查分离器的输入端
|
||||
if G.has_edge(node, vessel_id):
|
||||
for neighbor in G.neighbors(node):
|
||||
if neighbor != vessel_id:
|
||||
neighbor_type = G.nodes[neighbor].get('type', '')
|
||||
if neighbor_type == 'container':
|
||||
debug_print(f"✅ 通过连接找到底部容器: {neighbor}")
|
||||
return neighbor
|
||||
|
||||
debug_print(f"❌ 无法找到分离容器 {vessel_id} 的底部容器")
|
||||
return ""
|
||||
|
||||
|
||||
@@ -3,81 +3,14 @@ import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .utils.unit_parser import parse_time_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""调试输出"""
|
||||
print(f"🌪️ [STIR] {message}", flush=True)
|
||||
logger.info(f"[STIR] {message}")
|
||||
|
||||
def parse_time_input(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
||||
"""
|
||||
统一的时间解析函数(精简版)
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
default_unit: 默认单位(默认为秒)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 100.0 # 默认100秒
|
||||
|
||||
# 🔢 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
||||
return result
|
||||
|
||||
# 📝 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"🔍 解析时间: '{time_str}'")
|
||||
|
||||
# ❓ 特殊值处理
|
||||
special_times = {
|
||||
'?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
|
||||
'briefly': 30.0, 'quickly': 60.0, 'slowly': 600.0,
|
||||
'several minutes': 300.0, 'few minutes': 180.0, 'overnight': 3600.0
|
||||
}
|
||||
|
||||
if time_str in special_times:
|
||||
result = special_times[time_str]
|
||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
|
||||
return result
|
||||
|
||||
# 🔢 纯数字处理
|
||||
try:
|
||||
result = float(time_str)
|
||||
debug_print(f"⏰ 纯数字: {time_str} → {result}s")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 📐 正则表达式解析
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 100s")
|
||||
return 100.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit
|
||||
|
||||
# 📏 单位转换
|
||||
unit_multipliers = {
|
||||
's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
|
||||
'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
|
||||
'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
|
||||
'd': 86400.0, 'day': 86400.0, 'days': 86400.0
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
|
||||
return result
|
||||
|
||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
|
||||
"""查找与指定容器相连的搅拌设备"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,108 +4,12 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .logger_util import debug_print
|
||||
|
||||
def debug_print(message, prefix="[UNIT_PARSER]"):
|
||||
"""调试输出"""
|
||||
print(f"{prefix} {message}", flush=True)
|
||||
logger.info(f"{prefix} {message}")
|
||||
|
||||
def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
||||
"""
|
||||
解析带单位的时间输入
|
||||
|
||||
Args:
|
||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
||||
default_unit: 默认单位(默认为秒)
|
||||
|
||||
Returns:
|
||||
float: 时间(秒)
|
||||
"""
|
||||
if not time_input:
|
||||
return 0.0
|
||||
|
||||
# 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
|
||||
return result
|
||||
|
||||
# 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
debug_print(f"解析时间字符串: '{time_str}'")
|
||||
|
||||
# 处理特殊值
|
||||
if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
||||
default_time = 300.0 # 5分钟默认值
|
||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
||||
return default_time
|
||||
|
||||
# 如果是纯数字,使用默认单位
|
||||
try:
|
||||
value = float(time_str)
|
||||
if default_unit == "s":
|
||||
result = value
|
||||
elif default_unit in ["min", "minute"]:
|
||||
result = value * 60.0
|
||||
elif default_unit in ["h", "hour"]:
|
||||
result = value * 3600.0
|
||||
else:
|
||||
result = value # 默认秒
|
||||
debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
|
||||
return result
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 使用正则表达式匹配数字和单位
|
||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
||||
match = re.match(pattern, time_str)
|
||||
|
||||
if not match:
|
||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
|
||||
return 60.0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) or default_unit
|
||||
|
||||
# 单位转换映射
|
||||
unit_multipliers = {
|
||||
# 秒
|
||||
's': 1.0,
|
||||
'sec': 1.0,
|
||||
'second': 1.0,
|
||||
'seconds': 1.0,
|
||||
|
||||
# 分钟
|
||||
'm': 60.0,
|
||||
'min': 60.0,
|
||||
'mins': 60.0,
|
||||
'minute': 60.0,
|
||||
'minutes': 60.0,
|
||||
|
||||
# 小时
|
||||
'h': 3600.0,
|
||||
'hr': 3600.0,
|
||||
'hrs': 3600.0,
|
||||
'hour': 3600.0,
|
||||
'hours': 3600.0,
|
||||
|
||||
# 天
|
||||
'd': 86400.0,
|
||||
'day': 86400.0,
|
||||
'days': 86400.0,
|
||||
}
|
||||
|
||||
multiplier = unit_multipliers.get(unit, 1.0)
|
||||
result = value * multiplier
|
||||
|
||||
debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
|
||||
return result
|
||||
|
||||
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||
def parse_volume_input(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||
"""
|
||||
解析带单位的体积输入
|
||||
|
||||
@@ -175,6 +79,111 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
|
||||
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():
|
||||
"""测试单位解析功能"""
|
||||
@@ -187,7 +196,7 @@ def test_unit_parser():
|
||||
|
||||
print("\n时间解析测试:")
|
||||
for time_input in time_tests:
|
||||
result = parse_time_with_units(time_input)
|
||||
result = parse_time_input(time_input)
|
||||
print(f" {time_input} → {result}s ({result/60:.1f}min)")
|
||||
|
||||
# 测试体积解析
|
||||
@@ -197,7 +206,7 @@ def test_unit_parser():
|
||||
|
||||
print("\n体积解析测试:")
|
||||
for volume_input in volume_tests:
|
||||
result = parse_volume_with_units(volume_input)
|
||||
result = parse_volume_input(volume_input)
|
||||
print(f" {volume_input} → {result}mL")
|
||||
|
||||
print("\n✅ 测试完成")
|
||||
|
||||
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 ""
|
||||
@@ -3,118 +3,14 @@ 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):
|
||||
"""调试输出"""
|
||||
print(f"🧼 [WASH_SOLID] {message}", flush=True)
|
||||
logger.info(f"[WASH_SOLID] {message}")
|
||||
|
||||
def parse_time_input(time_input: Union[str, float, int]) -> float:
|
||||
"""统一时间解析函数(精简版)"""
|
||||
if not time_input:
|
||||
return 0.0
|
||||
|
||||
# 🔢 处理数值输入
|
||||
if isinstance(time_input, (int, float)):
|
||||
result = float(time_input)
|
||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
||||
return result
|
||||
|
||||
# 📝 处理字符串输入
|
||||
time_str = str(time_input).lower().strip()
|
||||
|
||||
# ❓ 特殊值快速处理
|
||||
special_times = {
|
||||
'?': 60.0, 'unknown': 60.0, 'briefly': 30.0,
|
||||
'quickly': 45.0, 'slowly': 120.0
|
||||
}
|
||||
|
||||
if time_str in special_times:
|
||||
result = special_times[time_str]
|
||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s")
|
||||
return result
|
||||
|
||||
# 🔢 数字提取(简化正则)
|
||||
try:
|
||||
# 提取数字
|
||||
numbers = re.findall(r'\d+\.?\d*', time_str)
|
||||
if numbers:
|
||||
value = float(numbers[0])
|
||||
|
||||
# 简化单位判断
|
||||
if any(unit in time_str for unit in ['min', 'm']):
|
||||
result = value * 60.0
|
||||
elif any(unit in time_str for unit in ['h', 'hour']):
|
||||
result = value * 3600.0
|
||||
else:
|
||||
result = value # 默认秒
|
||||
|
||||
debug_print(f"✅ 时间解析: '{time_str}' → {result}s")
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s")
|
||||
return 60.0
|
||||
|
||||
def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float:
|
||||
"""统一体积解析函数(精简版)"""
|
||||
debug_print(f"💧 解析体积: volume={volume}, spec='{volume_spec}', mass='{mass}'")
|
||||
|
||||
# 🎯 优先级1:volume_spec(快速映射)
|
||||
if volume_spec:
|
||||
spec_map = {
|
||||
'small': 20.0, 'medium': 50.0, 'large': 100.0,
|
||||
'minimal': 10.0, 'normal': 50.0, 'generous': 150.0
|
||||
}
|
||||
for key, val in spec_map.items():
|
||||
if key in volume_spec.lower():
|
||||
debug_print(f"🎯 规格匹配: '{volume_spec}' → {val}mL")
|
||||
return val
|
||||
|
||||
# 🧮 优先级2:mass转体积(简化:1g=1mL)
|
||||
if mass:
|
||||
try:
|
||||
numbers = re.findall(r'\d+\.?\d*', mass)
|
||||
if numbers:
|
||||
value = float(numbers[0])
|
||||
if 'mg' in mass.lower():
|
||||
result = value / 1000.0
|
||||
elif 'kg' in mass.lower():
|
||||
result = value * 1000.0
|
||||
else:
|
||||
result = value # 默认g
|
||||
debug_print(f"⚖️ 质量转换: {mass} → {result}mL")
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
# 📦 优先级3:volume
|
||||
if volume:
|
||||
if isinstance(volume, (int, float)):
|
||||
result = float(volume)
|
||||
debug_print(f"💧 数值体积: {volume} → {result}mL")
|
||||
return result
|
||||
elif isinstance(volume, str):
|
||||
try:
|
||||
# 提取数字
|
||||
numbers = re.findall(r'\d+\.?\d*', volume)
|
||||
if numbers:
|
||||
value = float(numbers[0])
|
||||
# 简化单位判断
|
||||
if 'l' in volume.lower() and 'ml' not in volume.lower():
|
||||
result = value * 1000.0 # L转mL
|
||||
else:
|
||||
result = value # 默认mL
|
||||
debug_print(f"💧 字符串体积: '{volume}' → {result}mL")
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
# 默认值
|
||||
debug_print(f"⚠️ 体积解析失败,使用默认50mL")
|
||||
return 50.0
|
||||
|
||||
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
|
||||
"""查找溶剂源(精简版)"""
|
||||
|
||||
@@ -17,6 +17,7 @@ class BasicConfig:
|
||||
machine_name = "undefined"
|
||||
vis_2d_enable = False
|
||||
enable_resource_load = True
|
||||
direct_end = False
|
||||
|
||||
|
||||
# MQTT配置
|
||||
@@ -109,13 +110,13 @@ def _update_config_from_module(module, override_labid: str):
|
||||
|
||||
|
||||
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
|
||||
@@ -163,12 +164,12 @@ def _update_config_from_env():
|
||||
def load_config(config_path=None, override_labid=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)
|
||||
@@ -179,6 +180,7 @@ def load_config(config_path=None, override_labid=None):
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
_update_config_from_module(module, override_labid)
|
||||
logger.info(f"[ENV] 配置文件 {config_path} 加载成功")
|
||||
_update_config_from_env()
|
||||
except Exception as e:
|
||||
logger.error(f"[ENV] 加载配置文件 {config_path} 失败")
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -8,9 +8,9 @@ class MQConfig:
|
||||
broker_url = ""
|
||||
port = 1883
|
||||
|
||||
ca_file = "CA.crt"
|
||||
cert_file = "lab.crt"
|
||||
key_file = "lab.key"
|
||||
ca_file = "./CA.crt"
|
||||
cert_file = "./lab.crt"
|
||||
key_file = "./lab.key"
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
|
||||
@@ -138,6 +138,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
||||
offsets: Optional[List[Coordinate]] = None,
|
||||
**backend_kwargs,
|
||||
):
|
||||
if not offsets or (isinstance(offsets, list) and len(offsets) != len(use_channels)):
|
||||
offsets = [Coordinate.zero()] * len(use_channels)
|
||||
if self._simulator:
|
||||
return await self._simulate_handler.discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
||||
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
||||
|
||||
@@ -67,7 +67,7 @@ class PRCXI9300Deck(Deck):
|
||||
|
||||
|
||||
class PRCXI9300Container(Plate, TipRack):
|
||||
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
|
||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate和TipRack。
|
||||
|
||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||
"""
|
||||
|
||||
44
unilabos/devices/liquid_handling/prcxi/prcxi_res.py
Normal file
44
unilabos/devices/liquid_handling/prcxi/prcxi_res.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import collections
|
||||
|
||||
from pylabrobot.resources import opentrons_96_tiprack_10ul
|
||||
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
|
||||
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container, PRCXI9300Trash
|
||||
|
||||
|
||||
def get_well_container(name: str) -> PRCXI9300Container:
|
||||
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
|
||||
plate = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="plate",
|
||||
ordering=collections.OrderedDict())
|
||||
plate_serialized = plate.serialize()
|
||||
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
|
||||
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
||||
return new_plate
|
||||
|
||||
def get_tip_rack(name: str) -> PRCXI9300Container:
|
||||
tip_racks = opentrons_96_tiprack_10ul("name").serialize()
|
||||
tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
|
||||
ordering=collections.OrderedDict())
|
||||
tip_rack_serialized = tip_rack.serialize()
|
||||
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
|
||||
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
|
||||
return new_tip_rack
|
||||
|
||||
def prcxi_96_wellplate_360ul_flat(name: str):
|
||||
return get_well_container(name)
|
||||
|
||||
def prcxi_opentrons_96_tiprack_10ul(name: str):
|
||||
return get_tip_rack(name)
|
||||
|
||||
def prcxi_trash(name: str = None):
|
||||
return PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
test_plate = prcxi_96_wellplate_360ul_flat("test_plate")
|
||||
test_rack = prcxi_opentrons_96_tiprack_10ul("test_rack")
|
||||
tash = prcxi_trash("trash")
|
||||
print(test_plate)
|
||||
print(test_rack)
|
||||
print(tash)
|
||||
# Output will be a dictionary representation of the PRCXI9300Container with well details
|
||||
@@ -1,177 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockChiller:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
self._current_temperature: float = 25.0 # 室温开始
|
||||
self._target_temperature: float = 25.0
|
||||
self._status: str = "Idle"
|
||||
self._is_cooling: bool = False
|
||||
self._is_heating: bool = False
|
||||
self._vessel = "Unknown"
|
||||
self._purpose = "Unknown"
|
||||
|
||||
# 模拟温度变化的线程
|
||||
self._temperature_thread = None
|
||||
self._running = True
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""当前温度 - 会被自动识别的设备属性"""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""目标温度"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def is_cooling(self) -> bool:
|
||||
"""是否正在冷却"""
|
||||
return self._is_cooling
|
||||
|
||||
@property
|
||||
def is_heating(self) -> bool:
|
||||
"""是否正在加热"""
|
||||
return self._is_heating
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
"""当前操作的容器名称"""
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
"""当前操作目的"""
|
||||
return self._purpose
|
||||
|
||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str):
|
||||
"""设置目标温度并记录容器和目的"""
|
||||
self._vessel = str(vessel)
|
||||
self._purpose = str(purpose)
|
||||
self._target_temperature = float(temp)
|
||||
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
elif diff < 0:
|
||||
self._status = "Cooling"
|
||||
self._is_cooling = True
|
||||
self._is_heating = False
|
||||
else:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._is_cooling = False
|
||||
|
||||
self._start_temperature_control()
|
||||
return True
|
||||
|
||||
def heat_chill_stop(self, vessel: str):
|
||||
"""停止加热/制冷"""
|
||||
if vessel != self._vessel:
|
||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
||||
|
||||
# 停止温度控制线程,锁定当前温度
|
||||
self._stop_temperature_control()
|
||||
|
||||
# 更新状态
|
||||
self._status = "Stopped"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
|
||||
# 重新启动线程但保持温度
|
||||
self._running = True
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def _start_temperature_control(self):
|
||||
"""启动温度控制线程"""
|
||||
self._running = True
|
||||
if self._temperature_thread is None or not self._temperature_thread.is_alive():
|
||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
||||
self._temperature_thread.daemon = True
|
||||
self._temperature_thread.start()
|
||||
|
||||
def _stop_temperature_control(self):
|
||||
"""停止温度控制"""
|
||||
self._running = False
|
||||
if self._temperature_thread:
|
||||
self._temperature_thread.join(timeout=1.0)
|
||||
|
||||
def _temperature_control_loop(self):
|
||||
"""温度控制循环 - 模拟真实冷却器的温度变化"""
|
||||
while self._running:
|
||||
# 如果状态是 Stopped,不改变温度
|
||||
if self._status == "Stopped":
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
elif temp_diff < 0:
|
||||
self._status = "Cooling"
|
||||
self._is_cooling = True
|
||||
self._is_heating = False
|
||||
self._current_temperature -= 0.5
|
||||
else:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._is_cooling = False
|
||||
self._current_temperature += 0.3
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_temperature_control()
|
||||
self._is_cooling = False
|
||||
self._is_heating = False
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取完整状态信息"""
|
||||
return {
|
||||
"current_temperature": self._current_temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"status": self._status,
|
||||
"is_cooling": self._is_cooling,
|
||||
"is_heating": self._is_heating,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
chiller = MockChiller()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动冷却器测试...")
|
||||
print(f"初始状态: {chiller.get_status_info()}")
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}")
|
||||
|
||||
chiller.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,235 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockFilter:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
# 基本参数初始化
|
||||
self.port = port
|
||||
self._status: str = "Idle"
|
||||
self._is_filtering: bool = False
|
||||
|
||||
# 过滤性能参数
|
||||
self._flow_rate: float = 1.0 # 流速(L/min)
|
||||
self._pressure_drop: float = 0.0 # 压降(Pa)
|
||||
self._filter_life: float = 100.0 # 滤芯寿命(%)
|
||||
|
||||
# 过滤操作参数
|
||||
self._vessel: str = "" # 源容器
|
||||
self._filtrate_vessel: str = "" # 目标容器
|
||||
self._stir: bool = False # 是否搅拌
|
||||
self._stir_speed: float = 0.0 # 搅拌速度
|
||||
self._temperature: float = 25.0 # 温度(℃)
|
||||
self._continue_heatchill: bool = False # 是否继续加热/制冷
|
||||
self._target_volume: float = 0.0 # 目标过滤体积(L)
|
||||
self._filtered_volume: float = 0.0 # 已过滤体积(L)
|
||||
self._progress: float = 0.0 # 过滤进度(%)
|
||||
|
||||
# 线程控制
|
||||
self._filter_thread = None
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def is_filtering(self) -> bool:
|
||||
return self._is_filtering
|
||||
|
||||
@property
|
||||
def flow_rate(self) -> float:
|
||||
return self._flow_rate
|
||||
|
||||
@property
|
||||
def pressure_drop(self) -> float:
|
||||
return self._pressure_drop
|
||||
|
||||
@property
|
||||
def filter_life(self) -> float:
|
||||
return self._filter_life
|
||||
# 新增 property
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def filtrate_vessel(self) -> str:
|
||||
return self._filtrate_vessel
|
||||
|
||||
@property
|
||||
def filtered_volume(self) -> float:
|
||||
return self._filtered_volume
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self._progress
|
||||
|
||||
@property
|
||||
def stir(self) -> bool:
|
||||
return self._stir
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def continue_heatchill(self) -> bool:
|
||||
return self._continue_heatchill
|
||||
|
||||
@property
|
||||
def target_volume(self) -> float:
|
||||
return self._target_volume
|
||||
|
||||
def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict:
|
||||
"""新的过滤操作"""
|
||||
# 停止任何正在进行的过滤
|
||||
if self._is_filtering:
|
||||
self.stop_filtering()
|
||||
# 验证参数
|
||||
if volume <= 0:
|
||||
return {"success": False, "message": "Target volume must be greater than 0"}
|
||||
# 设置新的过滤参数
|
||||
self._vessel = vessel
|
||||
self._filtrate_vessel = filtrate_vessel
|
||||
self._stir = stir
|
||||
self._stir_speed = stir_speed
|
||||
self._temperature = temp
|
||||
self._continue_heatchill = continue_heatchill
|
||||
self._target_volume = volume
|
||||
# 重置过滤状态
|
||||
self._filtered_volume = 0.0
|
||||
self._progress = 0.0
|
||||
self._status = "Starting Filter"
|
||||
# 启动过滤过程
|
||||
self._flow_rate = 1.0 # 设置默认流速
|
||||
self._start_filter_process()
|
||||
|
||||
return {"success": True, "message": "Filter started"}
|
||||
|
||||
def stop_filtering(self):
|
||||
"""停止过滤"""
|
||||
self._status = "Stopping Filter"
|
||||
self._stop_filter_process()
|
||||
self._flow_rate = 0.0
|
||||
self._is_filtering = False
|
||||
self._status = "Stopped"
|
||||
return True
|
||||
|
||||
def replace_filter(self):
|
||||
"""更换滤芯"""
|
||||
self._filter_life = 100.0
|
||||
self._status = "Filter Replaced"
|
||||
return True
|
||||
|
||||
def _start_filter_process(self):
|
||||
"""启动过滤过程线程"""
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._is_filtering = True
|
||||
self._filter_thread = threading.Thread(target=self._filter_loop)
|
||||
self._filter_thread.daemon = True
|
||||
self._filter_thread.start()
|
||||
|
||||
def _stop_filter_process(self):
|
||||
"""停止过滤过程"""
|
||||
self._running = False
|
||||
if self._filter_thread:
|
||||
self._filter_thread.join(timeout=1.0)
|
||||
|
||||
def _filter_loop(self):
|
||||
"""过滤进程主循环"""
|
||||
update_interval = 1.0 # 更新间隔(秒)
|
||||
|
||||
while self._running and self._is_filtering:
|
||||
try:
|
||||
self._status = "Filtering"
|
||||
|
||||
# 计算这一秒过滤的体积 (L/min -> L/s)
|
||||
volume_increment = (self._flow_rate / 60.0) * update_interval
|
||||
|
||||
# 更新已过滤体积
|
||||
self._filtered_volume += volume_increment
|
||||
|
||||
# 更新进度 (避免除零错误)
|
||||
if self._target_volume > 0:
|
||||
self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0)
|
||||
|
||||
# 更新滤芯寿命 (每过滤1L减少0.5%寿命)
|
||||
self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5))
|
||||
|
||||
# 更新压降 (根据滤芯寿命和流速动态计算)
|
||||
life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子
|
||||
flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速)
|
||||
base_pressure = 100.0 # 基础压降
|
||||
# 压降随滤芯寿命降低而增加,随流速增加而增加
|
||||
self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor
|
||||
|
||||
# 检查是否完成目标体积
|
||||
if self._target_volume > 0 and self._filtered_volume >= self._target_volume:
|
||||
self._status = "Completed"
|
||||
self._progress = 100.0
|
||||
self.stop_filtering()
|
||||
break
|
||||
|
||||
# 检查滤芯寿命
|
||||
if self._filter_life <= 10.0:
|
||||
self._status = "Filter Needs Replacement"
|
||||
|
||||
time.sleep(update_interval)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in filter loop: {e}")
|
||||
self.emergency_stop()
|
||||
break
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_filter_process()
|
||||
self._is_filtering = False
|
||||
self._flow_rate = 0.0
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""扩展的状态信息"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"is_filtering": self._is_filtering,
|
||||
"flow_rate": self._flow_rate,
|
||||
"pressure_drop": self._pressure_drop,
|
||||
"filter_life": self._filter_life,
|
||||
"vessel": self._vessel,
|
||||
"filtrate_vessel": self._filtrate_vessel,
|
||||
"filtered_volume": self._filtered_volume,
|
||||
"target_volume": self._target_volume,
|
||||
"progress": self._progress,
|
||||
"temperature": self._temperature,
|
||||
"stir": self._stir,
|
||||
"stir_speed": self._stir_speed
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
filter_device = MockFilter()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动过滤器测试...")
|
||||
print(f"初始状态: {filter_device.get_status_info()}")
|
||||
|
||||
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: "
|
||||
f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}"
|
||||
)
|
||||
|
||||
filter_device.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,247 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
class MockHeater:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
self._current_temperature: float = 25.0 # 室温开始
|
||||
self._target_temperature: float = 25.0
|
||||
self._status: str = "Idle"
|
||||
self._is_heating: bool = False
|
||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
||||
self._max_temperature: float = 300.0 # 最大加热温度
|
||||
|
||||
# 新增加的属性
|
||||
self._vessel: str = "Unknown"
|
||||
self._purpose: str = "Unknown"
|
||||
self._stir: bool = False
|
||||
self._stir_speed: float = 0.0
|
||||
|
||||
# 模拟加热过程的线程
|
||||
self._heating_thread = None
|
||||
self._running = True
|
||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
||||
self._heating_thread.daemon = True
|
||||
self._heating_thread.start()
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""当前温度 - 会被自动识别的设备属性"""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""目标温度"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def is_heating(self) -> bool:
|
||||
"""是否正在加热"""
|
||||
return self._is_heating
|
||||
|
||||
@property
|
||||
def heating_power(self) -> float:
|
||||
"""加热功率百分比"""
|
||||
return self._heating_power
|
||||
|
||||
@property
|
||||
def max_temperature(self) -> float:
|
||||
"""最大加热温度"""
|
||||
return self._max_temperature
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
"""当前操作的容器名称"""
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
"""操作目的"""
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def stir(self) -> bool:
|
||||
"""是否搅拌"""
|
||||
return self._stir
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
"""搅拌速度"""
|
||||
return self._stir_speed
|
||||
|
||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict:
|
||||
"""开始加热/制冷过程"""
|
||||
self._vessel = str(vessel)
|
||||
self._purpose = str(purpose)
|
||||
self._target_temperature = float(temp)
|
||||
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
elif diff > 0:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def heat_chill_stop(self, vessel: str) -> dict:
|
||||
"""停止加热/制冷"""
|
||||
if vessel != self._vessel:
|
||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
||||
|
||||
self._status = "Stopped"
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def heat_chill(self, vessel: str, temp: float, time: float,
|
||||
stir: bool = False, stir_speed: float = 0.0,
|
||||
purpose: str = "Unknown") -> dict:
|
||||
"""完整的加热/制冷控制"""
|
||||
self._vessel = str(vessel)
|
||||
self._target_temperature = float(temp)
|
||||
self._purpose = str(purpose)
|
||||
self._stir = stir
|
||||
self._stir_speed = stir_speed
|
||||
|
||||
diff = self._target_temperature - self._current_temperature
|
||||
if abs(diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
elif diff > 0:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
|
||||
return {"success": True, "status": self._status}
|
||||
|
||||
def set_temperature(self, temperature: float):
|
||||
"""设置目标温度 - 需要在注册表添加的设备动作"""
|
||||
try:
|
||||
temperature = float(temperature)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid temperature value"
|
||||
return False
|
||||
|
||||
if temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)"
|
||||
return False
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Setting Temperature"
|
||||
|
||||
# 启动加热控制
|
||||
self._start_heating_control()
|
||||
return True
|
||||
|
||||
def set_heating_power(self, power: float):
|
||||
"""设置加热功率"""
|
||||
try:
|
||||
power = float(power)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid power value"
|
||||
return False
|
||||
|
||||
self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100%
|
||||
return True
|
||||
|
||||
def _start_heating_control(self):
|
||||
"""启动加热控制线程"""
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
||||
self._heating_thread.daemon = True
|
||||
self._heating_thread.start()
|
||||
|
||||
def _stop_heating_control(self):
|
||||
"""停止加热控制"""
|
||||
self._running = False
|
||||
if self._heating_thread:
|
||||
self._heating_thread.join(timeout=1.0)
|
||||
|
||||
def _heating_control_loop(self):
|
||||
"""加热控制循环"""
|
||||
while self._running:
|
||||
# 如果状态是 Stopped,不改变温度
|
||||
if self._status == "Stopped":
|
||||
time.sleep(1.0)
|
||||
continue
|
||||
|
||||
temp_diff = self._target_temperature - self._current_temperature
|
||||
|
||||
if abs(temp_diff) < 0.1:
|
||||
self._status = "At Target Temperature"
|
||||
self._is_heating = False
|
||||
self._heating_power = 10.0
|
||||
elif temp_diff > 0:
|
||||
self._status = "Heating"
|
||||
self._is_heating = True
|
||||
self._heating_power = min(100.0, abs(temp_diff) * 2)
|
||||
self._current_temperature += 0.5
|
||||
else:
|
||||
self._status = "Cooling Down"
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
self._current_temperature -= 0.2
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
def emergency_stop(self):
|
||||
"""紧急停止"""
|
||||
self._status = "Emergency Stop"
|
||||
self._stop_heating_control()
|
||||
self._is_heating = False
|
||||
self._heating_power = 0.0
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取完整状态信息"""
|
||||
return {
|
||||
"current_temperature": self._current_temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"status": self._status,
|
||||
"is_heating": self._is_heating,
|
||||
"heating_power": self._heating_power,
|
||||
"max_temperature": self._max_temperature,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
"stir": self._stir,
|
||||
"stir_speed": self._stir_speed
|
||||
}
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
heater = MockHeater()
|
||||
|
||||
print("启动加热器测试...")
|
||||
print(f"初始状态: {heater.get_status_info()}")
|
||||
|
||||
# 设置目标温度为80度
|
||||
heater.set_temperature(80.0)
|
||||
|
||||
# 模拟运行15秒
|
||||
try:
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
status = heater.get_status_info()
|
||||
print(
|
||||
f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | "
|
||||
f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}",
|
||||
end=""
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
heater.emergency_stop()
|
||||
print("\n测试被手动停止")
|
||||
|
||||
print("\n测试完成")
|
||||
@@ -1,360 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockPump:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._current_device = "MockPump1" # 设备标识符
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused
|
||||
|
||||
# 流量相关属性
|
||||
self._flow_rate: float = 0.0 # 当前流速 (mL/min)
|
||||
self._target_flow_rate: float = 0.0 # 目标流速 (mL/min)
|
||||
self._max_flow_rate: float = 100.0 # 最大流速 (mL/min)
|
||||
self._total_volume: float = 0.0 # 累计流量 (mL)
|
||||
|
||||
# 压力相关属性
|
||||
self._pressure: float = 0.0 # 当前压力 (bar)
|
||||
self._max_pressure: float = 10.0 # 最大压力 (bar)
|
||||
|
||||
# 运行控制线程
|
||||
self._pump_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 新增 PumpTransfer 相关属性
|
||||
self._from_vessel: str = ""
|
||||
self._to_vessel: str = ""
|
||||
self._transfer_volume: float = 0.0
|
||||
self._amount: str = ""
|
||||
self._transfer_time: float = 0.0
|
||||
self._is_viscous: bool = False
|
||||
self._rinsing_solvent: str = ""
|
||||
self._rinsing_volume: float = 0.0
|
||||
self._rinsing_repeats: int = 0
|
||||
self._is_solid: bool = False
|
||||
|
||||
# 时间追踪
|
||||
self._start_time: datetime = None
|
||||
self._time_spent: timedelta = timedelta()
|
||||
self._time_remaining: timedelta = timedelta()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def current_device(self) -> str:
|
||||
"""当前设备标识符"""
|
||||
return self._current_device
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def flow_rate(self) -> float:
|
||||
return self._flow_rate
|
||||
|
||||
@property
|
||||
def target_flow_rate(self) -> float:
|
||||
return self._target_flow_rate
|
||||
|
||||
@property
|
||||
def pressure(self) -> float:
|
||||
return self._pressure
|
||||
|
||||
@property
|
||||
def total_volume(self) -> float:
|
||||
return self._total_volume
|
||||
|
||||
@property
|
||||
def max_flow_rate(self) -> float:
|
||||
return self._max_flow_rate
|
||||
|
||||
@property
|
||||
def max_pressure(self) -> float:
|
||||
return self._max_pressure
|
||||
|
||||
# 添加新的属性访问器
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self._from_vessel
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self._to_vessel
|
||||
|
||||
@property
|
||||
def transfer_volume(self) -> float:
|
||||
return self._transfer_volume
|
||||
|
||||
@property
|
||||
def amount(self) -> str:
|
||||
return self._amount
|
||||
|
||||
@property
|
||||
def transfer_time(self) -> float:
|
||||
return self._transfer_time
|
||||
|
||||
@property
|
||||
def is_viscous(self) -> bool:
|
||||
return self._is_viscous
|
||||
|
||||
@property
|
||||
def rinsing_solvent(self) -> str:
|
||||
return self._rinsing_solvent
|
||||
|
||||
@property
|
||||
def rinsing_volume(self) -> float:
|
||||
return self._rinsing_volume
|
||||
|
||||
@property
|
||||
def rinsing_repeats(self) -> int:
|
||||
return self._rinsing_repeats
|
||||
|
||||
@property
|
||||
def is_solid(self) -> bool:
|
||||
return self._is_solid
|
||||
|
||||
# 修改这两个属性装饰器
|
||||
@property
|
||||
def time_spent(self) -> float:
|
||||
"""已用时间(秒)"""
|
||||
if isinstance(self._time_spent, timedelta):
|
||||
return self._time_spent.total_seconds()
|
||||
return float(self._time_spent)
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> float:
|
||||
"""剩余时间(秒)"""
|
||||
if isinstance(self._time_remaining, timedelta):
|
||||
return self._time_remaining.total_seconds()
|
||||
return float(self._time_remaining)
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
||||
amount: str = "", time: float = 0.0, viscous: bool = False,
|
||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
||||
rinsing_repeats: int = 0, solid: bool = False) -> dict:
|
||||
"""Execute pump transfer operation"""
|
||||
# Stop any existing operation first
|
||||
self._stop_pump_operation()
|
||||
|
||||
# Set transfer parameters
|
||||
self._from_vessel = from_vessel
|
||||
self._to_vessel = to_vessel
|
||||
self._transfer_volume = float(volume)
|
||||
self._amount = amount
|
||||
self._transfer_time = float(time)
|
||||
self._is_viscous = viscous
|
||||
self._rinsing_solvent = rinsing_solvent
|
||||
self._rinsing_volume = float(rinsing_volume)
|
||||
self._rinsing_repeats = int(rinsing_repeats)
|
||||
self._is_solid = solid
|
||||
|
||||
# Calculate flow rate
|
||||
if self._transfer_time > 0 and self._transfer_volume > 0:
|
||||
self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0
|
||||
else:
|
||||
self._target_flow_rate = 10.0 if not self._is_viscous else 5.0
|
||||
|
||||
# Reset timers and counters
|
||||
self._start_time = datetime.now()
|
||||
self._time_spent = timedelta()
|
||||
self._time_remaining = timedelta(seconds=self._transfer_time)
|
||||
self._total_volume = 0.0
|
||||
self._flow_rate = 0.0
|
||||
|
||||
# Start pump operation
|
||||
self._pump_state = "Running"
|
||||
self._status = "Starting Transfer"
|
||||
self._running = True
|
||||
|
||||
# Start pump operation thread
|
||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
||||
self._pump_thread.daemon = True
|
||||
self._pump_thread.start()
|
||||
|
||||
# Wait briefly to ensure thread starts
|
||||
time.sleep(0.1)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": self._status,
|
||||
"current_device": self._current_device,
|
||||
"time_spent": 0.0,
|
||||
"time_remaining": float(self._transfer_time)
|
||||
}
|
||||
|
||||
def pause_pump(self) -> str:
|
||||
|
||||
if self._pump_state != "Running":
|
||||
self._status = "Error: Pump not running"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Paused"
|
||||
self._status = "Pump Paused"
|
||||
self._stop_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def resume_pump(self) -> str:
|
||||
|
||||
if self._pump_state != "Paused":
|
||||
self._status = "Error: Pump not paused"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Resuming Pump"
|
||||
self._start_pump_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def reset_volume_counter(self) -> str:
|
||||
self._total_volume = 0.0
|
||||
self._status = "Volume counter reset"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
self._status = "Emergency Stop"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_pump_operation()
|
||||
self._flow_rate = 0.0
|
||||
self._pressure = 0.0
|
||||
self._target_flow_rate = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_pump_operation(self):
|
||||
with self._thread_lock:
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
||||
self._pump_thread.daemon = True
|
||||
self._pump_thread.start()
|
||||
|
||||
def _stop_pump_operation(self):
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._pump_thread and self._pump_thread.is_alive():
|
||||
self._pump_thread.join(timeout=2.0)
|
||||
|
||||
def _pump_operation_loop(self):
|
||||
"""泵运行主循环"""
|
||||
print("Pump operation loop started") # Debug print
|
||||
|
||||
while self._running and self._pump_state == "Running":
|
||||
try:
|
||||
# Calculate flow rate adjustment
|
||||
flow_diff = self._target_flow_rate - self._flow_rate
|
||||
|
||||
# Adjust flow rate more aggressively (50% of difference)
|
||||
adjustment = flow_diff * 0.5
|
||||
self._flow_rate += adjustment
|
||||
|
||||
# Ensure flow rate is within bounds
|
||||
self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate))
|
||||
|
||||
# Update status based on flow rate
|
||||
if abs(flow_diff) < 0.1:
|
||||
self._status = "Running at Target Flow Rate"
|
||||
else:
|
||||
self._status = "Adjusting Flow Rate"
|
||||
|
||||
# Calculate volume increment
|
||||
volume_increment = (self._flow_rate / 60.0) # mL/s
|
||||
self._total_volume += volume_increment
|
||||
|
||||
# Update time tracking
|
||||
self._time_spent = datetime.now() - self._start_time
|
||||
if self._transfer_time > 0:
|
||||
remaining = self._transfer_time - self._time_spent.total_seconds()
|
||||
self._time_remaining = timedelta(seconds=max(0, remaining))
|
||||
|
||||
# Check completion
|
||||
if self._total_volume >= self._transfer_volume:
|
||||
self._status = "Transfer Completed"
|
||||
self._pump_state = "Stopped"
|
||||
self._running = False
|
||||
break
|
||||
|
||||
# Update pressure
|
||||
self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure
|
||||
|
||||
print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in pump operation: {str(e)}")
|
||||
self._status = "Error in pump operation"
|
||||
self._pump_state = "Stopped"
|
||||
self._running = False
|
||||
break
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"pump_state": self._pump_state,
|
||||
"flow_rate": self._flow_rate,
|
||||
"target_flow_rate": self._target_flow_rate,
|
||||
"pressure": self._pressure,
|
||||
"total_volume": self._total_volume,
|
||||
"max_flow_rate": self._max_flow_rate,
|
||||
"max_pressure": self._max_pressure,
|
||||
"current_device": self._current_device,
|
||||
"from_vessel": self._from_vessel,
|
||||
"to_vessel": self._to_vessel,
|
||||
"transfer_volume": self._transfer_volume,
|
||||
"amount": self._amount,
|
||||
"transfer_time": self._transfer_time,
|
||||
"is_viscous": self._is_viscous,
|
||||
"rinsing_solvent": self._rinsing_solvent,
|
||||
"rinsing_volume": self._rinsing_volume,
|
||||
"rinsing_repeats": self._rinsing_repeats,
|
||||
"is_solid": self._is_solid,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
pump = MockPump()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动泵设备测试...")
|
||||
print(f"初始状态: {pump.get_status_info()}")
|
||||
|
||||
# 设置流速并启动
|
||||
pump.set_flow_rate(50.0)
|
||||
pump.start_pump()
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(f"第{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}")
|
||||
|
||||
# 测试方向切换
|
||||
print("切换泵方向...")
|
||||
|
||||
|
||||
pump.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,390 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
import json
|
||||
|
||||
|
||||
class MockRotavap:
|
||||
"""
|
||||
模拟旋转蒸发器设备类
|
||||
|
||||
这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、
|
||||
真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockRotavap实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
|
||||
# 旋转相关属性
|
||||
self._rotate_state: str = "Stopped" # 旋转状态:Running, Stopped
|
||||
self._rotate_time: float = 0.0 # 旋转剩余时间 (秒)
|
||||
self._rotate_speed: float = 0.0 # 旋转速度 (rpm)
|
||||
self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm)
|
||||
|
||||
# 真空泵相关属性
|
||||
self._pump_state: str = "Stopped" # 泵状态:Running, Stopped
|
||||
self._pump_time: float = 0.0 # 泵剩余时间 (秒)
|
||||
self._vacuum_level: float = 0.0 # 真空度 (mbar)
|
||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
||||
|
||||
# 温度相关属性
|
||||
self._temperature: float = 25.0 # 水浴温度 (°C)
|
||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
||||
self._max_temperature: float = 180.0 # 最大温度 (°C)
|
||||
|
||||
# 运行控制线程
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 操作成功标志
|
||||
self.success: str = "True" # 使用字符串而不是布尔值
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def rotate_state(self) -> str:
|
||||
return self._rotate_state
|
||||
|
||||
@property
|
||||
def rotate_time(self) -> float:
|
||||
return self._rotate_time
|
||||
|
||||
@property
|
||||
def rotate_speed(self) -> float:
|
||||
return self._rotate_speed
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def pump_time(self) -> float:
|
||||
return self._pump_time
|
||||
|
||||
@property
|
||||
def vacuum_level(self) -> float:
|
||||
return self._vacuum_level
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
return self._target_temperature
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def set_timer(self, command: str) -> str:
|
||||
"""
|
||||
设置定时器 - 兼容现有RotavapOne接口
|
||||
|
||||
Args:
|
||||
command (str): JSON格式的命令字符串,包含rotate_time和pump_time
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
try:
|
||||
timer = json.loads(command)
|
||||
rotate_time = timer.get("rotate_time", 0)
|
||||
pump_time = timer.get("pump_time", 0)
|
||||
|
||||
self.success = "False"
|
||||
self._rotate_time = float(rotate_time)
|
||||
self._pump_time = float(pump_time)
|
||||
self.success = "True"
|
||||
|
||||
self._status = "Timer Set"
|
||||
return "Success"
|
||||
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
self._status = f"Error: Invalid command format - {str(e)}"
|
||||
self.success = "False"
|
||||
return "Error"
|
||||
|
||||
def set_rotate_time(self, time_seconds: float) -> str:
|
||||
"""
|
||||
设置旋转时间
|
||||
|
||||
Args:
|
||||
time_seconds (float): 旋转时间 (秒)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
self.success = "False"
|
||||
self._rotate_time = max(0.0, float(time_seconds))
|
||||
self.success = "True"
|
||||
self._status = "Rotate time set"
|
||||
return "Success"
|
||||
|
||||
def set_pump_time(self, time_seconds: float) -> str:
|
||||
"""
|
||||
设置泵时间
|
||||
|
||||
Args:
|
||||
time_seconds (float): 泵时间 (秒)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
self.success = "False"
|
||||
self._pump_time = max(0.0, float(time_seconds))
|
||||
self.success = "True"
|
||||
self._status = "Pump time set"
|
||||
return "Success"
|
||||
|
||||
def set_rotate_speed(self, speed: float) -> str:
|
||||
"""
|
||||
设置旋转速度
|
||||
|
||||
Args:
|
||||
speed (float): 旋转速度 (rpm)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if speed < 0 or speed > self._max_rotate_speed:
|
||||
self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})"
|
||||
return "Error"
|
||||
|
||||
self._rotate_speed = speed
|
||||
self._status = "Rotate speed set"
|
||||
return "Success"
|
||||
|
||||
def set_temperature(self, temperature: float) -> str:
|
||||
"""
|
||||
设置水浴温度
|
||||
|
||||
Args:
|
||||
temperature (float): 目标温度 (°C)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if temperature < 0 or temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
||||
return "Error"
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Temperature set"
|
||||
|
||||
# 启动操作线程以开始温度控制
|
||||
self._start_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_rotation(self) -> str:
|
||||
"""
|
||||
启动旋转
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if self._rotate_time <= 0:
|
||||
self._status = "Error: No rotate time set"
|
||||
return "Error"
|
||||
|
||||
self._rotate_state = "Running"
|
||||
self._status = "Rotation started"
|
||||
return "Success"
|
||||
|
||||
def start_pump(self) -> str:
|
||||
"""
|
||||
启动真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
|
||||
if self._pump_time <= 0:
|
||||
self._status = "Error: No pump time set"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Pump started"
|
||||
return "Success"
|
||||
|
||||
def stop_all_operations(self) -> str:
|
||||
"""
|
||||
停止所有操作
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._rotate_state = "Stopped"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_operation()
|
||||
self._rotate_time = 0.0
|
||||
self._pump_time = 0.0
|
||||
self._vacuum_level = 0.0
|
||||
self._status = "All operations stopped"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self.stop_all_operations()
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_operation(self):
|
||||
"""
|
||||
启动操作线程
|
||||
|
||||
这个方法启动一个后台线程来模拟旋蒸的实际运行过程。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
def _stop_operation(self):
|
||||
"""
|
||||
停止操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=2.0)
|
||||
|
||||
def _operation_loop(self):
|
||||
"""
|
||||
操作主循环
|
||||
|
||||
这个方法在后台线程中运行,模拟真实旋蒸的工作过程:
|
||||
1. 时间倒计时
|
||||
2. 温度控制
|
||||
3. 真空度控制
|
||||
4. 状态更新
|
||||
"""
|
||||
while self._running:
|
||||
try:
|
||||
# 处理旋转时间倒计时
|
||||
if self._rotate_time > 0:
|
||||
self._rotate_state = "Running"
|
||||
self._rotate_time = max(0.0, self._rotate_time - 1.0)
|
||||
else:
|
||||
self._rotate_state = "Stopped"
|
||||
|
||||
# 处理泵时间倒计时
|
||||
if self._pump_time > 0:
|
||||
self._pump_state = "Running"
|
||||
self._pump_time = max(0.0, self._pump_time - 1.0)
|
||||
# 模拟真空度变化
|
||||
if self._vacuum_level > self._target_vacuum:
|
||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0)
|
||||
else:
|
||||
self._pump_state = "Stopped"
|
||||
# 真空度逐渐回升
|
||||
self._vacuum_level = min(1013.25, self._vacuum_level + 2.0)
|
||||
|
||||
# 模拟温度控制
|
||||
temp_diff = self._target_temperature - self._temperature
|
||||
if abs(temp_diff) > 0.5:
|
||||
if temp_diff > 0:
|
||||
self._temperature += min(1.0, temp_diff * 0.1)
|
||||
else:
|
||||
self._temperature += max(-1.0, temp_diff * 0.1)
|
||||
|
||||
# 更新整体状态
|
||||
if self._rotate_state == "Running" or self._pump_state == "Running":
|
||||
self._status = "Operating"
|
||||
elif self._rotate_time > 0 or self._pump_time > 0:
|
||||
self._status = "Ready"
|
||||
else:
|
||||
self._status = "Idle"
|
||||
|
||||
# 等待1秒后继续下一次循环
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
self._status = f"Error in operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束时的清理工作
|
||||
self._status = "Idle"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"rotate_state": self._rotate_state,
|
||||
"rotate_time": self._rotate_time,
|
||||
"rotate_speed": self._rotate_speed,
|
||||
"pump_state": self._pump_state,
|
||||
"pump_time": self._pump_time,
|
||||
"vacuum_level": self._vacuum_level,
|
||||
"temperature": self._temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"success": self.success,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
rotavap = MockRotavap()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动旋转蒸发器测试...")
|
||||
print(f"初始状态: {rotavap.get_status_info()}")
|
||||
|
||||
# 设置定时器
|
||||
timer_command = '{"rotate_time": 300, "pump_time": 600}'
|
||||
rotavap.set_timer(timer_command)
|
||||
|
||||
# 设置温度和转速
|
||||
rotavap.set_temperature(60.0)
|
||||
rotavap.set_rotate_speed(120.0)
|
||||
|
||||
# 启动操作
|
||||
rotavap.start_rotation()
|
||||
rotavap.start_pump()
|
||||
|
||||
# 模拟运行10秒
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, "
|
||||
f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar"
|
||||
)
|
||||
|
||||
rotavap.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,399 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockSeparator:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 基本状态属性
|
||||
self._status: str = "Idle" # 当前总体状态
|
||||
self._valve_state: str = "Closed" # 阀门状态:Open 或 Closed
|
||||
self._settling_time: float = 0.0 # 静置时间(秒)
|
||||
|
||||
# 搅拌相关属性
|
||||
self._shake_time: float = 0.0 # 剩余摇摆时间(秒)
|
||||
self._shake_status: str = "Not Shaking" # 摇摆状态
|
||||
|
||||
# 用于后台模拟 shake 动作
|
||||
self._operation_thread = None
|
||||
self._thread_lock = threading.Lock()
|
||||
self._running = False
|
||||
|
||||
# Separate action 相关属性
|
||||
self._current_device: str = "MockSeparator1"
|
||||
self._purpose: str = "" # wash or extract
|
||||
self._product_phase: str = "" # top or bottom
|
||||
self._from_vessel: str = ""
|
||||
self._separation_vessel: str = ""
|
||||
self._to_vessel: str = ""
|
||||
self._waste_phase_to_vessel: str = ""
|
||||
self._solvent: str = ""
|
||||
self._solvent_volume: float = 0.0
|
||||
self._through: str = ""
|
||||
self._repeats: int = 1
|
||||
self._stir_time: float = 0.0
|
||||
self._stir_speed: float = 0.0
|
||||
self._time_spent = timedelta()
|
||||
self._time_remaining = timedelta()
|
||||
self._start_time = datetime.now() # 添加这一行
|
||||
|
||||
@property
|
||||
def current_device(self) -> str:
|
||||
return self._current_device
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def valve_state(self) -> str:
|
||||
return self._valve_state
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def shake_time(self) -> float:
|
||||
with self._thread_lock:
|
||||
return self._shake_time
|
||||
|
||||
@property
|
||||
def shake_status(self) -> str:
|
||||
with self._thread_lock:
|
||||
return self._shake_status
|
||||
|
||||
@property
|
||||
def product_phase(self) -> str:
|
||||
return self._product_phase
|
||||
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self._from_vessel
|
||||
|
||||
@property
|
||||
def separation_vessel(self) -> str:
|
||||
return self._separation_vessel
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self._to_vessel
|
||||
|
||||
@property
|
||||
def waste_phase_to_vessel(self) -> str:
|
||||
return self._waste_phase_to_vessel
|
||||
|
||||
@property
|
||||
def solvent(self) -> str:
|
||||
return self._solvent
|
||||
|
||||
@property
|
||||
def solvent_volume(self) -> float:
|
||||
return self._solvent_volume
|
||||
|
||||
@property
|
||||
def through(self) -> str:
|
||||
return self._through
|
||||
|
||||
@property
|
||||
def repeats(self) -> int:
|
||||
return self._repeats
|
||||
|
||||
@property
|
||||
def stir_time(self) -> float:
|
||||
return self._stir_time
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def time_spent(self) -> float:
|
||||
if self._running:
|
||||
self._time_spent = datetime.now() - self._start_time
|
||||
return self._time_spent.total_seconds()
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> float:
|
||||
if self._running:
|
||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
||||
remain = max(0, total_time - elapsed)
|
||||
self._time_remaining = timedelta(seconds=remain)
|
||||
return self._time_remaining.total_seconds()
|
||||
|
||||
def separate(self, purpose: str, product_phase: str, from_vessel: str,
|
||||
separation_vessel: str, to_vessel: str, waste_phase_to_vessel: str = "",
|
||||
solvent: str = "", solvent_volume: float = 0.0, through: str = "",
|
||||
repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0,
|
||||
settling_time: float = 60.0) -> dict:
|
||||
"""
|
||||
执行分离操作
|
||||
"""
|
||||
with self._thread_lock:
|
||||
# 检查是否已经在运行
|
||||
if self._running:
|
||||
return {
|
||||
"success": False,
|
||||
"status": "Error: Operation already in progress"
|
||||
}
|
||||
# 必填参数验证
|
||||
if not all([from_vessel, separation_vessel, to_vessel]):
|
||||
self._status = "Error: Missing required vessel parameters"
|
||||
return {"success": False}
|
||||
# 验证参数
|
||||
if purpose not in ["wash", "extract"]:
|
||||
self._status = "Error: Invalid purpose"
|
||||
return {"success": False}
|
||||
|
||||
if product_phase not in ["top", "bottom"]:
|
||||
self._status = "Error: Invalid product phase"
|
||||
return {"success": False}
|
||||
# 数值参数验证
|
||||
try:
|
||||
solvent_volume = float(solvent_volume)
|
||||
repeats = int(repeats)
|
||||
stir_time = float(stir_time)
|
||||
stir_speed = float(stir_speed)
|
||||
settling_time = float(settling_time)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid numeric parameters"
|
||||
return {"success": False}
|
||||
|
||||
# 设置参数
|
||||
self._purpose = purpose
|
||||
self._product_phase = product_phase
|
||||
self._from_vessel = from_vessel
|
||||
self._separation_vessel = separation_vessel
|
||||
self._to_vessel = to_vessel
|
||||
self._waste_phase_to_vessel = waste_phase_to_vessel
|
||||
self._solvent = solvent
|
||||
self._solvent_volume = float(solvent_volume)
|
||||
self._through = through
|
||||
self._repeats = int(repeats)
|
||||
self._stir_time = float(stir_time)
|
||||
self._stir_speed = float(stir_speed)
|
||||
self._settling_time = float(settling_time)
|
||||
|
||||
# 重置计时器
|
||||
self._start_time = datetime.now()
|
||||
self._time_spent = timedelta()
|
||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
||||
self._time_remaining = timedelta(seconds=total_time)
|
||||
|
||||
# 启动分离操作
|
||||
self._status = "Starting Separation"
|
||||
self._running = True
|
||||
|
||||
# 在锁内创建和启动线程
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
# 等待确认操作已经开始
|
||||
time.sleep(0.1) # 短暂等待确保操作线程已启动
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": self._status,
|
||||
"current_device": self._current_device,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
def shake(self, shake_time: float) -> str:
|
||||
"""
|
||||
模拟 shake(搅拌)操作:
|
||||
- 进入 "Shaking" 状态,倒计时 shake_time 秒
|
||||
- shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒
|
||||
- 最后恢复为 Idle
|
||||
"""
|
||||
try:
|
||||
shake_time = float(shake_time)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid shake time"
|
||||
return "Error"
|
||||
|
||||
with self._thread_lock:
|
||||
self._status = "Shaking"
|
||||
self._settling_time = 0.0
|
||||
self._shake_time = shake_time
|
||||
self._shake_status = "Shaking"
|
||||
|
||||
def _run_shake():
|
||||
remaining = shake_time
|
||||
while remaining > 0:
|
||||
time.sleep(1)
|
||||
remaining -= 1
|
||||
with self._thread_lock:
|
||||
self._shake_time = remaining
|
||||
with self._thread_lock:
|
||||
self._status = "Settling"
|
||||
self._settling_time = 60.0 # 固定静置时间为60秒
|
||||
self._shake_status = "Settling"
|
||||
while True:
|
||||
with self._thread_lock:
|
||||
if self._settling_time <= 0:
|
||||
self._status = "Idle"
|
||||
self._shake_status = "Idle"
|
||||
break
|
||||
time.sleep(1)
|
||||
with self._thread_lock:
|
||||
self._settling_time = max(0.0, self._settling_time - 1)
|
||||
|
||||
self._operation_thread = threading.Thread(target=_run_shake)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
return "Success"
|
||||
|
||||
def set_valve(self, command: str) -> str:
|
||||
"""
|
||||
阀门控制命令:传入 "open" 或 "close"
|
||||
"""
|
||||
|
||||
command = command.lower()
|
||||
if command == "open":
|
||||
self._valve_state = "Open"
|
||||
self._status = "Valve Opened"
|
||||
elif command == "close":
|
||||
self._valve_state = "Closed"
|
||||
self._status = "Valve Closed"
|
||||
else:
|
||||
self._status = "Error: Invalid valve command"
|
||||
return "Error"
|
||||
return "Success"
|
||||
|
||||
def _operation_loop(self):
|
||||
"""分离操作主循环"""
|
||||
try:
|
||||
current_repeat = 1
|
||||
|
||||
# 立即更新状态,确保不会停留在Starting Separation
|
||||
with self._thread_lock:
|
||||
self._status = f"Separation Cycle {current_repeat}/{self._repeats}"
|
||||
|
||||
while self._running and current_repeat <= self._repeats:
|
||||
# 第一步:搅拌
|
||||
if self._stir_time > 0:
|
||||
with self._thread_lock:
|
||||
self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})"
|
||||
remaining_stir = self._stir_time
|
||||
while remaining_stir > 0 and self._running:
|
||||
time.sleep(1)
|
||||
remaining_stir -= 1
|
||||
|
||||
# 第二步:静置
|
||||
if self._settling_time > 0:
|
||||
with self._thread_lock:
|
||||
self._status = f"Settling (Repeat {current_repeat}/{self._repeats})"
|
||||
remaining_settle = self._settling_time
|
||||
while remaining_settle > 0 and self._running:
|
||||
time.sleep(1)
|
||||
remaining_settle -= 1
|
||||
|
||||
# 第三步:打开阀门排出
|
||||
with self._thread_lock:
|
||||
self._valve_state = "Open"
|
||||
self._status = f"Draining (Repeat {current_repeat}/{self._repeats})"
|
||||
|
||||
# 模拟排出时间(5秒)
|
||||
time.sleep(10)
|
||||
|
||||
# 关闭阀门
|
||||
with self._thread_lock:
|
||||
self._valve_state = "Closed"
|
||||
|
||||
# 检查是否继续下一次重复
|
||||
if current_repeat < self._repeats:
|
||||
current_repeat += 1
|
||||
else:
|
||||
with self._thread_lock:
|
||||
self._status = "Separation Complete"
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
with self._thread_lock:
|
||||
self._status = f"Error in separation: {str(e)}"
|
||||
finally:
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
self._valve_state = "Closed"
|
||||
if self._status == "Starting Separation":
|
||||
self._status = "Error: Operation failed to start"
|
||||
elif self._status != "Separation Complete":
|
||||
self._status = "Stopped"
|
||||
|
||||
def stop_operations(self) -> str:
|
||||
"""停止任何正在执行的操作"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=1.0)
|
||||
self._operation_thread = None
|
||||
self._settling_time = 0.0
|
||||
self._status = "Idle"
|
||||
self._shake_status = "Idle"
|
||||
self._shake_time = 0.0
|
||||
self._time_remaining = timedelta()
|
||||
return "Success"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取当前设备状态信息"""
|
||||
with self._thread_lock:
|
||||
current_time = datetime.now()
|
||||
if self._start_time:
|
||||
self._time_spent = current_time - self._start_time
|
||||
|
||||
return {
|
||||
"status": self._status,
|
||||
"valve_state": self._valve_state,
|
||||
"settling_time": self._settling_time,
|
||||
"shake_time": self._shake_time,
|
||||
"shake_status": self._shake_status,
|
||||
"current_device": self._current_device,
|
||||
"purpose": self._purpose,
|
||||
"product_phase": self._product_phase,
|
||||
"from_vessel": self._from_vessel,
|
||||
"separation_vessel": self._separation_vessel,
|
||||
"to_vessel": self._to_vessel,
|
||||
"waste_phase_to_vessel": self._waste_phase_to_vessel,
|
||||
"solvent": self._solvent,
|
||||
"solvent_volume": self._solvent_volume,
|
||||
"through": self._through,
|
||||
"repeats": self._repeats,
|
||||
"stir_time": self._stir_time,
|
||||
"stir_speed": self._stir_speed,
|
||||
"time_spent": self._time_spent.total_seconds(),
|
||||
"time_remaining": self._time_remaining.total_seconds()
|
||||
}
|
||||
|
||||
|
||||
# 主函数用于测试
|
||||
if __name__ == "__main__":
|
||||
separator = MockSeparator()
|
||||
|
||||
print("启动简单版分离器测试...")
|
||||
print("初始状态:", separator.get_status_info())
|
||||
|
||||
# 触发 shake 操作,模拟 10 秒的搅拌
|
||||
print("执行 shake 操作...")
|
||||
print(separator.shake(10.0))
|
||||
|
||||
# 循环显示状态变化
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
info = separator.get_status_info()
|
||||
print(
|
||||
f"第{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, "
|
||||
f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, "
|
||||
f"shake_status={info['shake_status']}"
|
||||
)
|
||||
|
||||
# 模拟打开阀门
|
||||
print("打开阀门...", separator.set_valve("open"))
|
||||
print("最终状态:", separator.get_status_info())
|
||||
@@ -1,89 +0,0 @@
|
||||
import time
|
||||
|
||||
|
||||
class MockSolenoidValve:
|
||||
"""
|
||||
模拟电磁阀设备类 - 简化版本
|
||||
|
||||
这个类提供了电磁阀的基本功能:开启、关闭和状态查询
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockSolenoidValve实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
self._status: str = "Idle"
|
||||
self._valve_status: str = "Closed" # 阀门位置:Open, Closed
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态 - 会被自动识别的设备属性"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def valve_status(self) -> str:
|
||||
"""阀门状态"""
|
||||
return self._valve_status
|
||||
|
||||
def set_valve_status(self, status: str) -> str:
|
||||
"""
|
||||
设置阀门位置
|
||||
|
||||
Args:
|
||||
position (str): 阀门位置,可选值:"Open", "Closed"
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if status not in ["Open", "Closed"]:
|
||||
self._status = "Error: Invalid position"
|
||||
return "Error"
|
||||
|
||||
self._status = "Moving"
|
||||
time.sleep(1) # 模拟阀门动作时间
|
||||
|
||||
self._valve_status = status
|
||||
self._status = "Idle"
|
||||
return "Success"
|
||||
|
||||
def open_valve(self) -> str:
|
||||
"""打开阀门"""
|
||||
return self.set_valve_status("Open")
|
||||
|
||||
def close_valve(self) -> str:
|
||||
"""关闭阀门"""
|
||||
return self.set_valve_status("Closed")
|
||||
|
||||
def get_valve_status(self) -> str:
|
||||
"""获取阀门位置"""
|
||||
return self._valve_status
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""检查阀门是否打开"""
|
||||
return self._valve_status == "Open"
|
||||
|
||||
def is_closed(self) -> bool:
|
||||
"""检查阀门是否关闭"""
|
||||
return self._valve_status == "Closed"
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
valve = MockSolenoidValve()
|
||||
|
||||
print("启动电磁阀测试...")
|
||||
print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
# 测试开启阀门
|
||||
valve.open_valve()
|
||||
print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
# 测试关闭阀门
|
||||
valve.close_valve()
|
||||
print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}")
|
||||
|
||||
print("测试完成")
|
||||
@@ -1,307 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockStirrer:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
|
||||
# 搅拌相关属性
|
||||
self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm)
|
||||
self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm)
|
||||
self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm)
|
||||
self._stir_state: str = "Stopped" # 搅拌状态:Running, Stopped
|
||||
|
||||
# 温度相关属性
|
||||
self._temperature: float = 25.0 # 当前温度 (°C)
|
||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
||||
self._max_temperature: float = 300.0 # 最大温度 (°C)
|
||||
self._heating_state: str = "Off" # 加热状态:On, Off
|
||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
||||
|
||||
# 运行控制线程
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def target_stir_speed(self) -> float:
|
||||
return self._target_stir_speed
|
||||
|
||||
@property
|
||||
def stir_state(self) -> str:
|
||||
return self._stir_state
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
"""
|
||||
当前温度
|
||||
|
||||
Returns:
|
||||
float: 当前温度 (°C)
|
||||
"""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""
|
||||
目标温度
|
||||
|
||||
Returns:
|
||||
float: 目标温度 (°C)
|
||||
"""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def heating_state(self) -> str:
|
||||
return self._heating_state
|
||||
|
||||
@property
|
||||
def heating_power(self) -> float:
|
||||
return self._heating_power
|
||||
|
||||
@property
|
||||
def max_stir_speed(self) -> float:
|
||||
return self._max_stir_speed
|
||||
|
||||
@property
|
||||
def max_temperature(self) -> float:
|
||||
return self._max_temperature
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def set_stir_speed(self, speed: float) -> str:
|
||||
|
||||
speed = float(speed) # 确保传入的速度是浮点数
|
||||
|
||||
if speed < 0 or speed > self._max_stir_speed:
|
||||
self._status = f"Error: Speed out of range (0-{self._max_stir_speed})"
|
||||
return "Error"
|
||||
|
||||
self._target_stir_speed = speed
|
||||
self._status = "Setting Stir Speed"
|
||||
|
||||
# 如果设置了非零速度,启动搅拌
|
||||
if speed > 0:
|
||||
self._stir_state = "Running"
|
||||
else:
|
||||
self._stir_state = "Stopped"
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_temperature(self, temperature: float) -> str:
|
||||
temperature = float(temperature) # 确保传入的温度是浮点数
|
||||
|
||||
if temperature < 0 or temperature > self._max_temperature:
|
||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
||||
return "Error"
|
||||
|
||||
self._target_temperature = temperature
|
||||
self._status = "Setting Temperature"
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_stirring(self) -> str:
|
||||
|
||||
if self._target_stir_speed <= 0:
|
||||
self._status = "Error: No target speed set"
|
||||
return "Error"
|
||||
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring Started"
|
||||
return "Success"
|
||||
|
||||
def stop_stirring(self) -> str:
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._status = "Stirring Stopped"
|
||||
return "Success"
|
||||
|
||||
def heating_control(self, heating_state: str = "On") -> str:
|
||||
|
||||
if heating_state not in ["On", "Off"]:
|
||||
self._status = "Error: Invalid heating state"
|
||||
return "Error"
|
||||
|
||||
self._heating_state = heating_state
|
||||
|
||||
if heating_state == "On":
|
||||
self._status = "Heating On"
|
||||
else:
|
||||
self._status = "Heating Off"
|
||||
self._heating_power = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
def stop_all_operations(self) -> str:
|
||||
self._stir_state = "Stopped"
|
||||
self._heating_state = "Off"
|
||||
self._stop_operation()
|
||||
self._stir_speed = 0.0
|
||||
self._target_stir_speed = 0.0
|
||||
self._heating_power = 0.0
|
||||
self._status = "All operations stopped"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self.stop_all_operations()
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_operation(self):
|
||||
with self._thread_lock:
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
def _stop_operation(self):
|
||||
"""
|
||||
停止操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._operation_thread and self._operation_thread.is_alive():
|
||||
self._operation_thread.join(timeout=2.0)
|
||||
|
||||
def _operation_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
# 处理搅拌速度控制
|
||||
if self._stir_state == "Running":
|
||||
speed_diff = self._target_stir_speed - self._stir_speed
|
||||
|
||||
if abs(speed_diff) < 1.0: # 速度接近目标值
|
||||
self._stir_speed = self._target_stir_speed
|
||||
if self._stir_speed > 0:
|
||||
self._status = "Stirring at Target Speed"
|
||||
else:
|
||||
# 模拟速度调节,每秒调整10%的差值
|
||||
adjustment = speed_diff * 0.1
|
||||
self._stir_speed += adjustment
|
||||
self._status = "Adjusting Stir Speed"
|
||||
|
||||
# 确保速度在合理范围内
|
||||
self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed))
|
||||
else:
|
||||
# 搅拌停止时,速度逐渐降为0
|
||||
if self._stir_speed > 0:
|
||||
self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm
|
||||
|
||||
# 处理温度控制
|
||||
if self._heating_state == "On":
|
||||
temp_diff = self._target_temperature - self._temperature
|
||||
|
||||
if abs(temp_diff) < 0.5: # 温度接近目标值
|
||||
self._heating_power = 20.0 # 维持温度的最小功率
|
||||
elif temp_diff > 0: # 需要加热
|
||||
# 根据温差调整加热功率
|
||||
if temp_diff > 50:
|
||||
self._heating_power = 100.0
|
||||
elif temp_diff > 20:
|
||||
self._heating_power = 80.0
|
||||
elif temp_diff > 10:
|
||||
self._heating_power = 60.0
|
||||
else:
|
||||
self._heating_power = 40.0
|
||||
|
||||
# 模拟加热过程
|
||||
heating_rate = self._heating_power / 100.0 * 1.5 # 最大每秒升温1.5度
|
||||
self._temperature += heating_rate
|
||||
else: # 目标温度低于当前温度
|
||||
self._heating_power = 0.0
|
||||
# 自然冷却
|
||||
self._temperature -= 0.1
|
||||
else:
|
||||
self._heating_power = 0.0
|
||||
# 自然冷却到室温
|
||||
if self._temperature > 25.0:
|
||||
self._temperature -= 0.2
|
||||
|
||||
# 限制温度范围
|
||||
self._temperature = max(20.0, min(self._max_temperature, self._temperature))
|
||||
|
||||
# 更新整体状态
|
||||
if self._stir_state == "Running" and self._heating_state == "On":
|
||||
self._status = "Stirring and Heating"
|
||||
elif self._stir_state == "Running":
|
||||
self._status = "Stirring Only"
|
||||
elif self._heating_state == "On":
|
||||
self._status = "Heating Only"
|
||||
else:
|
||||
self._status = "Idle"
|
||||
|
||||
# 等待1秒后继续下一次循环
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
self._status = f"Error in operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束时的清理工作
|
||||
self._status = "Idle"
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
return {
|
||||
"status": self._status,
|
||||
"stir_speed": self._stir_speed,
|
||||
"target_stir_speed": self._target_stir_speed,
|
||||
"stir_state": self._stir_state,
|
||||
"temperature": self._temperature,
|
||||
"target_temperature": self._target_temperature,
|
||||
"heating_state": self._heating_state,
|
||||
"heating_power": self._heating_power,
|
||||
"max_stir_speed": self._max_stir_speed,
|
||||
"max_temperature": self._max_temperature,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
stirrer = MockStirrer()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动搅拌器测试...")
|
||||
print(f"初始状态: {stirrer.get_status_info()}")
|
||||
|
||||
# 设置搅拌速度和温度
|
||||
stirrer.set_stir_speed(800.0)
|
||||
stirrer.set_temperature(60.0)
|
||||
stirrer.heating_control("On")
|
||||
|
||||
# 模拟运行15秒
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, "
|
||||
f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}"
|
||||
)
|
||||
|
||||
stirrer.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -1,229 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class MockStirrer_new:
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
self.port = port
|
||||
|
||||
# 基本状态属性
|
||||
self._status: str = "Idle"
|
||||
self._vessel: str = ""
|
||||
self._purpose: str = ""
|
||||
|
||||
# 搅拌相关属性
|
||||
self._stir_speed: float = 0.0
|
||||
self._target_stir_speed: float = 0.0
|
||||
self._max_stir_speed: float = 2000.0
|
||||
self._stir_state: str = "Stopped"
|
||||
|
||||
# 计时相关
|
||||
self._stir_time: float = 0.0
|
||||
self._settling_time: float = 0.0
|
||||
self._start_time = datetime.now()
|
||||
self._time_remaining = timedelta()
|
||||
|
||||
# 运行控制
|
||||
self._operation_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# 创建操作线程
|
||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
||||
self._operation_thread.daemon = True
|
||||
self._operation_thread.start()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self._stir_speed
|
||||
|
||||
@property
|
||||
def target_stir_speed(self) -> float:
|
||||
return self._target_stir_speed
|
||||
|
||||
@property
|
||||
def stir_state(self) -> str:
|
||||
return self._stir_state
|
||||
|
||||
@property
|
||||
def vessel(self) -> str:
|
||||
return self._vessel
|
||||
|
||||
@property
|
||||
def purpose(self) -> str:
|
||||
return self._purpose
|
||||
|
||||
@property
|
||||
def stir_time(self) -> float:
|
||||
return self._stir_time
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@property
|
||||
def max_stir_speed(self) -> float:
|
||||
return self._max_stir_speed
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
"""返回当前操作的进度(0-100)"""
|
||||
if not self._running:
|
||||
return 0.0
|
||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
||||
total_time = self._stir_time + self._settling_time
|
||||
if total_time <= 0:
|
||||
return 100.0
|
||||
return min(100.0, (elapsed / total_time) * 100)
|
||||
|
||||
# ==================== Action Server 方法 ====================
|
||||
def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict:
|
||||
"""
|
||||
StartStir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if self._running:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Operation already in progress"
|
||||
}
|
||||
|
||||
try:
|
||||
# 重置所有参数
|
||||
self._vessel = vessel
|
||||
self._purpose = purpose
|
||||
self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间
|
||||
self._settling_time = 0.0
|
||||
self._start_time = datetime.now() # 重置开始时间
|
||||
|
||||
if stir_speed > 0:
|
||||
self._target_stir_speed = min(stir_speed, self._max_stir_speed)
|
||||
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring Started"
|
||||
self._running = True
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Stirring started successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
|
||||
def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict:
|
||||
"""
|
||||
Stir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
try:
|
||||
# 如果已经在运行,先停止当前操作
|
||||
if self._running:
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
time.sleep(0.1) # 给一个短暂的停止时间
|
||||
|
||||
|
||||
# 重置所有参数
|
||||
self._stir_time = float(stir_time)
|
||||
self._settling_time = float(settling_time)
|
||||
self._target_stir_speed = min(float(stir_speed), self._max_stir_speed)
|
||||
self._start_time = datetime.now() # 重置开始时间
|
||||
self._stir_state = "Running"
|
||||
self._status = "Stirring"
|
||||
self._running = True
|
||||
|
||||
return {"success": True}
|
||||
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid parameters"
|
||||
return {"success": False}
|
||||
|
||||
def stop_stir(self, vessel: str) -> dict:
|
||||
"""
|
||||
StopStir.action 对应的方法
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if vessel != self._vessel:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Vessel mismatch"
|
||||
}
|
||||
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._status = "Stirring Stopped"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Stirring stopped successfully"
|
||||
}
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _operation_loop(self):
|
||||
"""操作主循环"""
|
||||
while True:
|
||||
try:
|
||||
current_time = datetime.now()
|
||||
|
||||
with self._thread_lock: # 添加锁保护
|
||||
if self._stir_state == "Running":
|
||||
# 实际搅拌逻辑
|
||||
speed_diff = self._target_stir_speed - self._stir_speed
|
||||
if abs(speed_diff) > 0.1:
|
||||
adjustment = speed_diff * 0.1
|
||||
self._stir_speed += adjustment
|
||||
else:
|
||||
self._stir_speed = self._target_stir_speed
|
||||
|
||||
# 更新进度
|
||||
if self._running:
|
||||
if self._stir_time > 0: # 定时搅拌模式
|
||||
elapsed = (current_time - self._start_time).total_seconds()
|
||||
if elapsed >= self._stir_time + self._settling_time:
|
||||
self._running = False
|
||||
self._stir_state = "Stopped"
|
||||
self._target_stir_speed = 0.0
|
||||
self._stir_speed = 0.0
|
||||
self._status = "Stirring Complete"
|
||||
elif elapsed >= self._stir_time:
|
||||
self._status = "Settling"
|
||||
else: # 连续搅拌模式
|
||||
self._status = "Stirring"
|
||||
else:
|
||||
# 停止状态下慢慢降低速度
|
||||
if self._stir_speed > 0:
|
||||
self._stir_speed = max(0, self._stir_speed - 20.0)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in operation loop: {str(e)}") # 添加错误输出
|
||||
self._status = f"Error: {str(e)}"
|
||||
time.sleep(1.0) # 错误发生时等待较长时间
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""获取设备状态信息"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"vessel": self._vessel,
|
||||
"purpose": self._purpose,
|
||||
"stir_speed": self._stir_speed,
|
||||
"target_stir_speed": self._target_stir_speed,
|
||||
"stir_state": self._stir_state,
|
||||
"stir_time": self._stir_time, # 添加
|
||||
"settling_time": self._settling_time, # 添加
|
||||
"progress": self.progress,
|
||||
"max_stir_speed": self._max_stir_speed
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class MockVacuum:
|
||||
"""
|
||||
模拟真空泵设备类
|
||||
|
||||
这个类模拟了一个实验室真空泵的行为,包括真空度控制、
|
||||
压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "MOCK"):
|
||||
"""
|
||||
初始化MockVacuum实例
|
||||
|
||||
Args:
|
||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
||||
"""
|
||||
self.port = port
|
||||
|
||||
# 设备基本状态属性
|
||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
||||
self._power_state: str = "Off" # 电源状态:On, Off
|
||||
self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused
|
||||
|
||||
# 真空相关属性
|
||||
self._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始
|
||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
||||
self._min_vacuum: float = 1.0 # 最小真空度 (mbar)
|
||||
self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压
|
||||
|
||||
# 泵性能相关属性
|
||||
self._pump_speed: float = 0.0 # 泵速 (L/s)
|
||||
self._max_pump_speed: float = 100.0 # 最大泵速 (L/s)
|
||||
self._pump_efficiency: float = 95.0 # 泵效率百分比
|
||||
|
||||
# 运行控制线程
|
||||
self._vacuum_thread = None
|
||||
self._running = False
|
||||
self._thread_lock = threading.Lock()
|
||||
|
||||
# ==================== 状态属性 ====================
|
||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""
|
||||
设备状态 - 会被自动识别的设备属性
|
||||
|
||||
Returns:
|
||||
str: 当前设备状态 (Idle, Running, Error, Stopped)
|
||||
"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def power_state(self) -> str:
|
||||
"""
|
||||
电源状态
|
||||
|
||||
Returns:
|
||||
str: 电源状态 (On, Off)
|
||||
"""
|
||||
return self._power_state
|
||||
|
||||
@property
|
||||
def pump_state(self) -> str:
|
||||
"""
|
||||
泵运行状态
|
||||
|
||||
Returns:
|
||||
str: 泵状态 (Running, Stopped, Paused)
|
||||
"""
|
||||
return self._pump_state
|
||||
|
||||
@property
|
||||
def vacuum_level(self) -> float:
|
||||
"""
|
||||
当前真空度
|
||||
|
||||
Returns:
|
||||
float: 当前真空度 (mbar)
|
||||
"""
|
||||
return self._vacuum_level
|
||||
|
||||
@property
|
||||
def target_vacuum(self) -> float:
|
||||
"""
|
||||
目标真空度
|
||||
|
||||
Returns:
|
||||
float: 目标真空度 (mbar)
|
||||
"""
|
||||
return self._target_vacuum
|
||||
|
||||
@property
|
||||
def pump_speed(self) -> float:
|
||||
"""
|
||||
泵速
|
||||
|
||||
Returns:
|
||||
float: 泵速 (L/s)
|
||||
"""
|
||||
return self._pump_speed
|
||||
|
||||
@property
|
||||
def pump_efficiency(self) -> float:
|
||||
"""
|
||||
泵效率
|
||||
|
||||
Returns:
|
||||
float: 泵效率百分比
|
||||
"""
|
||||
return self._pump_efficiency
|
||||
|
||||
@property
|
||||
def max_pump_speed(self) -> float:
|
||||
"""
|
||||
最大泵速
|
||||
|
||||
Returns:
|
||||
float: 最大泵速 (L/s)
|
||||
"""
|
||||
return self._max_pump_speed
|
||||
|
||||
# ==================== 设备控制方法 ====================
|
||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
||||
|
||||
def power_control(self, power_state: str = "On") -> str:
|
||||
"""
|
||||
电源控制方法
|
||||
|
||||
Args:
|
||||
power_state (str): 电源状态,可选值:"On", "Off"
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if power_state not in ["On", "Off"]:
|
||||
self._status = "Error: Invalid power state"
|
||||
return "Error"
|
||||
|
||||
self._power_state = power_state
|
||||
|
||||
if power_state == "On":
|
||||
self._status = "Power On"
|
||||
self._start_vacuum_operation()
|
||||
else:
|
||||
self._status = "Power Off"
|
||||
self.stop_vacuum()
|
||||
|
||||
return "Success"
|
||||
|
||||
def set_vacuum_level(self, vacuum_level: float) -> str:
|
||||
"""
|
||||
设置目标真空度
|
||||
|
||||
Args:
|
||||
vacuum_level (float): 目标真空度 (mbar)
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
try:
|
||||
vacuum_level = float(vacuum_level)
|
||||
except ValueError:
|
||||
self._status = "Error: Invalid vacuum level"
|
||||
return "Error"
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum:
|
||||
self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})"
|
||||
return "Error"
|
||||
|
||||
self._target_vacuum = vacuum_level
|
||||
self._status = "Setting Vacuum Level"
|
||||
|
||||
return "Success"
|
||||
|
||||
def start_vacuum(self) -> str:
|
||||
"""
|
||||
启动真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Starting Vacuum Pump"
|
||||
self._start_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def stop_vacuum(self) -> str:
|
||||
"""
|
||||
停止真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._pump_state = "Stopped"
|
||||
self._status = "Stopping Vacuum Pump"
|
||||
self._stop_vacuum_operation()
|
||||
self._pump_speed = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
def pause_vacuum(self) -> str:
|
||||
"""
|
||||
暂停真空泵
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._pump_state != "Running":
|
||||
self._status = "Error: Pump not running"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Paused"
|
||||
self._status = "Vacuum Pump Paused"
|
||||
self._stop_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def resume_vacuum(self) -> str:
|
||||
"""
|
||||
恢复真空泵运行
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
if self._pump_state != "Paused":
|
||||
self._status = "Error: Pump not paused"
|
||||
return "Error"
|
||||
|
||||
if self._power_state != "On":
|
||||
self._status = "Error: Power Off"
|
||||
return "Error"
|
||||
|
||||
self._pump_state = "Running"
|
||||
self._status = "Resuming Vacuum Pump"
|
||||
self._start_vacuum_operation()
|
||||
|
||||
return "Success"
|
||||
|
||||
def vent_to_atmosphere(self) -> str:
|
||||
"""
|
||||
通大气 - 将真空度恢复到大气压
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._target_vacuum = self._max_vacuum # 设置为大气压
|
||||
self._status = "Venting to Atmosphere"
|
||||
return "Success"
|
||||
|
||||
def emergency_stop(self) -> str:
|
||||
"""
|
||||
紧急停止
|
||||
|
||||
Returns:
|
||||
str: 操作结果状态 ("Success", "Error")
|
||||
"""
|
||||
self._status = "Emergency Stop"
|
||||
self._pump_state = "Stopped"
|
||||
self._stop_vacuum_operation()
|
||||
self._pump_speed = 0.0
|
||||
|
||||
return "Success"
|
||||
|
||||
# ==================== 内部控制方法 ====================
|
||||
|
||||
def _start_vacuum_operation(self):
|
||||
"""
|
||||
启动真空操作线程
|
||||
|
||||
这个方法启动一个后台线程来模拟真空泵的实际运行过程。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
if not self._running and self._power_state == "On":
|
||||
self._running = True
|
||||
self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop)
|
||||
self._vacuum_thread.daemon = True
|
||||
self._vacuum_thread.start()
|
||||
|
||||
def _stop_vacuum_operation(self):
|
||||
"""
|
||||
停止真空操作线程
|
||||
|
||||
安全地停止后台运行线程并等待其完成。
|
||||
"""
|
||||
with self._thread_lock:
|
||||
self._running = False
|
||||
if self._vacuum_thread and self._vacuum_thread.is_alive():
|
||||
self._vacuum_thread.join(timeout=2.0)
|
||||
|
||||
def _vacuum_operation_loop(self):
|
||||
"""
|
||||
真空操作主循环
|
||||
|
||||
这个方法在后台线程中运行,模拟真空泵的工作过程:
|
||||
1. 检查电源状态和运行状态
|
||||
2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度
|
||||
3. 否则等待
|
||||
"""
|
||||
while self._running and self._power_state == "On":
|
||||
try:
|
||||
with self._thread_lock:
|
||||
# 只有泵状态为 Running 时才进行更新
|
||||
if self._pump_state == "Running":
|
||||
vacuum_diff = self._vacuum_level - self._target_vacuum
|
||||
|
||||
if abs(vacuum_diff) < 1.0: # 真空度接近目标值
|
||||
self._status = "At Target Vacuum"
|
||||
self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速
|
||||
elif vacuum_diff > 0: # 需要抽真空(降低压力)
|
||||
self._status = "Pumping Down"
|
||||
if vacuum_diff > 500:
|
||||
self._pump_speed = self._max_pump_speed
|
||||
elif vacuum_diff > 100:
|
||||
self._pump_speed = self._max_pump_speed * 0.8
|
||||
elif vacuum_diff > 50:
|
||||
self._pump_speed = self._max_pump_speed * 0.6
|
||||
else:
|
||||
self._pump_speed = self._max_pump_speed * 0.4
|
||||
|
||||
# 根据泵速和效率计算真空降幅
|
||||
pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0
|
||||
vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar
|
||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction)
|
||||
else: # 目标真空度高于当前值,需要通气
|
||||
self._status = "Venting"
|
||||
self._pump_speed = 0.0
|
||||
self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0)
|
||||
|
||||
# 限制真空度范围
|
||||
self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level))
|
||||
else:
|
||||
# 当泵状态不是 Running 时,可保持原状态
|
||||
self._status = "Vacuum Pump Not Running"
|
||||
# 释放锁后等待1秒钟
|
||||
time.sleep(1.0)
|
||||
except Exception as e:
|
||||
with self._thread_lock:
|
||||
self._status = f"Error in vacuum operation: {str(e)}"
|
||||
break
|
||||
|
||||
# 循环结束后的清理工作
|
||||
if self._pump_state == "Running":
|
||||
self._status = "Idle"
|
||||
# 停止泵后,真空度逐渐回升到大气压
|
||||
while self._vacuum_level < self._max_vacuum * 0.9:
|
||||
with self._thread_lock:
|
||||
self._vacuum_level += 2.0
|
||||
time.sleep(0.1)
|
||||
|
||||
def get_status_info(self) -> dict:
|
||||
"""
|
||||
获取完整的设备状态信息
|
||||
|
||||
Returns:
|
||||
dict: 包含所有设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"status": self._status,
|
||||
"power_state": self._power_state,
|
||||
"pump_state": self._pump_state,
|
||||
"vacuum_level": self._vacuum_level,
|
||||
"target_vacuum": self._target_vacuum,
|
||||
"pump_speed": self._pump_speed,
|
||||
"pump_efficiency": self._pump_efficiency,
|
||||
"max_pump_speed": self._max_pump_speed,
|
||||
}
|
||||
|
||||
|
||||
# 用于测试的主函数
|
||||
if __name__ == "__main__":
|
||||
vacuum = MockVacuum()
|
||||
|
||||
# 测试基本功能
|
||||
print("启动真空泵测试...")
|
||||
vacuum.power_control("On")
|
||||
print(f"初始状态: {vacuum.get_status_info()}")
|
||||
|
||||
# 设置目标真空度并启动
|
||||
vacuum.set_vacuum_level(10.0) # 设置为10mbar
|
||||
vacuum.start_vacuum()
|
||||
|
||||
# 模拟运行15秒
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
print(
|
||||
f"第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}"
|
||||
)
|
||||
# 测试通大气
|
||||
print("测试通大气...")
|
||||
vacuum.vent_to_atmosphere()
|
||||
|
||||
# 继续运行5秒观察通大气过程
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}")
|
||||
|
||||
vacuum.emergency_stop()
|
||||
print("测试完成")
|
||||
@@ -3,6 +3,7 @@ from threading import Lock, Event
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Union, Optional, overload
|
||||
|
||||
import serial.tools.list_ports
|
||||
@@ -386,3 +387,8 @@ class RunzeSyringePump:
|
||||
def list():
|
||||
for item in serial.tools.list_ports.comports():
|
||||
yield RunzeSyringePumpInfo(port=item.device)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = RunzeSyringePump("/dev/tty.usbserial-D30JUGG5", "1", 25.0)
|
||||
r.initialize()
|
||||
386
unilabos/devices/pump_and_valve/runze_multiple_backbone.py
Normal file
386
unilabos/devices/pump_and_valve/runze_multiple_backbone.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Runze Syringe Pump Controller (SY-03B-T08)
|
||||
|
||||
本模块用于控制润泽注射泵 SY-03B-T08 型号的多泵系统。
|
||||
支持通过串口同时控制多个具有不同地址的泵。
|
||||
泵每次连接前要先进行初始化。
|
||||
|
||||
基础用法:
|
||||
# 创建控制器实例
|
||||
pump_controller = RunzeMultiplePump("COM3") # 或 "/dev/ttyUSB0" (Linux)
|
||||
|
||||
# 初始化特定地址的泵
|
||||
pump_controller.initialize("1")
|
||||
|
||||
# 设置阀门位置
|
||||
pump_controller.set_valve_position("1", 1) # 设置到位置1
|
||||
|
||||
# 移动到绝对位置
|
||||
pump_controller.set_position("1", 10.0) # 移动到10ml位置
|
||||
|
||||
# 推拉柱塞操作
|
||||
pump_controller.pull_plunger("1", 5.0) # 吸取5ml
|
||||
pump_controller.push_plunger("1", 5.0) # 推出5ml
|
||||
|
||||
# 关闭连接
|
||||
pump_controller.close()
|
||||
|
||||
支持的泵地址: 1-8 (字符串格式,如 "1", "2", "3" 等)
|
||||
默认最大容量: 25.0 ml
|
||||
通信协议: RS485, 9600波特率
|
||||
"""
|
||||
|
||||
from threading import Lock, Event
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Union, Optional, List, Dict
|
||||
|
||||
import serial.tools.list_ports
|
||||
from serial import Serial
|
||||
from serial.serialutil import SerialException
|
||||
|
||||
|
||||
class RunzeSyringePumpMode(Enum):
|
||||
Normal = 0
|
||||
AccuratePos = 1
|
||||
AccuratePosVel = 2
|
||||
|
||||
|
||||
pulse_freq_grades = {
|
||||
6000: "0",
|
||||
5600: "1",
|
||||
5000: "2",
|
||||
4400: "3",
|
||||
3800: "4",
|
||||
3200: "5",
|
||||
2600: "6",
|
||||
2200: "7",
|
||||
2000: "8",
|
||||
1800: "9",
|
||||
1600: "10",
|
||||
1400: "11",
|
||||
1200: "12",
|
||||
1000: "13",
|
||||
800: "14",
|
||||
600: "15",
|
||||
400: "16",
|
||||
200: "17",
|
||||
190: "18",
|
||||
180: "19",
|
||||
170: "20",
|
||||
160: "21",
|
||||
150: "22",
|
||||
140: "23",
|
||||
130: "24",
|
||||
120: "25",
|
||||
110: "26",
|
||||
100: "27",
|
||||
90: "28",
|
||||
80: "29",
|
||||
70: "30",
|
||||
60: "31",
|
||||
50: "32",
|
||||
40: "33",
|
||||
30: "34",
|
||||
20: "35",
|
||||
18: "36",
|
||||
16: "37",
|
||||
14: "38",
|
||||
12: "39",
|
||||
10: "40",
|
||||
}
|
||||
|
||||
|
||||
class RunzeSyringePumpConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PumpConfig:
|
||||
address: str
|
||||
max_volume: float = 25.0
|
||||
mode: RunzeSyringePumpMode = RunzeSyringePumpMode.Normal
|
||||
|
||||
|
||||
class RunzeMultiplePump:
|
||||
"""
|
||||
Multi-address Runze Syringe Pump Controller
|
||||
|
||||
Supports controlling multiple pumps on the same serial port with different addresses.
|
||||
"""
|
||||
|
||||
def __init__(self, port: str):
|
||||
"""
|
||||
Initialize multiple pump controller
|
||||
|
||||
Args:
|
||||
port (str): Serial port path
|
||||
"""
|
||||
self.port = port
|
||||
|
||||
# Default pump parameters
|
||||
self.max_volume = 25.0
|
||||
self.total_steps = 6000
|
||||
self.total_steps_vel = 6000
|
||||
|
||||
# Connection management
|
||||
try:
|
||||
self.hardware_interface = Serial(baudrate=9600, port=port, timeout=1.0)
|
||||
print(f"✓ 成功连接到串口: {port}")
|
||||
except (OSError, SerialException) as e:
|
||||
print(f"✗ 串口连接失败: {e}")
|
||||
raise RunzeSyringePumpConnectionError(f"无法连接到串口 {port}: {e}") from e
|
||||
|
||||
# Thread safety
|
||||
self._query_lock = Lock()
|
||||
self._run_lock = Lock()
|
||||
self._closing = False
|
||||
|
||||
# Pump status tracking
|
||||
self._pump_status: Dict[str, str] = {} # address -> status
|
||||
|
||||
def _adjust_total_steps(self, mode: RunzeSyringePumpMode):
|
||||
total_steps = 6000 if mode == RunzeSyringePumpMode.Normal else 48000
|
||||
total_steps_vel = 48000 if mode == RunzeSyringePumpMode.AccuratePosVel else 6000
|
||||
return total_steps, total_steps_vel
|
||||
|
||||
def send_command(self, full_command: str) -> str:
|
||||
"""Send command to hardware and get response"""
|
||||
full_command_data = bytearray(full_command, "ascii")
|
||||
self.hardware_interface.write(full_command_data)
|
||||
time.sleep(0.05)
|
||||
response = self.hardware_interface.read_until(b"\n")
|
||||
output = self._receive(response)
|
||||
return output
|
||||
|
||||
def _query(self, address: str, command: str) -> str:
|
||||
"""
|
||||
Send query command to specific pump
|
||||
|
||||
Args:
|
||||
address (str): Pump address (e.g., "1", "2", "3")
|
||||
command (str): Command to send
|
||||
|
||||
Returns:
|
||||
str: Response from pump
|
||||
"""
|
||||
with self._query_lock:
|
||||
if self._closing:
|
||||
raise RunzeSyringePumpConnectionError("Connection is closing")
|
||||
|
||||
run = "R" if "?" not in command else ""
|
||||
full_command = f"/{address}{command}{run}\r\n"
|
||||
|
||||
output = self.send_command(full_command)[3:-3]
|
||||
return output
|
||||
|
||||
def _receive(self, data: bytes) -> str:
|
||||
if not data:
|
||||
return ""
|
||||
ascii_string = "".join(chr(byte) for byte in data)
|
||||
return ascii_string
|
||||
|
||||
def _run(self, address: str, command: str) -> str:
|
||||
"""
|
||||
Run command and wait for completion
|
||||
|
||||
Args:
|
||||
address (str): Pump address
|
||||
command (str): Command to execute
|
||||
|
||||
Returns:
|
||||
str: Command response
|
||||
"""
|
||||
with self._run_lock:
|
||||
try:
|
||||
print(f"[泵 {address}] 执行命令: {command}")
|
||||
response = self._query(address, command)
|
||||
|
||||
# Wait for operation completion
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
status = self.get_status(address)
|
||||
if status == "Idle":
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"[泵 {address}] 命令执行错误: {e}")
|
||||
response = ""
|
||||
|
||||
return response
|
||||
|
||||
def _standardize_status(self, status_raw: str) -> str:
|
||||
"""Convert raw status to standard format"""
|
||||
return "Idle" if status_raw == "`" else "Busy"
|
||||
|
||||
# === Core Operations ===
|
||||
|
||||
def initialize(self, address: str) -> str:
|
||||
"""Initialize specific pump"""
|
||||
print(f"[泵 {address}] 正在初始化...")
|
||||
response = self._run(address, "Z")
|
||||
print(f"[泵 {address}] 初始化完成")
|
||||
return response
|
||||
|
||||
# === Status Queries ===
|
||||
|
||||
def get_status(self, address: str) -> str:
|
||||
"""Get pump status"""
|
||||
try:
|
||||
status_raw = self._query(address, "Q")
|
||||
status = self._standardize_status(status_raw)
|
||||
self._pump_status[address] = status
|
||||
return status
|
||||
except Exception:
|
||||
return "Error"
|
||||
|
||||
# === Velocity Control ===
|
||||
|
||||
def set_max_velocity(self, address: str, velocity: float, max_volume: float = None) -> str:
|
||||
"""Set maximum velocity for pump"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
pulse_freq = int(velocity / max_volume * self.total_steps_vel)
|
||||
pulse_freq = min(6000, pulse_freq)
|
||||
return self._run(address, f"V{pulse_freq}")
|
||||
|
||||
def get_max_velocity(self, address: str, max_volume: float = None) -> float:
|
||||
"""Get maximum velocity of pump"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
response = self._query(address, "?2")
|
||||
status_raw, pulse_freq = response[0], int(response[1:])
|
||||
velocity = pulse_freq / self.total_steps_vel * max_volume
|
||||
return velocity
|
||||
|
||||
def set_velocity_grade(self, address: str, velocity: Union[int, str]) -> str:
|
||||
"""Set velocity grade"""
|
||||
return self._run(address, f"S{velocity}")
|
||||
|
||||
# === Position Control ===
|
||||
|
||||
def get_position(self, address: str, max_volume: float = None) -> float:
|
||||
"""Get current plunger position in ml"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
response = self._query(address, "?0")
|
||||
status_raw, pos_step = response[0], int(response[1:])
|
||||
position = pos_step / self.total_steps * max_volume
|
||||
return position
|
||||
|
||||
def set_position(self, address: str, position: float, max_velocity: float = None, max_volume: float = None) -> str:
|
||||
"""
|
||||
Move to absolute volume position
|
||||
|
||||
Args:
|
||||
address (str): Pump address
|
||||
position (float): Target position in ml
|
||||
max_velocity (float): Maximum velocity in ml/s
|
||||
max_volume (float): Maximum syringe volume in ml
|
||||
"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
velocity_cmd = ""
|
||||
if max_velocity is not None:
|
||||
pulse_freq = int(max_velocity / max_volume * self.total_steps_vel)
|
||||
pulse_freq = min(6000, pulse_freq)
|
||||
velocity_cmd = f"V{pulse_freq}"
|
||||
|
||||
pos_step = int(position / max_volume * self.total_steps)
|
||||
return self._run(address, f"{velocity_cmd}A{pos_step}")
|
||||
|
||||
def pull_plunger(self, address: str, volume: float, max_volume: float = None) -> str:
|
||||
"""Pull plunger by specified volume"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
pos_step = int(volume / max_volume * self.total_steps)
|
||||
return self._run(address, f"P{pos_step}")
|
||||
|
||||
def push_plunger(self, address: str, volume: float, max_volume: float = None) -> str:
|
||||
"""Push plunger by specified volume"""
|
||||
if max_volume is None:
|
||||
max_volume = self.max_volume
|
||||
|
||||
pos_step = int(volume / max_volume * self.total_steps)
|
||||
return self._run(address, f"D{pos_step}")
|
||||
|
||||
# === Valve Control ===
|
||||
|
||||
def set_valve_position(self, address: str, position: Union[int, str, float]) -> str:
|
||||
"""Set valve position"""
|
||||
if isinstance(position, float):
|
||||
position = round(position / 120)
|
||||
command = f"I{position}" if isinstance(position, int) or ord(str(position)) <= 57 else str(position).upper()
|
||||
return self._run(address, command)
|
||||
|
||||
def get_valve_position(self, address: str) -> str:
|
||||
"""Get current valve position"""
|
||||
response = self._query(address, "?6")
|
||||
status_raw, pos_valve = response[0], response[1].upper()
|
||||
return pos_valve
|
||||
|
||||
# === Utility Functions ===
|
||||
|
||||
def stop_operation(self, address: str) -> str:
|
||||
"""Stop current operation"""
|
||||
return self._run(address, "T")
|
||||
|
||||
def close(self):
|
||||
"""Close connection"""
|
||||
if self._closing:
|
||||
raise RunzeSyringePumpConnectionError("Already closing")
|
||||
|
||||
self._closing = True
|
||||
self.hardware_interface.close()
|
||||
print("✓ 串口连接已关闭")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
示例:初始化3个泵(地址1、2、3),然后断开连接
|
||||
"""
|
||||
try:
|
||||
# 请根据实际串口修改端口号
|
||||
# Windows: "COM3", "COM4", 等
|
||||
# Linux/Mac: "/dev/ttyUSB0", "/dev/ttyACM0", 等
|
||||
port = "/dev/cn." # 修改为实际使用的串口
|
||||
|
||||
print("正在创建泵控制器...")
|
||||
pump_controller = RunzeMultiplePump(port)
|
||||
|
||||
# 初始化3个泵 (地址: 1, 2, 3)
|
||||
pump_addresses = ["1", "2", "3"]
|
||||
|
||||
for address in pump_addresses:
|
||||
try:
|
||||
print(f"\n正在初始化泵 {address}...")
|
||||
pump_controller.initialize(address)
|
||||
|
||||
# 检查泵状态
|
||||
status = pump_controller.get_status(address)
|
||||
print(f"泵 {address} 状态: {status}")
|
||||
except Exception as e:
|
||||
print(f"泵 {address} 初始化失败: {e}")
|
||||
|
||||
print("\n所有泵初始化完成!")
|
||||
|
||||
# 断开连接
|
||||
print("\n正在断开连接...")
|
||||
pump_controller.close()
|
||||
print("程序结束")
|
||||
|
||||
except RunzeSyringePumpConnectionError as e:
|
||||
print(f"连接错误: {e}")
|
||||
print("请检查:")
|
||||
print("1. 串口是否正确")
|
||||
print("2. 设备是否已连接")
|
||||
print("3. 串口是否被其他程序占用")
|
||||
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
282
unilabos/devices/separator/chinwe.py
Normal file
282
unilabos/devices/separator/chinwe.py
Normal file
@@ -0,0 +1,282 @@
|
||||
import sys
|
||||
import threading
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import re
|
||||
import time
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
|
||||
class ChinweDevice:
|
||||
"""
|
||||
ChinWe设备控制类
|
||||
提供串口通信、电机控制、传感器数据读取等功能
|
||||
"""
|
||||
|
||||
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
|
||||
"""
|
||||
初始化ChinWe设备
|
||||
|
||||
Args:
|
||||
port: 串口名称,如果为None则自动检测
|
||||
baudrate: 波特率,默认115200
|
||||
"""
|
||||
self.debug = debug
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.serial_port: Optional[serial.Serial] = None
|
||||
self._voltage: float = 0.0
|
||||
self._ec_value: float = 0.0
|
||||
self._ec_adc_value: int = 0
|
||||
self._is_connected = False
|
||||
self.connect()
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""获取连接状态"""
|
||||
return self._is_connected and self.serial_port and self.serial_port.is_open
|
||||
|
||||
@property
|
||||
def voltage(self) -> float:
|
||||
"""获取电源电压值"""
|
||||
return self._voltage
|
||||
|
||||
@property
|
||||
def ec_value(self) -> float:
|
||||
"""获取电导率值 (ms/cm)"""
|
||||
return self._ec_value
|
||||
|
||||
@property
|
||||
def ec_adc_value(self) -> int:
|
||||
"""获取EC ADC原始值"""
|
||||
return self._ec_adc_value
|
||||
|
||||
|
||||
@property
|
||||
def device_status(self) -> Dict[str, any]:
|
||||
"""
|
||||
获取设备状态信息
|
||||
|
||||
Returns:
|
||||
包含设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"connected": self.is_connected,
|
||||
"port": self.port,
|
||||
"baudrate": self.baudrate,
|
||||
"voltage": self.voltage,
|
||||
"ec_value": self.ec_value,
|
||||
"ec_adc_value": self.ec_adc_value
|
||||
}
|
||||
|
||||
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
|
||||
"""
|
||||
连接到串口设备
|
||||
|
||||
Args:
|
||||
port: 串口名称,如果为None则使用初始化时的port或自动检测
|
||||
baudrate: 波特率,如果为None则使用初始化时的baudrate
|
||||
|
||||
Returns:
|
||||
连接是否成功
|
||||
"""
|
||||
if self.is_connected:
|
||||
return True
|
||||
|
||||
target_port = port or self.port
|
||||
target_baudrate = baudrate or self.baudrate
|
||||
|
||||
try:
|
||||
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
|
||||
self._is_connected = True
|
||||
self.port = target_port
|
||||
self.baudrate = target_baudrate
|
||||
connect_allow_times = 5
|
||||
while not self.serial_port.is_open and connect_allow_times > 0:
|
||||
time.sleep(0.5)
|
||||
connect_allow_times -= 1
|
||||
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
|
||||
raise ValueError("串口未打开,请检查设备连接")
|
||||
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
|
||||
threading.Thread(target=self._read_data, daemon=True).start()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ChinweDevice连接失败: {e}")
|
||||
self._is_connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
"""
|
||||
断开串口连接
|
||||
|
||||
Returns:
|
||||
断开是否成功
|
||||
"""
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
try:
|
||||
self.serial_port.close()
|
||||
self._is_connected = False
|
||||
print("已断开串口连接")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"断开连接失败: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _send_motor_command(self, command: str) -> bool:
|
||||
"""
|
||||
发送电机控制命令
|
||||
|
||||
Args:
|
||||
command: 电机命令字符串,例如 "M 1 CW 1.5"
|
||||
|
||||
Returns:
|
||||
发送是否成功
|
||||
"""
|
||||
if not self.is_connected:
|
||||
print("设备未连接")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.serial_port.write((command + "\n").encode('utf-8'))
|
||||
print(f"发送命令: {command}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"发送命令失败: {e}")
|
||||
return False
|
||||
|
||||
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
|
||||
"""
|
||||
使电机转动指定圈数
|
||||
|
||||
Args:
|
||||
motor_id: 电机ID(1, 2, 3...)
|
||||
turns: 转动圈数,支持小数
|
||||
clockwise: True为顺时针,False为逆时针
|
||||
|
||||
Returns:
|
||||
命令发送是否成功
|
||||
"""
|
||||
if clockwise:
|
||||
command = f"M {motor_id} CW {turns}"
|
||||
else:
|
||||
command = f"M {motor_id} CCW {turns}"
|
||||
return self._send_motor_command(command)
|
||||
|
||||
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
|
||||
"""
|
||||
设置电机转速(如果设备支持)
|
||||
|
||||
Args:
|
||||
motor_id: 电机ID(1, 2, 3...)
|
||||
speed: 转速值
|
||||
|
||||
Returns:
|
||||
命令发送是否成功
|
||||
"""
|
||||
command = f"M {motor_id} SPEED {speed}"
|
||||
return self._send_motor_command(command)
|
||||
|
||||
def _read_data(self) -> List[str]:
|
||||
"""
|
||||
读取串口数据并解析
|
||||
|
||||
Returns:
|
||||
读取到的数据行列表
|
||||
"""
|
||||
print("开始读取串口数据...")
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
data_lines = []
|
||||
try:
|
||||
while self.serial_port.in_waiting:
|
||||
time.sleep(0.1) # 等待数据稳定
|
||||
try:
|
||||
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
|
||||
if line:
|
||||
data_lines.append(line)
|
||||
self._parse_sensor_data(line)
|
||||
except Exception as ex:
|
||||
print(f"解码数据错误: {ex}")
|
||||
except Exception as e:
|
||||
print(f"读取串口数据错误: {e}")
|
||||
|
||||
return data_lines
|
||||
|
||||
def _parse_sensor_data(self, line: str) -> None:
|
||||
"""
|
||||
解析传感器数据
|
||||
|
||||
Args:
|
||||
line: 接收到的数据行
|
||||
"""
|
||||
# 解析电源电压
|
||||
if "电源电压" in line:
|
||||
try:
|
||||
val = float(line.split(":")[1].replace("V", "").strip())
|
||||
self._voltage = val
|
||||
if self.debug:
|
||||
print(f"电源电压更新: {val}V")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 解析电导率和ADC原始值(支持两种格式)
|
||||
if "电导率" in line and "ADC原始值" in line:
|
||||
try:
|
||||
# 支持格式如:电导率:2.50ms/cm, ADC原始值:2052
|
||||
ec_match = re.search(r"电导率[::]\s*([\d\.]+)", line)
|
||||
adc_match = re.search(r"ADC原始值[::]\s*(\d+)", line)
|
||||
if ec_match:
|
||||
ec_val = float(ec_match.group(1))
|
||||
self._ec_value = ec_val
|
||||
if self.debug:
|
||||
print(f"电导率更新: {ec_val:.2f} ms/cm")
|
||||
if adc_match:
|
||||
adc_val = int(adc_match.group(1))
|
||||
self._ec_adc_value = adc_val
|
||||
if self.debug:
|
||||
print(f"EC ADC原始值更新: {adc_val}")
|
||||
except Exception:
|
||||
pass
|
||||
# 仅电导率,无ADC原始值
|
||||
elif "电导率" in line:
|
||||
try:
|
||||
val = float(line.split(":")[1].replace("ms/cm", "").strip())
|
||||
self._ec_value = val
|
||||
if self.debug:
|
||||
print(f"电导率更新: {val:.2f} ms/cm")
|
||||
except Exception:
|
||||
pass
|
||||
# 仅ADC原始值(如有分开回传场景)
|
||||
elif "ADC原始值" in line:
|
||||
try:
|
||||
adc_val = int(line.split(":")[1].strip())
|
||||
self._ec_adc_value = adc_val
|
||||
if self.debug:
|
||||
print(f"EC ADC原始值更新: {adc_val}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def spin_when_ec_ge_0():
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""测试函数"""
|
||||
print("=== ChinWe设备测试 ===")
|
||||
|
||||
# 创建设备实例
|
||||
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
|
||||
try:
|
||||
# 测试5: 发送电机命令
|
||||
print("\n5. 发送电机命令测试:")
|
||||
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
|
||||
device.rotate_motor(2, 20.0, clockwise=True)
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
time.sleep(10)
|
||||
# 测试7: 断开连接
|
||||
print("\n7. 断开连接:")
|
||||
device.disconnect()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,6 +3,8 @@ import logging
|
||||
import time as time_module
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||
|
||||
|
||||
class VirtualFilter:
|
||||
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
||||
@@ -40,7 +42,6 @@ class VirtualFilter:
|
||||
"progress": 0.0, # Filter.action feedback
|
||||
"current_temp": 25.0, # Filter.action feedback
|
||||
"filtered_volume": 0.0, # Filter.action feedback
|
||||
"current_status": "Ready for filtration", # Filter.action feedback
|
||||
"message": "Ready for filtration"
|
||||
})
|
||||
|
||||
@@ -52,9 +53,7 @@ class VirtualFilter:
|
||||
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
||||
|
||||
self.data.update({
|
||||
"status": "Offline",
|
||||
"current_status": "System offline",
|
||||
"message": "System offline"
|
||||
"status": "Offline"
|
||||
})
|
||||
|
||||
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
||||
@@ -62,8 +61,8 @@ class VirtualFilter:
|
||||
|
||||
async def filter(
|
||||
self,
|
||||
vessel: str,
|
||||
filtrate_vessel: str = "",
|
||||
vessel: dict,
|
||||
filtrate_vessel: dict = {},
|
||||
stir: bool = False,
|
||||
stir_speed: float = 300.0,
|
||||
temp: float = 25.0,
|
||||
@@ -71,7 +70,9 @@ class VirtualFilter:
|
||||
volume: float = 0.0
|
||||
) -> bool:
|
||||
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
||||
|
||||
vessel_id, _ = get_vessel(vessel)
|
||||
filtrate_vessel_id, _ = get_vessel(filtrate_vessel) if filtrate_vessel else (f"{vessel_id}_filtrate", {})
|
||||
|
||||
# 🔧 新增:温度自动调整
|
||||
original_temp = temp
|
||||
if temp == 0.0:
|
||||
@@ -81,7 +82,7 @@ class VirtualFilter:
|
||||
temp = 4.0 # 小于4度自动设置为4度
|
||||
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
|
||||
|
||||
self.logger.info(f"🌊 开始过滤操作: {vessel} → {filtrate_vessel} 🚰")
|
||||
self.logger.info(f"🌊 开始过滤操作: {vessel_id} → {filtrate_vessel_id} 🚰")
|
||||
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
||||
self.logger.info(f" 🌡️ 温度: {temp}°C")
|
||||
self.logger.info(f" 💧 体积: {volume}mL")
|
||||
@@ -93,7 +94,6 @@ class VirtualFilter:
|
||||
self.logger.error(f"❌ {error_msg}")
|
||||
self.data.update({
|
||||
"status": f"Error: 温度超出范围 ⚠️",
|
||||
"current_status": f"Error: 温度超出范围 ⚠️",
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
@@ -103,7 +103,6 @@ class VirtualFilter:
|
||||
self.logger.error(f"❌ {error_msg}")
|
||||
self.data.update({
|
||||
"status": f"Error: 搅拌速度超出范围 ⚠️",
|
||||
"current_status": f"Error: 搅拌速度超出范围 ⚠️",
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
@@ -112,8 +111,7 @@ class VirtualFilter:
|
||||
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
||||
self.logger.error(f"❌ {error_msg}")
|
||||
self.data.update({
|
||||
"status": f"Error: 体积超出范围 ⚠️",
|
||||
"current_status": f"Error: 体积超出范围 ⚠️",
|
||||
"status": f"Error",
|
||||
"message": error_msg
|
||||
})
|
||||
return False
|
||||
@@ -123,12 +121,11 @@ class VirtualFilter:
|
||||
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
||||
|
||||
self.data.update({
|
||||
"status": f"🌊 过滤中: {vessel}",
|
||||
"status": f"Running",
|
||||
"current_temp": temp,
|
||||
"filtered_volume": 0.0,
|
||||
"progress": 0.0,
|
||||
"current_status": f"🌊 Filtering {vessel} → {filtrate_vessel}",
|
||||
"message": f"🚀 Starting filtration: {vessel} → {filtrate_vessel}"
|
||||
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}"
|
||||
})
|
||||
|
||||
try:
|
||||
@@ -164,8 +161,7 @@ class VirtualFilter:
|
||||
"progress": progress, # Filter.action feedback
|
||||
"current_temp": temp, # Filter.action feedback
|
||||
"filtered_volume": current_filtered, # Filter.action feedback
|
||||
"current_status": f"🌊 Filtering: {progress:.1f}% complete", # Filter.action feedback
|
||||
"status": status_msg,
|
||||
"status": "Running",
|
||||
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
|
||||
})
|
||||
|
||||
@@ -190,11 +186,10 @@ class VirtualFilter:
|
||||
"progress": 100.0, # Filter.action feedback
|
||||
"current_temp": final_temp, # Filter.action feedback
|
||||
"filtered_volume": filter_volume, # Filter.action feedback
|
||||
"current_status": f"✅ Filtration completed: {filter_volume}mL", # Filter.action feedback
|
||||
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel}"
|
||||
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}"
|
||||
})
|
||||
|
||||
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel} 过滤到 {filtrate_vessel} ✨")
|
||||
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨")
|
||||
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
||||
return True
|
||||
|
||||
@@ -202,8 +197,7 @@ class VirtualFilter:
|
||||
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
||||
self.logger.error(f"❌ {error_msg}")
|
||||
self.data.update({
|
||||
"status": f"❌ 过滤错误: {str(e)}",
|
||||
"current_status": f"❌ Filtration failed: {str(e)}",
|
||||
"status": f"Error",
|
||||
"message": f"❌ Filtration failed: {str(e)}"
|
||||
})
|
||||
return False
|
||||
@@ -222,17 +216,17 @@ class VirtualFilter:
|
||||
def current_temp(self) -> float:
|
||||
"""Filter.action feedback 字段 🌡️"""
|
||||
return self.data.get("current_temp", 25.0)
|
||||
|
||||
@property
|
||||
def filtered_volume(self) -> float:
|
||||
"""Filter.action feedback 字段 💧"""
|
||||
return self.data.get("filtered_volume", 0.0)
|
||||
|
||||
|
||||
@property
|
||||
def current_status(self) -> str:
|
||||
"""Filter.action feedback 字段 📋"""
|
||||
return self.data.get("current_status", "")
|
||||
|
||||
|
||||
@property
|
||||
def filtered_volume(self) -> float:
|
||||
"""Filter.action feedback 字段 💧"""
|
||||
return self.data.get("filtered_volume", 0.0)
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.data.get("message", "")
|
||||
|
||||
@@ -67,8 +67,8 @@ class VirtualHeatChill:
|
||||
self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤")
|
||||
return True
|
||||
|
||||
async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
|
||||
stir_speed: float, purpose: str) -> bool:
|
||||
async def heat_chill(self, temp: float, time, stir: bool,
|
||||
stir_speed: float, purpose: str, vessel: dict = {}) -> bool:
|
||||
"""Execute heat chill action - 🔧 修复:确保参数类型正确"""
|
||||
|
||||
# 🔧 关键修复:确保所有参数类型正确
|
||||
@@ -77,7 +77,6 @@ class VirtualHeatChill:
|
||||
time_value = float(time) # 强制转换为浮点数
|
||||
stir_speed = float(stir_speed)
|
||||
stir = bool(stir)
|
||||
vessel = str(vessel)
|
||||
purpose = str(purpose)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
|
||||
@@ -102,8 +101,7 @@ class VirtualHeatChill:
|
||||
operation_mode = "Maintaining"
|
||||
status_action = "保温"
|
||||
|
||||
self.logger.info(f"🌡️ 开始温控操作: {vessel} → {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" 🥽 容器: {vessel}")
|
||||
self.logger.info(f"🌡️ 开始温控操作: {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" ⏰ 持续时间: {time_value}s")
|
||||
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
||||
@@ -147,7 +145,7 @@ class VirtualHeatChill:
|
||||
stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||
|
||||
self.data.update({
|
||||
"status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
|
||||
"status": f"{temp_emoji} 运行中: {status_action} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
|
||||
"operation_mode": operation_mode,
|
||||
"is_stirring": stir,
|
||||
"stir_speed": stir_speed if stir else 0.0,
|
||||
@@ -165,7 +163,7 @@ class VirtualHeatChill:
|
||||
# 更新剩余时间和状态
|
||||
self.data.update({
|
||||
"remaining_time": remaining,
|
||||
"status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
|
||||
"status": f"{temp_emoji} 运行中: {status_action} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
|
||||
"progress": progress
|
||||
})
|
||||
|
||||
@@ -185,7 +183,7 @@ class VirtualHeatChill:
|
||||
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||
|
||||
self.data.update({
|
||||
"status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
|
||||
"status": f"✅ 完成: 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
|
||||
"operation_mode": "Completed",
|
||||
"remaining_time": 0.0,
|
||||
"is_stirring": False,
|
||||
@@ -195,7 +193,6 @@ class VirtualHeatChill:
|
||||
|
||||
self.logger.info(f"🎉 温控操作完成! ✨")
|
||||
self.logger.info(f"📊 操作结果:")
|
||||
self.logger.info(f" 🥽 容器: {vessel}")
|
||||
self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
|
||||
if stir:
|
||||
@@ -204,13 +201,12 @@ class VirtualHeatChill:
|
||||
|
||||
return True
|
||||
|
||||
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
|
||||
async def heat_chill_start(self, temp: float, purpose: str, vessel: dict = {}) -> bool:
|
||||
"""Start continuous heat chill 🔄"""
|
||||
|
||||
# 🔧 添加类型转换
|
||||
try:
|
||||
temp = float(temp)
|
||||
vessel = str(vessel)
|
||||
purpose = str(purpose)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: {str(e)}"
|
||||
@@ -235,8 +231,7 @@ class VirtualHeatChill:
|
||||
operation_mode = "Maintaining"
|
||||
status_action = "恒温保持"
|
||||
|
||||
self.logger.info(f"🔄 启动持续温控: {vessel} → {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" 🥽 容器: {vessel}")
|
||||
self.logger.info(f"🔄 启动持续温控: {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
||||
self.logger.info(f" 🔄 模式: {status_action}")
|
||||
self.logger.info(f" 📝 目的: {purpose}")
|
||||
@@ -252,7 +247,7 @@ class VirtualHeatChill:
|
||||
return False
|
||||
|
||||
self.data.update({
|
||||
"status": f"🔄 启动: {status_action} {vessel} 至 {temp}°C {temp_emoji} | ♾️ 持续运行",
|
||||
"status": f"🔄 启动: {status_action} 至 {temp}°C {temp_emoji} | ♾️ 持续运行",
|
||||
"operation_mode": operation_mode,
|
||||
"is_stirring": False,
|
||||
"stir_speed": 0.0,
|
||||
@@ -262,28 +257,20 @@ class VirtualHeatChill:
|
||||
self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀")
|
||||
return True
|
||||
|
||||
async def heat_chill_stop(self, vessel: str) -> bool:
|
||||
async def heat_chill_stop(self, vessel: dict = {}) -> bool:
|
||||
"""Stop heat chill 🛑"""
|
||||
|
||||
# 🔧 添加类型转换
|
||||
try:
|
||||
vessel = str(vessel)
|
||||
except (ValueError, TypeError) as e:
|
||||
error_msg = f"参数类型转换错误: {str(e)}"
|
||||
self.logger.error(f"❌ {error_msg}")
|
||||
return False
|
||||
|
||||
self.logger.info(f"🛑 停止温控: {vessel}")
|
||||
self.logger.info(f"🛑 停止温控:")
|
||||
|
||||
self.data.update({
|
||||
"status": f"🛑 已停止: {vessel} 温控停止",
|
||||
"status": f"🛑 {self.device_id} 温控停止",
|
||||
"operation_mode": "Stopped",
|
||||
"is_stirring": False,
|
||||
"stir_speed": 0.0,
|
||||
"remaining_time": 0.0,
|
||||
})
|
||||
|
||||
self.logger.info(f"✅ 温控设备已停止 {vessel} 的温度控制 🏁")
|
||||
self.logger.info(f"✅ 温控设备已停止 {self.device_id} 温度控制 🏁")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
|
||||
@@ -21,19 +21,6 @@ class VirtualMultiwayValve:
|
||||
self._current_position = 0 # 默认在0号位(transfer pump位置)
|
||||
self._target_position = 0
|
||||
|
||||
# 位置映射说明
|
||||
self.position_map = {
|
||||
0: "transfer_pump", # 0号位连接转移泵
|
||||
1: "port_1", # 1号位
|
||||
2: "port_2", # 2号位
|
||||
3: "port_3", # 3号位
|
||||
4: "port_4", # 4号位
|
||||
5: "port_5", # 5号位
|
||||
6: "port_6", # 6号位
|
||||
7: "port_7", # 7号位
|
||||
8: "port_8" # 8号位
|
||||
}
|
||||
|
||||
print(f"🔄 === 虚拟多通阀门已创建 === ✨")
|
||||
print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)")
|
||||
self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}")
|
||||
@@ -60,7 +47,7 @@ class VirtualMultiwayValve:
|
||||
|
||||
def get_current_port(self) -> str:
|
||||
"""获取当前连接的端口名称 🔌"""
|
||||
return self.position_map.get(self._current_position, "unknown")
|
||||
return self._current_position
|
||||
|
||||
def set_position(self, command: Union[int, str]):
|
||||
"""
|
||||
@@ -115,7 +102,7 @@ class VirtualMultiwayValve:
|
||||
old_position = self._current_position
|
||||
old_port = self.get_current_port()
|
||||
|
||||
self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos}({self.position_map.get(pos, 'unknown')}) {pos_emoji}")
|
||||
self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos} {pos_emoji}")
|
||||
|
||||
self._status = "Busy"
|
||||
self._valve_state = "Moving"
|
||||
@@ -190,6 +177,17 @@ class VirtualMultiwayValve:
|
||||
"""获取阀门位置 - 兼容性方法 📍"""
|
||||
return self._current_position
|
||||
|
||||
def set_valve_position(self, command: Union[int, str]):
|
||||
"""
|
||||
设置阀门位置 - 兼容pump_protocol调用 🎯
|
||||
这是set_position的别名方法,用于兼容pump_protocol.py
|
||||
|
||||
Args:
|
||||
command: 目标位置 (0-8) 或位置字符串
|
||||
"""
|
||||
# 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
|
||||
return self.set_position(command)
|
||||
|
||||
def is_at_position(self, position: int) -> bool:
|
||||
"""检查是否在指定位置 🎯"""
|
||||
result = self._current_position == position
|
||||
@@ -210,17 +208,6 @@ class VirtualMultiwayValve:
|
||||
# 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
|
||||
return result
|
||||
|
||||
def get_available_positions(self) -> list:
|
||||
"""获取可用位置列表 📋"""
|
||||
positions = list(range(0, self.max_positions + 1))
|
||||
# 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}")
|
||||
return positions
|
||||
|
||||
def get_available_ports(self) -> Dict[int, str]:
|
||||
"""获取可用端口映射 🗺️"""
|
||||
# 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
|
||||
return self.position_map.copy()
|
||||
|
||||
def reset(self):
|
||||
"""重置阀门到transfer pump位置(0号位)🔄"""
|
||||
self.logger.info(f"🔄 重置阀门到泵位置...")
|
||||
@@ -253,41 +240,12 @@ class VirtualMultiwayValve:
|
||||
# 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}")
|
||||
return flow_path
|
||||
|
||||
def get_info(self) -> dict:
|
||||
"""获取阀门详细信息 📊"""
|
||||
info = {
|
||||
"port": self.port,
|
||||
"max_positions": self.max_positions,
|
||||
"total_positions": self.total_positions,
|
||||
"current_position": self._current_position,
|
||||
"current_port": self.get_current_port(),
|
||||
"target_position": self._target_position,
|
||||
"status": self._status,
|
||||
"valve_state": self._valve_state,
|
||||
"flow_path": self.get_flow_path(),
|
||||
"position_map": self.position_map
|
||||
}
|
||||
|
||||
# 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
|
||||
return info
|
||||
|
||||
def __str__(self):
|
||||
current_port = self.get_current_port()
|
||||
status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌"
|
||||
|
||||
return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
|
||||
|
||||
def set_valve_position(self, command: Union[int, str]):
|
||||
"""
|
||||
设置阀门位置 - 兼容pump_protocol调用 🎯
|
||||
这是set_position的别名方法,用于兼容pump_protocol.py
|
||||
|
||||
Args:
|
||||
command: 目标位置 (0-8) 或位置字符串
|
||||
"""
|
||||
# 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
|
||||
return self.set_position(command)
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
@@ -309,13 +267,6 @@ if __name__ == "__main__":
|
||||
print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}")
|
||||
print(f"📍 当前状态: {valve}")
|
||||
|
||||
# 显示所有可用位置
|
||||
print(f"\n📋 可用位置: {valve.get_available_positions()}")
|
||||
print(f"🗺️ 端口映射: {valve.get_available_ports()}")
|
||||
|
||||
# 获取详细信息
|
||||
print(f"\n📊 详细信息: {valve.get_info()}")
|
||||
|
||||
# 测试切换功能
|
||||
print(f"\n🔄 智能切换测试:")
|
||||
print(f"当前位置: {valve._current_position}")
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class VirtualPump:
|
||||
"""Virtual pump device for transfer and cleaning operations"""
|
||||
|
||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||
# 处理可能的不同调用方式
|
||||
if device_id is None and 'id' in kwargs:
|
||||
device_id = kwargs.pop('id')
|
||||
if config is None and 'config' in kwargs:
|
||||
config = kwargs.pop('config')
|
||||
|
||||
# 设置默认值
|
||||
self.device_id = device_id or "unknown_pump"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
|
||||
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 10.0)
|
||||
|
||||
print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===")
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual pump"""
|
||||
self.logger.info(f"Initializing virtual pump {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"valve_position": 0,
|
||||
"current_volume": 0.0,
|
||||
"max_volume": self._max_volume,
|
||||
"transfer_rate": self._transfer_rate,
|
||||
"from_vessel": "",
|
||||
"to_vessel": "",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0,
|
||||
"current_status": "Ready"
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual pump"""
|
||||
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
|
||||
return True
|
||||
|
||||
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
||||
amount: str = "", time: float = 0.0, viscous: bool = False,
|
||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
||||
rinsing_repeats: int = 0, solid: bool = False) -> bool:
|
||||
"""Execute transfer operation"""
|
||||
self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}")
|
||||
|
||||
# 计算转移时间
|
||||
transfer_time = volume / self._transfer_rate if time == 0 else time
|
||||
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"current_status": "Transferring",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0
|
||||
})
|
||||
|
||||
# 模拟转移过程
|
||||
steps = 10
|
||||
step_time = transfer_time / steps
|
||||
step_volume = volume / steps
|
||||
|
||||
for i in range(steps):
|
||||
await asyncio.sleep(step_time)
|
||||
progress = (i + 1) / steps * 100
|
||||
current_volume = step_volume * (i + 1)
|
||||
|
||||
self.data.update({
|
||||
"progress": progress,
|
||||
"transferred_volume": current_volume,
|
||||
"current_status": f"Transferring: {progress:.1f}%"
|
||||
})
|
||||
|
||||
self.logger.info(f"Transfer progress: {progress:.1f}%")
|
||||
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_status": "Transfer completed",
|
||||
"progress": 100.0,
|
||||
"transferred_volume": volume
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
async def clean_vessel(self, vessel: str, solvent: str, volume: float,
|
||||
temp: float, repeats: int = 1) -> bool:
|
||||
"""Execute vessel cleaning operation - matches CleanVessel action"""
|
||||
self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)")
|
||||
|
||||
# 更新设备状态
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"from_vessel": f"flask_{solvent}",
|
||||
"to_vessel": vessel,
|
||||
"current_status": "Cleaning in progress",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0
|
||||
})
|
||||
|
||||
# 计算清洗时间(基于体积和重复次数)
|
||||
# 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放)
|
||||
cleaning_rate = self._transfer_rate / 2
|
||||
cleaning_time_per_cycle = volume / cleaning_rate
|
||||
total_cleaning_time = cleaning_time_per_cycle * repeats
|
||||
|
||||
# 模拟清洗过程
|
||||
steps_per_repeat = 10 # 每次重复清洗分10个步骤
|
||||
total_steps = steps_per_repeat * repeats
|
||||
step_time = total_cleaning_time / total_steps
|
||||
|
||||
for repeat in range(repeats):
|
||||
self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}")
|
||||
|
||||
for step in range(steps_per_repeat):
|
||||
await asyncio.sleep(step_time)
|
||||
|
||||
# 计算当前进度
|
||||
current_step = repeat * steps_per_repeat + step + 1
|
||||
progress = (current_step / total_steps) * 100
|
||||
|
||||
# 计算已处理的体积
|
||||
volume_processed = (current_step / total_steps) * volume * repeats
|
||||
|
||||
# 更新状态
|
||||
self.data.update({
|
||||
"progress": progress,
|
||||
"transferred_volume": volume_processed,
|
||||
"current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)"
|
||||
})
|
||||
|
||||
self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})")
|
||||
|
||||
# 清洗完成
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_status": "Cleaning completed successfully",
|
||||
"progress": 100.0,
|
||||
"transferred_volume": volume * repeats,
|
||||
"from_vessel": "",
|
||||
"to_vessel": ""
|
||||
})
|
||||
|
||||
self.logger.info(f"Vessel cleaning completed: {vessel}")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@property
|
||||
def valve_position(self) -> int:
|
||||
return self.data.get("valve_position", 0)
|
||||
|
||||
@property
|
||||
def current_volume(self) -> float:
|
||||
return self.data.get("current_volume", 0.0)
|
||||
|
||||
@property
|
||||
def max_volume(self) -> float:
|
||||
return self.data.get("max_volume", 0.0)
|
||||
|
||||
@property
|
||||
def transfer_rate(self) -> float:
|
||||
return self.data.get("transfer_rate", 0.0)
|
||||
|
||||
@property
|
||||
def from_vessel(self) -> str:
|
||||
return self.data.get("from_vessel", "")
|
||||
|
||||
@property
|
||||
def to_vessel(self) -> str:
|
||||
return self.data.get("to_vessel", "")
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self.data.get("progress", 0.0)
|
||||
|
||||
@property
|
||||
def transferred_volume(self) -> float:
|
||||
return self.data.get("transferred_volume", 0.0)
|
||||
|
||||
@property
|
||||
def current_status(self) -> str:
|
||||
return self.data.get("current_status", "Ready")
|
||||
@@ -99,8 +99,8 @@ class VirtualRotavap:
|
||||
self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒")
|
||||
time = 180.0
|
||||
|
||||
# 确保time是float类型
|
||||
time = float(time)
|
||||
# 确保time是float类型; 并加速
|
||||
time = float(time) / 10.0
|
||||
|
||||
# 🔧 简化处理:如果vessel就是设备自己,直接操作
|
||||
if vessel == self.device_id:
|
||||
|
||||
@@ -48,20 +48,6 @@ class VirtualSolenoidValve:
|
||||
"""获取阀门位置状态"""
|
||||
return "OPEN" if self._is_open else "CLOSED"
|
||||
|
||||
@property
|
||||
def state(self) -> dict:
|
||||
"""获取阀门完整状态"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"port": self.port,
|
||||
"voltage": self.voltage,
|
||||
"response_time": self.response_time,
|
||||
"is_open": self._is_open,
|
||||
"valve_state": self._valve_state,
|
||||
"status": self._status,
|
||||
"position": self.valve_position
|
||||
}
|
||||
|
||||
async def set_valve_position(self, command: str = None, **kwargs):
|
||||
"""
|
||||
设置阀门位置 - ROS动作接口
|
||||
|
||||
@@ -319,21 +319,6 @@ class VirtualSolidDispenser:
|
||||
def total_operations(self) -> int:
|
||||
return self._total_operations
|
||||
|
||||
def get_device_info(self) -> Dict[str, Any]:
|
||||
"""获取设备状态信息 📊"""
|
||||
info = {
|
||||
"device_id": self.device_id,
|
||||
"status": self._status,
|
||||
"current_reagent": self._current_reagent,
|
||||
"last_dispensed_amount": self._dispensed_amount,
|
||||
"total_operations": self._total_operations,
|
||||
"max_capacity": self.max_capacity,
|
||||
"precision": self.precision
|
||||
}
|
||||
|
||||
self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g")
|
||||
return info
|
||||
|
||||
def __str__(self):
|
||||
status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠"
|
||||
return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
|
||||
@@ -380,8 +365,6 @@ async def test_solid_dispenser():
|
||||
mass="150 g" # 超过100g限制
|
||||
)
|
||||
print(f"📊 测试4结果: {result4}")
|
||||
|
||||
print(f"\n📊 最终设备信息: {dispenser.get_device_info()}")
|
||||
print(f"✅ === 测试完成 === 🎉")
|
||||
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ class VirtualStirrer:
|
||||
"min_speed": self._min_speed
|
||||
}
|
||||
|
||||
self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
|
||||
# self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
|
||||
return info
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -380,22 +380,6 @@ class VirtualTransferPump:
|
||||
"""检查是否已满"""
|
||||
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
||||
|
||||
# 调试和状态信息
|
||||
def get_pump_info(self) -> dict:
|
||||
"""获取泵的详细信息"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"status": self._status,
|
||||
"position": self._position,
|
||||
"current_volume": self._current_volume,
|
||||
"max_volume": self.max_volume,
|
||||
"max_velocity": self._max_velocity,
|
||||
"mode": self.mode.name,
|
||||
"is_empty": self.is_empty(),
|
||||
"is_full": self.is_full(),
|
||||
"remaining_capacity": self.get_remaining_capacity()
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||
|
||||
@@ -425,8 +409,6 @@ async def demo():
|
||||
result = await pump.set_position(0.0)
|
||||
print(f"Empty result: {result}")
|
||||
print(f"After emptying: {pump}")
|
||||
|
||||
print("\nPump info:", pump.get_pump_info())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
serial:
|
||||
category:
|
||||
- serial
|
||||
- communication_devices
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-handle_serial_request:
|
||||
@@ -1,4 +1,4 @@
|
||||
camera:
|
||||
camera.USB:
|
||||
category:
|
||||
- camera
|
||||
class:
|
||||
|
||||
404
unilabos/registry/devices/characterization_chromatic.yaml
Normal file
404
unilabos/registry/devices/characterization_chromatic.yaml
Normal file
@@ -0,0 +1,404 @@
|
||||
hplc.agilent:
|
||||
category:
|
||||
- characterization_chromatic
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态,确保系统稳定运行,及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-extract_data_from_txt:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件,提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理,为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
type: string
|
||||
required:
|
||||
- file_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: extract_data_from_txt参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start_sequence:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
params: null
|
||||
resource: null
|
||||
wf_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列,包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能,实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
params:
|
||||
type: string
|
||||
resource:
|
||||
type: object
|
||||
wf_name:
|
||||
type: string
|
||||
required:
|
||||
- wf_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start_sequence参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_close_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块,确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制,避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_close_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_open_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块,建立设备通信并进行自检。该函数提供连接验证和错误恢复机制,确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_open_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
execute_command_from_outer:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||
status_types:
|
||||
could_run: bool
|
||||
data_file: list
|
||||
device_status: str
|
||||
driver_init_ok: bool
|
||||
finish_status: str
|
||||
is_running: bool
|
||||
status_text: str
|
||||
success: bool
|
||||
type: python
|
||||
config_info: []
|
||||
description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
driver_debug:
|
||||
default: false
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
could_run:
|
||||
type: boolean
|
||||
data_file:
|
||||
type: array
|
||||
device_status:
|
||||
type: string
|
||||
driver_init_ok:
|
||||
type: boolean
|
||||
finish_status:
|
||||
type: string
|
||||
is_running:
|
||||
type: boolean
|
||||
status_text:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- status_text
|
||||
- device_status
|
||||
- could_run
|
||||
- driver_init_ok
|
||||
- is_running
|
||||
- success
|
||||
- finish_status
|
||||
- data_file
|
||||
type: object
|
||||
version: 1.0.0
|
||||
hplc.agilent-zhida:
|
||||
category:
|
||||
- characterization_chromatic
|
||||
class:
|
||||
action_value_mappings:
|
||||
abort:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接,释放网络资源。该函数确保连接的正确关闭,避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-connect:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接,配置通信超时参数。该函数是设备使用前的必要步骤,建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: connect参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
get_methods:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
start:
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.zhida_hplc.zhida:ZhidaClient
|
||||
status_types:
|
||||
methods: dict
|
||||
status: dict
|
||||
type: python
|
||||
config_info: []
|
||||
description: 智达高效液相色谱(HPLC)分析设备,用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信,支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能,可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
host:
|
||||
default: 192.168.1.47
|
||||
type: string
|
||||
port:
|
||||
default: 5792
|
||||
type: string
|
||||
timeout:
|
||||
default: 10.0
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
methods:
|
||||
type: object
|
||||
status:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- methods
|
||||
type: object
|
||||
version: 1.0.0
|
||||
@@ -1,225 +1,4 @@
|
||||
hplc.agilent:
|
||||
category:
|
||||
- characterization_optic
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态,确保系统稳定运行,及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-extract_data_from_txt:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件,提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理,为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
type: string
|
||||
required:
|
||||
- file_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: extract_data_from_txt参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-start_sequence:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
params: null
|
||||
resource: null
|
||||
wf_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列,包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能,实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
params:
|
||||
type: string
|
||||
resource:
|
||||
type: object
|
||||
wf_name:
|
||||
type: string
|
||||
required:
|
||||
- wf_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start_sequence参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_close_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块,确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制,避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_close_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-try_open_sub_device:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
device_name: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块,建立设备通信并进行自检。该函数提供连接验证和错误恢复机制,确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
device_name:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: try_open_sub_device参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
execute_command_from_outer:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||
status_types:
|
||||
could_run: bool
|
||||
data_file: list
|
||||
device_status: str
|
||||
driver_init_ok: bool
|
||||
finish_status: str
|
||||
is_running: bool
|
||||
status_text: str
|
||||
success: bool
|
||||
type: python
|
||||
config_info: []
|
||||
description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
driver_debug:
|
||||
default: false
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
could_run:
|
||||
type: boolean
|
||||
data_file:
|
||||
type: array
|
||||
device_status:
|
||||
type: string
|
||||
driver_init_ok:
|
||||
type: boolean
|
||||
finish_status:
|
||||
type: string
|
||||
is_running:
|
||||
type: boolean
|
||||
status_text:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- status_text
|
||||
- device_status
|
||||
- could_run
|
||||
- driver_init_ok
|
||||
- is_running
|
||||
- success
|
||||
- finish_status
|
||||
- data_file
|
||||
type: object
|
||||
version: 1.0.0
|
||||
raman_home_made:
|
||||
raman.home_made:
|
||||
category:
|
||||
- characterization_optic
|
||||
class:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
gas_source.mock:
|
||||
category:
|
||||
- vacuum_and_purge
|
||||
- gas_handler
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-is_closed:
|
||||
@@ -180,6 +180,7 @@ gas_source.mock:
|
||||
vacuum_pump.mock:
|
||||
category:
|
||||
- vacuum_and_purge
|
||||
- gas_handler
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-is_closed:
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,704 +0,0 @@
|
||||
moveit.arm_slider:
|
||||
category:
|
||||
- moveit_config
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: 机械臂与滑块运动系统,基于MoveIt2运动规划框架的多自由度机械臂控制设备。该系统集成机械臂和线性滑块,通过ROS2和MoveIt2实现精确的轨迹规划和协调运动控制。支持笛卡尔空间和关节空间的运动规划、碰撞检测、逆运动学求解等功能。适用于复杂的pick-and-place操作、精密装配、多工位协作等需要高精度多轴协调运动的实验室自动化应用。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: arm_slider
|
||||
type: device
|
||||
version: 1.0.0
|
||||
moveit.toyo_xyz:
|
||||
category:
|
||||
- moveit_config
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: 东洋XYZ三轴运动平台,基于MoveIt2运动规划框架的精密定位设备。该设备通过ROS2和MoveIt2实现三维空间的精确运动控制,支持复杂轨迹规划、多点定位、速度控制等功能。具备高精度定位、平稳运动、实时轨迹监控等特性。适用于精密加工、样品定位、检测扫描、自动化装配等需要高精度三维运动控制的实验室和工业应用场景。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: toyo_xyz
|
||||
type: device
|
||||
version: 1.0.0
|
||||
@@ -336,7 +336,7 @@ separator.homemade:
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
title: orientation
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
@@ -350,12 +350,12 @@ separator.homemade:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
title: position
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
title: pose
|
||||
type: object
|
||||
sample_id:
|
||||
type: string
|
||||
@@ -372,7 +372,7 @@ separator.homemade:
|
||||
- pose
|
||||
- config
|
||||
- data
|
||||
title: Resource
|
||||
title: vessel
|
||||
type: object
|
||||
required:
|
||||
- vessel
|
||||
|
||||
@@ -366,7 +366,7 @@ solenoid_valve.mock:
|
||||
- valve_position
|
||||
type: object
|
||||
version: 1.0.0
|
||||
syringe_pump_with_valve.runze:
|
||||
syringe_pump_with_valve.runze.SY03B-T06:
|
||||
category:
|
||||
- pump_and_valve
|
||||
class:
|
||||
@@ -764,7 +764,583 @@ syringe_pump_with_valve.runze:
|
||||
type: python
|
||||
config_info: []
|
||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||
handles: []
|
||||
handles:
|
||||
- data_key: fluid_port_1
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口1
|
||||
handler_key: '1'
|
||||
io_type: source
|
||||
label: '1'
|
||||
side: NORTH
|
||||
- data_key: fluid_port_2
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口2
|
||||
handler_key: '2'
|
||||
io_type: source
|
||||
label: '2'
|
||||
side: EAST
|
||||
- data_key: fluid_port_3
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口3
|
||||
handler_key: '3'
|
||||
io_type: source
|
||||
label: '3'
|
||||
side: SOUTH
|
||||
- data_key: fluid_port_4
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口4
|
||||
handler_key: '4'
|
||||
io_type: source
|
||||
label: '4'
|
||||
side: SOUTH
|
||||
- data_key: fluid_port_5
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口5
|
||||
handler_key: '5'
|
||||
io_type: source
|
||||
label: '5'
|
||||
side: EAST
|
||||
- data_key: fluid_port_6
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口6
|
||||
handler_key: '6'
|
||||
io_type: source
|
||||
label: '6'
|
||||
side: NORTH
|
||||
- data_key: fluid_port_6
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 六通阀门端口6-特殊输入
|
||||
handler_key: '6'
|
||||
io_type: target
|
||||
label: 6-in
|
||||
side: WEST
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
address:
|
||||
default: '1'
|
||||
type: string
|
||||
max_volume:
|
||||
default: 25.0
|
||||
type: number
|
||||
mode:
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
required:
|
||||
- port
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
max_velocity:
|
||||
type: number
|
||||
mode:
|
||||
type: integer
|
||||
plunger_position:
|
||||
type: string
|
||||
position:
|
||||
type: number
|
||||
status:
|
||||
type: string
|
||||
valve_position:
|
||||
type: string
|
||||
velocity_end:
|
||||
type: string
|
||||
velocity_grade:
|
||||
type: string
|
||||
velocity_init:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- mode
|
||||
- max_velocity
|
||||
- velocity_grade
|
||||
- velocity_init
|
||||
- velocity_end
|
||||
- valve_position
|
||||
- position
|
||||
- plunger_position
|
||||
type: object
|
||||
version: 1.0.0
|
||||
syringe_pump_with_valve.runze.SY03B-T08:
|
||||
category:
|
||||
- pump_and_valve
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: close的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-initialize:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: initialize的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: initialize参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-pull_plunger:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
volume: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: pull_plunger的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
volume:
|
||||
type: number
|
||||
required:
|
||||
- volume
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: pull_plunger参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-push_plunger:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
volume: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: push_plunger的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
volume:
|
||||
type: number
|
||||
required:
|
||||
- volume
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: push_plunger参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_aux_input_status_1:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_aux_input_status_1的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_aux_input_status_1参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_aux_input_status_2:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_aux_input_status_2的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_aux_input_status_2参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_backlash_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_backlash_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_backlash_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_command_buffer_status:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_command_buffer_status的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_command_buffer_status参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-query_software_version:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: query_software_version的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: query_software_version参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_command:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
full_command: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_command的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
full_command:
|
||||
type: string
|
||||
required:
|
||||
- full_command
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_command参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_baudrate:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
baudrate: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_baudrate的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
baudrate:
|
||||
type: string
|
||||
required:
|
||||
- baudrate
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_baudrate参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_max_velocity:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_max_velocity的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
velocity:
|
||||
type: number
|
||||
required:
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_max_velocity参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
max_velocity: null
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
max_velocity:
|
||||
type: number
|
||||
position:
|
||||
type: number
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_valve_position:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
position: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_valve_position的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
position:
|
||||
type: string
|
||||
required:
|
||||
- position
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_valve_position参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-set_velocity_grade:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
velocity: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: set_velocity_grade的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
velocity:
|
||||
type: string
|
||||
required:
|
||||
- velocity
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: set_velocity_grade参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop_operation:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: stop_operation的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop_operation参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_error:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_error的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_error参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
hardware_interface:
|
||||
name: hardware_interface
|
||||
read: send_command
|
||||
write: send_command
|
||||
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
|
||||
status_types:
|
||||
max_velocity: float
|
||||
mode: int
|
||||
plunger_position: String
|
||||
position: float
|
||||
status: str
|
||||
valve_position: str
|
||||
velocity_end: String
|
||||
velocity_grade: String
|
||||
velocity_init: String
|
||||
type: python
|
||||
config_info: []
|
||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||
handles:
|
||||
- data_key: fluid_port_1
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口1
|
||||
handler_key: '1'
|
||||
io_type: source
|
||||
label: '1'
|
||||
side: NORTH
|
||||
- data_key: fluid_port_2
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口2
|
||||
handler_key: '2'
|
||||
io_type: source
|
||||
label: '2'
|
||||
side: EAST
|
||||
- data_key: fluid_port_3
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口3
|
||||
handler_key: '3'
|
||||
io_type: source
|
||||
label: '3'
|
||||
side: EAST
|
||||
- data_key: fluid_port_4
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口4
|
||||
handler_key: '4'
|
||||
io_type: source
|
||||
label: '4'
|
||||
side: SOUTH
|
||||
- data_key: fluid_port_5
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口5
|
||||
handler_key: '5'
|
||||
io_type: source
|
||||
label: '5'
|
||||
side: SOUTH
|
||||
- data_key: fluid_port_6
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口6
|
||||
handler_key: '6'
|
||||
io_type: source
|
||||
label: '6'
|
||||
side: WEST
|
||||
- data_key: fluid_port_7
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口7
|
||||
handler_key: '7'
|
||||
io_type: source
|
||||
label: '7'
|
||||
side: WEST
|
||||
- data_key: fluid_port_8
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口8-特殊输入
|
||||
handler_key: '8'
|
||||
io_type: target
|
||||
label: '8'
|
||||
side: WEST
|
||||
- data_key: fluid_port_8
|
||||
data_source: executor
|
||||
data_type: fluid
|
||||
description: 八通阀门端口8
|
||||
handler_key: '8'
|
||||
io_type: source
|
||||
label: '8'
|
||||
side: NORTH
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
|
||||
@@ -1,3 +1,355 @@
|
||||
robotic_arm.SCARA_with_slider.virtual:
|
||||
category:
|
||||
- robot_arm
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: 机械臂与滑块运动系统,基于MoveIt2运动规划框架的多自由度机械臂控制设备。该系统集成机械臂和线性滑块,通过ROS2和MoveIt2实现精确的轨迹规划和协调运动控制。支持笛卡尔空间和关节空间的运动规划、碰撞检测、逆运动学求解等功能。适用于复杂的pick-and-place操作、精密装配、多工位协作等需要高精度多轴协调运动的实验室自动化应用。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: arm_slider
|
||||
type: device
|
||||
version: 1.0.0
|
||||
robotic_arm.UR:
|
||||
category:
|
||||
- robot_arm
|
||||
|
||||
@@ -533,7 +533,7 @@ gripper.mock:
|
||||
required:
|
||||
- position
|
||||
- max_effort
|
||||
title: GripperCommand
|
||||
title: command
|
||||
type: object
|
||||
required:
|
||||
- command
|
||||
|
||||
@@ -139,12 +139,12 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
title: stamp
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
title: header
|
||||
type: object
|
||||
pose:
|
||||
properties:
|
||||
@@ -163,7 +163,7 @@ linear_motion.grbl:
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
title: orientation
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
@@ -177,17 +177,17 @@ linear_motion.grbl:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
title: position
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
title: pose
|
||||
type: object
|
||||
required:
|
||||
- header
|
||||
- pose
|
||||
title: PoseStamped
|
||||
title: current_pose
|
||||
type: object
|
||||
distance_remaining:
|
||||
type: number
|
||||
@@ -204,7 +204,7 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
title: estimated_time_remaining
|
||||
type: object
|
||||
navigation_time:
|
||||
properties:
|
||||
@@ -219,7 +219,7 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
title: navigation_time
|
||||
type: object
|
||||
number_of_poses_remaining:
|
||||
maximum: 32767
|
||||
@@ -262,12 +262,12 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
title: stamp
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
title: header
|
||||
type: object
|
||||
pose:
|
||||
properties:
|
||||
@@ -286,7 +286,7 @@ linear_motion.grbl:
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
title: orientation
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
@@ -300,17 +300,17 @@ linear_motion.grbl:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
title: position
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
title: pose
|
||||
type: object
|
||||
required:
|
||||
- header
|
||||
- pose
|
||||
title: PoseStamped
|
||||
title: poses
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
@@ -323,7 +323,7 @@ linear_motion.grbl:
|
||||
result:
|
||||
properties: {}
|
||||
required: []
|
||||
title: Empty
|
||||
title: result
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
@@ -371,12 +371,12 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Time
|
||||
title: stamp
|
||||
type: object
|
||||
required:
|
||||
- stamp
|
||||
- frame_id
|
||||
title: Header
|
||||
title: header
|
||||
type: object
|
||||
position:
|
||||
type: number
|
||||
@@ -406,7 +406,7 @@ linear_motion.grbl:
|
||||
required:
|
||||
- sec
|
||||
- nanosec
|
||||
title: Duration
|
||||
title: min_duration
|
||||
type: object
|
||||
position:
|
||||
type: number
|
||||
@@ -470,6 +470,358 @@ linear_motion.grbl:
|
||||
- spindle_speed
|
||||
type: object
|
||||
version: 1.0.0
|
||||
linear_motion.toyo_xyz.sim:
|
||||
category:
|
||||
- robot_linear_motion
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_joint_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_names: null
|
||||
joint_positions: null
|
||||
move_group: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_joint_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_names:
|
||||
type: string
|
||||
joint_positions:
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- joint_positions
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_joint_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-moveit_task:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
cartesian: false
|
||||
move_group: null
|
||||
offsets:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
position: null
|
||||
quaternion: null
|
||||
retry: 10
|
||||
speed: 1
|
||||
target_link: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: moveit_task的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
cartesian:
|
||||
default: false
|
||||
type: string
|
||||
move_group:
|
||||
type: string
|
||||
offsets:
|
||||
default:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
type: string
|
||||
position:
|
||||
type: string
|
||||
quaternion:
|
||||
type: string
|
||||
retry:
|
||||
default: 10
|
||||
type: string
|
||||
speed:
|
||||
default: 1
|
||||
type: string
|
||||
target_link:
|
||||
type: string
|
||||
required:
|
||||
- move_group
|
||||
- position
|
||||
- quaternion
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: moveit_task参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: post_init的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: string
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_manager:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_link: null
|
||||
resource: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_manager的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_link:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
required:
|
||||
- resource
|
||||
- parent_link
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_manager参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-wait_for_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: wait_for_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: wait_for_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pick_and_place:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_position:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
set_status:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: command
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: 东洋XYZ三轴运动平台,基于MoveIt2运动规划框架的精密定位设备。该设备通过ROS2和MoveIt2实现三维空间的精确运动控制,支持复杂轨迹规划、多点定位、速度控制等功能。具备高精度定位、平稳运动、实时轨迹监控等特性。适用于精密加工、样品定位、检测扫描、自动化装配等需要高精度三维运动控制的实验室和工业应用场景。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_config:
|
||||
type: string
|
||||
joint_poses:
|
||||
type: string
|
||||
moveit_type:
|
||||
type: string
|
||||
rotation:
|
||||
type: string
|
||||
required:
|
||||
- moveit_type
|
||||
- joint_poses
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
model:
|
||||
mesh: toyo_xyz
|
||||
type: device
|
||||
version: 1.0.0
|
||||
motor.iCL42:
|
||||
category:
|
||||
- robot_linear_motion
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
lh_joint_publisher:
|
||||
category:
|
||||
- sim_nodes
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-check_tf_update_actions:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: check_tf_update_actions的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: check_tf_update_actions参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-find_resource_parent:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
resource_id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: find_resource_parent的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
resource_id:
|
||||
type: string
|
||||
required:
|
||||
- resource_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: find_resource_parent参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-inverse_kinematics:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parent_id: null
|
||||
x: null
|
||||
x_joint: null
|
||||
y: null
|
||||
y_joint: null
|
||||
z: null
|
||||
z_joint: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: inverse_kinematics的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parent_id:
|
||||
type: string
|
||||
x:
|
||||
type: string
|
||||
x_joint:
|
||||
type: object
|
||||
y:
|
||||
type: string
|
||||
y_joint:
|
||||
type: object
|
||||
z:
|
||||
type: string
|
||||
z_joint:
|
||||
type: object
|
||||
required:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- parent_id
|
||||
- x_joint
|
||||
- y_joint
|
||||
- z_joint
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: inverse_kinematics参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-lh_joint_action_callback:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
goal_handle: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: lh_joint_action_callback的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
goal_handle:
|
||||
type: string
|
||||
required:
|
||||
- goal_handle
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: lh_joint_action_callback参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-lh_joint_pub_callback:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: lh_joint_pub_callback的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: lh_joint_pub_callback参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_joints:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
option: null
|
||||
resource_names: null
|
||||
speed: 0.1
|
||||
x: null
|
||||
x_joint: null
|
||||
y: null
|
||||
y_joint: null
|
||||
z: null
|
||||
z_joint: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_joints的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
option:
|
||||
type: string
|
||||
resource_names:
|
||||
type: string
|
||||
speed:
|
||||
default: 0.1
|
||||
type: string
|
||||
x:
|
||||
type: string
|
||||
x_joint:
|
||||
type: string
|
||||
y:
|
||||
type: string
|
||||
y_joint:
|
||||
type: string
|
||||
z:
|
||||
type: string
|
||||
z_joint:
|
||||
type: string
|
||||
required:
|
||||
- resource_names
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- option
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_joints参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-move_to:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
joint_positions: null
|
||||
parent_id: null
|
||||
speed: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: move_to的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
joint_positions:
|
||||
type: string
|
||||
parent_id:
|
||||
type: string
|
||||
speed:
|
||||
type: string
|
||||
required:
|
||||
- joint_positions
|
||||
- speed
|
||||
- parent_id
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: move_to参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-resource_move:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
channels: null
|
||||
link_name: null
|
||||
resource_id: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: resource_move的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
link_name:
|
||||
type: string
|
||||
resource_id:
|
||||
type: string
|
||||
required:
|
||||
- resource_id
|
||||
- link_name
|
||||
- channels
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: resource_move参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-send_resource_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
link_name: null
|
||||
resource_id_list: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: send_resource_action的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
link_name:
|
||||
type: string
|
||||
resource_id_list:
|
||||
type: array
|
||||
required:
|
||||
- resource_id_list
|
||||
- link_name
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: send_resource_action参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
|
||||
status_types: {}
|
||||
type: ros2
|
||||
config_info: []
|
||||
description: 液体处理器关节发布器,用于ROS2仿真系统中的液体处理设备运动控制。该节点通过发布关节状态驱动仿真模型中的机械臂运动,支持三维坐标到关节空间的逆运动学转换、多关节协调控制、资源跟踪和TF变换。具备精确的位置控制、速度调节、pick-and-place操作等功能。适用于液体处理系统的虚拟仿真、运动规划验证、系统集成测试等应用场景。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
device_id:
|
||||
default: lh_joint_publisher
|
||||
type: string
|
||||
rate:
|
||||
default: 50
|
||||
type: string
|
||||
resource_tracker:
|
||||
type: string
|
||||
resources_config:
|
||||
type: array
|
||||
required:
|
||||
- resources_config
|
||||
- resource_tracker
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
@@ -1,6 +1,6 @@
|
||||
laiyu_add_solid:
|
||||
solid_dispenser.laiyu:
|
||||
category:
|
||||
- laiyu_add_solid
|
||||
- solid_dispenser
|
||||
class:
|
||||
action_value_mappings:
|
||||
add_powder_tube:
|
||||
@@ -353,7 +353,7 @@ laiyu_add_solid:
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
module: unilabos.devices.laiyu_add_solid.laiyu:Laiyu
|
||||
module: unilabos.devices.powder_dispense.laiyu:Laiyu
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
@@ -362,7 +362,7 @@ heaterstirrer.dalong:
|
||||
- y
|
||||
- z
|
||||
- w
|
||||
title: Quaternion
|
||||
title: orientation
|
||||
type: object
|
||||
position:
|
||||
properties:
|
||||
@@ -376,12 +376,12 @@ heaterstirrer.dalong:
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
title: Point
|
||||
title: position
|
||||
type: object
|
||||
required:
|
||||
- position
|
||||
- orientation
|
||||
title: Pose
|
||||
title: pose
|
||||
type: object
|
||||
sample_id:
|
||||
type: string
|
||||
@@ -398,7 +398,7 @@ heaterstirrer.dalong:
|
||||
- pose
|
||||
- config
|
||||
- data
|
||||
title: Resource
|
||||
title: vessel
|
||||
type: object
|
||||
required:
|
||||
- vessel
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1262,6 +1262,11 @@ workstation:
|
||||
data_type: resource
|
||||
handler_key: solvent
|
||||
label: Solvent
|
||||
- data_key: reagent
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: reagent
|
||||
label: Reagent
|
||||
output:
|
||||
- data_key: vessel
|
||||
data_source: executor
|
||||
@@ -1798,13 +1803,18 @@ workstation:
|
||||
- data_key: vessel
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: vessel
|
||||
handler_key: Vessel
|
||||
label: Evaporation Vessel
|
||||
- data_key: solvent
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: solvent
|
||||
label: Eluting Solvent
|
||||
output:
|
||||
- data_key: vessel
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: vessel_out
|
||||
handler_key: VesselOut
|
||||
label: Evaporation Vessel
|
||||
placeholder_keys:
|
||||
vessel: unilabos_nodes
|
||||
@@ -2031,7 +2041,7 @@ workstation:
|
||||
- data_key: filtrate_vessel
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: filtrate_vessel
|
||||
handler_key: FiltrateVessel
|
||||
label: Filtrate Vessel
|
||||
output:
|
||||
- data_key: vessel
|
||||
@@ -2042,7 +2052,7 @@ workstation:
|
||||
- data_key: filtrate_vessel
|
||||
data_source: executor
|
||||
data_type: resource
|
||||
handler_key: filtrate_out
|
||||
handler_key: FiltrateOut
|
||||
label: Filtrate Vessel
|
||||
placeholder_keys:
|
||||
filtrate_vessel: unilabos_resources
|
||||
@@ -6131,3 +6141,109 @@ workstation:
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
workstation.example:
|
||||
category:
|
||||
- work_station
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-append_resource:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: append_resource参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-create_resource:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
bind_location: null
|
||||
bind_parent_id: null
|
||||
liquid_input_slot: null
|
||||
liquid_type: null
|
||||
liquid_volume: null
|
||||
resource_tracker: null
|
||||
resources: null
|
||||
slot_on_deck: null
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
bind_location:
|
||||
type: object
|
||||
bind_parent_id:
|
||||
type: string
|
||||
liquid_input_slot:
|
||||
type: array
|
||||
liquid_type:
|
||||
type: array
|
||||
liquid_volume:
|
||||
type: array
|
||||
resource_tracker:
|
||||
type: string
|
||||
resources:
|
||||
type: array
|
||||
slot_on_deck:
|
||||
type: integer
|
||||
required:
|
||||
- resource_tracker
|
||||
- resources
|
||||
- bind_parent_id
|
||||
- bind_location
|
||||
- liquid_input_slot
|
||||
- liquid_type
|
||||
- liquid_volume
|
||||
- slot_on_deck
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: create_resource参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.ros.nodes.presets.workstation:WorkStationExample
|
||||
status_types: {}
|
||||
type: ros2
|
||||
config_info: []
|
||||
description: ''
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
children:
|
||||
type: object
|
||||
device_id:
|
||||
type: string
|
||||
protocol_type:
|
||||
type: string
|
||||
resource_tracker:
|
||||
type: string
|
||||
required:
|
||||
- device_id
|
||||
- children
|
||||
- protocol_type
|
||||
- resource_tracker
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
version: 1.0.0
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
zhida_hplc:
|
||||
category:
|
||||
- zhida_hplc
|
||||
class:
|
||||
action_value_mappings:
|
||||
abort:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
auto-close:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接,释放网络资源。该函数确保连接的正确关闭,避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: close参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-connect:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接,配置通信超时参数。该函数是设备使用前的必要步骤,建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: connect参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
get_methods:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
title: EmptyIn_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: EmptyIn_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: EmptyIn
|
||||
type: object
|
||||
type: EmptyIn
|
||||
start:
|
||||
feedback: {}
|
||||
goal:
|
||||
string: string
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: []
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: StrSingleInput_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
string:
|
||||
type: string
|
||||
required:
|
||||
- string
|
||||
title: StrSingleInput_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: StrSingleInput_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
module: unilabos.devices.zhida_hplc.zhida:ZhidaClient
|
||||
status_types:
|
||||
methods: dict
|
||||
status: dict
|
||||
type: python
|
||||
config_info: []
|
||||
description: 智达高效液相色谱(HPLC)分析设备,用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信,支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能,可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
host:
|
||||
default: 192.168.1.47
|
||||
type: string
|
||||
port:
|
||||
default: 5792
|
||||
type: string
|
||||
timeout:
|
||||
default: 10.0
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
methods:
|
||||
type: object
|
||||
status:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- methods
|
||||
type: object
|
||||
version: 1.0.0
|
||||
@@ -53,7 +53,7 @@ class Registry:
|
||||
# 其他状态变量
|
||||
# self.is_host_mode = False # 移至BasicConfig中
|
||||
|
||||
def setup(self, complete_registry=False):
|
||||
def setup(self, complete_registry=False, upload_registry=False):
|
||||
# 检查是否已调用过setup
|
||||
if self._setup_called:
|
||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||
@@ -152,22 +152,22 @@ class Registry:
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.debug(f"[UniLab Registry] ----------Setup----------")
|
||||
logger.trace(f"[UniLab Registry] ----------Setup----------")
|
||||
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
||||
for i, path in enumerate(self.registry_paths):
|
||||
sys_path = path.parent
|
||||
logger.debug(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
|
||||
logger.trace(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
|
||||
sys.path.append(str(sys_path))
|
||||
self.load_device_types(path, complete_registry)
|
||||
if BasicConfig.enable_resource_load:
|
||||
self.load_resource_types(path, complete_registry)
|
||||
self.load_resource_types(path, complete_registry, upload_registry)
|
||||
else:
|
||||
logger.warning("跳过了资源注册表加载!")
|
||||
logger.info("[UniLab Registry] 注册表设置完成")
|
||||
# 标记setup已被调用
|
||||
self._setup_called = True
|
||||
|
||||
def load_resource_types(self, path: os.PathLike, complete_registry: bool):
|
||||
def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool):
|
||||
abs_path = Path(path).absolute()
|
||||
resource_path = abs_path / "resources"
|
||||
files = list(resource_path.glob("*/*.yaml"))
|
||||
@@ -194,7 +194,12 @@ class Registry:
|
||||
resource_info["handles"] = []
|
||||
if "init_param_schema" not in resource_info:
|
||||
resource_info["init_param_schema"] = {}
|
||||
if complete_registry:
|
||||
if "config_info" in resource_info:
|
||||
del resource_info["config_info"]
|
||||
if "file_path" in resource_info:
|
||||
del resource_info["file_path"]
|
||||
complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items())))
|
||||
if upload_registry:
|
||||
class_info = resource_info.get("class", {})
|
||||
if len(class_info) and "module" in class_info:
|
||||
if class_info.get("type") == "pylabrobot":
|
||||
@@ -205,7 +210,6 @@ class Registry:
|
||||
res_instance = res_class(res_class.__name__)
|
||||
res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)])
|
||||
resource_info["config_info"] = res_ulr
|
||||
complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items()))) # 稍后dump到文件
|
||||
resource_info["registry_type"] = "resource"
|
||||
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
complete_data = dict(sorted(complete_data.items()))
|
||||
@@ -215,7 +219,7 @@ class Registry:
|
||||
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
|
||||
|
||||
self.resource_type_registry.update(data)
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
|
||||
+ f"Add {list(data.keys())}"
|
||||
)
|
||||
@@ -402,7 +406,7 @@ class Registry:
|
||||
devices_path = abs_path / "devices"
|
||||
device_comms_path = abs_path / "device_comms"
|
||||
files = list(devices_path.glob("*.yaml")) + list(device_comms_path.glob("*.yaml"))
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"[UniLab Registry] devices: {devices_path.exists()}, device_comms: {device_comms_path.exists()}, "
|
||||
+ f"total: {len(files)}"
|
||||
)
|
||||
@@ -447,6 +451,8 @@ class Registry:
|
||||
if complete_registry:
|
||||
device_config["class"]["status_types"].clear()
|
||||
enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True)
|
||||
if not enhanced_info.get("dynamic_import_success", False):
|
||||
continue
|
||||
device_config["class"]["status_types"].update(
|
||||
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()}
|
||||
)
|
||||
@@ -565,7 +571,7 @@ class Registry:
|
||||
}
|
||||
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||
device_config["registry_type"] = "device"
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
|
||||
+ f"[{data[device_id].get('name', '未命名设备')}]"
|
||||
)
|
||||
@@ -627,7 +633,7 @@ class Registry:
|
||||
lab_registry = Registry()
|
||||
|
||||
|
||||
def build_registry(registry_paths=None, complete_registry=False):
|
||||
def build_registry(registry_paths=None, complete_registry=False, upload_registry=False):
|
||||
"""
|
||||
构建或获取Registry单例实例
|
||||
|
||||
@@ -651,6 +657,6 @@ def build_registry(registry_paths=None, complete_registry=False):
|
||||
lab_registry.registry_paths.append(path)
|
||||
|
||||
# 初始化注册表
|
||||
lab_registry.setup(complete_registry)
|
||||
lab_registry.setup(complete_registry, upload_registry)
|
||||
|
||||
return lab_registry
|
||||
|
||||
@@ -4,9 +4,7 @@ hplc_plate:
|
||||
class:
|
||||
module: unilabos.devices.resource_container.container:PlateContainer
|
||||
type: python
|
||||
config_info: []
|
||||
description: HPLC板
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/common/resource_container.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
@@ -28,9 +26,7 @@ plate_96_high:
|
||||
class:
|
||||
module: unilabos.devices.resource_container.container:PlateContainer
|
||||
type: python
|
||||
config_info: []
|
||||
description: 96孔板
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/common/resource_container.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
@@ -52,9 +48,7 @@ tiprack_96_high:
|
||||
class:
|
||||
module: unilabos.devices.resource_container.container:TipRackContainer
|
||||
type: python
|
||||
config_info: []
|
||||
description: 96孔板
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/common/resource_container.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
@@ -4,9 +4,7 @@ OTDeck:
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.deck:OTDeck
|
||||
type: pylabrobot
|
||||
config_info: []
|
||||
description: Opentrons deck
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/opentrons/deck.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
@@ -21,9 +19,7 @@ hplc_station:
|
||||
class:
|
||||
module: unilabos.devices.resource_container.container:DeckContainer
|
||||
type: python
|
||||
config_info: []
|
||||
description: hplc_station deck
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/opentrons/deck.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
@@ -4,34 +4,7 @@ Opentrons_96_adapter_Vb:
|
||||
class:
|
||||
module: pylabrobot.resources.opentrons.plate_adapters:Opentrons_96_adapter_Vb
|
||||
type: pylabrobot
|
||||
config_info:
|
||||
- children: []
|
||||
class: ''
|
||||
config:
|
||||
barcode: null
|
||||
category: plate_adapter
|
||||
model: Opentrons_96_adapter_Vb
|
||||
rotation:
|
||||
type: Rotation
|
||||
x: 0
|
||||
y: 0
|
||||
z: 0
|
||||
size_x: 127.76
|
||||
size_y: 85.48
|
||||
size_z: 18.55
|
||||
type: PlateAdapter
|
||||
data: {}
|
||||
id: Opentrons_96_adapter_Vb
|
||||
name: Opentrons_96_adapter_Vb
|
||||
parent: null
|
||||
position:
|
||||
x: 0
|
||||
y: 0
|
||||
z: 0
|
||||
sample_id: null
|
||||
type: container
|
||||
description: Opentrons 96 adapter Vb
|
||||
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/opentrons/plate_adapters.yaml
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user