mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-26 09:33:06 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
266366cc25 | ||
|
|
121c3985cc | ||
|
|
6ca5c72fc6 | ||
|
|
bc8c49ddda | ||
|
|
28f93737ac | ||
|
|
5dc81ec9be | ||
|
|
13a6795657 | ||
|
|
53219d8b04 |
@@ -1,62 +0,0 @@
|
|||||||
# unilabos: Production package (depends on unilabos-env + pip unilabos)
|
|
||||||
# For production deployment
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: unilabos
|
|
||||||
version: 0.10.19
|
|
||||||
|
|
||||||
source:
|
|
||||||
path: ../../unilabos
|
|
||||||
target_directory: unilabos
|
|
||||||
|
|
||||||
build:
|
|
||||||
python:
|
|
||||||
entry_points:
|
|
||||||
- unilab = unilabos.app.main:main
|
|
||||||
script:
|
|
||||||
- set PIP_NO_INDEX=
|
|
||||||
- if: win
|
|
||||||
then:
|
|
||||||
- copy %RECIPE_DIR%\..\..\MANIFEST.in %SRC_DIR%
|
|
||||||
- copy %RECIPE_DIR%\..\..\setup.cfg %SRC_DIR%
|
|
||||||
- copy %RECIPE_DIR%\..\..\setup.py %SRC_DIR%
|
|
||||||
- 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
|
|
||||||
- pip install $SRC_DIR
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
host:
|
|
||||||
- python ==3.11.14
|
|
||||||
- pip
|
|
||||||
- setuptools
|
|
||||||
- zstd
|
|
||||||
- zstandard
|
|
||||||
run:
|
|
||||||
- zstd
|
|
||||||
- zstandard
|
|
||||||
- networkx
|
|
||||||
- typing_extensions
|
|
||||||
- websockets
|
|
||||||
- pint
|
|
||||||
- fastapi
|
|
||||||
- jinja2
|
|
||||||
- requests
|
|
||||||
- uvicorn
|
|
||||||
- if: not osx
|
|
||||||
then:
|
|
||||||
- opcua
|
|
||||||
- pyserial
|
|
||||||
- pandas
|
|
||||||
- pymodbus
|
|
||||||
- matplotlib
|
|
||||||
- pylibftdi
|
|
||||||
- uni-lab::unilabos-env ==0.10.19
|
|
||||||
|
|
||||||
about:
|
|
||||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
|
||||||
license: GPL-3.0-only
|
|
||||||
description: "UniLabOS - Production package with minimal ROS2 dependencies"
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# unilabos-env: conda environment dependencies (ROS2 + conda packages)
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: unilabos-env
|
|
||||||
version: 0.10.17
|
|
||||||
|
|
||||||
build:
|
|
||||||
noarch: generic
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
run:
|
|
||||||
# Python
|
|
||||||
- zstd
|
|
||||||
- zstandard
|
|
||||||
- conda-forge::python ==3.11.14
|
|
||||||
- conda-forge::opencv
|
|
||||||
# ROS2 dependencies (from ci-check.yml)
|
|
||||||
- robostack-staging::ros-humble-ros-core
|
|
||||||
- robostack-staging::ros-humble-action-msgs
|
|
||||||
- robostack-staging::ros-humble-std-msgs
|
|
||||||
- robostack-staging::ros-humble-geometry-msgs
|
|
||||||
- robostack-staging::ros-humble-control-msgs
|
|
||||||
- robostack-staging::ros-humble-nav2-msgs
|
|
||||||
- robostack-staging::ros-humble-cv-bridge
|
|
||||||
- robostack-staging::ros-humble-vision-opencv
|
|
||||||
- robostack-staging::ros-humble-tf-transformations
|
|
||||||
- robostack-staging::ros-humble-moveit-msgs
|
|
||||||
- robostack-staging::ros-humble-tf2-ros
|
|
||||||
- robostack-staging::ros-humble-tf2-ros-py
|
|
||||||
- conda-forge::transforms3d
|
|
||||||
- conda-forge::uv
|
|
||||||
|
|
||||||
# UniLabOS custom messages
|
|
||||||
- uni-lab::ros-humble-unilabos-msgs
|
|
||||||
|
|
||||||
about:
|
|
||||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
|
||||||
license: GPL-3.0-only
|
|
||||||
description: "UniLabOS Environment - ROS2 and conda dependencies"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# unilabos-full: Full package with all features
|
|
||||||
# Depends on unilabos + complete ROS2 desktop + dev tools
|
|
||||||
|
|
||||||
package:
|
|
||||||
name: unilabos-full
|
|
||||||
version: 0.10.19
|
|
||||||
|
|
||||||
build:
|
|
||||||
noarch: generic
|
|
||||||
|
|
||||||
requirements:
|
|
||||||
run:
|
|
||||||
# Base unilabos package (includes unilabos-env)
|
|
||||||
- uni-lab::unilabos ==0.10.19
|
|
||||||
# Documentation tools
|
|
||||||
- sphinx
|
|
||||||
- sphinx_rtd_theme
|
|
||||||
# Web UI
|
|
||||||
- gradio
|
|
||||||
- flask
|
|
||||||
# Interactive development
|
|
||||||
- ipython
|
|
||||||
- jupyter
|
|
||||||
- jupyros
|
|
||||||
- colcon-common-extensions
|
|
||||||
# ROS2 full desktop (includes rviz2, gazebo, etc.)
|
|
||||||
- robostack-staging::ros-humble-desktop-full
|
|
||||||
# Navigation and motion control
|
|
||||||
- ros-humble-navigation2
|
|
||||||
- ros-humble-ros2-control
|
|
||||||
- ros-humble-robot-state-publisher
|
|
||||||
- ros-humble-joint-state-publisher
|
|
||||||
# MoveIt motion planning
|
|
||||||
- ros-humble-moveit
|
|
||||||
- ros-humble-moveit-servo
|
|
||||||
# Simulation
|
|
||||||
- ros-humble-simulation
|
|
||||||
|
|
||||||
about:
|
|
||||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
|
||||||
license: GPL-3.0-only
|
|
||||||
description: "UniLabOS Full - Complete package with ROS2 Desktop, MoveIt, Navigation2, Gazebo, Jupyter"
|
|
||||||
91
.conda/recipe.yaml
Normal file
91
.conda/recipe.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package:
|
||||||
|
name: unilabos
|
||||||
|
version: 0.10.14
|
||||||
|
|
||||||
|
source:
|
||||||
|
path: ../unilabos
|
||||||
|
target_directory: unilabos
|
||||||
|
|
||||||
|
build:
|
||||||
|
python:
|
||||||
|
entry_points:
|
||||||
|
- unilab = unilabos.app.main:main
|
||||||
|
script:
|
||||||
|
- set PIP_NO_INDEX=
|
||||||
|
- if: win
|
||||||
|
then:
|
||||||
|
- copy %RECIPE_DIR%\..\MANIFEST.in %SRC_DIR%
|
||||||
|
- copy %RECIPE_DIR%\..\setup.cfg %SRC_DIR%
|
||||||
|
- copy %RECIPE_DIR%\..\setup.py %SRC_DIR%
|
||||||
|
- call %PYTHON% -m pip install %SRC_DIR%
|
||||||
|
- if: unix
|
||||||
|
then:
|
||||||
|
- cp $RECIPE_DIR/../MANIFEST.in $SRC_DIR
|
||||||
|
- cp $RECIPE_DIR/../setup.cfg $SRC_DIR
|
||||||
|
- cp $RECIPE_DIR/../setup.py $SRC_DIR
|
||||||
|
- $PYTHON -m pip install $SRC_DIR
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
host:
|
||||||
|
- python ==3.11.11
|
||||||
|
- pip
|
||||||
|
- setuptools
|
||||||
|
- zstd
|
||||||
|
- zstandard
|
||||||
|
run:
|
||||||
|
- conda-forge::python ==3.11.11
|
||||||
|
- compilers
|
||||||
|
- cmake
|
||||||
|
- zstd
|
||||||
|
- zstandard
|
||||||
|
- ninja
|
||||||
|
- if: unix
|
||||||
|
then:
|
||||||
|
- make
|
||||||
|
- sphinx
|
||||||
|
- sphinx_rtd_theme
|
||||||
|
- numpy
|
||||||
|
- scipy
|
||||||
|
- pandas
|
||||||
|
- networkx
|
||||||
|
- matplotlib
|
||||||
|
- pint
|
||||||
|
- pyserial
|
||||||
|
- pyusb
|
||||||
|
- pylibftdi
|
||||||
|
- pymodbus
|
||||||
|
- python-can
|
||||||
|
- pyvisa
|
||||||
|
- opencv
|
||||||
|
- pydantic
|
||||||
|
- fastapi
|
||||||
|
- uvicorn
|
||||||
|
- gradio
|
||||||
|
- flask
|
||||||
|
- websockets
|
||||||
|
- ipython
|
||||||
|
- jupyter
|
||||||
|
- jupyros
|
||||||
|
- colcon-common-extensions
|
||||||
|
- robostack-staging::ros-humble-desktop-full
|
||||||
|
- robostack-staging::ros-humble-control-msgs
|
||||||
|
- robostack-staging::ros-humble-sensor-msgs
|
||||||
|
- robostack-staging::ros-humble-trajectory-msgs
|
||||||
|
- ros-humble-navigation2
|
||||||
|
- ros-humble-ros2-control
|
||||||
|
- ros-humble-robot-state-publisher
|
||||||
|
- ros-humble-joint-state-publisher
|
||||||
|
- ros-humble-rosbridge-server
|
||||||
|
- ros-humble-cv-bridge
|
||||||
|
- ros-humble-tf2
|
||||||
|
- ros-humble-moveit
|
||||||
|
- ros-humble-moveit-servo
|
||||||
|
- ros-humble-simulation
|
||||||
|
- ros-humble-tf-transformations
|
||||||
|
- transforms3d
|
||||||
|
- uni-lab::ros-humble-unilabos-msgs
|
||||||
|
|
||||||
|
about:
|
||||||
|
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||||
|
license: GPL-3.0-only
|
||||||
|
description: "Uni-Lab-OS"
|
||||||
67
.github/workflows/ci-check.yml
vendored
67
.github/workflows/ci-check.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: CI Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, dev]
|
|
||||||
pull_request:
|
|
||||||
branches: [main, dev]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
registry-check:
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Fix Unicode encoding issue on Windows runner (cp1252 -> utf-8)
|
|
||||||
PYTHONIOENCODING: utf-8
|
|
||||||
PYTHONUTF8: 1
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: cmd
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Miniforge
|
|
||||||
uses: conda-incubator/setup-miniconda@v3
|
|
||||||
with:
|
|
||||||
miniforge-version: latest
|
|
||||||
use-mamba: true
|
|
||||||
channels: robostack-staging,conda-forge,uni-lab
|
|
||||||
channel-priority: flexible
|
|
||||||
activate-environment: check-env
|
|
||||||
auto-update-conda: false
|
|
||||||
show-channel-urls: true
|
|
||||||
|
|
||||||
- name: Install ROS dependencies, uv and unilabos-msgs
|
|
||||||
run: |
|
|
||||||
echo Installing ROS dependencies...
|
|
||||||
mamba install -n check-env conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -c robostack-staging -c conda-forge -c uni-lab -y
|
|
||||||
|
|
||||||
- name: Install pip dependencies and unilabos
|
|
||||||
run: |
|
|
||||||
call conda activate check-env
|
|
||||||
echo Installing pip dependencies...
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git
|
|
||||||
uv pip uninstall enum34 || echo enum34 not installed, skipping
|
|
||||||
uv pip install .
|
|
||||||
|
|
||||||
- name: Run check mode (AST registry validation)
|
|
||||||
run: |
|
|
||||||
call conda activate check-env
|
|
||||||
echo Running check mode...
|
|
||||||
python -m unilabos --check_mode --skip_env_check
|
|
||||||
|
|
||||||
- name: Check for uncommitted changes
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if ! git diff --exit-code; then
|
|
||||||
echo "::error::检测到文件变化!请先在本地运行 'python -m unilabos --complete_registry' 并提交变更"
|
|
||||||
echo "变化的文件:"
|
|
||||||
git diff --name-only
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "检查通过:无文件变化"
|
|
||||||
45
.github/workflows/conda-pack-build.yml
vendored
45
.github/workflows/conda-pack-build.yml
vendored
@@ -13,11 +13,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: 'win-64'
|
default: 'win-64'
|
||||||
type: string
|
type: string
|
||||||
build_full:
|
|
||||||
description: '是否构建完整版 unilabos-full (默认构建轻量版 unilabos)'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-conda-pack:
|
build-conda-pack:
|
||||||
@@ -29,7 +24,7 @@ jobs:
|
|||||||
platform: linux-64
|
platform: linux-64
|
||||||
env_file: unilabos-linux-64.yaml
|
env_file: unilabos-linux-64.yaml
|
||||||
script_ext: sh
|
script_ext: sh
|
||||||
- os: macos-15 # Intel (via Rosetta)
|
- os: macos-13 # Intel
|
||||||
platform: osx-64
|
platform: osx-64
|
||||||
env_file: unilabos-osx-64.yaml
|
env_file: unilabos-osx-64.yaml
|
||||||
script_ext: sh
|
script_ext: sh
|
||||||
@@ -62,7 +57,7 @@ jobs:
|
|||||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.branch }}
|
ref: ${{ github.event.inputs.branch }}
|
||||||
@@ -74,7 +69,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
miniforge-version: latest
|
miniforge-version: latest
|
||||||
use-mamba: true
|
use-mamba: true
|
||||||
python-version: '3.11.14'
|
python-version: '3.11.11'
|
||||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||||
channel-priority: flexible
|
channel-priority: flexible
|
||||||
activate-environment: unilab
|
activate-environment: unilab
|
||||||
@@ -86,14 +81,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo Installing unilabos and dependencies to unilab environment...
|
echo Installing unilabos and dependencies to unilab environment...
|
||||||
echo Using mamba for faster and more reliable dependency resolution...
|
echo Using mamba for faster and more reliable dependency resolution...
|
||||||
echo Build full: ${{ github.event.inputs.build_full }}
|
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||||
if "${{ github.event.inputs.build_full }}"=="true" (
|
|
||||||
echo Installing unilabos-full ^(complete package^)...
|
|
||||||
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
|
||||||
) else (
|
|
||||||
echo Installing unilabos ^(minimal package^)...
|
|
||||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
|
||||||
)
|
|
||||||
|
|
||||||
- name: Install conda-pack, unilabos and dependencies (Unix)
|
- name: Install conda-pack, unilabos and dependencies (Unix)
|
||||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||||
@@ -101,14 +89,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Installing unilabos and dependencies to unilab environment..."
|
echo "Installing unilabos and dependencies to unilab environment..."
|
||||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||||
echo "Build full: ${{ github.event.inputs.build_full }}"
|
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
|
||||||
echo "Installing unilabos-full (complete package)..."
|
|
||||||
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
|
||||||
else
|
|
||||||
echo "Installing unilabos (minimal package)..."
|
|
||||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Get latest ros-humble-unilabos-msgs version (Windows)
|
- name: Get latest ros-humble-unilabos-msgs version (Windows)
|
||||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||||
@@ -312,7 +293,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload distribution package
|
- name: Upload distribution package
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||||
path: dist-package/
|
path: dist-package/
|
||||||
@@ -327,12 +308,7 @@ jobs:
|
|||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo Platform: ${{ matrix.platform }}
|
echo Platform: ${{ matrix.platform }}
|
||||||
echo Branch: ${{ github.event.inputs.branch }}
|
echo Branch: ${{ github.event.inputs.branch }}
|
||||||
echo Python version: 3.11.14
|
echo Python version: 3.11.11
|
||||||
if "${{ github.event.inputs.build_full }}"=="true" (
|
|
||||||
echo Package: unilabos-full ^(complete^)
|
|
||||||
) else (
|
|
||||||
echo Package: unilabos ^(minimal^)
|
|
||||||
)
|
|
||||||
echo.
|
echo.
|
||||||
echo Distribution package contents:
|
echo Distribution package contents:
|
||||||
dir dist-package
|
dir dist-package
|
||||||
@@ -352,12 +328,7 @@ jobs:
|
|||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "Platform: ${{ matrix.platform }}"
|
echo "Platform: ${{ matrix.platform }}"
|
||||||
echo "Branch: ${{ github.event.inputs.branch }}"
|
echo "Branch: ${{ github.event.inputs.branch }}"
|
||||||
echo "Python version: 3.11.14"
|
echo "Python version: 3.11.11"
|
||||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
|
||||||
echo "Package: unilabos-full (complete)"
|
|
||||||
else
|
|
||||||
echo "Package: unilabos (minimal)"
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Distribution package contents:"
|
echo "Distribution package contents:"
|
||||||
ls -lh dist-package/
|
ls -lh dist-package/
|
||||||
|
|||||||
37
.github/workflows/deploy-docs.yml
vendored
37
.github/workflows/deploy-docs.yml
vendored
@@ -1,12 +1,10 @@
|
|||||||
name: Deploy Docs
|
name: Deploy Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# 在 CI Check 成功后自动触发(仅 main 分支)
|
push:
|
||||||
workflow_run:
|
branches: [main]
|
||||||
workflows: ["CI Check"]
|
pull_request:
|
||||||
types: [completed]
|
|
||||||
branches: [main]
|
branches: [main]
|
||||||
# 手动触发
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
branch:
|
branch:
|
||||||
@@ -35,19 +33,12 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
# Build documentation
|
# Build documentation
|
||||||
build:
|
build:
|
||||||
# 只在以下情况运行:
|
|
||||||
# 1. workflow_run 触发且 CI Check 成功
|
|
||||||
# 2. 手动触发
|
|
||||||
if: |
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# workflow_run 时使用触发工作流的分支,手动触发时使用输入的分支
|
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||||
ref: ${{ github.event.workflow_run.head_branch || github.event.inputs.branch || github.ref }}
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Miniforge (with mamba)
|
- name: Setup Miniforge (with mamba)
|
||||||
@@ -55,7 +46,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
miniforge-version: latest
|
miniforge-version: latest
|
||||||
use-mamba: true
|
use-mamba: true
|
||||||
python-version: '3.11.14'
|
python-version: '3.11.11'
|
||||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||||
channel-priority: flexible
|
channel-priority: flexible
|
||||||
activate-environment: unilab
|
activate-environment: unilab
|
||||||
@@ -84,10 +75,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
id: pages
|
id: pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v4
|
||||||
if: |
|
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||||
github.event.workflow_run.head_branch == 'main' ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
|
||||||
|
|
||||||
- name: Build Sphinx documentation
|
- name: Build Sphinx documentation
|
||||||
run: |
|
run: |
|
||||||
@@ -105,18 +94,14 @@ jobs:
|
|||||||
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
|
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-pages-artifact@v4
|
uses: actions/upload-pages-artifact@v3
|
||||||
if: |
|
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||||
github.event.workflow_run.head_branch == 'main' ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
|
||||||
with:
|
with:
|
||||||
path: docs/_build/html
|
path: docs/_build/html
|
||||||
|
|
||||||
# Deploy to GitHub Pages
|
# Deploy to GitHub Pages
|
||||||
deploy:
|
deploy:
|
||||||
if: |
|
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||||
github.event.workflow_run.head_branch == 'main' ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|||||||
48
.github/workflows/multi-platform-build.yml
vendored
48
.github/workflows/multi-platform-build.yml
vendored
@@ -1,16 +1,11 @@
|
|||||||
name: Multi-Platform Conda Build
|
name: Multi-Platform Conda Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# 在 CI Check 工作流完成后触发(仅限 main/dev 分支)
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["CI Check"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
branches: [main, dev]
|
|
||||||
# 支持 tag 推送(不依赖 CI Check)
|
|
||||||
push:
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
# 手动触发
|
pull_request:
|
||||||
|
branches: [main, dev]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
platforms:
|
platforms:
|
||||||
@@ -22,37 +17,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
skip_ci_check:
|
|
||||||
description: '跳过等待 CI Check (手动触发时可选)'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# 等待 CI Check 完成的 job (仅用于 workflow_run 触发)
|
|
||||||
wait-for-ci:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'workflow_run'
|
|
||||||
outputs:
|
|
||||||
should_continue: ${{ steps.check.outputs.should_continue }}
|
|
||||||
steps:
|
|
||||||
- name: Check CI status
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
|
||||||
echo "should_continue=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "CI Check passed, proceeding with build"
|
|
||||||
else
|
|
||||||
echo "should_continue=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build"
|
|
||||||
fi
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [wait-for-ci]
|
|
||||||
# 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式
|
|
||||||
if: |
|
|
||||||
always() &&
|
|
||||||
(needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true')
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -60,7 +27,7 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
platform: linux-64
|
platform: linux-64
|
||||||
env_file: unilabos-linux-64.yaml
|
env_file: unilabos-linux-64.yaml
|
||||||
- os: macos-15 # Intel (via Rosetta)
|
- os: macos-13 # Intel
|
||||||
platform: osx-64
|
platform: osx-64
|
||||||
env_file: unilabos-osx-64.yaml
|
env_file: unilabos-osx-64.yaml
|
||||||
- os: macos-latest # ARM64
|
- os: macos-latest # ARM64
|
||||||
@@ -77,10 +44,8 @@ jobs:
|
|||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
|
|
||||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Check if platform should be built
|
- name: Check if platform should be built
|
||||||
@@ -104,6 +69,7 @@ jobs:
|
|||||||
channels: conda-forge,robostack-staging,defaults
|
channels: conda-forge,robostack-staging,defaults
|
||||||
channel-priority: strict
|
channel-priority: strict
|
||||||
activate-environment: build-env
|
activate-environment: build-env
|
||||||
|
auto-activate-base: false
|
||||||
auto-update-conda: false
|
auto-update-conda: false
|
||||||
show-channel-urls: true
|
show-channel-urls: true
|
||||||
|
|
||||||
@@ -149,7 +115,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload conda package artifacts
|
- name: Upload conda package artifacts
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: conda-package-${{ matrix.platform }}
|
name: conda-package-${{ matrix.platform }}
|
||||||
path: conda-packages-temp
|
path: conda-packages-temp
|
||||||
|
|||||||
115
.github/workflows/unilabos-conda-build.yml
vendored
115
.github/workflows/unilabos-conda-build.yml
vendored
@@ -1,69 +1,32 @@
|
|||||||
name: UniLabOS Conda Build
|
name: UniLabOS Conda Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# 在 CI Check 成功后自动触发
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["CI Check"]
|
|
||||||
types: [completed]
|
|
||||||
branches: [main, dev]
|
|
||||||
# 标签推送时直接触发(发布版本)
|
|
||||||
push:
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
# 手动触发
|
pull_request:
|
||||||
|
branches: [main, dev]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
platforms:
|
platforms:
|
||||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||||
required: false
|
required: false
|
||||||
default: 'linux-64'
|
default: 'linux-64'
|
||||||
build_full:
|
|
||||||
description: '是否构建 unilabos-full 完整包 (默认只构建 unilabos 基础包)'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
upload_to_anaconda:
|
upload_to_anaconda:
|
||||||
description: '是否上传到Anaconda.org'
|
description: '是否上传到Anaconda.org'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
skip_ci_check:
|
|
||||||
description: '跳过等待 CI Check (手动触发时可选)'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# 等待 CI Check 完成的 job (仅用于 workflow_run 触发)
|
|
||||||
wait-for-ci:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'workflow_run'
|
|
||||||
outputs:
|
|
||||||
should_continue: ${{ steps.check.outputs.should_continue }}
|
|
||||||
steps:
|
|
||||||
- name: Check CI status
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
|
||||||
echo "should_continue=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "CI Check passed, proceeding with build"
|
|
||||||
else
|
|
||||||
echo "should_continue=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build"
|
|
||||||
fi
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [wait-for-ci]
|
|
||||||
# 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式
|
|
||||||
if: |
|
|
||||||
always() &&
|
|
||||||
(needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true')
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
platform: linux-64
|
platform: linux-64
|
||||||
- os: macos-15 # Intel (via Rosetta)
|
- os: macos-13 # Intel
|
||||||
platform: osx-64
|
platform: osx-64
|
||||||
- os: macos-latest # ARM64
|
- os: macos-latest # ARM64
|
||||||
platform: osx-arm64
|
platform: osx-arm64
|
||||||
@@ -77,10 +40,8 @@ jobs:
|
|||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
|
|
||||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Check if platform should be built
|
- name: Check if platform should be built
|
||||||
@@ -104,6 +65,7 @@ jobs:
|
|||||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||||
channel-priority: strict
|
channel-priority: strict
|
||||||
activate-environment: build-env
|
activate-environment: build-env
|
||||||
|
auto-activate-base: false
|
||||||
auto-update-conda: false
|
auto-update-conda: false
|
||||||
show-channel-urls: true
|
show-channel-urls: true
|
||||||
|
|
||||||
@@ -119,61 +81,12 @@ jobs:
|
|||||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||||
echo "Platform: ${{ matrix.platform }}"
|
echo "Platform: ${{ matrix.platform }}"
|
||||||
echo "OS: ${{ matrix.os }}"
|
echo "OS: ${{ matrix.os }}"
|
||||||
echo "Build full package: ${{ github.event.inputs.build_full || 'false' }}"
|
echo "Building UniLabOS package"
|
||||||
echo "Building packages:"
|
|
||||||
echo " - unilabos-env (environment dependencies)"
|
|
||||||
echo " - unilabos (with pip package)"
|
|
||||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
|
||||||
echo " - unilabos-full (complete package)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build unilabos-env (conda environment only, noarch)
|
- name: Build conda package
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
run: |
|
run: |
|
||||||
echo "Building unilabos-env (conda environment dependencies)..."
|
rattler-build build -r .conda/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||||
rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
- name: Upload unilabos-env to Anaconda.org (if enabled)
|
|
||||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
|
||||||
run: |
|
|
||||||
echo "Uploading unilabos-env to uni-lab organization..."
|
|
||||||
for package in $(find ./output -name "unilabos-env*.conda"); do
|
|
||||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Build unilabos (with pip package)
|
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
|
||||||
run: |
|
|
||||||
echo "Building unilabos package..."
|
|
||||||
# 如果已上传到 Anaconda,从 uni-lab channel 获取 unilabos-env;否则从本地 output 获取
|
|
||||||
rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
|
||||||
|
|
||||||
- name: Upload unilabos to Anaconda.org (if enabled)
|
|
||||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
|
||||||
run: |
|
|
||||||
echo "Uploading unilabos to uni-lab organization..."
|
|
||||||
for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do
|
|
||||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Build unilabos-full - Only when explicitly requested
|
|
||||||
if: |
|
|
||||||
steps.should_build.outputs.should_build == 'true' &&
|
|
||||||
github.event.inputs.build_full == 'true'
|
|
||||||
run: |
|
|
||||||
echo "Building unilabos-full package on ${{ matrix.platform }}..."
|
|
||||||
rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
|
||||||
|
|
||||||
- name: Upload unilabos-full to Anaconda.org (if enabled)
|
|
||||||
if: |
|
|
||||||
steps.should_build.outputs.should_build == 'true' &&
|
|
||||||
github.event.inputs.build_full == 'true' &&
|
|
||||||
github.event.inputs.upload_to_anaconda == 'true'
|
|
||||||
run: |
|
|
||||||
echo "Uploading unilabos-full to uni-lab organization..."
|
|
||||||
for package in $(find ./output -name "unilabos-full*.conda"); do
|
|
||||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: List built packages
|
- name: List built packages
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
@@ -195,9 +108,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload conda package artifacts
|
- name: Upload conda package artifacts
|
||||||
if: steps.should_build.outputs.should_build == 'true'
|
if: steps.should_build.outputs.should_build == 'true'
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: conda-package-unilabos-${{ matrix.platform }}
|
name: conda-package-unilabos-${{ matrix.platform }}
|
||||||
path: conda-packages-temp
|
path: conda-packages-temp
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
retention-days: 30
|
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
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -4,8 +4,6 @@ temp/
|
|||||||
output/
|
output/
|
||||||
unilabos_data/
|
unilabos_data/
|
||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
.cursorignore
|
|
||||||
device_package*/
|
|
||||||
## Python
|
## Python
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
@@ -252,9 +250,3 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
|||||||
test_config.py
|
test_config.py
|
||||||
|
|
||||||
|
|
||||||
/.claude
|
|
||||||
/.conda
|
|
||||||
/.cursor
|
|
||||||
/.github
|
|
||||||
/.conda/base
|
|
||||||
.conda/base/recipe.yaml
|
|
||||||
|
|||||||
87
AGENTS.md
87
AGENTS.md
@@ -1,87 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
Also follow the monorepo-level rules in `../AGENTS.md`.
|
|
||||||
|
|
||||||
## Build & Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install in editable mode (requires mamba env with python 3.11)
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
|
|
||||||
# Run with a device graph
|
|
||||||
unilab --graph <graph.json> --config <config.py> --backend ros
|
|
||||||
unilab --graph <graph.json> --config <config.py> --backend simple # no ROS2 needed
|
|
||||||
|
|
||||||
# Common CLI flags
|
|
||||||
unilab --app_bridges websocket fastapi # communication bridges
|
|
||||||
unilab --test_mode # simulate hardware, no real execution
|
|
||||||
unilab --check_mode # CI validation of registry imports
|
|
||||||
unilab --skip_env_check # skip auto-install of dependencies
|
|
||||||
unilab --visual rviz|web|disable # visualization mode
|
|
||||||
unilab --is_slave # run as slave node
|
|
||||||
|
|
||||||
# Workflow upload subcommand
|
|
||||||
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
pytest tests/ # all tests
|
|
||||||
pytest tests/resources/test_resourcetreeset.py # single test file
|
|
||||||
pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Startup Flow
|
|
||||||
|
|
||||||
`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client.
|
|
||||||
|
|
||||||
### Core Layers
|
|
||||||
|
|
||||||
**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`.
|
|
||||||
|
|
||||||
**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict` → `ResourceDictInstance` → `ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`).
|
|
||||||
|
|
||||||
**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers.
|
|
||||||
|
|
||||||
**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases).
|
|
||||||
|
|
||||||
**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences.
|
|
||||||
|
|
||||||
**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud.
|
|
||||||
|
|
||||||
**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default.
|
|
||||||
|
|
||||||
### Configuration System
|
|
||||||
|
|
||||||
- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files
|
|
||||||
- Config files are `.py` files with matching class names (see `config/example_config.py`)
|
|
||||||
- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`)
|
|
||||||
- Device topology defined in graph files (JSON with node-link format, or GraphML)
|
|
||||||
|
|
||||||
### Key Data Flow
|
|
||||||
|
|
||||||
1. Graph file → `graphio.read_node_link_json()` → `(nx.Graph, ResourceTreeSet, resource_links)`
|
|
||||||
2. `ResourceTreeSet` + `Registry` → `initialize_device.initialize_device_from_dict()` → `ROS2DeviceNode` instances
|
|
||||||
3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend)
|
|
||||||
4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`)
|
|
||||||
|
|
||||||
### Test Data
|
|
||||||
|
|
||||||
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
|
|
||||||
|
|
||||||
## Code Conventions
|
|
||||||
|
|
||||||
- Code comments and log messages in simplified Chinese
|
|
||||||
- Python 3.11+, type hints expected
|
|
||||||
- Pydantic models for data validation (`resource_tracker.py`)
|
|
||||||
- Singleton pattern via `@singleton` decorator (`utils/decorator.py`)
|
|
||||||
- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths
|
|
||||||
- CLI argument dashes auto-converted to underscores for consistency
|
|
||||||
|
|
||||||
## Licensing
|
|
||||||
|
|
||||||
- Framework code: GPL-3.0
|
|
||||||
- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
recursive-include unilabos/test *
|
recursive-include unilabos/test *
|
||||||
recursive-include unilabos/utils *
|
|
||||||
recursive-include unilabos/registry *.yaml
|
recursive-include unilabos/registry *.yaml
|
||||||
recursive-include unilabos/app/web/static *
|
recursive-include unilabos/app/web/static *
|
||||||
recursive-include unilabos/app/web/templates *
|
recursive-include unilabos/app/web/templates *
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -31,63 +31,37 @@ Detailed documentation can be found at:
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Setup Conda Environment
|
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
|
||||||
|
|
||||||
Uni-Lab-OS recommends using `mamba` for environment management. Choose the package that fits your needs:
|
|
||||||
|
|
||||||
| Package | Use Case | Contents |
|
|
||||||
|---------|----------|----------|
|
|
||||||
| `unilabos` | **Recommended for most users** | Complete package, ready to use |
|
|
||||||
| `unilabos-env` | Developers (editable install) | Environment only, install unilabos via pip |
|
|
||||||
| `unilabos-full` | Simulation/Visualization | unilabos + ROS2 Desktop + Gazebo + MoveIt |
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create new environment
|
# Create new environment
|
||||||
mamba create -n unilab python=3.11.14
|
mamba create -n unilab python=3.11.11
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
# Option A: Standard installation (recommended for most users)
|
|
||||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
# Option B: For developers (editable mode development)
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
# Then install unilabos and dependencies:
|
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
|
|
||||||
# Option C: Full installation (simulation/visualization)
|
|
||||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**When to use which?**
|
## Install Dev Uni-Lab-OS
|
||||||
- **unilabos**: Standard installation for production deployment and general usage (recommended)
|
|
||||||
- **unilabos-env**: For developers who need `pip install -e .` editable mode, modify source code
|
|
||||||
- **unilabos-full**: For simulation (Gazebo), visualization (rviz2), and Jupyter notebooks
|
|
||||||
|
|
||||||
### 2. Clone Repository (Optional, for developers)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository (only needed for development or examples)
|
# Clone the repository
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||||
cd Uni-Lab-OS
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
|
# Install Uni-Lab-OS
|
||||||
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start Uni-Lab System
|
3. Start Uni-Lab System:
|
||||||
|
|
||||||
Please refer to [Documentation - Boot Examples](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
|
Please refer to [Documentation - Boot Examples](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
|
||||||
|
|
||||||
4. Best Practice
|
|
||||||
|
|
||||||
See [Best Practice Guide](https://deepmodeling.github.io/Uni-Lab-OS/user_guide/best_practice.html)
|
|
||||||
|
|
||||||
## Message Format
|
## Message Format
|
||||||
|
|
||||||
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) page.
|
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) page.
|
||||||
|
|
||||||
## Citation
|
## Citation
|
||||||
|
|
||||||
If you use [Uni-Lab-OS](https://arxiv.org/abs/2512.21766) in academic research, please cite:
|
If you use Uni-Lab-OS in academic research, please cite:
|
||||||
|
|
||||||
```bibtex
|
```bibtex
|
||||||
@article{gao2025unilabos,
|
@article{gao2025unilabos,
|
||||||
|
|||||||
46
README_zh.md
46
README_zh.md
@@ -31,63 +31,39 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. 配置 Conda 环境
|
1. 配置 Conda 环境
|
||||||
|
|
||||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的需求选择合适的安装包:
|
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||||
|
|
||||||
| 安装包 | 适用场景 | 包含内容 |
|
|
||||||
|--------|----------|----------|
|
|
||||||
| `unilabos` | **推荐大多数用户** | 完整安装包,开箱即用 |
|
|
||||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
|
||||||
| `unilabos-full` | 仿真/可视化 | unilabos + ROS2 桌面版 + Gazebo + MoveIt |
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建新环境
|
# 创建新环境
|
||||||
mamba create -n unilab python=3.11.14
|
mamba create -n unilab python=3.11.11
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
# 方案 A:标准安装(推荐大多数用户)
|
|
||||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
# 方案 B:开发者环境(可编辑模式开发)
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
# 然后安装 unilabos 和依赖:
|
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
|
|
||||||
# 方案 C:完整安装(仿真/可视化)
|
|
||||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**如何选择?**
|
2. 安装开发版 Uni-Lab-OS:
|
||||||
- **unilabos**:标准安装,适用于生产部署和日常使用(推荐)
|
|
||||||
- **unilabos-env**:开发者使用,支持 `pip install -e .` 可编辑模式,可修改源代码
|
|
||||||
- **unilabos-full**:需要仿真(Gazebo)、可视化(rviz2)或 Jupyter Notebook
|
|
||||||
|
|
||||||
### 2. 克隆仓库(可选,供开发者使用)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库(仅开发或查看示例时需要)
|
# 克隆仓库
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||||
cd Uni-Lab-OS
|
cd Uni-Lab-OS
|
||||||
|
|
||||||
|
# 安装 Uni-Lab-OS
|
||||||
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 启动 Uni-Lab 系统
|
3. 启动 Uni-Lab 系统:
|
||||||
|
|
||||||
请见[文档-启动样例](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
|
请见[文档-启动样例](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
|
||||||
|
|
||||||
4. 最佳实践
|
|
||||||
|
|
||||||
请见[最佳实践指南](https://deepmodeling.github.io/Uni-Lab-OS/user_guide/best_practice.html)
|
|
||||||
|
|
||||||
## 消息格式
|
## 消息格式
|
||||||
|
|
||||||
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) 页面找到已构建的版本。
|
||||||
|
|
||||||
## 引用
|
## 引用
|
||||||
|
|
||||||
如果您在学术研究中使用 [Uni-Lab-OS](https://arxiv.org/abs/2512.21766),请引用:
|
如果您在学术研究中使用 Uni-Lab-OS,请引用:
|
||||||
|
|
||||||
```bibtex
|
```bibtex
|
||||||
@article{gao2025unilabos,
|
@article{gao2025unilabos,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extensions = [
|
|||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||||
"sphinx_rtd_theme",
|
"sphinx_rtd_theme",
|
||||||
"sphinxcontrib.mermaid",
|
"sphinxcontrib.mermaid"
|
||||||
]
|
]
|
||||||
|
|
||||||
source_suffix = {
|
source_suffix = {
|
||||||
@@ -58,7 +58,7 @@ html_theme = "sphinx_rtd_theme"
|
|||||||
|
|
||||||
# sphinx-book-theme 主题选项
|
# sphinx-book-theme 主题选项
|
||||||
html_theme_options = {
|
html_theme_options = {
|
||||||
"repository_url": "https://github.com/deepmodeling/Uni-Lab-OS",
|
"repository_url": "https://github.com/用户名/Uni-Lab",
|
||||||
"use_repository_button": True,
|
"use_repository_button": True,
|
||||||
"use_issues_button": True,
|
"use_issues_button": True,
|
||||||
"use_edit_page_button": True,
|
"use_edit_page_button": True,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,6 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,
|
|||||||
**示例:**
|
**示例:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from unilabos.registry.decorators import device, topic_config
|
|
||||||
|
|
||||||
@device(id="mock_gripper", category=["gripper"], description="Mock Gripper")
|
|
||||||
class MockGripper:
|
class MockGripper:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._position: float = 0.0
|
self._position: float = 0.0
|
||||||
@@ -26,23 +23,19 @@ class MockGripper:
|
|||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config() # 添加 @topic_config 才会定时广播
|
|
||||||
def position(self) -> float:
|
def position(self) -> float:
|
||||||
return self._position
|
return self._position
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def velocity(self) -> float:
|
def velocity(self) -> float:
|
||||||
return self._velocity
|
return self._velocity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def torque(self) -> float:
|
def torque(self) -> float:
|
||||||
return self._torque
|
return self._torque
|
||||||
|
|
||||||
# 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播
|
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
||||||
@property
|
@property
|
||||||
@topic_config(period=2.0) # 可自定义发布周期
|
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@@ -156,7 +149,7 @@ my_device: # 设备唯一标识符
|
|||||||
|
|
||||||
系统会自动分析您的 Python 驱动类并生成:
|
系统会自动分析您的 Python 驱动类并生成:
|
||||||
|
|
||||||
- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性
|
- `status_types`:从 `@property` 装饰的方法自动识别状态属性
|
||||||
- `action_value_mappings`:从类方法自动生成动作映射
|
- `action_value_mappings`:从类方法自动生成动作映射
|
||||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||||
- `schema`:前端显示用的属性类型定义
|
- `schema`:前端显示用的属性类型定义
|
||||||
@@ -186,9 +179,7 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from unilabos.registry.decorators import device, topic_config
|
|
||||||
|
|
||||||
@device(id="my_device", category=["general"], description="My Device")
|
|
||||||
class MyDevice:
|
class MyDevice:
|
||||||
"""设备类文档字符串
|
"""设备类文档字符串
|
||||||
|
|
||||||
@@ -207,9 +198,8 @@ class MyDevice:
|
|||||||
# 初始化硬件连接
|
# 初始化硬件连接
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config() # 必须添加 @topic_config 才会广播
|
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态(通过 @topic_config 广播)"""
|
"""设备状态(会自动广播)"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
def my_action(self, param: float) -> Dict[str, Any]:
|
def my_action(self, param: float) -> Dict[str, Any]:
|
||||||
@@ -227,61 +217,34 @@ class MyDevice:
|
|||||||
|
|
||||||
## 状态属性 vs 动作方法
|
## 状态属性 vs 动作方法
|
||||||
|
|
||||||
### 状态属性(@property + @topic_config)
|
### 状态属性(@property)
|
||||||
|
|
||||||
状态属性需要同时使用 `@property` 和 `@topic_config` 装饰器才会被识别并定期广播:
|
状态属性会被自动识别并定期广播:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from unilabos.registry.decorators import topic_config
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config() # 必须添加,否则不会广播
|
|
||||||
def temperature(self) -> float:
|
def temperature(self) -> float:
|
||||||
"""当前温度"""
|
"""当前温度"""
|
||||||
return self._read_temperature()
|
return self._read_temperature()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config(period=2.0) # 可自定义发布周期(秒)
|
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态: idle, running, error"""
|
"""设备状态: idle, running, error"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config(name="ready") # 可自定义发布名称
|
|
||||||
def is_ready(self) -> bool:
|
def is_ready(self) -> bool:
|
||||||
"""设备是否就绪"""
|
"""设备是否就绪"""
|
||||||
return self._status == "idle"
|
return self._status == "idle"
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以使用普通方法(非 @property)配合 `@topic_config`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@topic_config(period=10.0)
|
|
||||||
def get_sensor_data(self) -> Dict[str, float]:
|
|
||||||
"""获取传感器数据(get_ 前缀会自动去除,发布名为 sensor_data)"""
|
|
||||||
return {"temp": self._temp, "humidity": self._humidity}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`@topic_config` 参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `period` | float | 5.0 | 发布周期(秒) |
|
|
||||||
| `print_publish` | bool | 节点默认 | 是否打印发布日志 |
|
|
||||||
| `qos` | int | 10 | QoS 深度 |
|
|
||||||
| `name` | str | None | 自定义发布名称 |
|
|
||||||
|
|
||||||
**发布名称优先级**:`@topic_config(name=...)` > `get_` 前缀去除 > 方法名
|
|
||||||
|
|
||||||
**特点**:
|
**特点**:
|
||||||
|
|
||||||
- 必须使用 `@topic_config` 装饰器
|
- 使用`@property`装饰器
|
||||||
- 支持 `@property` 和普通方法
|
- 只读,不能有参数
|
||||||
- 添加到注册表的 `status_types`
|
- 自动添加到注册表的`status_types`
|
||||||
- 定期发布到 ROS2 topic
|
- 定期发布到 ROS2 topic
|
||||||
|
|
||||||
> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。
|
|
||||||
|
|
||||||
### 动作方法
|
### 动作方法
|
||||||
|
|
||||||
动作方法是设备可以执行的操作:
|
动作方法是设备可以执行的操作:
|
||||||
@@ -534,7 +497,6 @@ class LiquidHandler:
|
|||||||
self._status = "idle"
|
self._status = "idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@@ -924,52 +886,7 @@ class MyDevice:
|
|||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
### 1. 使用 `@device` 装饰器标识设备类
|
### 1. 类型注解
|
||||||
|
|
||||||
```python
|
|
||||||
from unilabos.registry.decorators import device
|
|
||||||
|
|
||||||
@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp")
|
|
||||||
class MyDevice:
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- `id`:设备唯一标识符,用于注册表匹配
|
|
||||||
- `category`:分类列表,前端用于分组显示
|
|
||||||
- `description`:设备描述
|
|
||||||
- `icon`:图标文件名(可选)
|
|
||||||
|
|
||||||
### 2. 使用 `@topic_config` 声明需要广播的状态
|
|
||||||
|
|
||||||
```python
|
|
||||||
from unilabos.registry.decorators import topic_config
|
|
||||||
|
|
||||||
# ✓ @property + @topic_config → 会广播
|
|
||||||
@property
|
|
||||||
@topic_config(period=2.0)
|
|
||||||
def temperature(self) -> float:
|
|
||||||
return self._temp
|
|
||||||
|
|
||||||
# ✓ 普通方法 + @topic_config → 会广播(get_ 前缀自动去除)
|
|
||||||
@topic_config(period=10.0)
|
|
||||||
def get_sensor_data(self) -> Dict[str, float]:
|
|
||||||
return {"temp": self._temp}
|
|
||||||
|
|
||||||
# ✓ 使用 name 参数自定义发布名称
|
|
||||||
@property
|
|
||||||
@topic_config(name="ready")
|
|
||||||
def is_ready(self) -> bool:
|
|
||||||
return self._status == "idle"
|
|
||||||
|
|
||||||
# ✗ 仅有 @property,没有 @topic_config → 不会广播
|
|
||||||
@property
|
|
||||||
def internal_state(self) -> str:
|
|
||||||
return self._state
|
|
||||||
```
|
|
||||||
|
|
||||||
> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。
|
|
||||||
|
|
||||||
### 3. 类型注解
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
@@ -984,7 +901,7 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 文档字符串
|
### 2. 文档字符串
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def method(self, param: float) -> Dict[str, Any]:
|
def method(self, param: float) -> Dict[str, Any]:
|
||||||
@@ -1006,7 +923,7 @@ def method(self, param: float) -> Dict[str, Any]:
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 配置验证
|
### 3. 配置验证
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def __init__(self, config: Dict[str, Any]):
|
def __init__(self, config: Dict[str, Any]):
|
||||||
@@ -1020,7 +937,7 @@ def __init__(self, config: Dict[str, Any]):
|
|||||||
self.baudrate = config['baudrate']
|
self.baudrate = config['baudrate']
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 资源清理
|
### 4. 资源清理
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@@ -1029,7 +946,7 @@ def __del__(self):
|
|||||||
self.connection.close()
|
self.connection.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. 设计前端友好的返回值
|
### 5. 设计前端友好的返回值
|
||||||
|
|
||||||
**记住:返回值会直接显示在 Web 界面**
|
**记住:返回值会直接显示在 Web 界面**
|
||||||
|
|
||||||
|
|||||||
@@ -422,20 +422,18 @@ placeholder_keys:
|
|||||||
|
|
||||||
### status_types
|
### status_types
|
||||||
|
|
||||||
系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分:
|
系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
status_types:
|
status_types:
|
||||||
current_temperature: float # 从 @topic_config 装饰的 @property 或方法
|
current_temperature: float # 从 get_current_temperature() 或 @property current_temperature
|
||||||
is_heating: bool
|
is_heating: bool # 从 get_is_heating() 或 @property is_heating
|
||||||
status: str
|
status: str # 从 get_status() 或 @property status
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意事项**:
|
**注意事项**:
|
||||||
|
|
||||||
- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性
|
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性
|
||||||
- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播
|
|
||||||
- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`)
|
|
||||||
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
||||||
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
||||||
|
|
||||||
@@ -539,13 +537,11 @@ class AdvancedLiquidHandler:
|
|||||||
self._temperature = 25.0
|
self._temperature = 25.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
"""设备状态"""
|
"""设备状态"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def temperature(self) -> float:
|
def temperature(self) -> float:
|
||||||
"""当前温度"""
|
"""当前温度"""
|
||||||
return self._temperature
|
return self._temperature
|
||||||
@@ -813,23 +809,21 @@ my_temperature_controller:
|
|||||||
你的设备类需要符合以下要求:
|
你的设备类需要符合以下要求:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from unilabos.registry.decorators import device, topic_config
|
from unilabos.common.device_base import DeviceBase
|
||||||
|
|
||||||
@device(id="my_device", category=["temperature"], description="My Device")
|
class MyDevice(DeviceBase):
|
||||||
class MyDevice:
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""初始化,参数会自动分析到 init_param_schema.config"""
|
"""初始化,参数会自动分析到 init_param_schema.config"""
|
||||||
|
super().__init__(config)
|
||||||
self.port = config.get('port', '/dev/ttyUSB0')
|
self.port = config.get('port', '/dev/ttyUSB0')
|
||||||
|
|
||||||
# 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播)
|
# 状态方法(会自动生成到 status_types)
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def status(self):
|
def status(self):
|
||||||
"""返回设备状态"""
|
"""返回设备状态"""
|
||||||
return "idle"
|
return "idle"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@topic_config()
|
|
||||||
def temperature(self):
|
def temperature(self):
|
||||||
"""返回当前温度"""
|
"""返回当前温度"""
|
||||||
return 25.0
|
return 25.0
|
||||||
@@ -1045,34 +1039,7 @@ resource.type # "resource"
|
|||||||
|
|
||||||
### 代码规范
|
### 代码规范
|
||||||
|
|
||||||
1. **使用 `@device` 装饰器标识设备类**
|
1. **始终使用类型注解**
|
||||||
|
|
||||||
```python
|
|
||||||
from unilabos.registry.decorators import device
|
|
||||||
|
|
||||||
@device(id="my_device", category=["heating"], description="My Device")
|
|
||||||
class MyDevice:
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **使用 `@topic_config` 声明广播属性**
|
|
||||||
|
|
||||||
```python
|
|
||||||
from unilabos.registry.decorators import topic_config
|
|
||||||
|
|
||||||
# ✓ 需要广播的状态属性
|
|
||||||
@property
|
|
||||||
@topic_config(period=2.0)
|
|
||||||
def temperature(self) -> float:
|
|
||||||
return self._temp
|
|
||||||
|
|
||||||
# ✗ 仅有 @property 不会广播
|
|
||||||
@property
|
|
||||||
def internal_counter(self) -> int:
|
|
||||||
return self._counter
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **始终使用类型注解**
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ✓ 好
|
# ✓ 好
|
||||||
@@ -1084,7 +1051,7 @@ def method(self, resource, device):
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **提供有意义的参数名**
|
2. **提供有意义的参数名**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ✓ 好 - 清晰的参数名
|
# ✓ 好 - 清晰的参数名
|
||||||
@@ -1096,7 +1063,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot):
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **使用 Optional 表示可选参数**
|
3. **使用 Optional 表示可选参数**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -1109,7 +1076,7 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **添加详细的文档字符串**
|
4. **添加详细的文档字符串**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def method(
|
def method(
|
||||||
@@ -1129,13 +1096,13 @@ def method(
|
|||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **方法命名规范**
|
5. **方法命名规范**
|
||||||
|
|
||||||
- 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config`
|
- 状态方法使用 `@property` 装饰器或 `get_` 前缀
|
||||||
- 动作方法使用动词开头
|
- 动作方法使用动词开头
|
||||||
- 保持命名清晰、一致
|
- 保持命名清晰、一致
|
||||||
|
|
||||||
8. **完善的错误处理**
|
6. **完善的错误处理**
|
||||||
- 实现完善的错误处理
|
- 实现完善的错误处理
|
||||||
- 添加日志记录
|
- 添加日志记录
|
||||||
- 提供有意义的错误信息
|
- 提供有意义的错误信息
|
||||||
|
|||||||
@@ -221,10 +221,10 @@ Laboratory A Laboratory B
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 实验室A
|
# 实验室A
|
||||||
unilab --ak your_ak --sk your_sk --upload_registry
|
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||||
|
|
||||||
# 实验室B
|
# 实验室B
|
||||||
unilab --ak your_ak --sk your_sk --upload_registry
|
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,7 +12,3 @@ sphinx-copybutton>=0.5.0
|
|||||||
|
|
||||||
# 用于自动摘要生成
|
# 用于自动摘要生成
|
||||||
sphinx-autobuild>=2024.2.4
|
sphinx-autobuild>=2024.2.4
|
||||||
|
|
||||||
# 用于PDF导出 (rinohtype方案,纯Python无需LaTeX)
|
|
||||||
rinohtype>=0.5.4
|
|
||||||
sphinx-simplepdf>=1.6.0
|
|
||||||
@@ -31,14 +31,6 @@
|
|||||||
|
|
||||||
详细的安装步骤请参考 [安装指南](installation.md)。
|
详细的安装步骤请参考 [安装指南](installation.md)。
|
||||||
|
|
||||||
**选择合适的安装包:**
|
|
||||||
|
|
||||||
| 安装包 | 适用场景 | 包含组件 |
|
|
||||||
|--------|----------|----------|
|
|
||||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
|
||||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
|
||||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
|
||||||
|
|
||||||
**关键步骤:**
|
**关键步骤:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -46,30 +38,15 @@
|
|||||||
# 下载 Miniforge: https://github.com/conda-forge/miniforge/releases
|
# 下载 Miniforge: https://github.com/conda-forge/miniforge/releases
|
||||||
|
|
||||||
# 2. 创建 Conda 环境
|
# 2. 创建 Conda 环境
|
||||||
mamba create -n unilab python=3.11.14
|
mamba create -n unilab python=3.11.11
|
||||||
|
|
||||||
# 3. 激活环境
|
# 3. 激活环境
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
|
||||||
# 4. 安装 Uni-Lab-OS(选择其一)
|
# 4. 安装 Uni-Lab-OS
|
||||||
|
|
||||||
# 方案 A:标准安装(推荐大多数用户)
|
|
||||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
|
|
||||||
# 方案 B:开发者环境(可编辑模式开发)
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
pip install -e /path/to/Uni-Lab-OS # 可编辑安装
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt # 安装 pip 依赖
|
|
||||||
|
|
||||||
# 方案 C:完整版(仿真/可视化)
|
|
||||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**选择建议:**
|
|
||||||
- **日常使用/生产部署**:使用 `unilabos`(推荐),完整功能,开箱即用
|
|
||||||
- **开发者**:使用 `unilabos-env` + `pip install -e .` + `uv pip install -r unilabos/utils/requirements.txt`,代码修改立即生效
|
|
||||||
- **仿真/可视化**:使用 `unilabos-full`,含 Gazebo、rviz2、MoveIt
|
|
||||||
|
|
||||||
#### 1.2 验证安装
|
#### 1.2 验证安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -439,9 +416,6 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
|||||||
1. 访问 Web 界面,进入"仪器耗材"模块
|
1. 访问 Web 界面,进入"仪器耗材"模块
|
||||||
2. 在"仪器设备"区域找到并添加上述设备
|
2. 在"仪器设备"区域找到并添加上述设备
|
||||||
3. 在"物料耗材"区域找到并添加容器
|
3. 在"物料耗材"区域找到并添加容器
|
||||||
4. 在workstation中配置protocol_type包含PumpTransferProtocol
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -452,9 +426,8 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
|||||||
**操作步骤:**
|
**操作步骤:**
|
||||||
|
|
||||||
1. 将两个 `container` 拖拽到 `workstation` 中
|
1. 将两个 `container` 拖拽到 `workstation` 中
|
||||||
2. 将 `virtual_multiway_valve` 拖拽到 `workstation` 中
|
2. 将 `virtual_transfer_pump` 拖拽到 `workstation` 中
|
||||||
3. 将 `virtual_transfer_pump` 拖拽到 `workstation` 中
|
3. 在画布上连接它们(建立父子关系)
|
||||||
4. 在画布上连接它们(建立父子关系)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -795,43 +768,7 @@ Waiting for host service...
|
|||||||
|
|
||||||
详细的设备驱动编写指南请参考 [添加设备驱动](../developer_guide/add_device.md)。
|
详细的设备驱动编写指南请参考 [添加设备驱动](../developer_guide/add_device.md)。
|
||||||
|
|
||||||
#### 9.1 开发环境准备
|
#### 9.1 为什么需要自定义设备?
|
||||||
|
|
||||||
**推荐使用 `unilabos-env` + `pip install -e .` + `uv pip install`** 进行设备开发:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 创建环境并安装 unilabos-env(ROS2 + conda 依赖 + uv)
|
|
||||||
mamba create -n unilab python=3.11.14
|
|
||||||
conda activate unilab
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
# 2. 克隆代码
|
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
|
||||||
cd Uni-Lab-OS
|
|
||||||
|
|
||||||
# 3. 以可编辑模式安装(推荐使用脚本,自动检测中文环境)
|
|
||||||
python scripts/dev_install.py
|
|
||||||
|
|
||||||
# 或手动安装:
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
**为什么使用这种方式?**
|
|
||||||
- `unilabos-env` 提供 ROS2 核心组件和 uv(通过 conda 安装,避免编译)
|
|
||||||
- `unilabos/utils/requirements.txt` 包含所有运行时需要的 pip 依赖
|
|
||||||
- `dev_install.py` 自动检测中文环境,中文系统自动使用清华镜像
|
|
||||||
- 使用 `uv` 替代 `pip`,安装速度更快
|
|
||||||
- 可编辑模式:代码修改**立即生效**,无需重新安装
|
|
||||||
|
|
||||||
**如果安装失败或速度太慢**,可以手动执行(使用清华镜像):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 9.2 为什么需要自定义设备?
|
|
||||||
|
|
||||||
Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要集成:
|
Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要集成:
|
||||||
|
|
||||||
@@ -840,7 +777,7 @@ Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要
|
|||||||
- 特殊的实验流程
|
- 特殊的实验流程
|
||||||
- 第三方设备集成
|
- 第三方设备集成
|
||||||
|
|
||||||
#### 9.3 创建 Python 包
|
#### 9.2 创建 Python 包
|
||||||
|
|
||||||
为了方便开发和管理,建议为您的实验室创建独立的 Python 包。
|
为了方便开发和管理,建议为您的实验室创建独立的 Python 包。
|
||||||
|
|
||||||
@@ -877,7 +814,7 @@ touch my_lab_devices/my_lab_devices/__init__.py
|
|||||||
touch my_lab_devices/my_lab_devices/devices/__init__.py
|
touch my_lab_devices/my_lab_devices/devices/__init__.py
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 9.4 创建 setup.py
|
#### 9.3 创建 setup.py
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# my_lab_devices/setup.py
|
# my_lab_devices/setup.py
|
||||||
@@ -908,7 +845,7 @@ setup(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 9.5 开发安装
|
#### 9.4 开发安装
|
||||||
|
|
||||||
使用 `-e` 参数进行可编辑安装,这样代码修改后立即生效:
|
使用 `-e` 参数进行可编辑安装,这样代码修改后立即生效:
|
||||||
|
|
||||||
@@ -923,7 +860,7 @@ pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|||||||
- 方便调试和测试
|
- 方便调试和测试
|
||||||
- 支持版本控制(git)
|
- 支持版本控制(git)
|
||||||
|
|
||||||
#### 9.6 编写设备驱动
|
#### 9.5 编写设备驱动
|
||||||
|
|
||||||
创建设备驱动文件:
|
创建设备驱动文件:
|
||||||
|
|
||||||
@@ -1064,7 +1001,7 @@ class MyPump:
|
|||||||
- **返回 Dict**:所有动作方法返回字典类型
|
- **返回 Dict**:所有动作方法返回字典类型
|
||||||
- **文档字符串**:详细说明参数和功能
|
- **文档字符串**:详细说明参数和功能
|
||||||
|
|
||||||
#### 9.7 测试设备驱动
|
#### 9.6 测试设备驱动
|
||||||
|
|
||||||
创建简单的测试脚本:
|
创建简单的测试脚本:
|
||||||
|
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ Uni-Lab 使用 `ResourceDictInstance.get_resource_instance_from_dict()` 方法
|
|||||||
### 使用示例
|
### 使用示例
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from unilabos.resources.resource_tracker import ResourceDictInstance
|
from unilabos.ros.nodes.resource_tracker import ResourceDictInstance
|
||||||
|
|
||||||
# 旧格式节点
|
# 旧格式节点
|
||||||
old_format_node = {
|
old_format_node = {
|
||||||
@@ -477,10 +477,10 @@ old_format_node = {
|
|||||||
instance = ResourceDictInstance.get_resource_instance_from_dict(old_format_node)
|
instance = ResourceDictInstance.get_resource_instance_from_dict(old_format_node)
|
||||||
|
|
||||||
# 访问标准化后的数据
|
# 访问标准化后的数据
|
||||||
print(instance.res_content.id) # "pump_1"
|
print(instance.res_content.id) # "pump_1"
|
||||||
print(instance.res_content.uuid) # 自动生成的 UUID
|
print(instance.res_content.uuid) # 自动生成的 UUID
|
||||||
print(instance.res_content.config) # {}
|
print(instance.res_content.config) # {}
|
||||||
print(instance.res_content.data) # {}
|
print(instance.res_content.data) # {}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 格式迁移建议
|
### 格式迁移建议
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 275 KiB |
@@ -13,26 +13,15 @@
|
|||||||
- 开发者需要 Git 和基本的 Python 开发知识
|
- 开发者需要 Git 和基本的 Python 开发知识
|
||||||
- 自定义 msgs 需要 GitHub 账号
|
- 自定义 msgs 需要 GitHub 账号
|
||||||
|
|
||||||
## 安装包选择
|
|
||||||
|
|
||||||
Uni-Lab-OS 提供三个安装包版本,根据您的需求选择:
|
|
||||||
|
|
||||||
| 安装包 | 适用场景 | 包含组件 | 磁盘占用 |
|
|
||||||
|--------|----------|----------|----------|
|
|
||||||
| **unilabos** | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 | ~2-3 GB |
|
|
||||||
| **unilabos-env** | 开发者环境(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos | ~2 GB |
|
|
||||||
| **unilabos-full** | 仿真可视化、完整功能体验 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt | ~8-10 GB |
|
|
||||||
|
|
||||||
## 安装方式选择
|
## 安装方式选择
|
||||||
|
|
||||||
根据您的使用场景,选择合适的安装方式:
|
根据您的使用场景,选择合适的安装方式:
|
||||||
|
|
||||||
| 安装方式 | 适用人群 | 推荐安装包 | 特点 | 安装时间 |
|
| 安装方式 | 适用人群 | 特点 | 安装时间 |
|
||||||
| ---------------------- | -------------------- | ----------------- | ------------------------------ | ---------------------------- |
|
| ---------------------- | -------------------- | ------------------------------ | ---------------------------- |
|
||||||
| **方式一:一键安装** | 快速体验、演示 | 预打包环境 | 离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) |
|
| **方式一:一键安装** | 实验室用户、快速体验 | 预打包环境,离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) |
|
||||||
| **方式二:手动安装** | **大多数用户** | `unilabos` | 完整功能,开箱即用 | 10-20 分钟 |
|
| **方式二:手动安装** | 标准用户、生产环境 | 灵活配置,版本可控 | 10-20 分钟 |
|
||||||
| **方式三:开发者安装** | 开发者、需要修改源码 | `unilabos-env` | 可编辑模式,支持自定义开发 | 20-30 分钟 |
|
| **方式三:开发者安装** | 开发者、需要修改源码 | 可编辑模式,支持自定义 msgs | 20-30 分钟 |
|
||||||
| **仿真/可视化** | 仿真测试、可视化调试 | `unilabos-full` | 含 Gazebo、rviz2、MoveIt | 30-60 分钟 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,38 +144,17 @@ bash Miniforge3-$(uname)-$(uname -m).sh
|
|||||||
使用以下命令创建 Uni-Lab 专用环境:
|
使用以下命令创建 Uni-Lab 专用环境:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mamba create -n unilab python=3.11.14 # 目前ros2组件依赖版本大多为3.11.14
|
mamba create -n unilab python=3.11.11 # 目前ros2组件依赖版本大多为3.11.11
|
||||||
mamba activate unilab
|
mamba activate unilab
|
||||||
|
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
# 选择安装包(三选一):
|
|
||||||
|
|
||||||
# 方案 A:标准安装(推荐大多数用户)
|
|
||||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
|
||||||
|
|
||||||
# 方案 B:开发者环境(可编辑模式开发)
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
# 然后安装 unilabos 和 pip 依赖:
|
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
|
|
||||||
# 方案 C:完整版(含仿真和可视化工具)
|
|
||||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数说明**:
|
**参数说明**:
|
||||||
|
|
||||||
- `-n unilab`: 创建名为 "unilab" 的环境
|
- `-n unilab`: 创建名为 "unilab" 的环境
|
||||||
- `uni-lab::unilabos`: 安装 unilabos 完整包,开箱即用(推荐)
|
- `uni-lab::unilabos`: 从 uni-lab channel 安装 unilabos 包
|
||||||
- `uni-lab::unilabos-env`: 仅安装环境依赖,适合开发者使用 `pip install -e .`
|
|
||||||
- `uni-lab::unilabos-full`: 安装完整包(含 ROS2 Desktop、Gazebo、MoveIt 等)
|
|
||||||
- `-c robostack-staging -c conda-forge`: 添加额外的软件源
|
- `-c robostack-staging -c conda-forge`: 添加额外的软件源
|
||||||
|
|
||||||
**包选择建议**:
|
|
||||||
- **日常使用/生产部署**:安装 `unilabos`(推荐,完整功能,开箱即用)
|
|
||||||
- **开发者**:安装 `unilabos-env`,然后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖,再 `pip install -e .` 进行可编辑安装
|
|
||||||
- **仿真/可视化**:安装 `unilabos-full`(Gazebo、rviz2、MoveIt)
|
|
||||||
|
|
||||||
**如果遇到网络问题**,可以使用清华镜像源加速下载:
|
**如果遇到网络问题**,可以使用清华镜像源加速下载:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -195,14 +163,8 @@ mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/m
|
|||||||
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
||||||
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
|
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
|
||||||
|
|
||||||
# 然后重新执行安装命令(推荐标准安装)
|
# 然后重新执行安装命令
|
||||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging
|
mamba create -n unilab uni-lab::unilabos -c robostack-staging
|
||||||
|
|
||||||
# 或完整版(仿真/可视化)
|
|
||||||
mamba create -n unilab uni-lab::unilabos-full -c robostack-staging
|
|
||||||
|
|
||||||
# pip 安装时使用清华镜像(开发者安装时使用)
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第三步:激活环境
|
### 第三步:激活环境
|
||||||
@@ -241,87 +203,58 @@ cd Uni-Lab-OS
|
|||||||
cd Uni-Lab-OS
|
cd Uni-Lab-OS
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第二步:安装开发环境(unilabos-env)
|
### 第二步:安装基础环境
|
||||||
|
|
||||||
**重要**:开发者请使用 `unilabos-env` 包,它专为开发者设计:
|
**推荐方式**:先通过**方式一(一键安装)**或**方式二(手动安装)**完成基础环境的安装,这将包含所有必需的依赖项(ROS2、msgs 等)。
|
||||||
- 包含 ROS2 核心组件和消息包(ros-humble-ros-core、std-msgs、geometry-msgs 等)
|
|
||||||
- 包含 transforms3d、cv-bridge、tf2 等 conda 依赖
|
#### 选项 A:通过一键安装(推荐)
|
||||||
- 包含 `uv` 工具,用于快速安装 pip 依赖
|
|
||||||
- **不包含** pip 依赖和 unilabos 包(由 `pip install -e .` 和 `uv pip install` 安装)
|
参考上文"方式一:一键安装",完成基础环境的安装后,激活环境:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建并激活环境
|
|
||||||
mamba create -n unilab python=3.11.14
|
|
||||||
conda activate unilab
|
conda activate unilab
|
||||||
|
|
||||||
# 安装开发者环境包(ROS2 + conda 依赖 + uv)
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第三步:安装 pip 依赖和可编辑模式安装
|
#### 选项 B:通过手动安装
|
||||||
|
|
||||||
克隆代码并安装依赖:
|
参考上文"方式二:手动安装",创建并安装环境:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mamba create -n unilab python=3.11.11
|
||||||
|
conda activate unilab
|
||||||
|
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:这会安装包括 Python 3.11.11、ROS2 Humble、ros-humble-unilabos-msgs 和所有必需依赖
|
||||||
|
|
||||||
|
### 第三步:切换到开发版本
|
||||||
|
|
||||||
|
现在你已经有了一个完整可用的 Uni-Lab 环境,接下来将 unilabos 包切换为开发版本:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 确保环境已激活
|
# 确保环境已激活
|
||||||
conda activate unilab
|
conda activate unilab
|
||||||
|
|
||||||
# 克隆仓库(如果还未克隆)
|
# 卸载 pip 安装的 unilabos(保留所有 conda 依赖)
|
||||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
pip uninstall unilabos -y
|
||||||
cd Uni-Lab-OS
|
|
||||||
|
|
||||||
# 切换到 dev 分支(可选)
|
# 克隆 dev 分支(如果还未克隆)
|
||||||
|
cd /path/to/your/workspace
|
||||||
|
git clone -b dev https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||||
|
# 或者如果已经克隆,切换到 dev 分支
|
||||||
|
cd Uni-Lab-OS
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git pull
|
git pull
|
||||||
```
|
|
||||||
|
|
||||||
**推荐:使用安装脚本**(自动检测中文环境,使用 uv 加速):
|
# 以可编辑模式安装开发版 unilabos
|
||||||
|
|
||||||
```bash
|
|
||||||
# 自动检测中文环境,如果是中文系统则使用清华镜像
|
|
||||||
python scripts/dev_install.py
|
|
||||||
|
|
||||||
# 或者手动指定:
|
|
||||||
python scripts/dev_install.py --china # 强制使用清华镜像
|
|
||||||
python scripts/dev_install.py --no-mirror # 强制使用 PyPI
|
|
||||||
python scripts/dev_install.py --skip-deps # 跳过 pip 依赖安装
|
|
||||||
python scripts/dev_install.py --use-pip # 使用 pip 而非 uv
|
|
||||||
```
|
|
||||||
|
|
||||||
**手动安装**(如果脚本安装失败或速度太慢):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 安装 unilabos(可编辑模式)
|
|
||||||
pip install -e .
|
|
||||||
|
|
||||||
# 2. 使用 uv 安装 pip 依赖(推荐,速度更快)
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
|
|
||||||
# 国内用户使用清华镜像:
|
|
||||||
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**:
|
**参数说明**:
|
||||||
- `uv` 已包含在 `unilabos-env` 中,无需单独安装
|
|
||||||
- `unilabos/utils/requirements.txt` 包含运行 unilabos 所需的所有 pip 依赖
|
|
||||||
- 部分特殊包(如 pylabrobot)会在运行时由 unilabos 自动检测并安装
|
|
||||||
|
|
||||||
**为什么使用可编辑模式?**
|
- `-e`: editable mode(可编辑模式),代码修改立即生效,无需重新安装
|
||||||
|
- `-i`: 使用清华镜像源加速下载
|
||||||
- `-e` (editable mode):代码修改**立即生效**,无需重新安装
|
- `pip uninstall unilabos`: 只卸载 pip 安装的 unilabos 包,不影响 conda 安装的其他依赖(如 ROS2、msgs 等)
|
||||||
- 适合开发调试:修改代码后直接运行测试
|
|
||||||
- 与 `unilabos-env` 配合:环境依赖由 conda 管理,unilabos 代码由 pip 管理
|
|
||||||
|
|
||||||
**验证安装**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 检查 unilabos 版本
|
|
||||||
python -c "import unilabos; print(unilabos.__version__)"
|
|
||||||
|
|
||||||
# 检查安装位置(应该指向你的代码目录)
|
|
||||||
pip show unilabos | grep Location
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第四步:安装或自定义 ros-humble-unilabos-msgs(可选)
|
### 第四步:安装或自定义 ros-humble-unilabos-msgs(可选)
|
||||||
|
|
||||||
@@ -531,45 +464,7 @@ cd $CONDA_PREFIX/envs/unilab
|
|||||||
|
|
||||||
### 问题 8: 环境很大,有办法减小吗?
|
### 问题 8: 环境很大,有办法减小吗?
|
||||||
|
|
||||||
**解决方案**:
|
**解决方案**: 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用方式二手动安装,只安装需要的组件。
|
||||||
|
|
||||||
1. **使用 `unilabos` 标准版**(推荐大多数用户):
|
|
||||||
```bash
|
|
||||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
|
||||||
```
|
|
||||||
标准版包含完整功能,环境大小约 2-3GB(相比完整版的 8-10GB)。
|
|
||||||
|
|
||||||
2. **使用 `unilabos-env` 开发者版**(最小化):
|
|
||||||
```bash
|
|
||||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
|
||||||
# 然后手动安装依赖
|
|
||||||
pip install -e .
|
|
||||||
uv pip install -r unilabos/utils/requirements.txt
|
|
||||||
```
|
|
||||||
开发者版只包含环境依赖,体积最小约 2GB。
|
|
||||||
|
|
||||||
3. **按需安装额外组件**:
|
|
||||||
如果后续需要特定功能,可以单独安装:
|
|
||||||
```bash
|
|
||||||
# 需要 Jupyter
|
|
||||||
mamba install jupyter jupyros
|
|
||||||
|
|
||||||
# 需要可视化
|
|
||||||
mamba install matplotlib opencv
|
|
||||||
|
|
||||||
# 需要仿真(注意:这会安装大量依赖)
|
|
||||||
mamba install ros-humble-gazebo-ros
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **预打包环境问题**:
|
|
||||||
预打包环境(方式一)包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。
|
|
||||||
|
|
||||||
**包选择建议**:
|
|
||||||
| 需求 | 推荐包 | 预估大小 |
|
|
||||||
|------|--------|----------|
|
|
||||||
| 日常使用/生产部署 | `unilabos` | ~2-3 GB |
|
|
||||||
| 开发调试(可编辑模式) | `unilabos-env` | ~2 GB |
|
|
||||||
| 仿真/可视化 | `unilabos-full` | ~8-10 GB |
|
|
||||||
|
|
||||||
### 问题 9: 如何更新到最新版本?
|
### 问题 9: 如何更新到最新版本?
|
||||||
|
|
||||||
@@ -616,7 +511,6 @@ mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-f
|
|||||||
|
|
||||||
**提示**:
|
**提示**:
|
||||||
|
|
||||||
- **大多数用户**推荐使用方式二(手动安装)的 `unilabos` 标准版
|
- 生产环境推荐使用方式二(手动安装)的稳定版本
|
||||||
- **开发者**推荐使用方式三(开发者安装),安装 `unilabos-env` 后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖
|
- 开发和测试推荐使用方式三(开发者安装)
|
||||||
- **仿真/可视化**推荐安装 `unilabos-full` 完整版
|
- 快速体验和演示推荐使用方式一(一键安装)
|
||||||
- **快速体验和演示**推荐使用方式一(一键安装)
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ options:
|
|||||||
--is_slave Run the backend as slave node (without host privileges).
|
--is_slave Run the backend as slave node (without host privileges).
|
||||||
--slave_no_host Skip waiting for host service in slave mode
|
--slave_no_host Skip waiting for host service in slave mode
|
||||||
--upload_registry Upload registry information when starting unilab
|
--upload_registry Upload registry information when starting unilab
|
||||||
|
--use_remote_resource Use remote resources when starting unilab
|
||||||
--config CONFIG Configuration file path, supports .py format Python config files
|
--config CONFIG Configuration file path, supports .py format Python config files
|
||||||
--port PORT Port for web service information page
|
--port PORT Port for web service information page
|
||||||
--disable_browser Disable opening information page on startup
|
--disable_browser Disable opening information page on startup
|
||||||
@@ -84,7 +85,7 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
|||||||
支持两种方式:
|
支持两种方式:
|
||||||
|
|
||||||
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
||||||
- **远程资源**:不指定本地文件即可
|
- **远程资源**:使用 `--use_remote_resource` 从云端获取
|
||||||
|
|
||||||
### 7. 注册表构建
|
### 7. 注册表构建
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ unilab --config path/to/your/config.py
|
|||||||
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
||||||
|
|
||||||
# 使用远程资源启动
|
# 使用远程资源启动
|
||||||
unilab --ak your_ak --sk your_sk
|
unilab --ak your_ak --sk your_sk --use_remote_resource
|
||||||
|
|
||||||
# 更新注册表
|
# 更新注册表
|
||||||
unilab --ak your_ak --sk your_sk --complete_registry
|
unilab --ak your_ak --sk your_sk --complete_registry
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.10.19
|
version: 0.10.14
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
target_directory: src
|
target_directory: src
|
||||||
@@ -25,7 +25,7 @@ requirements:
|
|||||||
build:
|
build:
|
||||||
- ${{ compiler('cxx') }}
|
- ${{ compiler('cxx') }}
|
||||||
- ${{ compiler('c') }}
|
- ${{ compiler('c') }}
|
||||||
- python ==3.11.14
|
- python ==3.11.11
|
||||||
- numpy
|
- numpy
|
||||||
- if: build_platform != target_platform
|
- if: build_platform != target_platform
|
||||||
then:
|
then:
|
||||||
@@ -63,14 +63,14 @@ requirements:
|
|||||||
- robostack-staging::ros-humble-rosidl-default-generators
|
- robostack-staging::ros-humble-rosidl-default-generators
|
||||||
- robostack-staging::ros-humble-std-msgs
|
- robostack-staging::ros-humble-std-msgs
|
||||||
- robostack-staging::ros-humble-geometry-msgs
|
- robostack-staging::ros-humble-geometry-msgs
|
||||||
- robostack-staging::ros2-distro-mutex=0.7
|
- robostack-staging::ros2-distro-mutex=0.6
|
||||||
run:
|
run:
|
||||||
- robostack-staging::ros-humble-action-msgs
|
- robostack-staging::ros-humble-action-msgs
|
||||||
- robostack-staging::ros-humble-ros-workspace
|
- robostack-staging::ros-humble-ros-workspace
|
||||||
- robostack-staging::ros-humble-rosidl-default-runtime
|
- robostack-staging::ros-humble-rosidl-default-runtime
|
||||||
- robostack-staging::ros-humble-std-msgs
|
- robostack-staging::ros-humble-std-msgs
|
||||||
- robostack-staging::ros-humble-geometry-msgs
|
- robostack-staging::ros-humble-geometry-msgs
|
||||||
- robostack-staging::ros2-distro-mutex=0.7
|
- robostack-staging::ros2-distro-mutex=0.6
|
||||||
- if: osx and x86_64
|
- if: osx and x86_64
|
||||||
then:
|
then:
|
||||||
- __osx >=${{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
- __osx >=${{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.10.19"
|
version: "0.10.14"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Verification:
|
|||||||
-------------
|
-------------
|
||||||
|
|
||||||
The verify_installation.py script will check:
|
The verify_installation.py script will check:
|
||||||
- Python version (3.11.14)
|
- Python version (3.11.11)
|
||||||
- ROS2 rclpy installation
|
- ROS2 rclpy installation
|
||||||
- UniLabOS installation and dependencies
|
- UniLabOS installation and dependencies
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ Build Information:
|
|||||||
|
|
||||||
Branch: {branch}
|
Branch: {branch}
|
||||||
Platform: {platform}
|
Platform: {platform}
|
||||||
Python: 3.11.14
|
Python: 3.11.11
|
||||||
Date: {build_date}
|
Date: {build_date}
|
||||||
|
|
||||||
Troubleshooting:
|
Troubleshooting:
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Development installation script for UniLabOS.
|
|
||||||
Auto-detects Chinese locale and uses appropriate mirror.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python scripts/dev_install.py
|
|
||||||
python scripts/dev_install.py --no-mirror # Force no mirror
|
|
||||||
python scripts/dev_install.py --china # Force China mirror
|
|
||||||
python scripts/dev_install.py --skip-deps # Skip pip dependencies installation
|
|
||||||
|
|
||||||
Flow:
|
|
||||||
1. pip install -e . (install unilabos in editable mode)
|
|
||||||
2. Detect Chinese locale
|
|
||||||
3. Use uv to install pip dependencies from requirements.txt
|
|
||||||
4. Special packages (like pylabrobot) are handled by environment_check.py at runtime
|
|
||||||
"""
|
|
||||||
|
|
||||||
import locale
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Tsinghua mirror URL
|
|
||||||
TSINGHUA_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
|
||||||
|
|
||||||
|
|
||||||
def is_chinese_locale() -> bool:
|
|
||||||
"""
|
|
||||||
Detect if system is in Chinese locale.
|
|
||||||
Same logic as EnvironmentChecker._is_chinese_locale()
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
lang = locale.getdefaultlocale()[0]
|
|
||||||
if lang and ("zh" in lang.lower() or "chinese" in lang.lower()):
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def run_command(cmd: list, description: str, retry: int = 2) -> bool:
|
|
||||||
"""Run command with retry support."""
|
|
||||||
print(f"[INFO] {description}")
|
|
||||||
print(f"[CMD] {' '.join(cmd)}")
|
|
||||||
|
|
||||||
for attempt in range(retry + 1):
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, check=True, timeout=600)
|
|
||||||
print(f"[OK] {description}")
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
if attempt < retry:
|
|
||||||
print(f"[WARN] Attempt {attempt + 1} failed, retrying...")
|
|
||||||
else:
|
|
||||||
print(f"[ERROR] {description} failed: {e}")
|
|
||||||
return False
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
print(f"[ERROR] {description} timed out")
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def install_editable(project_root: Path, use_mirror: bool) -> bool:
|
|
||||||
"""Install unilabos in editable mode using pip."""
|
|
||||||
cmd = [sys.executable, "-m", "pip", "install", "-e", str(project_root)]
|
|
||||||
if use_mirror:
|
|
||||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
|
||||||
|
|
||||||
return run_command(cmd, "Installing unilabos in editable mode")
|
|
||||||
|
|
||||||
|
|
||||||
def install_requirements_uv(requirements_file: Path, use_mirror: bool) -> bool:
|
|
||||||
"""Install pip dependencies using uv (installed via conda-forge::uv)."""
|
|
||||||
cmd = ["uv", "pip", "install", "-r", str(requirements_file)]
|
|
||||||
if use_mirror:
|
|
||||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
|
||||||
|
|
||||||
return run_command(cmd, "Installing pip dependencies with uv", retry=2)
|
|
||||||
|
|
||||||
|
|
||||||
def install_requirements_pip(requirements_file: Path, use_mirror: bool) -> bool:
|
|
||||||
"""Fallback: Install pip dependencies using pip."""
|
|
||||||
cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]
|
|
||||||
if use_mirror:
|
|
||||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
|
||||||
|
|
||||||
return run_command(cmd, "Installing pip dependencies with pip", retry=2)
|
|
||||||
|
|
||||||
|
|
||||||
def check_uv_available() -> bool:
|
|
||||||
"""Check if uv is available (installed via conda-forge::uv)."""
|
|
||||||
try:
|
|
||||||
subprocess.run(["uv", "--version"], capture_output=True, check=True)
|
|
||||||
return True
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Development installation script for UniLabOS")
|
|
||||||
parser.add_argument("--china", action="store_true", help="Force use China mirror (Tsinghua)")
|
|
||||||
parser.add_argument("--no-mirror", action="store_true", help="Force use default PyPI (no mirror)")
|
|
||||||
parser.add_argument(
|
|
||||||
"--skip-deps", action="store_true", help="Skip pip dependencies installation (only install unilabos)"
|
|
||||||
)
|
|
||||||
parser.add_argument("--use-pip", action="store_true", help="Use pip instead of uv for dependencies")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Determine project root
|
|
||||||
script_dir = Path(__file__).parent
|
|
||||||
project_root = script_dir.parent
|
|
||||||
requirements_file = project_root / "unilabos" / "utils" / "requirements.txt"
|
|
||||||
|
|
||||||
if not (project_root / "setup.py").exists():
|
|
||||||
print(f"[ERROR] setup.py not found in {project_root}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("UniLabOS Development Installation")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Project root: {project_root}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Determine mirror usage based on locale
|
|
||||||
if args.no_mirror:
|
|
||||||
use_mirror = False
|
|
||||||
print("[INFO] Mirror disabled by --no-mirror flag")
|
|
||||||
elif args.china:
|
|
||||||
use_mirror = True
|
|
||||||
print("[INFO] China mirror enabled by --china flag")
|
|
||||||
else:
|
|
||||||
use_mirror = is_chinese_locale()
|
|
||||||
if use_mirror:
|
|
||||||
print("[INFO] Chinese locale detected, using Tsinghua mirror")
|
|
||||||
else:
|
|
||||||
print("[INFO] Non-Chinese locale detected, using default PyPI")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Step 1: Install unilabos in editable mode
|
|
||||||
print("[STEP 1] Installing unilabos in editable mode...")
|
|
||||||
if not install_editable(project_root, use_mirror):
|
|
||||||
print("[ERROR] Failed to install unilabos")
|
|
||||||
print()
|
|
||||||
print("Manual fallback:")
|
|
||||||
if use_mirror:
|
|
||||||
print(f" pip install -e {project_root} -i {TSINGHUA_MIRROR}")
|
|
||||||
else:
|
|
||||||
print(f" pip install -e {project_root}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Step 2: Install pip dependencies
|
|
||||||
if args.skip_deps:
|
|
||||||
print("[INFO] Skipping pip dependencies installation (--skip-deps)")
|
|
||||||
else:
|
|
||||||
print("[STEP 2] Installing pip dependencies...")
|
|
||||||
|
|
||||||
if not requirements_file.exists():
|
|
||||||
print(f"[WARN] Requirements file not found: {requirements_file}")
|
|
||||||
print("[INFO] Skipping dependencies installation")
|
|
||||||
else:
|
|
||||||
# Try uv first (faster), fallback to pip
|
|
||||||
if args.use_pip:
|
|
||||||
print("[INFO] Using pip (--use-pip flag)")
|
|
||||||
success = install_requirements_pip(requirements_file, use_mirror)
|
|
||||||
elif check_uv_available():
|
|
||||||
print("[INFO] Using uv (installed via conda-forge::uv)")
|
|
||||||
success = install_requirements_uv(requirements_file, use_mirror)
|
|
||||||
if not success:
|
|
||||||
print("[WARN] uv failed, falling back to pip...")
|
|
||||||
success = install_requirements_pip(requirements_file, use_mirror)
|
|
||||||
else:
|
|
||||||
print("[WARN] uv not available (should be installed via: mamba install conda-forge::uv)")
|
|
||||||
print("[INFO] Falling back to pip...")
|
|
||||||
success = install_requirements_pip(requirements_file, use_mirror)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
print()
|
|
||||||
print("[WARN] Failed to install some dependencies automatically.")
|
|
||||||
print("You can manually install them:")
|
|
||||||
if use_mirror:
|
|
||||||
print(f" uv pip install -r {requirements_file} -i {TSINGHUA_MIRROR}")
|
|
||||||
print(" or:")
|
|
||||||
print(f" pip install -r {requirements_file} -i {TSINGHUA_MIRROR}")
|
|
||||||
else:
|
|
||||||
print(f" uv pip install -r {requirements_file}")
|
|
||||||
print(" or:")
|
|
||||||
print(f" pip install -r {requirements_file}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=" * 60)
|
|
||||||
print("Installation complete!")
|
|
||||||
print("=" * 60)
|
|
||||||
print()
|
|
||||||
print("Note: Some special packages (like pylabrobot) are installed")
|
|
||||||
print("automatically at runtime by unilabos if needed.")
|
|
||||||
print()
|
|
||||||
print("Verify installation:")
|
|
||||||
print(' python -c "import unilabos; print(unilabos.__version__)"')
|
|
||||||
print()
|
|
||||||
print("If you encounter issues, you can manually install dependencies:")
|
|
||||||
if use_mirror:
|
|
||||||
print(f" uv pip install -r unilabos/utils/requirements.txt -i {TSINGHUA_MIRROR}")
|
|
||||||
else:
|
|
||||||
print(" uv pip install -r unilabos/utils/requirements.txt")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
@@ -24,15 +25,7 @@ class SimpleGraph:
|
|||||||
|
|
||||||
def add_edge(self, source, target, **attrs):
|
def add_edge(self, source, target, **attrs):
|
||||||
"""添加边"""
|
"""添加边"""
|
||||||
# edge = {"source": source, "target": target, **attrs}
|
edge = {"source": source, "target": target, **attrs}
|
||||||
edge = {
|
|
||||||
"source": source, "target": target,
|
|
||||||
"source_node_uuid": source,
|
|
||||||
"target_node_uuid": target,
|
|
||||||
"source_handle_io": "source",
|
|
||||||
"target_handle_io": "target",
|
|
||||||
**attrs
|
|
||||||
}
|
|
||||||
self.edges.append(edge)
|
self.edges.append(edge)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@@ -49,7 +42,6 @@ class SimpleGraph:
|
|||||||
"multigraph": False,
|
"multigraph": False,
|
||||||
"graph": {},
|
"graph": {},
|
||||||
"nodes": nodes_list,
|
"nodes": nodes_list,
|
||||||
"edges": self.edges,
|
|
||||||
"links": self.edges,
|
"links": self.edges,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +58,495 @@ def extract_json_from_markdown(text: str) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_type(val: str) -> Any:
|
||||||
|
"""将字符串值转换为适当的数据类型"""
|
||||||
|
if val == "True":
|
||||||
|
return True
|
||||||
|
if val == "False":
|
||||||
|
return False
|
||||||
|
if val == "?":
|
||||||
|
return None
|
||||||
|
if val.endswith(" g"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
if val.endswith("mg"):
|
||||||
|
return float(val.split("mg")[0])
|
||||||
|
elif val.endswith("mmol"):
|
||||||
|
return float(val.split("mmol")[0]) / 1000
|
||||||
|
elif val.endswith("mol"):
|
||||||
|
return float(val.split("mol")[0])
|
||||||
|
elif val.endswith("ml"):
|
||||||
|
return float(val.split("ml")[0])
|
||||||
|
elif val.endswith("RPM"):
|
||||||
|
return float(val.split("RPM")[0])
|
||||||
|
elif val.endswith(" °C"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
elif val.endswith(" %"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""统一的数据重构函数,根据操作类型自动选择模板"""
|
||||||
|
refactored_data = []
|
||||||
|
|
||||||
|
# 定义操作映射,包含生物实验和有机化学的所有操作
|
||||||
|
OPERATION_MAPPING = {
|
||||||
|
# 生物实验操作
|
||||||
|
"transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid",
|
||||||
|
"transfer": "SynBioFactory-liquid_handler.biomek-transfer",
|
||||||
|
"incubation": "SynBioFactory-liquid_handler.biomek-incubation",
|
||||||
|
"move_labware": "SynBioFactory-liquid_handler.biomek-move_labware",
|
||||||
|
"oscillation": "SynBioFactory-liquid_handler.biomek-oscillation",
|
||||||
|
# 有机化学操作
|
||||||
|
"HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol",
|
||||||
|
"StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol",
|
||||||
|
"StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol",
|
||||||
|
"HeatChill": "SynBioFactory-workstation-HeatChillProtocol",
|
||||||
|
"Dissolve": "SynBioFactory-workstation-DissolveProtocol",
|
||||||
|
"Transfer": "SynBioFactory-workstation-TransferProtocol",
|
||||||
|
"Evaporate": "SynBioFactory-workstation-EvaporateProtocol",
|
||||||
|
"Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol",
|
||||||
|
"Filter": "SynBioFactory-workstation-FilterProtocol",
|
||||||
|
"Dry": "SynBioFactory-workstation-DryProtocol",
|
||||||
|
"Add": "SynBioFactory-workstation-AddProtocol",
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
|
||||||
|
|
||||||
|
for step in data:
|
||||||
|
operation = step.get("action")
|
||||||
|
if not operation or operation in UNSUPPORTED_OPERATIONS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理重复操作
|
||||||
|
if operation == "Repeat":
|
||||||
|
times = step.get("times", step.get("parameters", {}).get("times", 1))
|
||||||
|
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
|
||||||
|
for i in range(int(times)):
|
||||||
|
sub_data = refactor_data(sub_steps)
|
||||||
|
refactored_data.extend(sub_data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 获取模板名称
|
||||||
|
template = OPERATION_MAPPING.get(operation)
|
||||||
|
if not template:
|
||||||
|
# 自动推断模板类型
|
||||||
|
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
|
||||||
|
template = f"SynBioFactory-liquid_handler.biomek-{operation}"
|
||||||
|
else:
|
||||||
|
template = f"SynBioFactory-workstation-{operation}Protocol"
|
||||||
|
|
||||||
|
# 创建步骤数据
|
||||||
|
step_data = {
|
||||||
|
"template": template,
|
||||||
|
"description": step.get("description", step.get("purpose", f"{operation} operation")),
|
||||||
|
"lab_node_type": "Device",
|
||||||
|
"parameters": step.get("parameters", step.get("action_args", {})),
|
||||||
|
}
|
||||||
|
refactored_data.append(step_data)
|
||||||
|
|
||||||
|
return refactored_data
|
||||||
|
|
||||||
|
|
||||||
|
def build_protocol_graph(
|
||||||
|
labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str
|
||||||
|
) -> SimpleGraph:
|
||||||
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑"""
|
||||||
|
G = SimpleGraph()
|
||||||
|
resource_last_writer = {}
|
||||||
|
LAB_NAME = "SynBioFactory"
|
||||||
|
|
||||||
|
protocol_steps = refactor_data(protocol_steps)
|
||||||
|
|
||||||
|
# 检查协议步骤中的模板来判断协议类型
|
||||||
|
has_biomek_template = any(
|
||||||
|
("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", ""))
|
||||||
|
for step in protocol_steps
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_biomek_template:
|
||||||
|
# 生物实验协议图构建
|
||||||
|
for labware_id, labware in labware_info.items():
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
labware_attrs = labware.copy()
|
||||||
|
labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}"))
|
||||||
|
labware_attrs["description"] = labware_id
|
||||||
|
labware_attrs["lab_node_type"] = (
|
||||||
|
"Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample"
|
||||||
|
)
|
||||||
|
labware_attrs["device_id"] = workstation_name
|
||||||
|
|
||||||
|
G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs)
|
||||||
|
resource_last_writer[labware_id] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
# 处理协议步骤
|
||||||
|
prev_node = None
|
||||||
|
for i, step in enumerate(protocol_steps):
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
G.add_node(node_id, **step)
|
||||||
|
|
||||||
|
# 添加控制流边
|
||||||
|
if prev_node is not None:
|
||||||
|
G.add_edge(prev_node, node_id, source_port="ready", target_port="ready")
|
||||||
|
prev_node = node_id
|
||||||
|
|
||||||
|
# 处理物料流
|
||||||
|
params = step.get("parameters", {})
|
||||||
|
if "sources" in params and params["sources"] in resource_last_writer:
|
||||||
|
source_node, source_port = resource_last_writer[params["sources"]].split(":")
|
||||||
|
G.add_edge(source_node, node_id, source_port=source_port, target_port="labware")
|
||||||
|
|
||||||
|
if "targets" in params:
|
||||||
|
resource_last_writer[params["targets"]] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
# 添加协议结束节点
|
||||||
|
end_id = str(uuid.uuid4())
|
||||||
|
G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol")
|
||||||
|
if prev_node is not None:
|
||||||
|
G.add_edge(prev_node, end_id, source_port="ready", target_port="ready")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 有机化学协议图构建
|
||||||
|
WORKSTATION_ID = workstation_name
|
||||||
|
|
||||||
|
# 为所有labware创建资源节点
|
||||||
|
for item_id, item in labware_info.items():
|
||||||
|
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 判断节点类型
|
||||||
|
if item.get("type") == "hardware" or "reactor" in str(item_id).lower():
|
||||||
|
if "reactor" not in str(item_id).lower():
|
||||||
|
continue
|
||||||
|
lab_node_type = "Sample"
|
||||||
|
description = f"Prepare Reactor: {item_id}"
|
||||||
|
liquid_type = []
|
||||||
|
liquid_volume = []
|
||||||
|
else:
|
||||||
|
lab_node_type = "Reagent"
|
||||||
|
description = f"Add Reagent to Flask: {item_id}"
|
||||||
|
liquid_type = [item_id]
|
||||||
|
liquid_volume = [1e5]
|
||||||
|
|
||||||
|
G.add_node(
|
||||||
|
node_id,
|
||||||
|
template=f"{LAB_NAME}-host_node-create_resource",
|
||||||
|
description=description,
|
||||||
|
lab_node_type=lab_node_type,
|
||||||
|
res_id=item_id,
|
||||||
|
device_id=WORKSTATION_ID,
|
||||||
|
class_name="container",
|
||||||
|
parent=WORKSTATION_ID,
|
||||||
|
bind_locations={"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
|
liquid_input_slot=[-1],
|
||||||
|
liquid_type=liquid_type,
|
||||||
|
liquid_volume=liquid_volume,
|
||||||
|
slot_on_deck="",
|
||||||
|
role=item.get("role", ""),
|
||||||
|
)
|
||||||
|
resource_last_writer[item_id] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
last_control_node_id = None
|
||||||
|
|
||||||
|
# 处理协议步骤
|
||||||
|
for step in protocol_steps:
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
G.add_node(node_id, **step)
|
||||||
|
|
||||||
|
# 控制流
|
||||||
|
if last_control_node_id is not None:
|
||||||
|
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
||||||
|
last_control_node_id = node_id
|
||||||
|
|
||||||
|
# 物料流
|
||||||
|
params = step.get("parameters", {})
|
||||||
|
input_resources = {
|
||||||
|
"Vessel": params.get("vessel"),
|
||||||
|
"ToVessel": params.get("to_vessel"),
|
||||||
|
"FromVessel": params.get("from_vessel"),
|
||||||
|
"reagent": params.get("reagent"),
|
||||||
|
"solvent": params.get("solvent"),
|
||||||
|
"compound": params.get("compound"),
|
||||||
|
"sources": params.get("sources"),
|
||||||
|
"targets": params.get("targets"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for target_port, resource_name in input_resources.items():
|
||||||
|
if resource_name and resource_name in resource_last_writer:
|
||||||
|
source_node, source_port = resource_last_writer[resource_name].split(":")
|
||||||
|
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
|
||||||
|
|
||||||
|
output_resources = {
|
||||||
|
"VesselOut": params.get("vessel"),
|
||||||
|
"FromVesselOut": params.get("from_vessel"),
|
||||||
|
"ToVesselOut": params.get("to_vessel"),
|
||||||
|
"FiltrateOut": params.get("filtrate_vessel"),
|
||||||
|
"reagent": params.get("reagent"),
|
||||||
|
"solvent": params.get("solvent"),
|
||||||
|
"compound": params.get("compound"),
|
||||||
|
"sources_out": params.get("sources"),
|
||||||
|
"targets_out": params.get("targets"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for source_port, resource_name in output_resources.items():
|
||||||
|
if resource_name:
|
||||||
|
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
|
||||||
|
|
||||||
|
return G
|
||||||
|
|
||||||
|
|
||||||
|
def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str):
|
||||||
|
"""
|
||||||
|
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
|
||||||
|
"""
|
||||||
|
if not protocol_graph:
|
||||||
|
print("Cannot draw graph: Graph object is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
G = nx.DiGraph()
|
||||||
|
|
||||||
|
for node_id, attrs in protocol_graph.nodes.items():
|
||||||
|
label = attrs.get("description", attrs.get("template", node_id[:8]))
|
||||||
|
G.add_node(node_id, label=label, **attrs)
|
||||||
|
|
||||||
|
for edge in protocol_graph.edges:
|
||||||
|
G.add_edge(edge["source"], edge["target"])
|
||||||
|
|
||||||
|
plt.figure(figsize=(20, 15))
|
||||||
|
try:
|
||||||
|
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
|
||||||
|
except Exception:
|
||||||
|
pos = nx.shell_layout(G) # Fallback layout
|
||||||
|
|
||||||
|
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
|
||||||
|
nx.draw(
|
||||||
|
G,
|
||||||
|
pos,
|
||||||
|
with_labels=False,
|
||||||
|
node_size=2500,
|
||||||
|
node_color="skyblue",
|
||||||
|
node_shape="o",
|
||||||
|
edge_color="gray",
|
||||||
|
width=1.5,
|
||||||
|
arrowsize=15,
|
||||||
|
)
|
||||||
|
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
|
||||||
|
|
||||||
|
plt.title("Chemical Protocol Workflow Graph", size=15)
|
||||||
|
plt.savefig(output_path, dpi=300, bbox_inches="tight")
|
||||||
|
plt.close()
|
||||||
|
print(f" - Visualization saved to '{output_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
from networkx.drawing.nx_agraph import to_agraph
|
||||||
|
import re
|
||||||
|
|
||||||
|
COMPASS = {"n","e","s","w","ne","nw","se","sw","c"}
|
||||||
|
|
||||||
|
def _is_compass(port: str) -> bool:
|
||||||
|
return isinstance(port, str) and port.lower() in COMPASS
|
||||||
|
|
||||||
|
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
|
||||||
|
"""
|
||||||
|
使用 Graphviz 端口语法绘制协议工作流图。
|
||||||
|
- 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。
|
||||||
|
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
|
||||||
|
最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。
|
||||||
|
"""
|
||||||
|
if not protocol_graph:
|
||||||
|
print("Cannot draw graph: Graph object is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1) 先用 networkx 搭建有向图,保留端口属性
|
||||||
|
G = nx.DiGraph()
|
||||||
|
for node_id, attrs in protocol_graph.nodes.items():
|
||||||
|
label = attrs.get("description", attrs.get("template", node_id[:8]))
|
||||||
|
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
|
||||||
|
G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)})
|
||||||
|
|
||||||
|
edges_data = []
|
||||||
|
in_ports_by_node = {} # 收集命名输入端口
|
||||||
|
out_ports_by_node = {} # 收集命名输出端口
|
||||||
|
|
||||||
|
for edge in protocol_graph.edges:
|
||||||
|
u = edge["source"]
|
||||||
|
v = edge["target"]
|
||||||
|
sp = edge.get("source_port")
|
||||||
|
tp = edge.get("target_port")
|
||||||
|
|
||||||
|
# 记录到图里(保留原始端口信息)
|
||||||
|
G.add_edge(u, v, source_port=sp, target_port=tp)
|
||||||
|
edges_data.append((u, v, sp, tp))
|
||||||
|
|
||||||
|
# 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record
|
||||||
|
if sp and not _is_compass(sp):
|
||||||
|
out_ports_by_node.setdefault(u, set()).add(str(sp))
|
||||||
|
if tp and not _is_compass(tp):
|
||||||
|
in_ports_by_node.setdefault(v, set()).add(str(tp))
|
||||||
|
|
||||||
|
# 2) 转为 AGraph,使用 Graphviz 渲染
|
||||||
|
A = to_agraph(G)
|
||||||
|
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
|
||||||
|
A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica")
|
||||||
|
A.edge_attr.update(arrowsize="0.8", color="#666666")
|
||||||
|
|
||||||
|
# 3) 为需要命名端口的节点设置 record 形状与 label
|
||||||
|
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
|
||||||
|
for n in A.nodes():
|
||||||
|
node = A.get_node(n)
|
||||||
|
core = G.nodes[n].get("_core_label", n)
|
||||||
|
|
||||||
|
in_ports = sorted(in_ports_by_node.get(n, []))
|
||||||
|
out_ports = sorted(out_ports_by_node.get(n, []))
|
||||||
|
|
||||||
|
# 如果该节点涉及命名端口,则用 record;否则保留原 box
|
||||||
|
if in_ports or out_ports:
|
||||||
|
def port_fields(ports):
|
||||||
|
if not ports:
|
||||||
|
return " " # 必须留一个空槽占位
|
||||||
|
# 每个端口一个小格子,<p> name
|
||||||
|
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
|
||||||
|
|
||||||
|
left = port_fields(in_ports)
|
||||||
|
right = port_fields(out_ports)
|
||||||
|
|
||||||
|
# 三栏:左(入) | 中(节点名) | 右(出)
|
||||||
|
record_label = f"{{ {left} | {core} | {right} }}"
|
||||||
|
node.attr.update(shape="record", label=record_label)
|
||||||
|
else:
|
||||||
|
# 没有命名端口:普通盒子,显示核心标签
|
||||||
|
node.attr.update(label=str(core))
|
||||||
|
|
||||||
|
# 4) 给边设置 headport / tailport
|
||||||
|
# - 若端口为 compass:直接用 compass(e.g., headport="e")
|
||||||
|
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
|
||||||
|
for (u, v, sp, tp) in edges_data:
|
||||||
|
e = A.get_edge(u, v)
|
||||||
|
|
||||||
|
# Graphviz 属性:tail 是源,head 是目标
|
||||||
|
if sp:
|
||||||
|
if _is_compass(sp):
|
||||||
|
e.attr["tailport"] = sp.lower()
|
||||||
|
else:
|
||||||
|
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
|
||||||
|
e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp))
|
||||||
|
|
||||||
|
if tp:
|
||||||
|
if _is_compass(tp):
|
||||||
|
e.attr["headport"] = tp.lower()
|
||||||
|
else:
|
||||||
|
e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp))
|
||||||
|
|
||||||
|
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
|
||||||
|
# e.attr["arrowhead"] = "vee"
|
||||||
|
|
||||||
|
# 5) 输出
|
||||||
|
A.draw(output_path, prog="dot")
|
||||||
|
print(f" - Port-aware workflow rendered to '{output_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]:
|
||||||
|
"""展平嵌套的XDL程序结构"""
|
||||||
|
flattened_operations = []
|
||||||
|
TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"]
|
||||||
|
|
||||||
|
def extract_operations(element: ET.Element):
|
||||||
|
if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]:
|
||||||
|
if element.tag not in TEMP_UNSUPPORTED_PROTOCOL:
|
||||||
|
flattened_operations.append(element)
|
||||||
|
|
||||||
|
for child in element:
|
||||||
|
extract_operations(child)
|
||||||
|
|
||||||
|
for child in procedure_elem:
|
||||||
|
extract_operations(child)
|
||||||
|
|
||||||
|
return flattened_operations
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xdl_content(xdl_content: str) -> tuple:
|
||||||
|
"""解析XDL内容"""
|
||||||
|
try:
|
||||||
|
xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable())
|
||||||
|
root = ET.fromstring(xdl_content_cleaned)
|
||||||
|
|
||||||
|
synthesis_elem = root.find("Synthesis")
|
||||||
|
if synthesis_elem is None:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# 解析硬件组件
|
||||||
|
hardware_elem = synthesis_elem.find("Hardware")
|
||||||
|
hardware = []
|
||||||
|
if hardware_elem is not None:
|
||||||
|
hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")]
|
||||||
|
|
||||||
|
# 解析试剂
|
||||||
|
reagents_elem = synthesis_elem.find("Reagents")
|
||||||
|
reagents = []
|
||||||
|
if reagents_elem is not None:
|
||||||
|
reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")]
|
||||||
|
|
||||||
|
# 解析程序
|
||||||
|
procedure_elem = synthesis_elem.find("Procedure")
|
||||||
|
if procedure_elem is None:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
flattened_operations = flatten_xdl_procedure(procedure_elem)
|
||||||
|
return hardware, reagents, flattened_operations
|
||||||
|
|
||||||
|
except ET.ParseError as e:
|
||||||
|
raise ValueError(f"Invalid XDL format: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
将XDL XML格式转换为标准的字典格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xdl_content: XDL XML内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
转换结果,包含步骤和器材信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hardware, reagents, flattened_operations = parse_xdl_content(xdl_content)
|
||||||
|
if hardware is None:
|
||||||
|
return {"error": "Failed to parse XDL content", "success": False}
|
||||||
|
|
||||||
|
# 将XDL元素转换为字典格式
|
||||||
|
steps_data = []
|
||||||
|
for elem in flattened_operations:
|
||||||
|
# 转换参数类型
|
||||||
|
parameters = {}
|
||||||
|
for key, val in elem.attrib.items():
|
||||||
|
converted_val = convert_to_type(val)
|
||||||
|
if converted_val is not None:
|
||||||
|
parameters[key] = converted_val
|
||||||
|
|
||||||
|
step_dict = {
|
||||||
|
"operation": elem.tag,
|
||||||
|
"parameters": parameters,
|
||||||
|
"description": elem.get("purpose", f"Operation: {elem.tag}"),
|
||||||
|
}
|
||||||
|
steps_data.append(step_dict)
|
||||||
|
|
||||||
|
# 合并硬件和试剂为统一的labware_info格式
|
||||||
|
labware_data = []
|
||||||
|
labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware)
|
||||||
|
labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"steps": steps_data,
|
||||||
|
"labware": labware_data,
|
||||||
|
"message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"XDL conversion failed: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
|
|
||||||
|
|
||||||
def create_workflow(
|
def create_workflow(
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.10.19',
|
version='0.10.14',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Liquid handling 集成测试
|
|
||||||
|
|
||||||
`test_transfer_liquid.py` 现在会调用 PRCXI 的 RViz 仿真 backend,运行前请确保:
|
|
||||||
|
|
||||||
1. 已安装包含 `pylabrobot`、`rclpy` 的运行环境;
|
|
||||||
2. 启动 ROS 依赖(`rviz` 可选,但是 `rviz_backend` 会创建 ROS 节点);
|
|
||||||
3. 在 shell 中设置 `UNILAB_SIM_TEST=1`,否则 pytest 会自动跳过这些慢速用例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export UNILAB_SIM_TEST=1
|
|
||||||
pytest tests/devices/liquid_handling/test_transfer_liquid.py -m slow
|
|
||||||
```
|
|
||||||
|
|
||||||
如果只需验证逻辑层(不依赖仿真),可以直接运行 `tests/devices/liquid_handling/unit_test.py`,该文件使用 Fake backend,适合作为 CI 的快速测试。***
|
|
||||||
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DummyContainer:
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma: no cover
|
|
||||||
return f"DummyContainer({self.name})"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DummyTipSpot:
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma: no cover
|
|
||||||
return f"DummyTipSpot({self.name})"
|
|
||||||
|
|
||||||
|
|
||||||
def make_tip_iter(n: int = 256) -> Iterable[List[DummyTipSpot]]:
|
|
||||||
"""Yield lists so code can safely call `tip.extend(next(self.current_tip))`."""
|
|
||||||
for i in range(n):
|
|
||||||
yield [DummyTipSpot(f"tip_{i}")]
|
|
||||||
|
|
||||||
|
|
||||||
class FakeLiquidHandler(LiquidHandlerAbstract):
|
|
||||||
"""不初始化真实 backend/deck;仅用来记录 transfer_liquid 内部调用序列。"""
|
|
||||||
|
|
||||||
def __init__(self, channel_num: int = 8):
|
|
||||||
# 不调用 super().__init__,避免真实硬件/后端依赖
|
|
||||||
self.channel_num = channel_num
|
|
||||||
self.support_touch_tip = True
|
|
||||||
self.current_tip = iter(make_tip_iter())
|
|
||||||
self.calls: List[Tuple[str, Any]] = []
|
|
||||||
|
|
||||||
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **backend_kwargs):
|
|
||||||
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
|
|
||||||
|
|
||||||
async def aspirate(
|
|
||||||
self,
|
|
||||||
resources: Sequence[Any],
|
|
||||||
vols: List[float],
|
|
||||||
use_channels: Optional[List[int]] = None,
|
|
||||||
flow_rates: Optional[List[Optional[float]]] = None,
|
|
||||||
offsets: Any = None,
|
|
||||||
liquid_height: Any = None,
|
|
||||||
blow_out_air_volume: Any = None,
|
|
||||||
spread: str = "wide",
|
|
||||||
**backend_kwargs,
|
|
||||||
):
|
|
||||||
self.calls.append(
|
|
||||||
(
|
|
||||||
"aspirate",
|
|
||||||
{
|
|
||||||
"resources": list(resources),
|
|
||||||
"vols": list(vols),
|
|
||||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
|
||||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
|
||||||
"offsets": list(offsets) if offsets is not None else None,
|
|
||||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
|
||||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def dispense(
|
|
||||||
self,
|
|
||||||
resources: Sequence[Any],
|
|
||||||
vols: List[float],
|
|
||||||
use_channels: Optional[List[int]] = None,
|
|
||||||
flow_rates: Optional[List[Optional[float]]] = None,
|
|
||||||
offsets: Any = None,
|
|
||||||
liquid_height: Any = None,
|
|
||||||
blow_out_air_volume: Any = None,
|
|
||||||
spread: str = "wide",
|
|
||||||
**backend_kwargs,
|
|
||||||
):
|
|
||||||
self.calls.append(
|
|
||||||
(
|
|
||||||
"dispense",
|
|
||||||
{
|
|
||||||
"resources": list(resources),
|
|
||||||
"vols": list(vols),
|
|
||||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
|
||||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
|
||||||
"offsets": list(offsets) if offsets is not None else None,
|
|
||||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
|
||||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def discard_tips(self, use_channels=None, *args, **kwargs):
|
|
||||||
# 有的分支是 discard_tips(use_channels=[0]),有的分支是 discard_tips([0..7])(位置参数)
|
|
||||||
self.calls.append(("discard_tips", {"use_channels": list(use_channels) if use_channels is not None else None}))
|
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
|
||||||
self.calls.append(("custom_delay", {"seconds": seconds, "msg": msg}))
|
|
||||||
|
|
||||||
async def touch_tip(self, targets):
|
|
||||||
# 原实现会访问 targets.get_size_x() 等;测试里只记录调用
|
|
||||||
self.calls.append(("touch_tip", {"targets": targets}))
|
|
||||||
|
|
||||||
def run(coro):
|
|
||||||
return asyncio.run(coro)
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_single_channel_basic_calls():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[1, 2, 3],
|
|
||||||
dis_vols=[4, 5, 6],
|
|
||||||
mix_times=None, # 应该仍能执行(不 mix)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("aspirate") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("dispense") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("discard_tips") == 3
|
|
||||||
|
|
||||||
# 每次 aspirate/dispense 都是单孔列表
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert aspirates[0]["resources"] == [sources[0]]
|
|
||||||
assert aspirates[0]["vols"] == [1.0]
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert dispenses[2]["resources"] == [targets[2]]
|
|
||||||
assert dispenses[2]["vols"] == [6.0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_single_channel_before_stage_mixes_prior_to_aspirate():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(16))
|
|
||||||
|
|
||||||
source = DummyContainer("S0")
|
|
||||||
target = DummyContainer("T0")
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[5],
|
|
||||||
dis_vols=[5],
|
|
||||||
mix_stage="before",
|
|
||||||
mix_times=1,
|
|
||||||
mix_vol=3,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
|
||||||
assert len(aspirate_calls) >= 2
|
|
||||||
mix_idx, mix_payload = aspirate_calls[0]
|
|
||||||
assert mix_payload["resources"] == [target]
|
|
||||||
assert mix_payload["vols"] == [3]
|
|
||||||
transfer_idx, transfer_payload = aspirate_calls[1]
|
|
||||||
assert transfer_payload["resources"] == [source]
|
|
||||||
assert mix_idx < transfer_idx
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_groups_by_8():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(256))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
|
||||||
asp_vols = list(range(1, 17))
|
|
||||||
dis_vols = list(range(101, 117))
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0, # 触发逻辑但不 mix
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 16 个任务 -> 2 组,每组 8 通道一起做
|
|
||||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 2
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == 2
|
|
||||||
assert len(dispenses) == 2
|
|
||||||
|
|
||||||
assert aspirates[0]["resources"] == sources[0:8]
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols[0:8]]
|
|
||||||
assert dispenses[1]["resources"] == targets[8:16]
|
|
||||||
assert dispenses[1]["vols"] == [float(v) for v in dis_vols[8:16]]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_requires_multiple_of_8_targets():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(9)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(9)]
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="multiple of 8"):
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=[1] * 9,
|
|
||||||
dis_vols=[1] * 9,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_parameter_lists_are_chunked_per_8():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(512))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
|
||||||
asp_vols = [i + 1 for i in range(16)]
|
|
||||||
dis_vols = [200 + i for i in range(16)]
|
|
||||||
asp_flow_rates = [0.1 * (i + 1) for i in range(16)]
|
|
||||||
dis_flow_rates = [0.2 * (i + 1) for i in range(16)]
|
|
||||||
offsets = [f"offset_{i}" for i in range(16)]
|
|
||||||
liquid_heights = [i * 0.5 for i in range(16)]
|
|
||||||
blow_out_air_volume = [i + 0.05 for i in range(16)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
asp_flow_rates=asp_flow_rates,
|
|
||||||
dis_flow_rates=dis_flow_rates,
|
|
||||||
offsets=offsets,
|
|
||||||
liquid_height=liquid_heights,
|
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == len(dispenses) == 2
|
|
||||||
|
|
||||||
for batch_idx in range(2):
|
|
||||||
start = batch_idx * 8
|
|
||||||
end = start + 8
|
|
||||||
asp_call = aspirates[batch_idx]
|
|
||||||
dis_call = dispenses[batch_idx]
|
|
||||||
assert asp_call["resources"] == sources[start:end]
|
|
||||||
assert asp_call["flow_rates"] == asp_flow_rates[start:end]
|
|
||||||
assert asp_call["offsets"] == offsets[start:end]
|
|
||||||
assert asp_call["liquid_height"] == liquid_heights[start:end]
|
|
||||||
assert asp_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
|
||||||
assert dis_call["flow_rates"] == dis_flow_rates[start:end]
|
|
||||||
assert dis_call["offsets"] == offsets[start:end]
|
|
||||||
assert dis_call["liquid_height"] == liquid_heights[start:end]
|
|
||||||
assert dis_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_handles_32_tasks_four_batches():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(1024))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(32)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(32)]
|
|
||||||
asp_vols = [i + 1 for i in range(32)]
|
|
||||||
dis_vols = [300 + i for i in range(32)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pick_calls = [name for name, _ in lh.calls if name == "pick_up_tips"]
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(pick_calls) == 4
|
|
||||||
assert len(aspirates) == len(dispenses) == 4
|
|
||||||
assert aspirates[0]["resources"] == sources[0:8]
|
|
||||||
assert aspirates[-1]["resources"] == sources[24:32]
|
|
||||||
assert dispenses[0]["resources"] == targets[0:8]
|
|
||||||
assert dispenses[-1]["resources"] == targets[24:32]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_many_single_channel_aspirates_total_when_asp_vol_too_small():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
source = DummyContainer("SRC")
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
|
||||||
dis_vols = [10, 20, 30] # sum=60
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=10, # 小于 sum(dis_vols) -> 应吸 60
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert len(aspirates) == 1
|
|
||||||
assert aspirates[0]["resources"] == [source]
|
|
||||||
assert aspirates[0]["vols"] == [60.0]
|
|
||||||
assert aspirates[0]["use_channels"] == [0]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [10.0, 20.0, 30.0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_many_eight_channel_basic():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
source = DummyContainer("SRC")
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(8)]
|
|
||||||
dis_vols = [i + 1 for i in range(8)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=999, # one-to-many 8ch 会按 dis_vols 吸(每通道各自)
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert aspirates[0]["resources"] == [source] * 8
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in dis_vols]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert dispenses[0]["resources"] == targets
|
|
||||||
assert dispenses[0]["vols"] == [float(v) for v in dis_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_standard_dispense_equals_asp_by_default():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [5, 6, 7]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=1, # many-to-one 允许标量;非比例模式下实际每次分液=对应 asp_vol
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in asp_vols]
|
|
||||||
assert all(d["resources"] == [target] for d in dispenses)
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_before_stage_mixes_target_once():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[5, 6],
|
|
||||||
dis_vols=1,
|
|
||||||
mix_stage="before",
|
|
||||||
mix_times=2,
|
|
||||||
mix_vol=4,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
|
||||||
assert len(aspirate_calls) >= 1
|
|
||||||
mix_idx, mix_payload = aspirate_calls[0]
|
|
||||||
assert mix_payload["resources"] == [target]
|
|
||||||
assert mix_payload["vols"] == [4]
|
|
||||||
# 第一個 mix 之後會真正開始吸 source
|
|
||||||
assert any(call["resources"] == [sources[0]] for _, call in aspirate_calls[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_proportional_mixing_uses_dis_vols_per_source():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [5, 6, 7]
|
|
||||||
dis_vols = [1, 2, 3]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols, # 比例模式
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in dis_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_eight_channel_basic():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(256))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(8)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [10 + i for i in range(8)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=999, # 非比例模式下每通道分液=对应 asp_vol
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert aspirates[0]["resources"] == sources
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols]
|
|
||||||
assert dispenses[0]["resources"] == [target] * 8
|
|
||||||
assert dispenses[0]["vols"] == [float(v) for v in asp_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_transfer_liquid_mode_detection_unsupported_shape_raises():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
|
||||||
targets = [DummyContainer("T0"), DummyContainer("T1"), DummyContainer("T2")]
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Unsupported transfer mode"):
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[1, 1],
|
|
||||||
dis_vols=[1, 1, 1],
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_single_target_produces_matching_cycles():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
target = DummyContainer("T_mix")
|
|
||||||
|
|
||||||
run(lh.mix(targets=[target], mix_time=2, mix_vol=5))
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == len(dispenses) == 2
|
|
||||||
assert all(call["resources"] == [target] for call in aspirates)
|
|
||||||
assert all(call["vols"] == [5] for call in aspirates)
|
|
||||||
assert all(call["resources"] == [target] for call in dispenses)
|
|
||||||
assert all(call["vols"] == [5] for call in dispenses)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_multiple_targets_supports_per_target_offsets():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
targets = [DummyContainer("T0"), DummyContainer("T1")]
|
|
||||||
offsets = ["left", "right"]
|
|
||||||
heights = [0.1, 0.2]
|
|
||||||
rates = [0.5, 1.0]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.mix(
|
|
||||||
targets=targets,
|
|
||||||
mix_time=1,
|
|
||||||
mix_vol=3,
|
|
||||||
offsets=offsets,
|
|
||||||
height_to_bottom=heights,
|
|
||||||
mix_rate=rates,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert len(aspirates) == 2
|
|
||||||
assert aspirates[0]["resources"] == [targets[0]]
|
|
||||||
assert aspirates[0]["offsets"] == [offsets[0]]
|
|
||||||
assert aspirates[0]["liquid_height"] == [heights[0]]
|
|
||||||
assert aspirates[0]["flow_rates"] == [rates[0]]
|
|
||||||
assert aspirates[1]["resources"] == [targets[1]]
|
|
||||||
assert aspirates[1]["offsets"] == [offsets[1]]
|
|
||||||
assert aspirates[1]["liquid_height"] == [heights[1]]
|
|
||||||
assert aspirates[1]["flow_rates"] == [rates[1]]
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,8 +2,9 @@ import pytest
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from pylabrobot.resources import Resource as ResourcePLR
|
||||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
{
|
|
||||||
"workflow": [
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines",
|
|
||||||
"targets": "Liquid_1",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines",
|
|
||||||
"targets": "Liquid_2",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines",
|
|
||||||
"targets": "Liquid_3",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_2",
|
|
||||||
"targets": "Liquid_4",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_2",
|
|
||||||
"targets": "Liquid_5",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_2",
|
|
||||||
"targets": "Liquid_6",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_3",
|
|
||||||
"targets": "dest_set",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_3",
|
|
||||||
"targets": "dest_set_2",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "transfer_liquid",
|
|
||||||
"action_args": {
|
|
||||||
"sources": "cell_lines_3",
|
|
||||||
"targets": "dest_set_3",
|
|
||||||
"asp_vol": 100.0,
|
|
||||||
"dis_vol": 74.75,
|
|
||||||
"asp_flow_rate": 94.0,
|
|
||||||
"dis_flow_rate": 95.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"reagent": {
|
|
||||||
"Liquid_1": {
|
|
||||||
"slot": 1,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A7",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 1"
|
|
||||||
},
|
|
||||||
"Liquid_4": {
|
|
||||||
"slot": 1,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A7",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 1"
|
|
||||||
},
|
|
||||||
"dest_set": {
|
|
||||||
"slot": 1,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A7",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 1"
|
|
||||||
},
|
|
||||||
"Liquid_2": {
|
|
||||||
"slot": 2,
|
|
||||||
"well": [
|
|
||||||
"A3",
|
|
||||||
"A5",
|
|
||||||
"A8"
|
|
||||||
],
|
|
||||||
"labware": "rep 2"
|
|
||||||
},
|
|
||||||
"Liquid_5": {
|
|
||||||
"slot": 2,
|
|
||||||
"well": [
|
|
||||||
"A3",
|
|
||||||
"A5",
|
|
||||||
"A8"
|
|
||||||
],
|
|
||||||
"labware": "rep 2"
|
|
||||||
},
|
|
||||||
"dest_set_2": {
|
|
||||||
"slot": 2,
|
|
||||||
"well": [
|
|
||||||
"A3",
|
|
||||||
"A5",
|
|
||||||
"A8"
|
|
||||||
],
|
|
||||||
"labware": "rep 2"
|
|
||||||
},
|
|
||||||
"Liquid_3": {
|
|
||||||
"slot": 3,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A6",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 3"
|
|
||||||
},
|
|
||||||
"Liquid_6": {
|
|
||||||
"slot": 3,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A6",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 3"
|
|
||||||
},
|
|
||||||
"dest_set_3": {
|
|
||||||
"slot": 3,
|
|
||||||
"well": [
|
|
||||||
"A4",
|
|
||||||
"A6",
|
|
||||||
"A10"
|
|
||||||
],
|
|
||||||
"labware": "rep 3"
|
|
||||||
},
|
|
||||||
"cell_lines": {
|
|
||||||
"slot": 4,
|
|
||||||
"well": [
|
|
||||||
"A1",
|
|
||||||
"A3",
|
|
||||||
"A5"
|
|
||||||
],
|
|
||||||
"labware": "DRUG + YOYO-MEDIA"
|
|
||||||
},
|
|
||||||
"cell_lines_2": {
|
|
||||||
"slot": 4,
|
|
||||||
"well": [
|
|
||||||
"A1",
|
|
||||||
"A3",
|
|
||||||
"A5"
|
|
||||||
],
|
|
||||||
"labware": "DRUG + YOYO-MEDIA"
|
|
||||||
},
|
|
||||||
"cell_lines_3": {
|
|
||||||
"slot": 4,
|
|
||||||
"well": [
|
|
||||||
"A1",
|
|
||||||
"A3",
|
|
||||||
"A5"
|
|
||||||
],
|
|
||||||
"labware": "DRUG + YOYO-MEDIA"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.10.19"
|
__version__ = "0.10.14"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@@ -18,93 +17,9 @@ unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
|||||||
if unilabos_dir not in sys.path:
|
if unilabos_dir not in sys.path:
|
||||||
sys.path.append(unilabos_dir)
|
sys.path.append(unilabos_dir)
|
||||||
|
|
||||||
from unilabos.app.utils import cleanup_for_restart
|
|
||||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||||
|
|
||||||
# Global restart flags (used by ws_client and web/server)
|
|
||||||
_restart_requested: bool = False
|
|
||||||
_restart_reason: str = ""
|
|
||||||
|
|
||||||
RESTART_EXIT_CODE = 42
|
|
||||||
|
|
||||||
|
|
||||||
def _build_child_argv():
|
|
||||||
"""Build sys.argv for child process, stripping supervisor-only arguments."""
|
|
||||||
result = []
|
|
||||||
skip_next = False
|
|
||||||
for arg in sys.argv:
|
|
||||||
if skip_next:
|
|
||||||
skip_next = False
|
|
||||||
continue
|
|
||||||
if arg in ("--restart_mode", "--restart-mode"):
|
|
||||||
continue
|
|
||||||
if arg in ("--auto_restart_count", "--auto-restart-count"):
|
|
||||||
skip_next = True
|
|
||||||
continue
|
|
||||||
if arg.startswith("--auto_restart_count=") or arg.startswith("--auto-restart-count="):
|
|
||||||
continue
|
|
||||||
result.append(arg)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _run_as_supervisor(max_restarts: int):
|
|
||||||
"""
|
|
||||||
Supervisor process that spawns and monitors child processes.
|
|
||||||
|
|
||||||
Similar to Uvicorn's --reload: the supervisor itself does no heavy work,
|
|
||||||
it only launches the real process as a child and restarts it when the child
|
|
||||||
exits with RESTART_EXIT_CODE.
|
|
||||||
"""
|
|
||||||
child_argv = [sys.executable] + _build_child_argv()
|
|
||||||
restart_count = 0
|
|
||||||
|
|
||||||
print_status(
|
|
||||||
f"[Supervisor] Restart mode enabled (max restarts: {max_restarts}), "
|
|
||||||
f"child command: {' '.join(child_argv)}",
|
|
||||||
"info",
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
print_status(
|
|
||||||
f"[Supervisor] Launching process (restart {restart_count}/{max_restarts})...",
|
|
||||||
"info",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
process = subprocess.Popen(child_argv)
|
|
||||||
exit_code = process.wait()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print_status("[Supervisor] Interrupted, terminating child process...", "info")
|
|
||||||
process.terminate()
|
|
||||||
try:
|
|
||||||
process.wait(timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
process.kill()
|
|
||||||
process.wait()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if exit_code == RESTART_EXIT_CODE:
|
|
||||||
restart_count += 1
|
|
||||||
if restart_count > max_restarts:
|
|
||||||
print_status(
|
|
||||||
f"[Supervisor] Maximum restart count ({max_restarts}) reached, exiting",
|
|
||||||
"warning",
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
print_status(
|
|
||||||
f"[Supervisor] Child requested restart ({restart_count}/{max_restarts}), restarting in 2s...",
|
|
||||||
"info",
|
|
||||||
)
|
|
||||||
time.sleep(2)
|
|
||||||
else:
|
|
||||||
if exit_code != 0:
|
|
||||||
print_status(f"[Supervisor] Child exited with code {exit_code}", "warning")
|
|
||||||
else:
|
|
||||||
print_status("[Supervisor] Child exited normally", "info")
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config_from_file(config_path):
|
def load_config_from_file(config_path):
|
||||||
if config_path is 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)
|
||||||
@@ -126,7 +41,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
|||||||
for i, arg in enumerate(sys.argv):
|
for i, arg in enumerate(sys.argv):
|
||||||
for option_string in option_strings:
|
for option_string in option_strings:
|
||||||
if arg.startswith(option_string):
|
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
|
sys.argv[i] = new_arg
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -134,8 +49,6 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
|||||||
def parse_args():
|
def parse_args():
|
||||||
"""解析命令行参数"""
|
"""解析命令行参数"""
|
||||||
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
||||||
subparsers = parser.add_subparsers(title="Valid subcommands", dest="command")
|
|
||||||
|
|
||||||
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
|
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
|
||||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
|
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -145,13 +58,6 @@ def parse_args():
|
|||||||
action="append",
|
action="append",
|
||||||
help="Path to the registry directory",
|
help="Path to the registry directory",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--devices",
|
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
action="append",
|
|
||||||
help="Path to Python code directory for AST-based device/resource scanning",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--working_dir",
|
"--working_dir",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -241,91 +147,11 @@ def parse_args():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Skip environment dependency check on startup",
|
help="Skip environment dependency check on startup",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--check_mode",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Run in check mode for CI: validates registry imports and ensures no file changes",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--complete_registry",
|
"--complete_registry",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Complete and rewrite YAML registry files using AST analysis results",
|
help="Complete registry information",
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no_update_feedback",
|
|
||||||
action="store_true",
|
|
||||||
help="Disable sending update feedback to server",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--test_mode",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--external_devices_only",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Only load external device packages (--devices), skip built-in unilabos/devices/ scanning and YAML device registry",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--extra_resource",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Load extra lab_ prefixed labware resources (529 auto-generated definitions from lab_resources.py)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--restart_mode",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Enable supervisor mode: automatically restart the process when triggered via WebSocket",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--auto_restart_count",
|
|
||||||
type=int,
|
|
||||||
default=500,
|
|
||||||
help="Maximum number of automatic restarts in restart mode (default: 500)",
|
|
||||||
)
|
|
||||||
# workflow upload subcommand
|
|
||||||
workflow_parser = subparsers.add_parser(
|
|
||||||
"workflow_upload",
|
|
||||||
aliases=["wf"],
|
|
||||||
help="Upload workflow from xdl/json/python files",
|
|
||||||
)
|
|
||||||
workflow_parser.add_argument(
|
|
||||||
"-f",
|
|
||||||
"--workflow_file",
|
|
||||||
type=str,
|
|
||||||
required=True,
|
|
||||||
help="Path to the workflow file (JSON format)",
|
|
||||||
)
|
|
||||||
workflow_parser.add_argument(
|
|
||||||
"-n",
|
|
||||||
"--workflow_name",
|
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
help="Workflow name, if not provided will use the name from file or filename",
|
|
||||||
)
|
|
||||||
workflow_parser.add_argument(
|
|
||||||
"--tags",
|
|
||||||
type=str,
|
|
||||||
nargs="*",
|
|
||||||
default=[],
|
|
||||||
help="Tags for the workflow (space-separated)",
|
|
||||||
)
|
|
||||||
workflow_parser.add_argument(
|
|
||||||
"--published",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Whether to publish the workflow (default: False)",
|
|
||||||
)
|
|
||||||
workflow_parser.add_argument(
|
|
||||||
"--description",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="Workflow description, used when publishing the workflow",
|
|
||||||
)
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -338,102 +164,62 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args_dict = vars(args)
|
args_dict = vars(args)
|
||||||
|
|
||||||
# Supervisor mode: spawn child processes and monitor for restart
|
|
||||||
if args_dict.get("restart_mode", False):
|
|
||||||
_run_as_supervisor(args_dict.get("auto_restart_count", 5))
|
|
||||||
return
|
|
||||||
|
|
||||||
# 环境检查 - 检查并自动安装必需的包 (可选)
|
# 环境检查 - 检查并自动安装必需的包 (可选)
|
||||||
skip_env_check = args_dict.get("skip_env_check", False)
|
if not args_dict.get("skip_env_check", False):
|
||||||
check_mode = args_dict.get("check_mode", False)
|
from unilabos.utils.environment_check import check_environment
|
||||||
|
|
||||||
if not skip_env_check:
|
|
||||||
from unilabos.utils.environment_check import check_environment, check_device_package_requirements
|
|
||||||
|
|
||||||
|
print_status("正在进行环境依赖检查...", "info")
|
||||||
if not check_environment(auto_install=True):
|
if not check_environment(auto_install=True):
|
||||||
print_status("环境检查失败,程序退出", "error")
|
print_status("环境检查失败,程序退出", "error")
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
# 第一次设备包依赖检查:build_registry 之前,确保 import map 可用
|
|
||||||
devices_dirs_for_req = args_dict.get("devices", None)
|
|
||||||
if devices_dirs_for_req:
|
|
||||||
if not check_device_package_requirements(devices_dirs_for_req):
|
|
||||||
print_status("设备包依赖检查失败,程序退出", "error")
|
|
||||||
os._exit(1)
|
|
||||||
else:
|
else:
|
||||||
print_status("跳过环境依赖检查", "warning")
|
print_status("跳过环境依赖检查", "warning")
|
||||||
|
|
||||||
# 加载配置文件,优先加载config,然后从env读取
|
# 加载配置文件,优先加载config,然后从env读取
|
||||||
config_path = args_dict.get("config")
|
config_path = args_dict.get("config")
|
||||||
|
if os.getcwd().endswith("unilabos_data"):
|
||||||
# === 解析 working_dir ===
|
|
||||||
# 规则1: working_dir 传入 → 检测 unilabos_data 子目录,已是则不修改
|
|
||||||
# 规则2: 仅 config_path 传入 → 用其父目录作为 working_dir
|
|
||||||
# 规则4: 两者都传入 → 各用各的,但 working_dir 仍做 unilabos_data 子目录检测
|
|
||||||
raw_working_dir = args_dict.get("working_dir")
|
|
||||||
if raw_working_dir:
|
|
||||||
working_dir = os.path.abspath(raw_working_dir)
|
|
||||||
elif config_path and os.path.exists(config_path):
|
|
||||||
working_dir = os.path.dirname(os.path.abspath(config_path))
|
|
||||||
else:
|
|
||||||
working_dir = os.path.abspath(os.getcwd())
|
working_dir = os.path.abspath(os.getcwd())
|
||||||
|
else:
|
||||||
|
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||||
|
|
||||||
# unilabos_data 子目录自动检测
|
if args_dict.get("working_dir"):
|
||||||
if os.path.basename(working_dir) != "unilabos_data":
|
working_dir = args_dict.get("working_dir", "")
|
||||||
unilabos_data_sub = os.path.join(working_dir, "unilabos_data")
|
if config_path and not os.path.exists(config_path):
|
||||||
if os.path.isdir(unilabos_data_sub):
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
working_dir = unilabos_data_sub
|
if not os.path.exists(config_path):
|
||||||
elif not raw_working_dir and not (config_path and os.path.exists(config_path)):
|
print_status(
|
||||||
# 未显式指定路径,默认使用 cwd/unilabos_data
|
f"当前工作目录 {working_dir} 未找到local_config.py,请通过 --config 传入 local_config.py 文件路径",
|
||||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
"error",
|
||||||
|
|
||||||
# === 解析 config_path ===
|
|
||||||
if config_path and not os.path.exists(config_path):
|
|
||||||
# config_path 传入但不存在,尝试在 working_dir 中查找
|
|
||||||
candidate = os.path.join(working_dir, "local_config.py")
|
|
||||||
if os.path.exists(candidate):
|
|
||||||
config_path = candidate
|
|
||||||
print_status(f"在工作目录中发现配置文件: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
print_status(
|
|
||||||
f"配置文件 {config_path} 不存在,工作目录 {working_dir} 中也未找到 local_config.py,"
|
|
||||||
f"请通过 --config 传入 local_config.py 文件路径",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
os._exit(1)
|
|
||||||
elif not config_path:
|
|
||||||
# 规则3: 未传入 config_path,尝试 working_dir/local_config.py
|
|
||||||
candidate = os.path.join(working_dir, "local_config.py")
|
|
||||||
if os.path.exists(candidate):
|
|
||||||
config_path = candidate
|
|
||||||
print_status(f"发现本地配置文件: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
|
||||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
|
||||||
if check_mode or input() != "n":
|
|
||||||
os.makedirs(working_dir, exist_ok=True)
|
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
|
||||||
shutil.copy(
|
|
||||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"),
|
|
||||||
config_path,
|
|
||||||
)
|
)
|
||||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
elif config_path and os.path.exists(config_path):
|
||||||
# 加载配置文件 (check_mode 跳过)
|
working_dir = os.path.dirname(config_path)
|
||||||
|
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
||||||
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
elif not config_path and (
|
||||||
|
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
|
||||||
|
):
|
||||||
|
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||||
|
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||||
|
if input() != "n":
|
||||||
|
os.makedirs(working_dir, exist_ok=True)
|
||||||
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
shutil.copy(
|
||||||
|
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
||||||
|
)
|
||||||
|
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||||
|
else:
|
||||||
|
os._exit(1)
|
||||||
|
# 加载配置文件
|
||||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||||
if not check_mode:
|
load_config_from_file(config_path)
|
||||||
load_config_from_file(config_path)
|
|
||||||
|
|
||||||
# 根据配置重新设置日志级别
|
# 根据配置重新设置日志级别
|
||||||
from unilabos.utils.log import configure_logger, logger
|
from unilabos.utils.log import configure_logger, logger
|
||||||
|
|
||||||
if hasattr(BasicConfig, "log_level"):
|
if hasattr(BasicConfig, "log_level"):
|
||||||
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||||
file_path = configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||||
if file_path is not None:
|
|
||||||
logger.info(f"[LOG_FILE] {file_path}")
|
|
||||||
|
|
||||||
if args.addr != parser.get_default("addr"):
|
if args.addr != parser.get_default("addr"):
|
||||||
if args.addr == "test":
|
if args.addr == "test":
|
||||||
@@ -455,12 +241,9 @@ def main():
|
|||||||
if args_dict.get("sk", ""):
|
if args_dict.get("sk", ""):
|
||||||
BasicConfig.sk = args_dict.get("sk", "")
|
BasicConfig.sk = args_dict.get("sk", "")
|
||||||
print_status("传入了sk参数,优先采用传入参数!", "info")
|
print_status("传入了sk参数,优先采用传入参数!", "info")
|
||||||
BasicConfig.working_dir = working_dir
|
|
||||||
|
|
||||||
workflow_upload = args_dict.get("command") in ("workflow_upload", "wf")
|
|
||||||
|
|
||||||
# 使用远程资源启动
|
# 使用远程资源启动
|
||||||
if not workflow_upload and args_dict["use_remote_resource"]:
|
if args_dict["use_remote_resource"]:
|
||||||
print_status("使用远程资源启动", "info")
|
print_status("使用远程资源启动", "info")
|
||||||
from unilabos.app.web import http_client
|
from unilabos.app.web import http_client
|
||||||
|
|
||||||
@@ -473,84 +256,37 @@ def main():
|
|||||||
|
|
||||||
BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port
|
BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port
|
||||||
BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser
|
BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser
|
||||||
|
BasicConfig.working_dir = working_dir
|
||||||
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
|
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
|
||||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||||
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
|
||||||
BasicConfig.test_mode = args_dict.get("test_mode", False)
|
|
||||||
if BasicConfig.test_mode:
|
|
||||||
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
|
||||||
BasicConfig.extra_resource = args_dict.get("extra_resource", False)
|
|
||||||
if BasicConfig.extra_resource:
|
|
||||||
print_status("启用额外资源加载:将加载lab_开头的labware资源定义", "info")
|
|
||||||
BasicConfig.communication_protocol = "websocket"
|
BasicConfig.communication_protocol = "websocket"
|
||||||
machine_name = platform.node()
|
machine_name = os.popen("hostname").read().strip()
|
||||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||||
BasicConfig.machine_name = machine_name
|
BasicConfig.machine_name = machine_name
|
||||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
BasicConfig.check_mode = check_mode
|
|
||||||
|
|
||||||
from unilabos.registry.registry import build_registry
|
|
||||||
|
|
||||||
# 显示启动横幅
|
|
||||||
print_unilab_banner(args_dict)
|
|
||||||
|
|
||||||
# Step 0: AST 分析优先 + YAML 注册表加载
|
|
||||||
# check_mode 和 upload_registry 都会执行实际 import 验证
|
|
||||||
devices_dirs = args_dict.get("devices", None)
|
|
||||||
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
|
||||||
external_only = args_dict.get("external_devices_only", False)
|
|
||||||
lab_registry = build_registry(
|
|
||||||
registry_paths=args_dict["registry_path"],
|
|
||||||
devices_dirs=devices_dirs,
|
|
||||||
upload_registry=BasicConfig.upload_registry,
|
|
||||||
check_mode=check_mode,
|
|
||||||
complete_registry=complete_registry,
|
|
||||||
external_only=external_only,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check mode: 注册表验证完成后直接退出
|
|
||||||
if check_mode:
|
|
||||||
device_count = len(lab_registry.device_type_registry)
|
|
||||||
resource_count = len(lab_registry.resource_type_registry)
|
|
||||||
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
# 以下导入依赖 ROS2 环境,check_mode 已退出不需要
|
|
||||||
from unilabos.resources.graphio import (
|
from unilabos.resources.graphio import (
|
||||||
read_node_link_json,
|
read_node_link_json,
|
||||||
read_graphml,
|
read_graphml,
|
||||||
dict_from_graph,
|
dict_from_graph,
|
||||||
modify_to_backend_format,
|
|
||||||
)
|
)
|
||||||
from unilabos.app.communication import get_communication_client
|
from unilabos.app.communication import get_communication_client
|
||||||
|
from unilabos.registry.registry import build_registry
|
||||||
from unilabos.app.backend import start_backend
|
from unilabos.app.backend import start_backend
|
||||||
from unilabos.app.web import http_client
|
from unilabos.app.web import http_client
|
||||||
from unilabos.app.web import start_server
|
from unilabos.app.web import start_server
|
||||||
from unilabos.app.register import register_devices_and_resources
|
from unilabos.app.register import register_devices_and_resources
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
from unilabos.resources.graphio import modify_to_backend_format
|
||||||
|
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
|
||||||
|
|
||||||
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
|
# 显示启动横幅
|
||||||
if BasicConfig.upload_registry:
|
print_unilab_banner(args_dict)
|
||||||
if BasicConfig.ak and BasicConfig.sk:
|
|
||||||
# print_status("开始注册设备到服务端...", "info")
|
|
||||||
try:
|
|
||||||
register_devices_and_resources(lab_registry)
|
|
||||||
# print_status("设备注册完成", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"设备注册失败: {e}", "error")
|
|
||||||
else:
|
|
||||||
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
|
||||||
else:
|
|
||||||
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
|
|
||||||
|
|
||||||
# 处理 workflow_upload 子命令
|
# 注册表
|
||||||
if workflow_upload:
|
lab_registry = build_registry(
|
||||||
from unilabos.workflow.wf_utils import handle_workflow_upload_command
|
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
|
||||||
|
)
|
||||||
handle_workflow_upload_command(args_dict)
|
|
||||||
print_status("工作流上传完成,程序退出", "info")
|
|
||||||
os._exit(0)
|
|
||||||
|
|
||||||
if not BasicConfig.ak or not BasicConfig.sk:
|
if not BasicConfig.ak or not BasicConfig.sk:
|
||||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||||
@@ -621,21 +357,31 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 如果从远端获取了物料信息,则与本地物料进行同步
|
# 如果从远端获取了物料信息,则与本地物料进行同步
|
||||||
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
|
if request_startup_json and "nodes" in request_startup_json:
|
||||||
print_status("开始同步远端物料到本地...", "info")
|
print_status("开始同步远端物料到本地...", "info")
|
||||||
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
||||||
resource_tree_set.merge_remote_resources(remote_tree_set)
|
resource_tree_set.merge_remote_resources(remote_tree_set)
|
||||||
print_status("远端物料同步完成", "info")
|
print_status("远端物料同步完成", "info")
|
||||||
|
|
||||||
# 第二次设备包依赖检查:云端物料同步后,community 包可能引入新的 requirements
|
|
||||||
# TODO: 当 community device package 功能上线后,在这里调用
|
|
||||||
# install_requirements_txt(community_pkg_path / "requirements.txt", label="community.xxx")
|
|
||||||
|
|
||||||
# 使用 ResourceTreeSet 代替 list
|
# 使用 ResourceTreeSet 代替 list
|
||||||
args_dict["resources_config"] = resource_tree_set
|
args_dict["resources_config"] = resource_tree_set
|
||||||
args_dict["devices_config"] = resource_tree_set
|
args_dict["devices_config"] = resource_tree_set
|
||||||
args_dict["graph"] = graph_res.physical_setup_graph
|
args_dict["graph"] = graph_res.physical_setup_graph
|
||||||
|
|
||||||
|
if BasicConfig.upload_registry:
|
||||||
|
# 设备注册到服务端 - 需要 ak 和 sk
|
||||||
|
if BasicConfig.ak and BasicConfig.sk:
|
||||||
|
print_status("开始注册设备到服务端...", "info")
|
||||||
|
try:
|
||||||
|
register_devices_and_resources(lab_registry)
|
||||||
|
print_status("设备注册完成", "info")
|
||||||
|
except Exception as e:
|
||||||
|
print_status(f"设备注册失败: {e}", "error")
|
||||||
|
else:
|
||||||
|
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
||||||
|
else:
|
||||||
|
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
|
||||||
|
|
||||||
if args_dict["controllers"] is not None:
|
if args_dict["controllers"] is not None:
|
||||||
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
||||||
else:
|
else:
|
||||||
@@ -650,7 +396,6 @@ def main():
|
|||||||
comm_client = get_communication_client()
|
comm_client = get_communication_client()
|
||||||
if "websocket" in args_dict["app_bridges"]:
|
if "websocket" in args_dict["app_bridges"]:
|
||||||
args_dict["bridges"].append(comm_client)
|
args_dict["bridges"].append(comm_client)
|
||||||
|
|
||||||
def _exit(signum, frame):
|
def _exit(signum, frame):
|
||||||
comm_client.stop()
|
comm_client.stop()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -692,13 +437,16 @@ def main():
|
|||||||
resource_visualization.start()
|
resource_visualization.start()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if "AMENT_PREFIX_PATH" in str(e):
|
if "AMENT_PREFIX_PATH" in str(e):
|
||||||
print_status(f"ROS 2环境未正确设置,跳过3D可视化启动。错误详情: {e}", "warning")
|
print_status(
|
||||||
|
f"ROS 2环境未正确设置,跳过3D可视化启动。错误详情: {e}",
|
||||||
|
"warning"
|
||||||
|
)
|
||||||
print_status(
|
print_status(
|
||||||
"建议解决方案:\n"
|
"建议解决方案:\n"
|
||||||
"1. 激活Conda环境: conda activate unilab\n"
|
"1. 激活Conda环境: conda activate unilab\n"
|
||||||
"2. 或使用 --backend simple 参数\n"
|
"2. 或使用 --backend simple 参数\n"
|
||||||
"3. 或使用 --visual disable 参数禁用可视化",
|
"3. 或使用 --visual disable 参数禁用可视化",
|
||||||
"info",
|
"info"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@@ -706,26 +454,16 @@ def main():
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
restart_requested = start_server(
|
start_server(
|
||||||
open_browser=not args_dict["disable_browser"],
|
open_browser=not args_dict["disable_browser"],
|
||||||
port=BasicConfig.port,
|
port=BasicConfig.port,
|
||||||
)
|
)
|
||||||
if restart_requested:
|
|
||||||
print_status("[Main] Restart requested, cleaning up...", "info")
|
|
||||||
cleanup_for_restart()
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
start_backend(**args_dict)
|
start_backend(**args_dict)
|
||||||
|
start_server(
|
||||||
# 启动服务器(默认支持WebSocket触发重启)
|
|
||||||
restart_requested = start_server(
|
|
||||||
open_browser=not args_dict["disable_browser"],
|
open_browser=not args_dict["disable_browser"],
|
||||||
port=BasicConfig.port,
|
port=BasicConfig.port,
|
||||||
)
|
)
|
||||||
if restart_requested:
|
|
||||||
print_status("[Main] Restart requested, cleaning up...", "info")
|
|
||||||
cleanup_for_restart()
|
|
||||||
os._exit(RESTART_EXIT_CODE)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class JobAddReq(BaseModel):
|
|||||||
action_type: str = Field(
|
action_type: str = Field(
|
||||||
examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default=""
|
examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default=""
|
||||||
)
|
)
|
||||||
sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid")
|
|
||||||
action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict)
|
action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict)
|
||||||
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
|
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
|
||||||
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")
|
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Optional, Tuple, Dict, Any
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
from unilabos.utils.tools import normalize_json as _normalize_device
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
|
|
||||||
|
|
||||||
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
||||||
@@ -10,63 +11,50 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
|
|||||||
注册设备和资源到服务器(仅支持HTTP)
|
注册设备和资源到服务器(仅支持HTTP)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 注册资源信息 - 使用HTTP方式
|
||||||
from unilabos.app.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
|
|
||||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||||
|
|
||||||
|
# 注册设备信息
|
||||||
devices_to_register = {}
|
devices_to_register = {}
|
||||||
for device_info in lab_registry.obtain_registry_device_info():
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
devices_to_register[device_info["id"]] = _normalize_device(device_info)
|
devices_to_register[device_info["id"]] = json.loads(
|
||||||
logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}")
|
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
|
||||||
|
)
|
||||||
|
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
|
||||||
|
|
||||||
resources_to_register = {}
|
resources_to_register = {}
|
||||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
resources_to_register[resource_info["id"]] = resource_info
|
resources_to_register[resource_info["id"]] = resource_info
|
||||||
logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||||
|
|
||||||
if gather_only:
|
if gather_only:
|
||||||
return devices_to_register, resources_to_register
|
return devices_to_register, resources_to_register
|
||||||
|
# 注册设备
|
||||||
if devices_to_register:
|
if devices_to_register:
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = http_client.resource_registry(
|
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
||||||
{"resources": list(devices_to_register.values())},
|
|
||||||
tag="device_registry",
|
|
||||||
)
|
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
res_data = response.json() if response.status_code == 200 else {}
|
if response.status_code in [200, 201]:
|
||||||
skipped = res_data.get("data", {}).get("skipped", False)
|
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
||||||
if skipped:
|
|
||||||
logger.info(
|
|
||||||
f"[UniLab Register] 设备注册跳过(内容未变化)"
|
|
||||||
f" {len(devices_to_register)} 个 {cost_time:.3f}s"
|
|
||||||
)
|
|
||||||
elif response.status_code in [200, 201]:
|
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s")
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||||
|
|
||||||
|
# 注册资源
|
||||||
if resources_to_register:
|
if resources_to_register:
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = http_client.resource_registry(
|
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
||||||
{"resources": list(resources_to_register.values())},
|
|
||||||
tag="resource_registry",
|
|
||||||
)
|
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
res_data = response.json() if response.status_code == 200 else {}
|
if response.status_code in [200, 201]:
|
||||||
skipped = res_data.get("data", {}).get("skipped", False)
|
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||||
if skipped:
|
|
||||||
logger.info(
|
|
||||||
f"[UniLab Register] 资源注册跳过(内容未变化)"
|
|
||||||
f" {len(resources_to_register)} 个 {cost_time:.3f}s"
|
|
||||||
)
|
|
||||||
elif response.status_code in [200, 201]:
|
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s")
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||||
|
|
||||||
|
logger.info("[UniLab Register] 设备和资源注册完成.")
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
"""
|
|
||||||
UniLabOS 应用工具函数
|
|
||||||
|
|
||||||
提供清理、重启等工具函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def patch_rclpy_dll_windows():
|
|
||||||
"""在 Windows + conda 环境下为 rclpy 打 DLL 加载补丁"""
|
|
||||||
if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"):
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
import rclpy
|
|
||||||
|
|
||||||
return
|
|
||||||
except ImportError as e:
|
|
||||||
if not str(e).startswith("DLL load failed"):
|
|
||||||
return
|
|
||||||
cp = os.environ["CONDA_PREFIX"]
|
|
||||||
impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py")
|
|
||||||
pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd"))
|
|
||||||
if not os.path.exists(impl) or not pyd:
|
|
||||||
return
|
|
||||||
with open(impl, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/")
|
|
||||||
patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n'
|
|
||||||
shutil.copy2(impl, impl + ".bak")
|
|
||||||
with open(impl, "w", encoding="utf-8") as f:
|
|
||||||
f.write(patch + content)
|
|
||||||
|
|
||||||
|
|
||||||
patch_rclpy_dll_windows()
|
|
||||||
|
|
||||||
import gc
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from unilabos.utils.banner_print import print_status
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_for_restart() -> bool:
|
|
||||||
"""
|
|
||||||
Clean up all resources for restart without exiting the process.
|
|
||||||
|
|
||||||
This function prepares the system for re-initialization by:
|
|
||||||
1. Stopping all communication clients
|
|
||||||
2. Destroying ROS nodes
|
|
||||||
3. Resetting singletons
|
|
||||||
4. Waiting for threads to finish
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if cleanup was successful, False otherwise
|
|
||||||
"""
|
|
||||||
print_status("[Restart] Starting cleanup for restart...", "info")
|
|
||||||
|
|
||||||
# Step 1: Stop WebSocket communication client
|
|
||||||
print_status("[Restart] Step 1: Stopping WebSocket client...", "info")
|
|
||||||
try:
|
|
||||||
from unilabos.app.communication import get_communication_client
|
|
||||||
|
|
||||||
comm_client = get_communication_client()
|
|
||||||
if comm_client is not None:
|
|
||||||
comm_client.stop()
|
|
||||||
print_status("[Restart] WebSocket client stopped", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error stopping WebSocket: {e}", "warning")
|
|
||||||
|
|
||||||
# Step 2: Get HostNode and cleanup ROS
|
|
||||||
print_status("[Restart] Step 2: Cleaning up ROS nodes...", "info")
|
|
||||||
try:
|
|
||||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
|
||||||
import rclpy
|
|
||||||
from rclpy.timer import Timer
|
|
||||||
|
|
||||||
host_instance = HostNode.get_instance(timeout=5)
|
|
||||||
if host_instance is not None:
|
|
||||||
print_status(f"[Restart] Found HostNode: {host_instance.device_id}", "info")
|
|
||||||
|
|
||||||
# Gracefully shutdown background threads
|
|
||||||
print_status("[Restart] Shutting down background threads...", "info")
|
|
||||||
HostNode.shutdown_background_threads(timeout=5.0)
|
|
||||||
print_status("[Restart] Background threads shutdown complete", "info")
|
|
||||||
|
|
||||||
# Stop discovery timer
|
|
||||||
if hasattr(host_instance, "_discovery_timer") and isinstance(host_instance._discovery_timer, Timer):
|
|
||||||
host_instance._discovery_timer.cancel()
|
|
||||||
print_status("[Restart] Discovery timer cancelled", "info")
|
|
||||||
|
|
||||||
# Destroy device nodes
|
|
||||||
device_count = len(host_instance.devices_instances)
|
|
||||||
print_status(f"[Restart] Destroying {device_count} device instances...", "info")
|
|
||||||
for device_id, device_node in list(host_instance.devices_instances.items()):
|
|
||||||
try:
|
|
||||||
if hasattr(device_node, "ros_node_instance") and device_node.ros_node_instance is not None:
|
|
||||||
device_node.ros_node_instance.destroy_node()
|
|
||||||
print_status(f"[Restart] Device {device_id} destroyed", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error destroying device {device_id}: {e}", "warning")
|
|
||||||
|
|
||||||
# Clear devices instances
|
|
||||||
host_instance.devices_instances.clear()
|
|
||||||
host_instance.devices_names.clear()
|
|
||||||
|
|
||||||
# Destroy host node
|
|
||||||
try:
|
|
||||||
host_instance.destroy_node()
|
|
||||||
print_status("[Restart] HostNode destroyed", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error destroying HostNode: {e}", "warning")
|
|
||||||
|
|
||||||
# Reset HostNode state
|
|
||||||
HostNode.reset_state()
|
|
||||||
print_status("[Restart] HostNode state reset", "info")
|
|
||||||
|
|
||||||
# Shutdown executor first (to stop executor.spin() gracefully)
|
|
||||||
if hasattr(rclpy, "__executor") and rclpy.__executor is not None:
|
|
||||||
try:
|
|
||||||
rclpy.__executor.shutdown()
|
|
||||||
rclpy.__executor = None # Clear for restart
|
|
||||||
print_status("[Restart] ROS executor shutdown complete", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error shutting down executor: {e}", "warning")
|
|
||||||
|
|
||||||
# Shutdown rclpy
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
print_status("[Restart] rclpy shutdown complete", "info")
|
|
||||||
|
|
||||||
except ImportError as e:
|
|
||||||
print_status(f"[Restart] ROS modules not available: {e}", "warning")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error in ROS cleanup: {e}", "warning")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Step 3: Reset communication client singleton
|
|
||||||
print_status("[Restart] Step 3: Resetting singletons...", "info")
|
|
||||||
try:
|
|
||||||
from unilabos.app import communication
|
|
||||||
|
|
||||||
if hasattr(communication, "_communication_client"):
|
|
||||||
communication._communication_client = None
|
|
||||||
print_status("[Restart] Communication client singleton reset", "info")
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"[Restart] Error resetting communication singleton: {e}", "warning")
|
|
||||||
|
|
||||||
# Step 4: Wait for threads to finish
|
|
||||||
print_status("[Restart] Step 4: Waiting for threads to finish...", "info")
|
|
||||||
time.sleep(3) # Give threads time to finish
|
|
||||||
|
|
||||||
# Check remaining threads
|
|
||||||
remaining_threads = []
|
|
||||||
for t in threading.enumerate():
|
|
||||||
if t.name != "MainThread" and t.is_alive():
|
|
||||||
remaining_threads.append(t.name)
|
|
||||||
|
|
||||||
if remaining_threads:
|
|
||||||
print_status(
|
|
||||||
f"[Restart] Warning: {len(remaining_threads)} threads still running: {remaining_threads}", "warning"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print_status("[Restart] All threads stopped", "info")
|
|
||||||
|
|
||||||
# Step 5: Force garbage collection
|
|
||||||
print_status("[Restart] Step 5: Running garbage collection...", "info")
|
|
||||||
gc.collect()
|
|
||||||
gc.collect() # Run twice for weak references
|
|
||||||
print_status("[Restart] Garbage collection complete", "info")
|
|
||||||
|
|
||||||
print_status("[Restart] Cleanup complete. Ready for re-initialization.", "info")
|
|
||||||
return True
|
|
||||||
@@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
|||||||
"result": {},
|
"result": {},
|
||||||
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
||||||
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||||
"handles": {},
|
"handles": [],
|
||||||
}
|
}
|
||||||
# 不生成已配置action的动作
|
# 不生成已配置action的动作
|
||||||
for k, v in enhanced_info["action_methods"].items()
|
for k, v in enhanced_info["action_methods"].items()
|
||||||
@@ -1340,5 +1340,5 @@ def setup_api_routes(app):
|
|||||||
# 启动广播任务
|
# 启动广播任务
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
asyncio.create_task(broadcast_device_status(), name="web-api-startup-device")
|
asyncio.create_task(broadcast_device_status())
|
||||||
asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status")
|
asyncio.create_task(broadcast_status_page_data())
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ HTTP客户端模块
|
|||||||
|
|
||||||
提供与远程服务器通信的客户端功能,只有host需要用
|
提供与远程服务器通信的客户端功能,只有host需要用
|
||||||
"""
|
"""
|
||||||
import gzip
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from threading import Thread
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
from unilabos.utils.tools import fast_dumps as _fast_dumps, fast_dumps_pretty as _fast_dumps_pretty
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.utils.log import info
|
from unilabos.utils.log import info
|
||||||
from unilabos.config.config import HTTPConfig, BasicConfig
|
from unilabos.config.config import HTTPConfig, BasicConfig
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
@@ -76,8 +76,7 @@ class HTTPClient:
|
|||||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||||
"""
|
"""
|
||||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||||
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
|
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
|
||||||
f.write(json.dumps(payload, indent=4))
|
|
||||||
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
||||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||||
if not self.initialized or first_add:
|
if not self.initialized or first_add:
|
||||||
@@ -282,54 +281,22 @@ class HTTPClient:
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def resource_registry(
|
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
||||||
self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry",
|
|
||||||
) -> requests.Response:
|
|
||||||
"""
|
"""
|
||||||
注册资源到服务器,同步保存请求/响应到 unilabos_data
|
注册资源到服务器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||||
tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry")
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
# 序列化一次,同时用于保存和发送
|
|
||||||
json_bytes = _fast_dumps(registry_data)
|
|
||||||
|
|
||||||
# 保存请求数据到 unilabos_data
|
|
||||||
req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json")
|
|
||||||
try:
|
|
||||||
os.makedirs(BasicConfig.working_dir, exist_ok=True)
|
|
||||||
with open(req_path, "wb") as f:
|
|
||||||
f.write(_fast_dumps_pretty(registry_data))
|
|
||||||
logger.trace(f"注册表请求数据已保存: {req_path}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"保存注册表请求数据失败: {e}")
|
|
||||||
|
|
||||||
compressed_body = gzip.compress(json_bytes)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Lab {self.auth}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Content-Encoding": "gzip",
|
|
||||||
}
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource",
|
f"{self.remote_addr}/lab/resource",
|
||||||
data=compressed_body,
|
json=registry_data,
|
||||||
headers=headers,
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 保存响应数据到 unilabos_data
|
|
||||||
res_path = os.path.join(BasicConfig.working_dir, f"res_{tag}_upload.json")
|
|
||||||
try:
|
|
||||||
with open(res_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(f"{response.status_code}\n{response.text}")
|
|
||||||
logger.trace(f"注册表响应数据已保存: {res_path}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"保存注册表响应数据失败: {e}")
|
|
||||||
|
|
||||||
if response.status_code not in [200, 201]:
|
if response.status_code not in [200, 201]:
|
||||||
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -368,106 +335,6 @@ class HTTPClient:
|
|||||||
logger.error(f"响应内容: {response.text}")
|
logger.error(f"响应内容: {response.text}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def workflow_import(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
workflow_uuid: str,
|
|
||||||
workflow_name: str,
|
|
||||||
nodes: List[Dict[str, Any]],
|
|
||||||
edges: List[Dict[str, Any]],
|
|
||||||
tags: Optional[List[str]] = None,
|
|
||||||
published: bool = False,
|
|
||||||
description: str = "",
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
导入工作流到服务器,如果 published 为 True,则额外发起发布请求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 工作流名称(顶层)
|
|
||||||
workflow_uuid: 工作流UUID
|
|
||||||
workflow_name: 工作流名称(data内部)
|
|
||||||
nodes: 工作流节点列表
|
|
||||||
edges: 工作流边列表
|
|
||||||
tags: 工作流标签列表,默认为空列表
|
|
||||||
published: 是否发布工作流,默认为False
|
|
||||||
description: 工作流描述,发布时使用
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"name": name,
|
|
||||||
"data": {
|
|
||||||
"workflow_uuid": workflow_uuid,
|
|
||||||
"workflow_name": workflow_name,
|
|
||||||
"nodes": nodes,
|
|
||||||
"edges": edges,
|
|
||||||
"tags": tags if tags is not None else [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# 保存请求到文件
|
|
||||||
with open(os.path.join(BasicConfig.working_dir, "req_workflow_upload.json"), "w", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(payload, indent=4, ensure_ascii=False))
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{self.remote_addr}/lab/workflow/owner/import",
|
|
||||||
json=payload,
|
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
# 保存响应到文件
|
|
||||||
with open(os.path.join(BasicConfig.working_dir, "res_workflow_upload.json"), "w", encoding="utf-8") as f:
|
|
||||||
f.write(f"{response.status_code}" + "\n" + response.text)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
res = response.json()
|
|
||||||
if "code" in res and res["code"] != 0:
|
|
||||||
logger.error(f"导入工作流失败: {response.text}")
|
|
||||||
return res
|
|
||||||
# 导入成功后,如果需要发布则额外发起发布请求
|
|
||||||
if published:
|
|
||||||
imported_uuid = res.get("data", {}).get("uuid", workflow_uuid)
|
|
||||||
publish_res = self.workflow_publish(imported_uuid, description)
|
|
||||||
res["publish_result"] = publish_res
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
|
||||||
return {"code": response.status_code, "message": response.text}
|
|
||||||
|
|
||||||
def workflow_publish(self, workflow_uuid: str, description: str = "") -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
发布工作流
|
|
||||||
|
|
||||||
Args:
|
|
||||||
workflow_uuid: 工作流UUID
|
|
||||||
description: 工作流描述
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: API响应数据
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"uuid": workflow_uuid,
|
|
||||||
"description": description,
|
|
||||||
"published": True,
|
|
||||||
}
|
|
||||||
logger.info(f"正在发布工作流: {workflow_uuid}")
|
|
||||||
response = requests.patch(
|
|
||||||
f"{self.remote_addr}/lab/workflow/owner",
|
|
||||||
json=payload,
|
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
res = response.json()
|
|
||||||
if "code" in res and res["code"] != 0:
|
|
||||||
logger.error(f"发布工作流失败: {response.text}")
|
|
||||||
else:
|
|
||||||
logger.info(f"工作流发布成功: {workflow_uuid}")
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
logger.error(f"发布工作流失败: {response.status_code}, {response.text}")
|
|
||||||
return {"code": response.status_code, "message": response.text}
|
|
||||||
|
|
||||||
|
|
||||||
# 创建默认客户端实例
|
# 创建默认客户端实例
|
||||||
http_client = HTTPClient()
|
http_client = HTTPClient()
|
||||||
|
|||||||
@@ -327,7 +327,6 @@ def job_add(req: JobAddReq) -> JobData:
|
|||||||
queue_item,
|
queue_item,
|
||||||
action_type=action_type,
|
action_type=action_type,
|
||||||
action_kwargs=action_args,
|
action_kwargs=action_args,
|
||||||
sample_material=req.sample_material,
|
|
||||||
server_info=server_info,
|
server_info=server_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Web服务器模块
|
|||||||
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
@@ -86,7 +87,7 @@ def setup_server() -> FastAPI:
|
|||||||
# 设置页面路由
|
# 设置页面路由
|
||||||
try:
|
try:
|
||||||
setup_web_pages(pages)
|
setup_web_pages(pages)
|
||||||
# info("[Web] 已加载Web UI模块")
|
info("[Web] 已加载Web UI模块")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
info(f"[Web] 未找到Web页面模块: {str(e)}")
|
info(f"[Web] 未找到Web页面模块: {str(e)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -95,7 +96,7 @@ def setup_server() -> FastAPI:
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = True) -> bool:
|
def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
启动服务器
|
启动服务器
|
||||||
|
|
||||||
@@ -103,14 +104,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
|
|||||||
host: 服务器主机
|
host: 服务器主机
|
||||||
port: 服务器端口
|
port: 服务器端口
|
||||||
open_browser: 是否自动打开浏览器
|
open_browser: 是否自动打开浏览器
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if restart was requested, False otherwise
|
|
||||||
"""
|
"""
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from uvicorn import Config, Server
|
|
||||||
|
|
||||||
# 设置服务器
|
# 设置服务器
|
||||||
setup_server()
|
setup_server()
|
||||||
|
|
||||||
@@ -129,37 +123,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
|
|||||||
|
|
||||||
# 启动服务器
|
# 启动服务器
|
||||||
info(f"[Web] 启动FastAPI服务器: {host}:{port}")
|
info(f"[Web] 启动FastAPI服务器: {host}:{port}")
|
||||||
|
uvicorn.run(app, host=host, port=port, log_config=log_config)
|
||||||
# 使用支持重启的模式
|
|
||||||
config = Config(app=app, host=host, port=port, log_config=log_config)
|
|
||||||
server = Server(config)
|
|
||||||
|
|
||||||
# 启动服务器线程
|
|
||||||
server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
|
|
||||||
server_thread.start()
|
|
||||||
|
|
||||||
# info("[Web] Server started, monitoring for restart requests...")
|
|
||||||
|
|
||||||
# 监控重启标志
|
|
||||||
import unilabos.app.main as main_module
|
|
||||||
|
|
||||||
while server_thread.is_alive():
|
|
||||||
if hasattr(main_module, "_restart_requested") and main_module._restart_requested:
|
|
||||||
info(
|
|
||||||
f"[Web] Restart requested via WebSocket, reason: {getattr(main_module, '_restart_reason', 'unknown')}"
|
|
||||||
)
|
|
||||||
main_module._restart_requested = False
|
|
||||||
|
|
||||||
# 停止服务器
|
|
||||||
server.should_exit = True
|
|
||||||
server_thread.join(timeout=5)
|
|
||||||
|
|
||||||
info("[Web] Server stopped, ready for restart")
|
|
||||||
return True
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# 当脚本直接运行时启动服务器
|
# 当脚本直接运行时启动服务器
|
||||||
|
|||||||
@@ -23,10 +23,9 @@ from typing import Optional, Dict, Any, List
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
from jedi.inference.gradual.typing import TypedDict
|
||||||
|
|
||||||
from unilabos.app.model import JobAddReq
|
from unilabos.app.model import JobAddReq
|
||||||
from unilabos.resources.resource_tracker import ResourceDictType
|
|
||||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
from unilabos.utils.type_check import serialize_result_info
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
from unilabos.app.communication import BaseCommunicationClient
|
from unilabos.app.communication import BaseCommunicationClient
|
||||||
@@ -77,7 +76,6 @@ class JobInfo:
|
|||||||
start_time: float
|
start_time: float
|
||||||
last_update_time: float = field(default_factory=time.time)
|
last_update_time: float = field(default_factory=time.time)
|
||||||
ready_timeout: Optional[float] = None # READY状态的超时时间
|
ready_timeout: Optional[float] = None # READY状态的超时时间
|
||||||
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
|
|
||||||
|
|
||||||
def update_timestamp(self):
|
def update_timestamp(self):
|
||||||
"""更新最后更新时间"""
|
"""更新最后更新时间"""
|
||||||
@@ -129,15 +127,6 @@ class DeviceActionManager:
|
|||||||
# 总是将job添加到all_jobs中
|
# 总是将job添加到all_jobs中
|
||||||
self.all_jobs[job_info.job_id] = job_info
|
self.all_jobs[job_info.job_id] = job_info
|
||||||
|
|
||||||
# always_free的动作不受排队限制,直接设为READY
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.READY
|
|
||||||
job_info.update_timestamp()
|
|
||||||
job_info.set_ready_timeout(10)
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
|
||||||
logger.trace(f"[DeviceActionManager] Job {job_log} always_free, start immediately")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 检查是否有正在执行或准备执行的任务
|
# 检查是否有正在执行或准备执行的任务
|
||||||
if device_key in self.active_jobs:
|
if device_key in self.active_jobs:
|
||||||
# 有正在执行或准备执行的任务,加入队列
|
# 有正在执行或准备执行的任务,加入队列
|
||||||
@@ -165,7 +154,7 @@ class DeviceActionManager:
|
|||||||
job_info.set_ready_timeout(10) # 设置10秒超时
|
job_info.set_ready_timeout(10) # 设置10秒超时
|
||||||
self.active_jobs[device_key] = job_info
|
self.active_jobs[device_key] = job_info
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.trace(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
logger.info(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def start_job(self, job_id: str) -> bool:
|
def start_job(self, job_id: str) -> bool:
|
||||||
@@ -187,15 +176,11 @@ class DeviceActionManager:
|
|||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# always_free的job不需要检查active_jobs
|
# 检查设备上是否是这个job
|
||||||
if not job_info.always_free:
|
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||||
# 检查设备上是否是这个job
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||||
job_log = format_job_log(
|
return False
|
||||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
|
||||||
)
|
|
||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 开始执行任务,将状态从READY转换为STARTED
|
# 开始执行任务,将状态从READY转换为STARTED
|
||||||
job_info.status = JobStatus.STARTED
|
job_info.status = JobStatus.STARTED
|
||||||
@@ -218,13 +203,6 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
# always_free的job直接清理,不影响队列
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.ENDED
|
|
||||||
job_info.update_timestamp()
|
|
||||||
del self.all_jobs[job_id]
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 移除活跃任务
|
# 移除活跃任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
del self.active_jobs[device_key]
|
del self.active_jobs[device_key]
|
||||||
@@ -232,9 +210,8 @@ class DeviceActionManager:
|
|||||||
job_info.update_timestamp()
|
job_info.update_timestamp()
|
||||||
# 从all_jobs中移除已结束的job
|
# 从all_jobs中移除已结束的job
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
# logger.debug(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
logger.info(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
||||||
@@ -250,20 +227,15 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
||||||
return next_job
|
return next_job
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_active_jobs(self) -> List[JobInfo]:
|
def get_active_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
|
"""获取所有正在执行的任务"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
jobs = list(self.active_jobs.values())
|
return list(self.active_jobs.values())
|
||||||
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
|
|
||||||
for job in self.all_jobs.values():
|
|
||||||
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
|
|
||||||
jobs.append(job)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def get_queued_jobs(self) -> List[JobInfo]:
|
def get_queued_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有排队中的任务"""
|
"""获取所有排队中的任务"""
|
||||||
@@ -288,14 +260,6 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
# always_free的job直接清理
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.ENDED
|
|
||||||
del self.all_jobs[job_id]
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
|
||||||
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 如果是正在执行的任务
|
# 如果是正在执行的任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
# 清理active job状态
|
# 清理active job状态
|
||||||
@@ -304,7 +268,7 @@ class DeviceActionManager:
|
|||||||
# 从all_jobs中移除
|
# 从all_jobs中移除
|
||||||
del self.all_jobs[job_id]
|
del self.all_jobs[job_id]
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
logger.trace(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
||||||
|
|
||||||
# 启动下一个任务
|
# 启动下一个任务
|
||||||
if device_key in self.device_queues and self.device_queues[device_key]:
|
if device_key in self.device_queues and self.device_queues[device_key]:
|
||||||
@@ -317,7 +281,7 @@ class DeviceActionManager:
|
|||||||
next_job_log = format_job_log(
|
next_job_log = format_job_log(
|
||||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 如果是排队中的任务
|
# 如果是排队中的任务
|
||||||
@@ -331,7 +295,7 @@ class DeviceActionManager:
|
|||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||||
)
|
)
|
||||||
logger.trace(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
logger.info(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
@@ -369,18 +333,13 @@ class DeviceActionManager:
|
|||||||
timeout_jobs = []
|
timeout_jobs = []
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
|
# 统计READY状态的任务数量
|
||||||
ready_candidates = list(self.active_jobs.values())
|
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY)
|
||||||
for job in self.all_jobs.values():
|
|
||||||
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
|
|
||||||
ready_candidates.append(job)
|
|
||||||
|
|
||||||
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
|
|
||||||
if ready_jobs_count > 0:
|
if ready_jobs_count > 0:
|
||||||
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
||||||
|
|
||||||
# 找到所有超时的READY任务(只检测,不处理)
|
# 找到所有超时的READY任务(只检测,不处理)
|
||||||
for job_info in ready_candidates:
|
for job_info in self.active_jobs.values():
|
||||||
if job_info.is_ready_timeout():
|
if job_info.is_ready_timeout():
|
||||||
timeout_jobs.append(job_info)
|
timeout_jobs.append(job_info)
|
||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
@@ -400,7 +359,7 @@ class MessageProcessor:
|
|||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self.queue_processor = None # 延迟设置
|
self.queue_processor = None # 延迟设置
|
||||||
self.websocket_client = None # 延迟设置
|
self.websocket_client = None # 延迟设置
|
||||||
self.session_id = str(uuid.uuid4())[:6] # 产生一个随机的session_id
|
self.session_id = ""
|
||||||
|
|
||||||
# WebSocket连接
|
# WebSocket连接
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
@@ -409,7 +368,6 @@ class MessageProcessor:
|
|||||||
# 线程控制
|
# 线程控制
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.thread = None
|
self.thread = None
|
||||||
self._loop = None # asyncio event loop引用,用于外部关闭websocket
|
|
||||||
self.reconnect_count = 0
|
self.reconnect_count = 0
|
||||||
|
|
||||||
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
|
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
|
||||||
@@ -436,31 +394,22 @@ class MessageProcessor:
|
|||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止消息处理线程"""
|
"""停止消息处理线程"""
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
# 主动关闭websocket以快速中断消息接收循环
|
|
||||||
ws = self.websocket
|
|
||||||
loop = self._loop
|
|
||||||
if ws and loop and loop.is_running():
|
|
||||||
try:
|
|
||||||
asyncio.run_coroutine_threadsafe(ws.close(), loop)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self.thread and self.thread.is_alive():
|
if self.thread and self.thread.is_alive():
|
||||||
self.thread.join(timeout=2)
|
self.thread.join(timeout=2)
|
||||||
logger.info("[MessageProcessor] Stopped")
|
logger.info("[MessageProcessor] Stopped")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""运行消息处理主循环"""
|
"""运行消息处理主循环"""
|
||||||
self._loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
asyncio.set_event_loop(self._loop)
|
asyncio.set_event_loop(loop)
|
||||||
self._loop.run_until_complete(self._connection_handler())
|
loop.run_until_complete(self._connection_handler())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Thread error: {str(e)}")
|
logger.error(f"[MessageProcessor] Thread error: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
if self._loop:
|
if loop:
|
||||||
self._loop.close()
|
loop.close()
|
||||||
self._loop = None
|
|
||||||
|
|
||||||
async def _connection_handler(self):
|
async def _connection_handler(self):
|
||||||
"""处理WebSocket连接和重连逻辑"""
|
"""处理WebSocket连接和重连逻辑"""
|
||||||
@@ -477,10 +426,8 @@ class MessageProcessor:
|
|||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
self.websocket_url,
|
self.websocket_url,
|
||||||
ssl=ssl_context,
|
ssl=ssl_context,
|
||||||
open_timeout=20,
|
|
||||||
ping_interval=WSConfig.ping_interval,
|
ping_interval=WSConfig.ping_interval,
|
||||||
ping_timeout=10,
|
ping_timeout=10,
|
||||||
close_timeout=5,
|
|
||||||
additional_headers={
|
additional_headers={
|
||||||
"Authorization": f"Lab {BasicConfig.auth_secret()}",
|
"Authorization": f"Lab {BasicConfig.auth_secret()}",
|
||||||
"EdgeSession": f"{self.session_id}",
|
"EdgeSession": f"{self.session_id}",
|
||||||
@@ -491,98 +438,72 @@ class MessageProcessor:
|
|||||||
self.connected = True
|
self.connected = True
|
||||||
self.reconnect_count = 0
|
self.reconnect_count = 0
|
||||||
|
|
||||||
logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}")
|
logger.info(f"[MessageProcessor] Connected to {self.websocket_url}")
|
||||||
|
|
||||||
# 启动发送协程
|
# 启动发送协程
|
||||||
send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task")
|
send_task = asyncio.create_task(self._send_handler())
|
||||||
|
|
||||||
# 每次连接(含重连)后重新向服务端注册,
|
|
||||||
# 否则服务端不知道客户端已上线,不会推送消息。
|
|
||||||
if self.websocket_client:
|
|
||||||
self.websocket_client.publish_host_ready()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 接收消息循环
|
# 接收消息循环
|
||||||
await self._message_handler()
|
await self._message_handler()
|
||||||
finally:
|
finally:
|
||||||
# 必须在 async with __aexit__ 之前停止 send_task,
|
|
||||||
# 否则 send_task 会在关闭握手期间继续发送数据,
|
|
||||||
# 干扰 websockets 库的内部清理,导致 task 泄漏。
|
|
||||||
self.connected = False
|
|
||||||
send_task.cancel()
|
send_task.cancel()
|
||||||
try:
|
try:
|
||||||
await send_task
|
await send_task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
logger.warning("[MessageProcessor] 与服务端连接中断")
|
logger.warning("[MessageProcessor] Connection closed")
|
||||||
except TimeoutError:
|
|
||||||
logger.warning(
|
|
||||||
f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况"
|
|
||||||
)
|
|
||||||
except websockets.exceptions.InvalidStatus as e:
|
|
||||||
logger.warning(
|
|
||||||
f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}")
|
|
||||||
finally:
|
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[MessageProcessor] Connection error: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
self.connected = False
|
||||||
|
finally:
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
|
|
||||||
# 重连逻辑
|
# 重连逻辑
|
||||||
if not self.is_running:
|
if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts:
|
||||||
break
|
|
||||||
if self.reconnect_count < WSConfig.max_reconnect_attempts:
|
|
||||||
self.reconnect_count += 1
|
self.reconnect_count += 1
|
||||||
backoff = WSConfig.reconnect_interval
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s "
|
||||||
|
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(backoff)
|
await asyncio.sleep(WSConfig.reconnect_interval)
|
||||||
else:
|
elif self.reconnect_count >= WSConfig.max_reconnect_attempts:
|
||||||
logger.error("[MessageProcessor] Max reconnection attempts reached")
|
logger.error("[MessageProcessor] Max reconnection attempts reached")
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
self.reconnect_count -= 1
|
||||||
|
|
||||||
async def _message_handler(self):
|
async def _message_handler(self):
|
||||||
"""处理接收到的消息。
|
"""处理接收到的消息"""
|
||||||
|
|
||||||
ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler,
|
|
||||||
以便 async with websockets.connect() 的 __aexit__ 能感知连接已断,
|
|
||||||
正确清理内部 task,避免 task 泄漏。
|
|
||||||
"""
|
|
||||||
if not self.websocket:
|
if not self.websocket:
|
||||||
logger.error("[MessageProcessor] WebSocket connection is None")
|
logger.error("[MessageProcessor] WebSocket connection is None")
|
||||||
return
|
return
|
||||||
|
|
||||||
async for message in self.websocket:
|
try:
|
||||||
try:
|
async for message in self.websocket:
|
||||||
data = json.loads(message)
|
try:
|
||||||
message_type = data.get("action", "")
|
data = json.loads(message)
|
||||||
message_data = data.get("data")
|
await self._process_message(data)
|
||||||
if self.session_id and self.session_id == data.get("edge_session"):
|
except json.JSONDecodeError:
|
||||||
await self._process_message(message_type, message_data)
|
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
||||||
else:
|
except Exception as e:
|
||||||
if message_type.endswith("_material"):
|
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
||||||
logger.trace(
|
logger.error(traceback.format_exc())
|
||||||
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}"
|
|
||||||
)
|
except websockets.exceptions.ConnectionClosed:
|
||||||
logger.debug(
|
logger.info("[MessageProcessor] Message handler stopped - connection closed")
|
||||||
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}"
|
except Exception as e:
|
||||||
)
|
logger.error(f"[MessageProcessor] Message handler error: {str(e)}")
|
||||||
else:
|
logger.error(traceback.format_exc())
|
||||||
await self._process_message(message_type, message_data)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
async def _send_handler(self):
|
async def _send_handler(self):
|
||||||
"""处理发送队列中的消息"""
|
"""处理发送队列中的消息"""
|
||||||
logger.trace("[MessageProcessor] Send handler started")
|
logger.debug("[MessageProcessor] Send handler started")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while self.connected and self.websocket:
|
while self.connected and self.websocket:
|
||||||
@@ -610,7 +531,7 @@ class MessageProcessor:
|
|||||||
try:
|
try:
|
||||||
message_str = json.dumps(msg, ensure_ascii=False)
|
message_str = json.dumps(msg, ensure_ascii=False)
|
||||||
await self.websocket.send(message_str)
|
await self.websocket.send(message_str)
|
||||||
# logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501
|
logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Failed to send message: {str(e)}")
|
logger.error(f"[MessageProcessor] Failed to send message: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -627,16 +548,18 @@ class MessageProcessor:
|
|||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug("[MessageProcessor] Send handler cancelled")
|
logger.debug("[MessageProcessor] Send handler cancelled")
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
|
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
logger.debug("[MessageProcessor] Send handler stopped")
|
logger.debug("[MessageProcessor] Send handler stopped")
|
||||||
|
|
||||||
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
async def _process_message(self, data: Dict[str, Any]):
|
||||||
"""处理收到的消息"""
|
"""处理收到的消息"""
|
||||||
logger.trace(f"[MessageProcessor] Processing message: {message_type}")
|
message_type = data.get("action", "")
|
||||||
|
message_data = data.get("data")
|
||||||
|
|
||||||
|
logger.debug(f"[MessageProcessor] Processing message: {message_type}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if message_type == "pong":
|
if message_type == "pong":
|
||||||
@@ -648,23 +571,16 @@ class MessageProcessor:
|
|||||||
elif message_type == "cancel_action" or message_type == "cancel_task":
|
elif message_type == "cancel_action" or message_type == "cancel_task":
|
||||||
await self._handle_cancel_action(message_data)
|
await self._handle_cancel_action(message_data)
|
||||||
elif message_type == "add_material":
|
elif message_type == "add_material":
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._handle_resource_tree_update(message_data, "add")
|
await self._handle_resource_tree_update(message_data, "add")
|
||||||
elif message_type == "update_material":
|
elif message_type == "update_material":
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._handle_resource_tree_update(message_data, "update")
|
await self._handle_resource_tree_update(message_data, "update")
|
||||||
elif message_type == "remove_material":
|
elif message_type == "remove_material":
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await self._handle_resource_tree_update(message_data, "remove")
|
await self._handle_resource_tree_update(message_data, "remove")
|
||||||
# elif message_type == "session_id":
|
elif message_type == "session_id":
|
||||||
# self.session_id = message_data.get("session_id")
|
self.session_id = message_data.get("session_id")
|
||||||
# logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
|
logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
|
||||||
elif message_type == "add_device":
|
elif message_type == "request_reload":
|
||||||
await self._handle_device_manage(message_data, "add")
|
await self._handle_request_reload(message_data)
|
||||||
elif message_type == "remove_device":
|
|
||||||
await self._handle_device_manage(message_data, "remove")
|
|
||||||
elif message_type == "request_restart":
|
|
||||||
await self._handle_request_restart(message_data)
|
|
||||||
else:
|
else:
|
||||||
logger.debug(f"[MessageProcessor] Unknown message type: {message_type}")
|
logger.debug(f"[MessageProcessor] Unknown message type: {message_type}")
|
||||||
|
|
||||||
@@ -678,24 +594,6 @@ class MessageProcessor:
|
|||||||
if host_node:
|
if host_node:
|
||||||
host_node.handle_pong_response(pong_data)
|
host_node.handle_pong_response(pong_data)
|
||||||
|
|
||||||
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
|
|
||||||
"""检查该action是否标记为always_free,通过HostNode统一的_action_value_mappings查找"""
|
|
||||||
try:
|
|
||||||
host_node = HostNode.get_instance(0)
|
|
||||||
if not host_node:
|
|
||||||
return False
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
action_mappings = host_node._action_value_mappings.get(device_id)
|
|
||||||
if not action_mappings:
|
|
||||||
return False
|
|
||||||
# 尝试直接匹配或 auto- 前缀匹配
|
|
||||||
for key in [action_name, f"auto-{action_name}"]:
|
|
||||||
if key in action_mappings:
|
|
||||||
return action_mappings[key].get("always_free", False)
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
||||||
"""处理query_action_state消息"""
|
"""处理query_action_state消息"""
|
||||||
device_id = data.get("device_id", "")
|
device_id = data.get("device_id", "")
|
||||||
@@ -710,9 +608,6 @@ class MessageProcessor:
|
|||||||
|
|
||||||
device_action_key = f"/devices/{device_id}/{action_name}"
|
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||||
|
|
||||||
# 检查action是否为always_free
|
|
||||||
action_always_free = self._check_action_always_free(device_id, action_name)
|
|
||||||
|
|
||||||
# 创建任务信息
|
# 创建任务信息
|
||||||
job_info = JobInfo(
|
job_info = JobInfo(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
@@ -722,7 +617,6 @@ class MessageProcessor:
|
|||||||
device_action_key=device_action_key,
|
device_action_key=device_action_key,
|
||||||
status=JobStatus.QUEUE,
|
status=JobStatus.QUEUE,
|
||||||
start_time=time.time(),
|
start_time=time.time(),
|
||||||
always_free=action_always_free,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到设备管理器
|
# 添加到设备管理器
|
||||||
@@ -734,13 +628,13 @@ class MessageProcessor:
|
|||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||||
)
|
)
|
||||||
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
|
logger.info(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||||
else:
|
else:
|
||||||
# 需要排队
|
# 需要排队
|
||||||
await self._send_action_state_response(
|
await self._send_action_state_response(
|
||||||
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||||
)
|
)
|
||||||
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
logger.info(f"[MessageProcessor] Job {job_log} queued")
|
||||||
|
|
||||||
# 通知QueueProcessor有新的队列更新
|
# 通知QueueProcessor有新的队列更新
|
||||||
if self.queue_processor:
|
if self.queue_processor:
|
||||||
@@ -749,37 +643,9 @@ class MessageProcessor:
|
|||||||
async def _handle_job_start(self, data: Dict[str, Any]):
|
async def _handle_job_start(self, data: Dict[str, Any]):
|
||||||
"""处理job_start消息"""
|
"""处理job_start消息"""
|
||||||
try:
|
try:
|
||||||
if not data.get("sample_material"):
|
|
||||||
data["sample_material"] = {}
|
|
||||||
req = JobAddReq(**data)
|
req = JobAddReq(**data)
|
||||||
|
|
||||||
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
||||||
|
|
||||||
# 服务端对always_free动作可能跳过query_action_state直接发job_start,
|
|
||||||
# 此时job尚未注册,需要自动补注册
|
|
||||||
existing_job = self.device_manager.get_job_info(req.job_id)
|
|
||||||
if not existing_job:
|
|
||||||
action_name = req.action
|
|
||||||
device_action_key = f"/devices/{req.device_id}/{action_name}"
|
|
||||||
action_always_free = self._check_action_always_free(req.device_id, action_name)
|
|
||||||
|
|
||||||
if action_always_free:
|
|
||||||
job_info = JobInfo(
|
|
||||||
job_id=req.job_id,
|
|
||||||
task_id=req.task_id,
|
|
||||||
device_id=req.device_id,
|
|
||||||
action_name=action_name,
|
|
||||||
device_action_key=device_action_key,
|
|
||||||
status=JobStatus.QUEUE,
|
|
||||||
start_time=time.time(),
|
|
||||||
always_free=True,
|
|
||||||
)
|
|
||||||
self.device_manager.add_queue_request(job_info)
|
|
||||||
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
|
|
||||||
else:
|
|
||||||
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
|
|
||||||
return
|
|
||||||
|
|
||||||
success = self.device_manager.start_job(req.job_id)
|
success = self.device_manager.start_job(req.job_id)
|
||||||
if not success:
|
if not success:
|
||||||
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
||||||
@@ -808,7 +674,6 @@ class MessageProcessor:
|
|||||||
queue_item,
|
queue_item,
|
||||||
action_type=req.action_type,
|
action_type=req.action_type,
|
||||||
action_kwargs=req.action_args,
|
action_kwargs=req.action_args,
|
||||||
sample_material=req.sample_material,
|
|
||||||
server_info=req.server_info,
|
server_info=req.server_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -973,7 +838,9 @@ class MessageProcessor:
|
|||||||
device_action_groups[key_add] = []
|
device_action_groups[key_add] = []
|
||||||
device_action_groups[key_add].append(item["uuid"])
|
device_action_groups[key_add].append(item["uuid"])
|
||||||
|
|
||||||
logger.info(f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}")
|
logger.info(
|
||||||
|
f"[MessageProcessor] Resource migrated: {item['uuid'][:8]} from {device_old_id} to {device_id}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 正常update
|
# 正常update
|
||||||
key = (device_id, "update")
|
key = (device_id, "update")
|
||||||
@@ -987,13 +854,11 @@ class MessageProcessor:
|
|||||||
device_action_groups[key] = []
|
device_action_groups[key] = []
|
||||||
device_action_groups[key].append(item["uuid"])
|
device_action_groups[key].append(item["uuid"])
|
||||||
|
|
||||||
logger.trace(
|
logger.info(f"触发物料更新 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
|
||||||
f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 为每个(device_id, action)创建独立的更新线程
|
# 为每个(device_id, action)创建独立的更新线程
|
||||||
for (device_id, actual_action), items in device_action_groups.items():
|
for (device_id, actual_action), items in device_action_groups.items():
|
||||||
logger.trace(f"[资源同步] {device_id} 物料动作 {actual_action} 数量: {len(items)}")
|
logger.info(f"设备 {device_id} 物料更新 {actual_action} 数量: {len(items)}")
|
||||||
|
|
||||||
def _notify_resource_tree(dev_id, act, item_list):
|
def _notify_resource_tree(dev_id, act, item_list):
|
||||||
try:
|
try:
|
||||||
@@ -1025,80 +890,19 @@ class MessageProcessor:
|
|||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
async def _handle_device_manage(self, device_list: list[ResourceDictType], action: str):
|
async def _handle_request_reload(self, data: Dict[str, Any]):
|
||||||
"""Handle add_device / remove_device from LabGo server."""
|
|
||||||
if not device_list:
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in device_list:
|
|
||||||
target_node_id = item.get("target_node_id", "host_node")
|
|
||||||
|
|
||||||
def _notify(target_id: str, act: str, cfg: ResourceDictType):
|
|
||||||
try:
|
|
||||||
host_node = HostNode.get_instance(timeout=5)
|
|
||||||
if not host_node:
|
|
||||||
logger.error(f"[DeviceManage] HostNode not available for {act}_device")
|
|
||||||
return
|
|
||||||
success = host_node.notify_device_manage(target_id, act, cfg)
|
|
||||||
if success:
|
|
||||||
logger.info(f"[DeviceManage] {act}_device completed on {target_id}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"[DeviceManage] {act}_device failed on {target_id}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[DeviceManage] Error in {act}_device: {e}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
thread = threading.Thread(
|
|
||||||
target=_notify,
|
|
||||||
args=(target_node_id, action, item),
|
|
||||||
daemon=True,
|
|
||||||
name=f"DeviceManage-{action}-{item.get('id', '')}",
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
async def _handle_request_restart(self, data: Dict[str, Any]):
|
|
||||||
"""
|
"""
|
||||||
处理重启请求
|
处理重载请求
|
||||||
|
|
||||||
当LabGo发送request_restart时,执行清理并触发重启
|
当LabGo发送request_reload时,重新发送设备注册信息
|
||||||
"""
|
"""
|
||||||
reason = data.get("reason", "unknown")
|
reason = data.get("reason", "unknown")
|
||||||
delay = data.get("delay", 2) # 默认延迟2秒
|
logger.info(f"[MessageProcessor] Received reload request, reason: {reason}")
|
||||||
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
|
|
||||||
|
# 重新发送host_node_ready信息
|
||||||
# 发送确认消息
|
if self.websocket_client:
|
||||||
self.send_message(
|
self.websocket_client.publish_host_ready()
|
||||||
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
|
logger.info("[MessageProcessor] Re-sent host_node_ready after reload request")
|
||||||
)
|
|
||||||
|
|
||||||
# 设置全局重启标志
|
|
||||||
import unilabos.app.main as main_module
|
|
||||||
|
|
||||||
main_module._restart_requested = True
|
|
||||||
main_module._restart_reason = reason
|
|
||||||
|
|
||||||
# 延迟后执行清理
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
|
|
||||||
# 在新线程中执行清理,避免阻塞当前事件循环
|
|
||||||
def do_cleanup():
|
|
||||||
import time
|
|
||||||
|
|
||||||
time.sleep(0.5) # 给当前消息处理完成的时间
|
|
||||||
logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}")
|
|
||||||
try:
|
|
||||||
from unilabos.app.utils import cleanup_for_restart
|
|
||||||
|
|
||||||
if cleanup_for_restart():
|
|
||||||
logger.info("[MessageProcessor] Cleanup successful, main() will restart")
|
|
||||||
else:
|
|
||||||
logger.error("[MessageProcessor] Cleanup failed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[MessageProcessor] Error during cleanup: {e}")
|
|
||||||
|
|
||||||
cleanup_thread = threading.Thread(target=do_cleanup, name="RestartCleanupThread", daemon=True)
|
|
||||||
cleanup_thread.start()
|
|
||||||
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
|
|
||||||
|
|
||||||
async def _send_action_state_response(
|
async def _send_action_state_response(
|
||||||
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
|
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
|
||||||
@@ -1171,14 +975,13 @@ class QueueProcessor:
|
|||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止队列处理线程"""
|
"""停止队列处理线程"""
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.queue_update_event.set() # 立即唤醒等待中的线程
|
|
||||||
if self.thread and self.thread.is_alive():
|
if self.thread and self.thread.is_alive():
|
||||||
self.thread.join(timeout=2)
|
self.thread.join(timeout=2)
|
||||||
logger.info("[QueueProcessor] Stopped")
|
logger.info("[QueueProcessor] Stopped")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""运行队列处理主循环"""
|
"""运行队列处理主循环"""
|
||||||
logger.trace("[QueueProcessor] Queue processor started")
|
logger.debug("[QueueProcessor] Queue processor started")
|
||||||
|
|
||||||
while self.is_running:
|
while self.is_running:
|
||||||
try:
|
try:
|
||||||
@@ -1272,11 +1075,6 @@ class QueueProcessor:
|
|||||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||||
|
|
||||||
for job_info in queued_jobs:
|
for job_info in queued_jobs:
|
||||||
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
|
||||||
# 此时不应再发送 busy/need_more,否则会覆盖已发出的 free=True 通知
|
|
||||||
if job_info.status != JobStatus.QUEUE:
|
|
||||||
continue
|
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"action": "report_action_state",
|
"action": "report_action_state",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -1292,7 +1090,7 @@ class QueueProcessor:
|
|||||||
success = self.message_processor.send_message(message)
|
success = self.message_processor.send_message(message)
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
if success:
|
if success:
|
||||||
logger.trace(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
logger.debug(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
||||||
|
|
||||||
@@ -1315,7 +1113,7 @@ class QueueProcessor:
|
|||||||
job_info.action_name,
|
job_info.action_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.trace(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
||||||
|
|
||||||
# 结束任务,获取下一个可执行的任务
|
# 结束任务,获取下一个可执行的任务
|
||||||
next_job = self.device_manager.end_job(job_id)
|
next_job = self.device_manager.end_job(job_id)
|
||||||
@@ -1335,8 +1133,8 @@ class QueueProcessor:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
||||||
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
logger.info(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||||
|
|
||||||
# 立即触发下一轮状态检查
|
# 立即触发下一轮状态检查
|
||||||
self.notify_queue_update()
|
self.notify_queue_update()
|
||||||
@@ -1393,6 +1191,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
else:
|
else:
|
||||||
url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule"
|
url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule"
|
||||||
|
|
||||||
|
logger.debug(f"[WebSocketClient] URL: {url}")
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@@ -1405,11 +1204,13 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
logger.error("[WebSocketClient] WebSocket URL not configured")
|
logger.error("[WebSocketClient] WebSocket URL not configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info(f"[WebSocketClient] Starting connection to {self.websocket_url}")
|
||||||
|
|
||||||
# 启动两个核心线程
|
# 启动两个核心线程
|
||||||
self.message_processor.start()
|
self.message_processor.start()
|
||||||
self.queue_processor.start()
|
self.queue_processor.start()
|
||||||
|
|
||||||
logger.trace("[WebSocketClient] All threads started")
|
logger.info("[WebSocketClient] All threads started")
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""停止WebSocket客户端"""
|
"""停止WebSocket客户端"""
|
||||||
@@ -1425,8 +1226,8 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
message = {"action": "normal_exit", "data": {"session_id": session_id}}
|
message = {"action": "normal_exit", "data": {"session_id": session_id}}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
|
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
|
||||||
# send_handler 每100ms检查一次队列,等300ms足以让消息发出
|
# 给一点时间让消息发送出去
|
||||||
time.sleep(0.3)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
|
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
|
||||||
|
|
||||||
@@ -1458,7 +1259,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.message_processor.send_message(message)
|
self.message_processor.send_message(message)
|
||||||
# logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
||||||
|
|
||||||
def publish_job_status(
|
def publish_job_status(
|
||||||
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
|
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
|
||||||
@@ -1478,7 +1279,7 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
||||||
|
|
||||||
# logger.debug(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
logger.info(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
||||||
|
|
||||||
# 通知队列处理器job完成(包括timeout的job)
|
# 通知队列处理器job完成(包括timeout的job)
|
||||||
self.queue_processor.handle_job_completed(item.job_id, status)
|
self.queue_processor.handle_job_completed(item.job_id, status)
|
||||||
@@ -1539,17 +1340,15 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
# 收集设备信息
|
# 收集设备信息
|
||||||
devices = []
|
devices = []
|
||||||
machine_name = BasicConfig.machine_name
|
machine_name = BasicConfig.machine_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
host_node = HostNode.get_instance(0)
|
host_node = HostNode.get_instance(0)
|
||||||
if host_node:
|
if host_node:
|
||||||
# 获取设备信息
|
# 获取设备信息
|
||||||
for device_id, namespace in host_node.devices_names.items():
|
for device_id, namespace in host_node.devices_names.items():
|
||||||
device_key = (
|
device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
||||||
f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
|
||||||
)
|
|
||||||
is_online = device_key in host_node._online_devices
|
is_online = device_key in host_node._online_devices
|
||||||
|
|
||||||
# 获取设备的动作信息
|
# 获取设备的动作信息
|
||||||
actions = {}
|
actions = {}
|
||||||
for action_id, client in host_node._action_clients.items():
|
for action_id, client in host_node._action_clients.items():
|
||||||
@@ -1560,18 +1359,16 @@ class WebSocketClient(BaseCommunicationClient):
|
|||||||
"action_path": action_id,
|
"action_path": action_id,
|
||||||
"action_type": str(type(client).__name__),
|
"action_type": str(type(client).__name__),
|
||||||
}
|
}
|
||||||
|
|
||||||
devices.append(
|
devices.append({
|
||||||
{
|
"device_id": device_id,
|
||||||
"device_id": device_id,
|
"namespace": namespace,
|
||||||
"namespace": namespace,
|
"device_key": device_key,
|
||||||
"device_key": device_key,
|
"is_online": is_online,
|
||||||
"is_online": is_online,
|
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
||||||
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
"actions": actions,
|
||||||
"actions": actions,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
|
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[WebSocketClient] Error collecting device info: {e}")
|
logger.warning(f"[WebSocketClient] Error collecting device info: {e}")
|
||||||
|
|||||||
@@ -95,29 +95,8 @@ def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
|||||||
return total_volume
|
return total_volume
|
||||||
|
|
||||||
|
|
||||||
def is_integrated_pump(node_class: str, node_name: str = "") -> bool:
|
def is_integrated_pump(node_name):
|
||||||
"""
|
return "pump" in node_name and "valve" in node_name
|
||||||
判断是否为泵阀一体设备
|
|
||||||
"""
|
|
||||||
class_lower = (node_class or "").lower()
|
|
||||||
name_lower = (node_name or "").lower()
|
|
||||||
|
|
||||||
if "pump" not in class_lower and "pump" not in name_lower:
|
|
||||||
return False
|
|
||||||
|
|
||||||
integrated_markers = [
|
|
||||||
"valve",
|
|
||||||
"pump_valve",
|
|
||||||
"pumpvalve",
|
|
||||||
"integrated",
|
|
||||||
"transfer_pump",
|
|
||||||
]
|
|
||||||
|
|
||||||
for marker in integrated_markers:
|
|
||||||
if marker in class_lower or marker in name_lower:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def find_connected_pump(G, valve_node):
|
def find_connected_pump(G, valve_node):
|
||||||
@@ -207,9 +186,7 @@ def build_pump_valve_maps(G, pump_backbone):
|
|||||||
debug_print(f"🔧 过滤后的骨架: {filtered_backbone}")
|
debug_print(f"🔧 过滤后的骨架: {filtered_backbone}")
|
||||||
|
|
||||||
for node in filtered_backbone:
|
for node in filtered_backbone:
|
||||||
node_data = G.nodes.get(node, {})
|
if is_integrated_pump(G.nodes[node]["class"]):
|
||||||
node_class = node_data.get("class", "") or ""
|
|
||||||
if is_integrated_pump(node_class, node):
|
|
||||||
pumps_from_node[node] = node
|
pumps_from_node[node] = node
|
||||||
valve_from_node[node] = node
|
valve_from_node[node] = node
|
||||||
debug_print(f" - 集成泵-阀: {node}")
|
debug_print(f" - 集成泵-阀: {node}")
|
||||||
|
|||||||
@@ -16,17 +16,12 @@ class BasicConfig:
|
|||||||
upload_registry = False
|
upload_registry = False
|
||||||
machine_name = "undefined"
|
machine_name = "undefined"
|
||||||
vis_2d_enable = False
|
vis_2d_enable = False
|
||||||
no_update_feedback = False
|
|
||||||
enable_resource_load = True
|
enable_resource_load = True
|
||||||
communication_protocol = "websocket"
|
communication_protocol = "websocket"
|
||||||
startup_json_path = None # 填写绝对路径
|
startup_json_path = None # 填写绝对路径
|
||||||
disable_browser = False # 禁止浏览器自动打开
|
disable_browser = False # 禁止浏览器自动打开
|
||||||
port = 8002 # 本地HTTP服务
|
port = 8002 # 本地HTTP服务
|
||||||
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||||
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
|
|
||||||
extra_resource = False # 是否加载lab_开头的额外资源
|
|
||||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
|
||||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def auth_secret(cls):
|
def auth_secret(cls):
|
||||||
@@ -41,7 +36,7 @@ class BasicConfig:
|
|||||||
class WSConfig:
|
class WSConfig:
|
||||||
reconnect_interval = 5 # 重连间隔(秒)
|
reconnect_interval = 5 # 重连间隔(秒)
|
||||||
max_reconnect_attempts = 999 # 最大重连次数
|
max_reconnect_attempts = 999 # 最大重连次数
|
||||||
ping_interval = 20 # ping间隔(秒)
|
ping_interval = 30 # ping间隔(秒)
|
||||||
|
|
||||||
|
|
||||||
# HTTP配置
|
# HTTP配置
|
||||||
@@ -70,14 +65,13 @@ def _update_config_from_module(module):
|
|||||||
if not attr.startswith("_"):
|
if not attr.startswith("_"):
|
||||||
setattr(obj, attr, getattr(getattr(module, name), attr))
|
setattr(obj, attr, getattr(getattr(module, name), attr))
|
||||||
|
|
||||||
|
|
||||||
def _update_config_from_env():
|
def _update_config_from_env():
|
||||||
prefix = "UNILABOS_"
|
prefix = "UNILABOS_"
|
||||||
for env_key, env_value in os.environ.items():
|
for env_key, env_value in os.environ.items():
|
||||||
if not env_key.startswith(prefix):
|
if not env_key.startswith(prefix):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
key_path = env_key[len(prefix) :] # Remove UNILAB_ prefix
|
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:
|
if len(class_field) != 2:
|
||||||
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")
|
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")
|
||||||
@@ -147,5 +141,5 @@ def load_config(config_path=None):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "example_config.py")
|
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||||
load_config(config_path)
|
load_config(config_path)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Coin Cell Assembly Workstation
|
|||||||
"""
|
"""
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
|
|
||||||
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
|
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
|
||||||
from unilabos.device_comms.workstation_communication import (
|
from unilabos.device_comms.workstation_communication import (
|
||||||
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
|
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
|
||||||
@@ -61,7 +61,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
# 创建资源跟踪器(如果没有提供)
|
# 创建资源跟踪器(如果没有提供)
|
||||||
if resource_tracker is None:
|
if resource_tracker is None:
|
||||||
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
resource_tracker = DeviceNodeResourceTracker()
|
resource_tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
# 初始化基类
|
# 初始化基类
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import inspect
|
import inspect
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from pylabrobot.resources import (
|
|||||||
import copy
|
import copy
|
||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
|
|
||||||
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker # type: ignore
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class LiquidHandlerBiomek:
|
class LiquidHandlerBiomek:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,150 +0,0 @@
|
|||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
from .prcxi import PRCXI9300ModuleSite
|
|
||||||
|
|
||||||
|
|
||||||
class PRCXI9300FunctionalModule(PRCXI9300ModuleSite):
|
|
||||||
"""
|
|
||||||
PRCXI 9300 功能模块基类(加热/冷却/震荡/加热震荡/磁吸等)。
|
|
||||||
|
|
||||||
设计目标:
|
|
||||||
- 作为一个可以在工作台上拖拽摆放的实体资源(继承自 PRCXI9300ModuleSite -> ItemizedCarrier)。
|
|
||||||
- 顶面存在一个站点(site),可吸附标准板类资源(plate / tip_rack / tube_rack 等)。
|
|
||||||
- 支持注入 `material_info` (UUID 等),并且在 serialize_state 时做安全过滤。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float,
|
|
||||||
size_y: float,
|
|
||||||
size_z: float,
|
|
||||||
module_type: Optional[str] = None,
|
|
||||||
category: str = "module",
|
|
||||||
model: Optional[str] = None,
|
|
||||||
material_info: Optional[Dict[str, Any]] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
super().__init__(
|
|
||||||
name=name,
|
|
||||||
size_x=size_x,
|
|
||||||
size_y=size_y,
|
|
||||||
size_z=size_z,
|
|
||||||
material_info=material_info,
|
|
||||||
model=model,
|
|
||||||
category=category,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 记录模块类型(加热 / 冷却 / 震荡 / 加热震荡 / 磁吸)
|
|
||||||
self.module_type = module_type or "generic"
|
|
||||||
|
|
||||||
# 与 PRCXI9300PlateAdapter 一致,使用 _unilabos_state 保存扩展信息
|
|
||||||
if not hasattr(self, "_unilabos_state") or self._unilabos_state is None:
|
|
||||||
self._unilabos_state = {}
|
|
||||||
|
|
||||||
# super().__init__ 已经在有 material_info 时写入 "Material",这里仅确保存在
|
|
||||||
if material_info is not None and "Material" not in self._unilabos_state:
|
|
||||||
self._unilabos_state["Material"] = material_info
|
|
||||||
|
|
||||||
# 额外标记 category 和模块类型,便于前端或上层逻辑区分
|
|
||||||
self._unilabos_state.setdefault("category", category)
|
|
||||||
self._unilabos_state["module_type"] = module_type
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 具体功能模块定义
|
|
||||||
# 这里的尺寸和 material_info 目前为占位参数,后续可根据实际测量/JSON 配置进行更新。
|
|
||||||
# 顶面站点尺寸与模块外形一致,保证可以吸附标准 96 板/储液槽等。
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
def PRCXI_Heating_Module(name: str) -> PRCXI9300FunctionalModule:
|
|
||||||
"""加热模块(顶面可吸附标准板)。"""
|
|
||||||
return PRCXI9300FunctionalModule(
|
|
||||||
name=name,
|
|
||||||
size_x=127.76,
|
|
||||||
size_y=85.48,
|
|
||||||
size_z=40.0,
|
|
||||||
module_type="heating",
|
|
||||||
model="PRCXI_Heating_Module",
|
|
||||||
material_info={
|
|
||||||
"uuid": "TODO-HEATING-MODULE-UUID",
|
|
||||||
"Code": "HEAT-MOD",
|
|
||||||
"Name": "PRCXI 加热模块",
|
|
||||||
"SupplyType": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def PRCXI_MetalCooling_Module(name: str) -> PRCXI9300FunctionalModule:
|
|
||||||
"""金属冷却模块(顶面可吸附标准板)。"""
|
|
||||||
return PRCXI9300FunctionalModule(
|
|
||||||
name=name,
|
|
||||||
size_x=127.76,
|
|
||||||
size_y=85.48,
|
|
||||||
size_z=40.0,
|
|
||||||
module_type="metal_cooling",
|
|
||||||
model="PRCXI_MetalCooling_Module",
|
|
||||||
material_info={
|
|
||||||
"uuid": "TODO-METAL-COOLING-MODULE-UUID",
|
|
||||||
"Code": "METAL-COOL-MOD",
|
|
||||||
"Name": "PRCXI 金属冷却模块",
|
|
||||||
"SupplyType": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def PRCXI_Shaking_Module(name: str) -> PRCXI9300FunctionalModule:
|
|
||||||
"""震荡模块(顶面可吸附标准板)。"""
|
|
||||||
return PRCXI9300FunctionalModule(
|
|
||||||
name=name,
|
|
||||||
size_x=127.76,
|
|
||||||
size_y=85.48,
|
|
||||||
size_z=50.0,
|
|
||||||
module_type="shaking",
|
|
||||||
model="PRCXI_Shaking_Module",
|
|
||||||
material_info={
|
|
||||||
"uuid": "TODO-SHAKING-MODULE-UUID",
|
|
||||||
"Code": "SHAKE-MOD",
|
|
||||||
"Name": "PRCXI 震荡模块",
|
|
||||||
"SupplyType": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def PRCXI_Heating_Shaking_Module(name: str) -> PRCXI9300FunctionalModule:
|
|
||||||
"""加热震荡模块(顶面可吸附标准板)。"""
|
|
||||||
return PRCXI9300FunctionalModule(
|
|
||||||
name=name,
|
|
||||||
size_x=127.76,
|
|
||||||
size_y=85.48,
|
|
||||||
size_z=55.0,
|
|
||||||
module_type="heating_shaking",
|
|
||||||
model="PRCXI_Heating_Shaking_Module",
|
|
||||||
material_info={
|
|
||||||
"uuid": "TODO-HEATING-SHAKING-MODULE-UUID",
|
|
||||||
"Code": "HEAT-SHAKE-MOD",
|
|
||||||
"Name": "PRCXI 加热震荡模块",
|
|
||||||
"SupplyType": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def PRCXI_Magnetic_Module(name: str) -> PRCXI9300FunctionalModule:
|
|
||||||
"""磁吸模块(顶面可吸附标准板)。"""
|
|
||||||
return PRCXI9300FunctionalModule(
|
|
||||||
name=name,
|
|
||||||
size_x=127.76,
|
|
||||||
size_y=85.48,
|
|
||||||
size_z=30.0,
|
|
||||||
module_type="magnetic",
|
|
||||||
model="PRCXI_Magnetic_Module",
|
|
||||||
material_info={
|
|
||||||
"uuid": "TODO-MAGNETIC-MODULE-UUID",
|
|
||||||
"Code": "MAG-MOD",
|
|
||||||
"Name": "PRCXI 磁吸模块",
|
|
||||||
"SupplyType": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -19,11 +19,10 @@ from rclpy.node import Node
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", registry_name: str = "lh_joint_publisher", **kwargs):
|
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
"""虚拟样品演示设备 — 用于前端 sample tracking 功能的极简 demo"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualSampleDemo:
|
|
||||||
"""虚拟样品追踪演示设备,提供两种典型返回模式:
|
|
||||||
- measure_samples: 等长输入输出 (前端按 index 自动对齐)
|
|
||||||
- split_and_measure: 输出比输入长,附带 samples 列标注归属
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
|
||||||
if device_id is None and "id" in kwargs:
|
|
||||||
device_id = kwargs.pop("id")
|
|
||||||
if config is None and "config" in kwargs:
|
|
||||||
config = kwargs.pop("config")
|
|
||||||
|
|
||||||
self.device_id = device_id or "unknown_sample_demo"
|
|
||||||
self.config = config or {}
|
|
||||||
self.logger = logging.getLogger(f"VirtualSampleDemo.{self.device_id}")
|
|
||||||
self.data: Dict[str, Any] = {"status": "Idle"}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 1: 等长输入输出,无 samples 列
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def measure_samples(self, concentrations: List[float]) -> Dict[str, Any]:
|
|
||||||
"""模拟光度测量。absorbance = concentration * 0.05 + noise
|
|
||||||
|
|
||||||
入参和出参 list 长度相等,前端按 index 自动对齐。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"measure_samples: concentrations={concentrations}")
|
|
||||||
absorbance = [round(c * 0.05 + random.gauss(0, 0.005), 4) for c in concentrations]
|
|
||||||
return {"concentrations": concentrations, "absorbance": absorbance}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 2: 输出比输入长,带 samples 列
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def split_and_measure(self, volumes: List[float], split_count: int = 3) -> Dict[str, Any]:
|
|
||||||
"""将每个样品均分为 split_count 份后逐份测量。
|
|
||||||
|
|
||||||
返回的 list 长度 = len(volumes) * split_count,
|
|
||||||
附带 samples 列标注每行属于第几个输入样品 (0-based index)。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"split_and_measure: volumes={volumes}, split_count={split_count}")
|
|
||||||
out_volumes: List[float] = []
|
|
||||||
readings: List[float] = []
|
|
||||||
samples: List[int] = []
|
|
||||||
|
|
||||||
for idx, vol in enumerate(volumes):
|
|
||||||
split_vol = round(vol / split_count, 2)
|
|
||||||
for _ in range(split_count):
|
|
||||||
out_volumes.append(split_vol)
|
|
||||||
readings.append(round(random.uniform(0.1, 1.0), 4))
|
|
||||||
samples.append(idx)
|
|
||||||
|
|
||||||
return {"volumes": out_volumes, "readings": readings, "unilabos_samples": samples}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 3: 入参和出参都带 samples 列(不等长)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def analyze_readings(self, readings: List[float], samples: List[int]) -> Dict[str, Any]:
|
|
||||||
"""对 split_and_measure 的输出做二次分析。
|
|
||||||
|
|
||||||
入参 readings/samples 长度相同但 > 原始样品数,
|
|
||||||
出参同样带 samples 列,长度与入参一致。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"analyze_readings: readings={readings}, samples={samples}")
|
|
||||||
scores: List[float] = []
|
|
||||||
passed: List[bool] = []
|
|
||||||
threshold = 0.4
|
|
||||||
|
|
||||||
for r in readings:
|
|
||||||
score = round(r * 100 + random.gauss(0, 2), 2)
|
|
||||||
scores.append(score)
|
|
||||||
passed.append(r >= threshold)
|
|
||||||
|
|
||||||
return {"scores": scores, "passed": passed, "unilabos_samples": samples}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 状态属性
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self.data.get("status", "Idle")
|
|
||||||
@@ -15,35 +15,35 @@ class VirtualPumpMode(Enum):
|
|||||||
|
|
||||||
class VirtualTransferPump:
|
class VirtualTransferPump:
|
||||||
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
||||||
|
|
||||||
_ros_node: BaseROS2DeviceNode
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
初始化虚拟转移泵
|
初始化虚拟转移泵
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
device_id: 设备ID
|
device_id: 设备ID
|
||||||
config: 配置字典,包含max_volume, port等参数
|
config: 配置字典,包含max_volume, port等参数
|
||||||
**kwargs: 其他参数,确保兼容性
|
**kwargs: 其他参数,确保兼容性
|
||||||
"""
|
"""
|
||||||
self.device_id = device_id or "virtual_transfer_pump"
|
self.device_id = device_id or "virtual_transfer_pump"
|
||||||
|
|
||||||
# 从config或kwargs中获取参数,确保类型正确
|
# 从config或kwargs中获取参数,确保类型正确
|
||||||
if config:
|
if config:
|
||||||
self.max_volume = float(config.get("max_volume", 25.0))
|
self.max_volume = float(config.get('max_volume', 25.0))
|
||||||
self.port = config.get("port", "VIRTUAL")
|
self.port = config.get('port', 'VIRTUAL')
|
||||||
else:
|
else:
|
||||||
self.max_volume = float(kwargs.get("max_volume", 25.0))
|
self.max_volume = float(kwargs.get('max_volume', 25.0))
|
||||||
self.port = kwargs.get("port", "VIRTUAL")
|
self.port = kwargs.get('port', 'VIRTUAL')
|
||||||
|
|
||||||
self._transfer_rate = float(kwargs.get("transfer_rate", 0))
|
self._transfer_rate = float(kwargs.get('transfer_rate', 0))
|
||||||
self.mode = kwargs.get("mode", VirtualPumpMode.Normal)
|
self.mode = kwargs.get('mode', VirtualPumpMode.Normal)
|
||||||
|
|
||||||
# 状态变量 - 确保都是正确类型
|
# 状态变量 - 确保都是正确类型
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
self._position = 0.0 # float
|
self._position = 0.0 # float
|
||||||
self._max_velocity = 5.0 # float
|
self._max_velocity = 5.0 # float
|
||||||
self._current_volume = 0.0 # float
|
self._current_volume = 0.0 # float
|
||||||
|
|
||||||
# 🚀 新增:快速模式设置 - 大幅缩短执行时间
|
# 🚀 新增:快速模式设置 - 大幅缩短执行时间
|
||||||
@@ -52,16 +52,14 @@ class VirtualTransferPump:
|
|||||||
self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
|
self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
|
||||||
|
|
||||||
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
||||||
|
|
||||||
print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
|
print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
|
||||||
print(
|
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
|
||||||
f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s"
|
|
||||||
)
|
|
||||||
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
|
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
|
||||||
|
|
||||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""初始化虚拟泵 🚀"""
|
"""初始化虚拟泵 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
||||||
@@ -70,33 +68,33 @@ class VirtualTransferPump:
|
|||||||
self._current_volume = 0.0
|
self._current_volume = 0.0
|
||||||
self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
|
self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def cleanup(self) -> bool:
|
async def cleanup(self) -> bool:
|
||||||
"""清理虚拟泵 🧹"""
|
"""清理虚拟泵 🧹"""
|
||||||
self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 基本属性
|
# 基本属性
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> float:
|
def position(self) -> float:
|
||||||
"""当前柱塞位置 (ml) 📍"""
|
"""当前柱塞位置 (ml) 📍"""
|
||||||
return self._position
|
return self._position
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_volume(self) -> float:
|
def current_volume(self) -> float:
|
||||||
"""当前注射器中的体积 (ml) 💧"""
|
"""当前注射器中的体积 (ml) 💧"""
|
||||||
return self._current_volume
|
return self._current_volume
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_velocity(self) -> float:
|
def max_velocity(self) -> float:
|
||||||
return self._max_velocity
|
return self._max_velocity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transfer_rate(self) -> float:
|
def transfer_rate(self) -> float:
|
||||||
return self._transfer_rate
|
return self._transfer_rate
|
||||||
@@ -105,17 +103,17 @@ class VirtualTransferPump:
|
|||||||
"""设置最大速度 (ml/s) 🌊"""
|
"""设置最大速度 (ml/s) 🌊"""
|
||||||
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
|
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
|
||||||
self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
|
self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
|
||||||
|
|
||||||
def get_status(self) -> str:
|
def get_status(self) -> str:
|
||||||
"""获取泵状态 📋"""
|
"""获取泵状态 📋"""
|
||||||
return self._status
|
return self._status
|
||||||
|
|
||||||
async def _simulate_operation(self, duration: float):
|
async def _simulate_operation(self, duration: float):
|
||||||
"""模拟操作延时 ⏱️"""
|
"""模拟操作延时 ⏱️"""
|
||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
await self._ros_node.sleep(duration)
|
await self._ros_node.sleep(duration)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||||
"""
|
"""
|
||||||
计算操作持续时间 ⏰
|
计算操作持续时间 ⏰
|
||||||
@@ -123,10 +121,10 @@ class VirtualTransferPump:
|
|||||||
"""
|
"""
|
||||||
if velocity is None:
|
if velocity is None:
|
||||||
velocity = self._max_velocity
|
velocity = self._max_velocity
|
||||||
|
|
||||||
# 📊 计算理论时间(用于日志显示)
|
# 📊 计算理论时间(用于日志显示)
|
||||||
theoretical_duration = abs(volume) / velocity
|
theoretical_duration = abs(volume) / velocity
|
||||||
|
|
||||||
# 🚀 如果启用快速模式,使用固定的快速时间
|
# 🚀 如果启用快速模式,使用固定的快速时间
|
||||||
if self._fast_mode:
|
if self._fast_mode:
|
||||||
# 根据操作类型选择快速时间
|
# 根据操作类型选择快速时间
|
||||||
@@ -134,13 +132,13 @@ class VirtualTransferPump:
|
|||||||
actual_duration = self._fast_move_time
|
actual_duration = self._fast_move_time
|
||||||
else: # 很小的操作
|
else: # 很小的操作
|
||||||
actual_duration = 0.5
|
actual_duration = 0.5
|
||||||
|
|
||||||
self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
|
self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
|
||||||
return actual_duration
|
return actual_duration
|
||||||
else:
|
else:
|
||||||
# 正常模式使用理论时间
|
# 正常模式使用理论时间
|
||||||
return theoretical_duration
|
return theoretical_duration
|
||||||
|
|
||||||
def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
|
def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
|
||||||
"""
|
"""
|
||||||
计算显示用的持续时间(用于日志) 📊
|
计算显示用的持续时间(用于日志) 📊
|
||||||
@@ -149,16 +147,16 @@ class VirtualTransferPump:
|
|||||||
if velocity is None:
|
if velocity is None:
|
||||||
velocity = self._max_velocity
|
velocity = self._max_velocity
|
||||||
return abs(volume) / velocity
|
return abs(volume) / velocity
|
||||||
|
|
||||||
# 新的set_position方法 - 专门用于SetPumpPosition动作
|
# 新的set_position方法 - 专门用于SetPumpPosition动作
|
||||||
async def set_position(self, position: float, max_velocity: float = None):
|
async def set_position(self, position: float, max_velocity: float = None):
|
||||||
"""
|
"""
|
||||||
移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
|
移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position (float): 目标位置 (ml)
|
position (float): 目标位置 (ml)
|
||||||
max_velocity (float): 移动速度 (ml/s)
|
max_velocity (float): 移动速度 (ml/s)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: 符合SetPumpPosition.action定义的结果
|
dict: 符合SetPumpPosition.action定义的结果
|
||||||
"""
|
"""
|
||||||
@@ -166,19 +164,19 @@ class VirtualTransferPump:
|
|||||||
# 验证并转换参数
|
# 验证并转换参数
|
||||||
target_position = float(position)
|
target_position = float(position)
|
||||||
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
|
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
|
||||||
|
|
||||||
# 限制位置在有效范围内
|
# 限制位置在有效范围内
|
||||||
target_position = max(0.0, min(float(self.max_volume), target_position))
|
target_position = max(0.0, min(float(self.max_volume), target_position))
|
||||||
|
|
||||||
# 计算移动距离
|
# 计算移动距离
|
||||||
volume_to_move = abs(target_position - self._position)
|
volume_to_move = abs(target_position - self._position)
|
||||||
|
|
||||||
# 📊 计算显示用的时间(用于日志)
|
# 📊 计算显示用的时间(用于日志)
|
||||||
display_duration = self._calculate_display_duration(volume_to_move, velocity)
|
display_duration = self._calculate_display_duration(volume_to_move, velocity)
|
||||||
|
|
||||||
# ⚡ 计算实际执行时间(快速模式)
|
# ⚡ 计算实际执行时间(快速模式)
|
||||||
actual_duration = self._calculate_duration(volume_to_move, velocity)
|
actual_duration = self._calculate_duration(volume_to_move, velocity)
|
||||||
|
|
||||||
# 🎯 确定操作类型和emoji
|
# 🎯 确定操作类型和emoji
|
||||||
if target_position > self._position:
|
if target_position > self._position:
|
||||||
operation_type = "吸液"
|
operation_type = "吸液"
|
||||||
@@ -189,34 +187,28 @@ class VirtualTransferPump:
|
|||||||
else:
|
else:
|
||||||
operation_type = "保持"
|
operation_type = "保持"
|
||||||
operation_emoji = "📍"
|
operation_emoji = "📍"
|
||||||
|
|
||||||
self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
|
self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
|
||||||
self.logger.info(
|
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)")
|
||||||
f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)"
|
|
||||||
)
|
|
||||||
self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
|
self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
|
||||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||||
|
|
||||||
if self._fast_mode:
|
if self._fast_mode:
|
||||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||||
|
|
||||||
# 🚀 模拟移动过程
|
# 🚀 模拟移动过程
|
||||||
if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
|
if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
|
||||||
start_position = self._position
|
start_position = self._position
|
||||||
steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
|
steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
|
||||||
step_duration = actual_duration / steps
|
step_duration = actual_duration / steps
|
||||||
|
|
||||||
self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
|
self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
|
||||||
|
|
||||||
for i in range(steps + 1):
|
for i in range(steps + 1):
|
||||||
# 计算当前位置和进度
|
# 计算当前位置和进度
|
||||||
progress = (i / steps) * 100 if steps > 0 else 100
|
progress = (i / steps) * 100 if steps > 0 else 100
|
||||||
current_pos = (
|
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
|
||||||
start_position + (target_position - start_position) * (i / steps)
|
|
||||||
if steps > 0
|
|
||||||
else target_position
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新状态
|
# 更新状态
|
||||||
if i < steps:
|
if i < steps:
|
||||||
self._status = f"{operation_type}中"
|
self._status = f"{operation_type}中"
|
||||||
@@ -224,10 +216,10 @@ class VirtualTransferPump:
|
|||||||
else:
|
else:
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
status_emoji = "✅"
|
status_emoji = "✅"
|
||||||
|
|
||||||
self._position = current_pos
|
self._position = current_pos
|
||||||
self._current_volume = current_pos
|
self._current_volume = current_pos
|
||||||
|
|
||||||
# 显示进度(每25%或最后一步)
|
# 显示进度(每25%或最后一步)
|
||||||
if i == 0:
|
if i == 0:
|
||||||
self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
|
self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
|
||||||
@@ -235,7 +227,7 @@ class VirtualTransferPump:
|
|||||||
self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
|
self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
|
||||||
elif i == steps:
|
elif i == steps:
|
||||||
self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
|
self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
|
||||||
|
|
||||||
# 等待一小步时间
|
# 等待一小步时间
|
||||||
if i < steps and step_duration > 0:
|
if i < steps and step_duration > 0:
|
||||||
await self._ros_node.sleep(step_duration)
|
await self._ros_node.sleep(step_duration)
|
||||||
@@ -244,27 +236,25 @@ class VirtualTransferPump:
|
|||||||
self._position = target_position
|
self._position = target_position
|
||||||
self._current_volume = target_position
|
self._current_volume = target_position
|
||||||
self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
|
self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
|
||||||
|
|
||||||
# 确保最终位置准确
|
# 确保最终位置准确
|
||||||
self._position = target_position
|
self._position = target_position
|
||||||
self._current_volume = target_position
|
self._current_volume = target_position
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
# 📊 最终状态日志
|
# 📊 最终状态日志
|
||||||
if volume_to_move > 0.01:
|
if volume_to_move > 0.01:
|
||||||
self.logger.info(
|
self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||||
f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 返回符合action定义的结果
|
# 返回符合action定义的结果
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
|
"message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
|
||||||
"final_position": self._position,
|
"final_position": self._position,
|
||||||
"final_volume": self._current_volume,
|
"final_volume": self._current_volume,
|
||||||
"operation_type": operation_type,
|
"operation_type": operation_type
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"❌ 设置位置失败: {str(e)}"
|
error_msg = f"❌ 设置位置失败: {str(e)}"
|
||||||
self.logger.error(error_msg)
|
self.logger.error(error_msg)
|
||||||
@@ -272,136 +262,134 @@ class VirtualTransferPump:
|
|||||||
"success": False,
|
"success": False,
|
||||||
"message": error_msg,
|
"message": error_msg,
|
||||||
"final_position": self._position,
|
"final_position": self._position,
|
||||||
"final_volume": self._current_volume,
|
"final_volume": self._current_volume
|
||||||
}
|
}
|
||||||
|
|
||||||
# 其他泵操作方法
|
# 其他泵操作方法
|
||||||
async def pull_plunger(self, volume: float, velocity: float = None):
|
async def pull_plunger(self, volume: float, velocity: float = None):
|
||||||
"""
|
"""
|
||||||
拉取柱塞(吸液) 📥
|
拉取柱塞(吸液) 📥
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume (float): 要拉取的体积 (ml)
|
volume (float): 要拉取的体积 (ml)
|
||||||
velocity (float): 拉取速度 (ml/s)
|
velocity (float): 拉取速度 (ml/s)
|
||||||
"""
|
"""
|
||||||
new_position = min(self.max_volume, self._position + volume)
|
new_position = min(self.max_volume, self._position + volume)
|
||||||
actual_volume = new_position - self._position
|
actual_volume = new_position - self._position
|
||||||
|
|
||||||
if actual_volume <= 0:
|
if actual_volume <= 0:
|
||||||
self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
|
self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
|
||||||
return
|
return
|
||||||
|
|
||||||
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
||||||
actual_duration = self._calculate_duration(actual_volume, velocity)
|
actual_duration = self._calculate_duration(actual_volume, velocity)
|
||||||
|
|
||||||
self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
|
self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
|
||||||
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
||||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||||
|
|
||||||
if self._fast_mode:
|
if self._fast_mode:
|
||||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||||
|
|
||||||
await self._simulate_operation(actual_duration)
|
await self._simulate_operation(actual_duration)
|
||||||
|
|
||||||
self._position = new_position
|
self._position = new_position
|
||||||
self._current_volume = new_position
|
self._current_volume = new_position
|
||||||
|
|
||||||
self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||||
|
|
||||||
async def push_plunger(self, volume: float, velocity: float = None):
|
async def push_plunger(self, volume: float, velocity: float = None):
|
||||||
"""
|
"""
|
||||||
推出柱塞(排液) 📤
|
推出柱塞(排液) 📤
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume (float): 要推出的体积 (ml)
|
volume (float): 要推出的体积 (ml)
|
||||||
velocity (float): 推出速度 (ml/s)
|
velocity (float): 推出速度 (ml/s)
|
||||||
"""
|
"""
|
||||||
new_position = max(0, self._position - volume)
|
new_position = max(0, self._position - volume)
|
||||||
actual_volume = self._position - new_position
|
actual_volume = self._position - new_position
|
||||||
|
|
||||||
if actual_volume <= 0:
|
if actual_volume <= 0:
|
||||||
self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
|
self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
|
||||||
return
|
return
|
||||||
|
|
||||||
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
||||||
actual_duration = self._calculate_duration(actual_volume, velocity)
|
actual_duration = self._calculate_duration(actual_volume, velocity)
|
||||||
|
|
||||||
self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
|
self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
|
||||||
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
||||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||||
|
|
||||||
if self._fast_mode:
|
if self._fast_mode:
|
||||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||||
|
|
||||||
await self._simulate_operation(actual_duration)
|
await self._simulate_operation(actual_duration)
|
||||||
|
|
||||||
self._position = new_position
|
self._position = new_position
|
||||||
self._current_volume = new_position
|
self._current_volume = new_position
|
||||||
|
|
||||||
self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||||
|
|
||||||
# 便捷操作方法
|
# 便捷操作方法
|
||||||
async def aspirate(self, volume: float, velocity: float = None):
|
async def aspirate(self, volume: float, velocity: float = None):
|
||||||
"""吸液操作 📥"""
|
"""吸液操作 📥"""
|
||||||
await self.pull_plunger(volume, velocity)
|
await self.pull_plunger(volume, velocity)
|
||||||
|
|
||||||
async def dispense(self, volume: float, velocity: float = None):
|
async def dispense(self, volume: float, velocity: float = None):
|
||||||
"""排液操作 📤"""
|
"""排液操作 📤"""
|
||||||
await self.push_plunger(volume, velocity)
|
await self.push_plunger(volume, velocity)
|
||||||
|
|
||||||
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
|
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
|
||||||
"""转移操作(先吸后排) 🔄"""
|
"""转移操作(先吸后排) 🔄"""
|
||||||
self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
|
self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
|
||||||
|
|
||||||
# 吸液
|
# 吸液
|
||||||
await self.aspirate(volume, aspirate_velocity)
|
await self.aspirate(volume, aspirate_velocity)
|
||||||
|
|
||||||
# 短暂停顿
|
# 短暂停顿
|
||||||
self.logger.debug("⏸️ 短暂停顿...")
|
self.logger.debug("⏸️ 短暂停顿...")
|
||||||
await self._ros_node.sleep(0.1)
|
await self._ros_node.sleep(0.1)
|
||||||
|
|
||||||
# 排液
|
# 排液
|
||||||
await self.dispense(volume, dispense_velocity)
|
await self.dispense(volume, dispense_velocity)
|
||||||
|
|
||||||
async def empty_syringe(self, velocity: float = None):
|
async def empty_syringe(self, velocity: float = None):
|
||||||
"""清空注射器"""
|
"""清空注射器"""
|
||||||
await self.set_position(0, velocity)
|
await self.set_position(0, velocity)
|
||||||
|
|
||||||
async def fill_syringe(self, velocity: float = None):
|
async def fill_syringe(self, velocity: float = None):
|
||||||
"""充满注射器"""
|
"""充满注射器"""
|
||||||
await self.set_position(self.max_volume, velocity)
|
await self.set_position(self.max_volume, velocity)
|
||||||
|
|
||||||
async def stop_operation(self):
|
async def stop_operation(self):
|
||||||
"""停止当前操作"""
|
"""停止当前操作"""
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
self.logger.info("Operation stopped")
|
self.logger.info("Operation stopped")
|
||||||
|
|
||||||
# 状态查询方法
|
# 状态查询方法
|
||||||
def get_position(self) -> float:
|
def get_position(self) -> float:
|
||||||
"""获取当前位置"""
|
"""获取当前位置"""
|
||||||
return self._position
|
return self._position
|
||||||
|
|
||||||
def get_current_volume(self) -> float:
|
def get_current_volume(self) -> float:
|
||||||
"""获取当前体积"""
|
"""获取当前体积"""
|
||||||
return self._current_volume
|
return self._current_volume
|
||||||
|
|
||||||
def get_remaining_capacity(self) -> float:
|
def get_remaining_capacity(self) -> float:
|
||||||
"""获取剩余容量"""
|
"""获取剩余容量"""
|
||||||
return self.max_volume - self._current_volume
|
return self.max_volume - self._current_volume
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
"""检查是否为空"""
|
"""检查是否为空"""
|
||||||
return self._current_volume <= 0.01 # 允许小量误差
|
return self._current_volume <= 0.01 # 允许小量误差
|
||||||
|
|
||||||
def is_full(self) -> bool:
|
def is_full(self) -> bool:
|
||||||
"""检查是否已满"""
|
"""检查是否已满"""
|
||||||
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||||
f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
@@ -410,20 +398,20 @@ class VirtualTransferPump:
|
|||||||
async def demo():
|
async def demo():
|
||||||
"""虚拟泵使用示例"""
|
"""虚拟泵使用示例"""
|
||||||
pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
|
pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
|
||||||
|
|
||||||
await pump.initialize()
|
await pump.initialize()
|
||||||
|
|
||||||
print(f"Initial state: {pump}")
|
print(f"Initial state: {pump}")
|
||||||
|
|
||||||
# 测试set_position方法
|
# 测试set_position方法
|
||||||
result = await pump.set_position(10.0, max_velocity=2.0)
|
result = await pump.set_position(10.0, max_velocity=2.0)
|
||||||
print(f"Set position result: {result}")
|
print(f"Set position result: {result}")
|
||||||
print(f"After setting position to 10ml: {pump}")
|
print(f"After setting position to 10ml: {pump}")
|
||||||
|
|
||||||
# 吸液测试
|
# 吸液测试
|
||||||
await pump.aspirate(5.0, velocity=2.0)
|
await pump.aspirate(5.0, velocity=2.0)
|
||||||
print(f"After aspirating 5ml: {pump}")
|
print(f"After aspirating 5ml: {pump}")
|
||||||
|
|
||||||
# 清空测试
|
# 清空测试
|
||||||
result = await pump.set_position(0.0)
|
result = await pump.set_position(0.0)
|
||||||
print(f"Empty result: {result}")
|
print(f"Empty result: {result}")
|
||||||
|
|||||||
@@ -1,874 +0,0 @@
|
|||||||
"""
|
|
||||||
Virtual Workbench Device - 模拟工作台设备
|
|
||||||
包含:
|
|
||||||
- 1个机械臂 (每次操作3s, 独占锁)
|
|
||||||
- 3个加热台 (每次加热10s, 可并行)
|
|
||||||
|
|
||||||
工作流程:
|
|
||||||
1. A1-A5 物料同时启动, 竞争机械臂
|
|
||||||
2. 机械臂将物料移动到空闲加热台
|
|
||||||
3. 加热完成后, 机械臂将物料移动到C1-C5
|
|
||||||
|
|
||||||
注意: 调用来自线程池, 使用 threading.Lock 进行同步
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from typing import Dict, Any, Optional, List
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from threading import Lock, RLock
|
|
||||||
|
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
|
||||||
from unilabos.registry.decorators import (
|
|
||||||
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
|
|
||||||
)
|
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
|
||||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
|
|
||||||
|
|
||||||
|
|
||||||
# ============ TypedDict 返回类型定义 ============
|
|
||||||
|
|
||||||
|
|
||||||
class MoveToHeatingStationResult(TypedDict):
|
|
||||||
"""move_to_heating_station 返回类型"""
|
|
||||||
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
material_number: int
|
|
||||||
message: str
|
|
||||||
unilabos_samples: List[LabSample]
|
|
||||||
|
|
||||||
|
|
||||||
class StartHeatingResult(TypedDict):
|
|
||||||
"""start_heating 返回类型"""
|
|
||||||
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
material_number: int
|
|
||||||
message: str
|
|
||||||
unilabos_samples: List[LabSample]
|
|
||||||
|
|
||||||
|
|
||||||
class MoveToOutputResult(TypedDict):
|
|
||||||
"""move_to_output 返回类型"""
|
|
||||||
|
|
||||||
success: bool
|
|
||||||
station_id: int
|
|
||||||
material_id: str
|
|
||||||
output_position: str
|
|
||||||
message: str
|
|
||||||
unilabos_samples: List[LabSample]
|
|
||||||
|
|
||||||
|
|
||||||
class PrepareMaterialsResult(TypedDict):
|
|
||||||
"""prepare_materials 返回类型 - 批量准备物料"""
|
|
||||||
|
|
||||||
success: bool
|
|
||||||
count: int
|
|
||||||
material_1: int # 物料编号1
|
|
||||||
material_2: int # 物料编号2
|
|
||||||
material_3: int # 物料编号3
|
|
||||||
material_4: int # 物料编号4
|
|
||||||
material_5: int # 物料编号5
|
|
||||||
message: str
|
|
||||||
unilabos_samples: List[LabSample]
|
|
||||||
|
|
||||||
|
|
||||||
# ============ 状态枚举 ============
|
|
||||||
|
|
||||||
|
|
||||||
class HeatingStationState(Enum):
|
|
||||||
"""加热台状态枚举"""
|
|
||||||
|
|
||||||
IDLE = "idle" # 空闲
|
|
||||||
OCCUPIED = "occupied" # 已放置物料, 等待加热
|
|
||||||
HEATING = "heating" # 加热中
|
|
||||||
COMPLETED = "completed" # 加热完成, 等待取走
|
|
||||||
|
|
||||||
|
|
||||||
class ArmState(Enum):
|
|
||||||
"""机械臂状态枚举"""
|
|
||||||
|
|
||||||
IDLE = "idle" # 空闲
|
|
||||||
BUSY = "busy" # 工作中
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HeatingStation:
|
|
||||||
"""加热台数据结构"""
|
|
||||||
|
|
||||||
station_id: int
|
|
||||||
state: HeatingStationState = HeatingStationState.IDLE
|
|
||||||
current_material: Optional[str] = None # 当前物料 (如 "A1", "A2")
|
|
||||||
material_number: Optional[int] = None # 物料编号 (1-5)
|
|
||||||
heating_start_time: Optional[float] = None
|
|
||||||
heating_progress: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
@device(
|
|
||||||
id="virtual_workbench",
|
|
||||||
category=["virtual_device"],
|
|
||||||
description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing",
|
|
||||||
)
|
|
||||||
class VirtualWorkbench:
|
|
||||||
"""
|
|
||||||
Virtual Workbench Device - 虚拟工作台设备
|
|
||||||
|
|
||||||
模拟一个包含1个机械臂和3个加热台的工作站
|
|
||||||
- 机械臂操作耗时3秒, 同一时间只能执行一个操作
|
|
||||||
- 加热台加热耗时10秒, 3个加热台可并行工作
|
|
||||||
|
|
||||||
工作流:
|
|
||||||
1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
|
|
||||||
2. 获取机械臂后, 查找空闲加热台
|
|
||||||
3. 机械臂将物料放入加热台, 开始加热
|
|
||||||
4. 加热完成后, 机械臂将物料移动到目标位置Cn
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ros_node: BaseROS2DeviceNode
|
|
||||||
|
|
||||||
# 配置常量
|
|
||||||
ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
|
|
||||||
HEATING_TIME: float = 60.0 # 加热时间(秒)
|
|
||||||
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
|
||||||
# 处理可能的不同调用方式
|
|
||||||
if device_id is None and "id" in kwargs:
|
|
||||||
device_id = kwargs.pop("id")
|
|
||||||
if config is None and "config" in kwargs:
|
|
||||||
config = kwargs.pop("config")
|
|
||||||
|
|
||||||
self.device_id = device_id or "virtual_workbench"
|
|
||||||
self.config = config or {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger(f"VirtualWorkbench.{self.device_id}")
|
|
||||||
self.data: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
# 从config中获取可配置参数
|
|
||||||
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
|
|
||||||
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
|
|
||||||
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
|
|
||||||
|
|
||||||
# 机械臂状态和锁
|
|
||||||
self._arm_lock = Lock()
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task: Optional[str] = None
|
|
||||||
|
|
||||||
# 加热台状态
|
|
||||||
self._heating_stations: Dict[int, HeatingStation] = {
|
|
||||||
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
|
||||||
}
|
|
||||||
self._stations_lock = RLock()
|
|
||||||
|
|
||||||
# 任务追踪
|
|
||||||
self._active_tasks: Dict[str, Dict[str, Any]] = {}
|
|
||||||
self._tasks_lock = Lock()
|
|
||||||
|
|
||||||
# 处理其他kwargs参数
|
|
||||||
skip_keys = {"arm_operation_time", "heating_time", "num_heating_stations"}
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
if key not in skip_keys and not hasattr(self, key):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
self.logger.info(f"=== 虚拟工作台 {self.device_id} 已创建 ===")
|
|
||||||
self.logger.info(
|
|
||||||
f"机械臂操作时间: {self.ARM_OPERATION_TIME}s | "
|
|
||||||
f"加热时间: {self.HEATING_TIME}s | "
|
|
||||||
f"加热台数量: {self.NUM_HEATING_STATIONS}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
|
||||||
"""ROS节点初始化后回调"""
|
|
||||||
self._ros_node = ros_node
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def initialize(self) -> bool:
|
|
||||||
"""初始化虚拟工作台"""
|
|
||||||
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
for station in self._heating_stations.values():
|
|
||||||
station.state = HeatingStationState.IDLE
|
|
||||||
station.current_material = None
|
|
||||||
station.material_number = None
|
|
||||||
station.heating_progress = 0.0
|
|
||||||
|
|
||||||
self.data.update(
|
|
||||||
{
|
|
||||||
"status": "Ready",
|
|
||||||
"arm_state": ArmState.IDLE.value,
|
|
||||||
"arm_current_task": None,
|
|
||||||
"heating_stations": self._get_stations_status(),
|
|
||||||
"active_tasks_count": 0,
|
|
||||||
"message": "工作台就绪",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪")
|
|
||||||
return True
|
|
||||||
|
|
||||||
@not_action
|
|
||||||
def cleanup(self) -> bool:
|
|
||||||
"""清理虚拟工作台"""
|
|
||||||
self.logger.info(f"清理虚拟工作台 {self.device_id}")
|
|
||||||
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task = None
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations.clear()
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks.clear()
|
|
||||||
|
|
||||||
self.data.update(
|
|
||||||
{
|
|
||||||
"status": "Offline",
|
|
||||||
"arm_state": ArmState.IDLE.value,
|
|
||||||
"heating_stations": {},
|
|
||||||
"message": "工作台已关闭",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_stations_status(self) -> Dict[int, Dict[str, Any]]:
|
|
||||||
"""获取所有加热台状态"""
|
|
||||||
with self._stations_lock:
|
|
||||||
return {
|
|
||||||
station_id: {
|
|
||||||
"state": station.state.value,
|
|
||||||
"current_material": station.current_material,
|
|
||||||
"material_number": station.material_number,
|
|
||||||
"heating_progress": station.heating_progress,
|
|
||||||
}
|
|
||||||
for station_id, station in self._heating_stations.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def _update_data_status(self, message: Optional[str] = None):
|
|
||||||
"""更新状态数据"""
|
|
||||||
self.data.update(
|
|
||||||
{
|
|
||||||
"arm_state": self._arm_state.value,
|
|
||||||
"arm_current_task": self._arm_current_task,
|
|
||||||
"heating_stations": self._get_stations_status(),
|
|
||||||
"active_tasks_count": len(self._active_tasks),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if message:
|
|
||||||
self.data["message"] = message
|
|
||||||
|
|
||||||
def _find_available_heating_station(self) -> Optional[int]:
|
|
||||||
"""查找空闲的加热台"""
|
|
||||||
with self._stations_lock:
|
|
||||||
for station_id, station in self._heating_stations.items():
|
|
||||||
if station.state == HeatingStationState.IDLE:
|
|
||||||
return station_id
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _acquire_arm(self, task_description: str) -> bool:
|
|
||||||
"""获取机械臂使用权(阻塞直到获取)"""
|
|
||||||
self.logger.info(f"[{task_description}] 等待获取机械臂...")
|
|
||||||
self._arm_lock.acquire()
|
|
||||||
self._arm_state = ArmState.BUSY
|
|
||||||
self._arm_current_task = task_description
|
|
||||||
self._update_data_status(f"机械臂执行: {task_description}")
|
|
||||||
self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _release_arm(self):
|
|
||||||
"""释放机械臂"""
|
|
||||||
task = self._arm_current_task
|
|
||||||
self._arm_state = ArmState.IDLE
|
|
||||||
self._arm_current_task = None
|
|
||||||
self._arm_lock.release()
|
|
||||||
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
|
||||||
self.logger.info(f"机械臂已释放 (完成: {task})")
|
|
||||||
|
|
||||||
@action(
|
|
||||||
auto_prefix=True,
|
|
||||||
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",
|
|
||||||
handles=[
|
|
||||||
ActionOutputHandle(key="channel_1", data_type="workbench_material",
|
|
||||||
label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="channel_2", data_type="workbench_material",
|
|
||||||
label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="channel_3", data_type="workbench_material",
|
|
||||||
label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="channel_4", data_type="workbench_material",
|
|
||||||
label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="channel_5", data_type="workbench_material",
|
|
||||||
label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def prepare_materials(
|
|
||||||
self,
|
|
||||||
sample_uuids: SampleUUIDsType,
|
|
||||||
count: int = 5,
|
|
||||||
) -> PrepareMaterialsResult:
|
|
||||||
"""
|
|
||||||
批量准备物料 - 虚拟起始节点
|
|
||||||
|
|
||||||
作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
|
|
||||||
输出5个handle (material_1 ~ material_5), 分别对应实验1~5。
|
|
||||||
"""
|
|
||||||
materials = [i for i in range(1, count + 1)]
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"count": count,
|
|
||||||
"material_1": materials[0] if len(materials) > 0 else 0,
|
|
||||||
"material_2": materials[1] if len(materials) > 1 else 0,
|
|
||||||
"material_3": materials[2] if len(materials) > 2 else 0,
|
|
||||||
"material_4": materials[3] if len(materials) > 3 else 0,
|
|
||||||
"material_5": materials[4] if len(materials) > 4 else 0,
|
|
||||||
"message": f"已准备 {count} 个物料: A1-A{count}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@action(
|
|
||||||
auto_prefix=True,
|
|
||||||
description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID",
|
|
||||||
handles=[
|
|
||||||
ActionInputHandle(key="material_input", data_type="workbench_material",
|
|
||||||
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
||||||
ActionOutputHandle(key="heating_station_output", data_type="workbench_station",
|
|
||||||
label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="material_number_output", data_type="workbench_material",
|
|
||||||
label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def move_to_heating_station(
|
|
||||||
self,
|
|
||||||
sample_uuids: SampleUUIDsType,
|
|
||||||
material_number: int,
|
|
||||||
) -> MoveToHeatingStationResult:
|
|
||||||
"""
|
|
||||||
将物料从An位置移动到加热台
|
|
||||||
|
|
||||||
多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
|
|
||||||
"""
|
|
||||||
material_id = f"A{material_number}"
|
|
||||||
task_desc = f"移动{material_id}到加热台"
|
|
||||||
self.logger.info(f"[任务] {task_desc} - 开始执行")
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id] = {
|
|
||||||
"status": "waiting_for_arm",
|
|
||||||
"start_time": time.time(),
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "finding_station"
|
|
||||||
station_id = None
|
|
||||||
|
|
||||||
while station_id is None:
|
|
||||||
station_id = self._find_available_heating_station()
|
|
||||||
if station_id is None:
|
|
||||||
self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
|
|
||||||
self._release_arm()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
|
|
||||||
self._heating_stations[station_id].current_material = material_id
|
|
||||||
self._heating_stations[station_id].material_number = material_number
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving"
|
|
||||||
self._active_tasks[material_id]["assigned_station"] = station_id
|
|
||||||
self.logger.info(f"[{material_id}] 机械臂正在移动到加热台{station_id}...")
|
|
||||||
|
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
|
||||||
|
|
||||||
self._update_data_status(f"{material_id}已放入加热台{station_id}")
|
|
||||||
self.logger.info(
|
|
||||||
f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._release_arm()
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
self._active_tasks[material_id]["status"] = "placed_on_station"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"{material_id}已成功移动到加热台{station_id}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"[{material_id}] 移动失败: {str(e)}")
|
|
||||||
if self._arm_lock.locked():
|
|
||||||
self._release_arm()
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": -1,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"移动失败: {str(e)}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@action(
|
|
||||||
auto_prefix=True,
|
|
||||||
always_free=True,
|
|
||||||
description="启动指定加热台的加热程序",
|
|
||||||
handles=[
|
|
||||||
ActionInputHandle(key="station_id_input", data_type="workbench_station",
|
|
||||||
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
|
||||||
ActionInputHandle(key="material_number_input", data_type="workbench_material",
|
|
||||||
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
||||||
ActionOutputHandle(key="heating_done_station", data_type="workbench_station",
|
|
||||||
label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
|
||||||
ActionOutputHandle(key="heating_done_material", data_type="workbench_material",
|
|
||||||
label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def start_heating(
|
|
||||||
self,
|
|
||||||
sample_uuids: SampleUUIDsType,
|
|
||||||
station_id: int,
|
|
||||||
material_number: int,
|
|
||||||
) -> StartHeatingResult:
|
|
||||||
"""
|
|
||||||
启动指定加热台的加热程序
|
|
||||||
"""
|
|
||||||
self.logger.info(f"[加热台{station_id}] 开始加热")
|
|
||||||
|
|
||||||
if station_id not in self._heating_stations:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations[station_id]
|
|
||||||
|
|
||||||
if station.current_material is None:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}上没有物料",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
if station.state == HeatingStationState.HEATING:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": station.current_material,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}已经在加热中",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
material_id = station.current_material
|
|
||||||
|
|
||||||
station.state = HeatingStationState.HEATING
|
|
||||||
station.heating_start_time = time.time()
|
|
||||||
station.heating_progress = 0.0
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "heating"
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
heating_list = [
|
|
||||||
f"加热台{sid}:{s.current_material}"
|
|
||||||
for sid, s in self._heating_stations.items()
|
|
||||||
if s.state == HeatingStationState.HEATING and s.current_material
|
|
||||||
]
|
|
||||||
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
last_countdown_log = start_time
|
|
||||||
while True:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
remaining = max(0.0, self.HEATING_TIME - elapsed)
|
|
||||||
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].heating_progress = progress
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
|
||||||
|
|
||||||
if time.time() - last_countdown_log >= 5.0:
|
|
||||||
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
|
||||||
last_countdown_log = time.time()
|
|
||||||
|
|
||||||
if elapsed >= self.HEATING_TIME:
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.COMPLETED
|
|
||||||
self._heating_stations[station_id].heating_progress = 100.0
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "heating_completed"
|
|
||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热完成")
|
|
||||||
self.logger.info(f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"material_number": material_number,
|
|
||||||
"message": f"加热台{station_id}加热完成",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@action(
|
|
||||||
auto_prefix=True,
|
|
||||||
description="将物料从加热台移动到输出位置Cn",
|
|
||||||
handles=[
|
|
||||||
ActionInputHandle(key="output_station_input", data_type="workbench_station",
|
|
||||||
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
|
||||||
ActionInputHandle(key="output_material_input", data_type="workbench_material",
|
|
||||||
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def move_to_output(
|
|
||||||
self,
|
|
||||||
sample_uuids: SampleUUIDsType,
|
|
||||||
station_id: int,
|
|
||||||
material_number: int,
|
|
||||||
) -> MoveToOutputResult:
|
|
||||||
"""
|
|
||||||
将物料从加热台移动到输出位置Cn
|
|
||||||
"""
|
|
||||||
output_number = material_number
|
|
||||||
|
|
||||||
if station_id not in self._heating_stations:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"无效的加热台ID: {station_id}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations[station_id]
|
|
||||||
material_id = station.current_material
|
|
||||||
|
|
||||||
if material_id is None:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"加热台{station_id}上没有物料",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
if station.state != HeatingStationState.COMPLETED:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"output_position": f"C{output_number}",
|
|
||||||
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
output_position = f"C{output_number}"
|
|
||||||
task_desc = f"从加热台{station_id}移动{material_id}到{output_position}"
|
|
||||||
self.logger.info(f"[任务] {task_desc}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
|
|
||||||
|
|
||||||
self._acquire_arm(task_desc)
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "arm_moving_to_output"
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..."
|
|
||||||
)
|
|
||||||
time.sleep(self.ARM_OPERATION_TIME)
|
|
||||||
|
|
||||||
with self._stations_lock:
|
|
||||||
self._heating_stations[station_id].state = HeatingStationState.IDLE
|
|
||||||
self._heating_stations[station_id].current_material = None
|
|
||||||
self._heating_stations[station_id].material_number = None
|
|
||||||
self._heating_stations[station_id].heating_progress = 0.0
|
|
||||||
self._heating_stations[station_id].heating_start_time = None
|
|
||||||
|
|
||||||
self._release_arm()
|
|
||||||
|
|
||||||
with self._tasks_lock:
|
|
||||||
if material_id in self._active_tasks:
|
|
||||||
self._active_tasks[material_id]["status"] = "completed"
|
|
||||||
self._active_tasks[material_id]["end_time"] = time.time()
|
|
||||||
|
|
||||||
self._update_data_status(f"{material_id}已移动到{output_position}")
|
|
||||||
self.logger.info(
|
|
||||||
f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": material_id,
|
|
||||||
"output_position": output_position,
|
|
||||||
"message": f"{material_id}已成功移动到{output_position}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str)
|
|
||||||
else (content.serialize() if content is not None else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"移动到输出位置失败: {str(e)}")
|
|
||||||
if self._arm_lock.locked():
|
|
||||||
self._release_arm()
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"station_id": station_id,
|
|
||||||
"material_id": "",
|
|
||||||
"output_position": output_position,
|
|
||||||
"message": f"移动失败: {str(e)}",
|
|
||||||
"unilabos_samples": [
|
|
||||||
LabSample(
|
|
||||||
sample_uuid=sample_uuid,
|
|
||||||
oss_path="",
|
|
||||||
extra=(
|
|
||||||
{"material_uuid": content}
|
|
||||||
if isinstance(content, str) else (content.serialize() if content else {})
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sample_uuid, content in sample_uuids.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============ 状态属性 ============
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def status(self) -> str:
|
|
||||||
return self.data.get("status", "Unknown")
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def arm_state(self) -> str:
|
|
||||||
return self._arm_state.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def arm_current_task(self) -> str:
|
|
||||||
return self._arm_current_task or ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_1_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_1_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_1_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(1)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_2_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_2_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_2_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(2)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_3_state(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.state.value if station else "unknown"
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_3_material(self) -> str:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.current_material or "" if station else ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def heating_station_3_progress(self) -> float:
|
|
||||||
with self._stations_lock:
|
|
||||||
station = self._heating_stations.get(3)
|
|
||||||
return station.heating_progress if station else 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def active_tasks_count(self) -> int:
|
|
||||||
with self._tasks_lock:
|
|
||||||
return len(self._active_tasks)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@topic_config()
|
|
||||||
def message(self) -> str:
|
|
||||||
return self.data.get("message", "")
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,658 +0,0 @@
|
|||||||
"""
|
|
||||||
装饰器注册表系统
|
|
||||||
|
|
||||||
通过 @device, @action, @resource 装饰器替代 YAML 配置文件来定义设备/动作/资源注册表信息。
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from unilabos.registry.decorators import (
|
|
||||||
device, action, resource,
|
|
||||||
InputHandle, OutputHandle,
|
|
||||||
ActionInputHandle, ActionOutputHandle,
|
|
||||||
HardwareInterface, Side, DataSource, NodeType,
|
|
||||||
)
|
|
||||||
|
|
||||||
@device(
|
|
||||||
id="solenoid_valve.mock",
|
|
||||||
category=["pump_and_valve"],
|
|
||||||
description="模拟电磁阀设备",
|
|
||||||
handles=[
|
|
||||||
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH),
|
|
||||||
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH),
|
|
||||||
],
|
|
||||||
hardware_interface=HardwareInterface(
|
|
||||||
name="hardware_interface",
|
|
||||||
read="send_command",
|
|
||||||
write="send_command",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
class SolenoidValveMock:
|
|
||||||
@action(action_type=EmptyIn)
|
|
||||||
def close(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
@action(
|
|
||||||
handles=[
|
|
||||||
ActionInputHandle(key="in", data_type="fluid", label="in"),
|
|
||||||
ActionOutputHandle(key="out", data_type="fluid", label="out"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def set_valve_position(self, position):
|
|
||||||
...
|
|
||||||
|
|
||||||
# 无 @action 装饰器 => auto- 前缀动作
|
|
||||||
def is_open(self):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from functools import wraps
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
F = TypeVar("F", bound=Callable[..., Any])
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 枚举
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class Side(str, Enum):
|
|
||||||
"""UI 上 Handle 的显示位置"""
|
|
||||||
|
|
||||||
NORTH = "NORTH"
|
|
||||||
SOUTH = "SOUTH"
|
|
||||||
EAST = "EAST"
|
|
||||||
WEST = "WEST"
|
|
||||||
|
|
||||||
|
|
||||||
class DataSource(str, Enum):
|
|
||||||
"""Handle 的数据来源"""
|
|
||||||
|
|
||||||
HANDLE = "handle" # 从上游 handle 获取数据 (用于 InputHandle)
|
|
||||||
EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeType(str, Enum):
|
|
||||||
"""动作的节点类型(用于区分 ILab 节点和人工确认节点等)"""
|
|
||||||
|
|
||||||
ILAB = "ILab"
|
|
||||||
MANUAL_CONFIRM = "manual_confirm"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class _DeviceHandleBase(BaseModel):
|
|
||||||
"""设备/资源端口基类 (内部使用)"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
|
||||||
|
|
||||||
key: str = Field(serialization_alias="handler_key")
|
|
||||||
data_type: str
|
|
||||||
label: str
|
|
||||||
side: Optional[Side] = None
|
|
||||||
data_key: Optional[str] = None
|
|
||||||
data_source: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
# 子类覆盖
|
|
||||||
io_type: str = ""
|
|
||||||
|
|
||||||
def to_registry_dict(self) -> Dict[str, Any]:
|
|
||||||
return self.model_dump(by_alias=True, exclude_none=True)
|
|
||||||
|
|
||||||
|
|
||||||
class InputHandle(_DeviceHandleBase):
|
|
||||||
"""
|
|
||||||
输入端口 (io_type="target"), 用于 @device / @resource handles
|
|
||||||
|
|
||||||
Example:
|
|
||||||
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH)
|
|
||||||
"""
|
|
||||||
|
|
||||||
io_type: str = "target"
|
|
||||||
|
|
||||||
|
|
||||||
class OutputHandle(_DeviceHandleBase):
|
|
||||||
"""
|
|
||||||
输出端口 (io_type="source"), 用于 @device / @resource handles
|
|
||||||
|
|
||||||
Example:
|
|
||||||
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH)
|
|
||||||
"""
|
|
||||||
|
|
||||||
io_type: str = "source"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Action Handle (动作级别端口, 序列化时不含 io_type, 按类型自动分组)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class _ActionHandleBase(BaseModel):
|
|
||||||
"""动作端口基类 (内部使用)"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
|
||||||
|
|
||||||
key: str = Field(serialization_alias="handler_key")
|
|
||||||
data_type: str
|
|
||||||
label: str
|
|
||||||
side: Optional[Side] = None
|
|
||||||
data_key: Optional[str] = None
|
|
||||||
data_source: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
io_type: Optional[str] = None # source/sink (dataflow) or target/source (device-style)
|
|
||||||
|
|
||||||
def to_registry_dict(self) -> Dict[str, Any]:
|
|
||||||
return self.model_dump(by_alias=True, exclude_none=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ActionInputHandle(_ActionHandleBase):
|
|
||||||
"""
|
|
||||||
动作输入端口, 用于 @action handles, 序列化后归入 "input" 组
|
|
||||||
|
|
||||||
Example:
|
|
||||||
ActionInputHandle(
|
|
||||||
key="material_input", data_type="workbench_material",
|
|
||||||
label="物料编号", data_key="material_number", data_source="handle",
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ActionOutputHandle(_ActionHandleBase):
|
|
||||||
"""
|
|
||||||
动作输出端口, 用于 @action handles, 序列化后归入 "output" 组
|
|
||||||
|
|
||||||
Example:
|
|
||||||
ActionOutputHandle(
|
|
||||||
key="station_output", data_type="workbench_station",
|
|
||||||
label="加热台ID", data_key="station_id", data_source="executor",
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# HardwareInterface
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class HardwareInterface(BaseModel):
|
|
||||||
"""
|
|
||||||
硬件通信接口定义
|
|
||||||
|
|
||||||
描述设备与底层硬件通信的方式 (串口、Modbus 等)。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
HardwareInterface(name="hardware_interface", read="send_command", write="send_command")
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
read: Optional[str] = None
|
|
||||||
write: Optional[str] = None
|
|
||||||
extra_info: Optional[List[str]] = None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 全局注册表 -- 记录所有被装饰器标记的类/函数
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
_registered_devices: Dict[str, type] = {} # device_id -> class
|
|
||||||
_registered_resources: Dict[str, Any] = {} # resource_id -> class or function
|
|
||||||
|
|
||||||
|
|
||||||
def _device_handles_to_list(
|
|
||||||
handles: Optional[List[_DeviceHandleBase]],
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""将设备/资源 Handle 列表序列化为字典列表 (含 io_type)"""
|
|
||||||
if handles is None:
|
|
||||||
return []
|
|
||||||
return [h.to_registry_dict() for h in handles]
|
|
||||||
|
|
||||||
|
|
||||||
def _action_handles_to_dict(
|
|
||||||
handles: Optional[List[_ActionHandleBase]],
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
将动作 Handle 列表序列化为 {"input": [...], "output": [...]} 格式。
|
|
||||||
|
|
||||||
ActionInputHandle => "input", ActionOutputHandle => "output"
|
|
||||||
"""
|
|
||||||
if handles is None:
|
|
||||||
return {}
|
|
||||||
input_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionInputHandle)]
|
|
||||||
output_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionOutputHandle)]
|
|
||||||
result: Dict[str, Any] = {}
|
|
||||||
if input_list:
|
|
||||||
result["input"] = input_list
|
|
||||||
if output_list:
|
|
||||||
result["output"] = output_list
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# @device 类装饰器
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
def device(
|
|
||||||
id: Optional[str] = None,
|
|
||||||
ids: Optional[List[str]] = None,
|
|
||||||
id_meta: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
||||||
category: Optional[List[str]] = None,
|
|
||||||
description: str = "",
|
|
||||||
display_name: str = "",
|
|
||||||
icon: str = "",
|
|
||||||
version: str = "1.0.0",
|
|
||||||
handles: Optional[List[_DeviceHandleBase]] = None,
|
|
||||||
model: Optional[Dict[str, Any]] = None,
|
|
||||||
device_type: str = "python",
|
|
||||||
hardware_interface: Optional[HardwareInterface] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
设备类装饰器
|
|
||||||
|
|
||||||
将类标记为一个 UniLab-OS 设备,并附加注册表元数据。
|
|
||||||
|
|
||||||
支持两种模式:
|
|
||||||
1. 单设备: id="xxx", category=[...]
|
|
||||||
2. 多设备: ids=["id1","id2"], id_meta={"id1":{handles:[...]}, "id2":{...}}
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id: 单设备时的注册表唯一标识
|
|
||||||
ids: 多设备时的 id 列表,与 id_meta 配合使用
|
|
||||||
id_meta: 每个 device_id 的覆盖元数据 (handles/description/icon/model)
|
|
||||||
category: 设备分类标签列表 (必填)
|
|
||||||
description: 设备描述
|
|
||||||
display_name: 人类可读的设备显示名称,缺失时默认使用 id
|
|
||||||
icon: 图标路径
|
|
||||||
version: 版本号
|
|
||||||
handles: 设备端口列表 (单设备或 id_meta 未覆盖时使用)
|
|
||||||
model: 可选的 3D 模型配置
|
|
||||||
device_type: 设备实现类型 ("python" / "ros2")
|
|
||||||
hardware_interface: 硬件通信接口 (HardwareInterface)
|
|
||||||
"""
|
|
||||||
# Resolve device ids
|
|
||||||
if ids is not None:
|
|
||||||
device_ids = list(ids)
|
|
||||||
if not device_ids:
|
|
||||||
raise ValueError("@device ids 不能为空")
|
|
||||||
id_meta = id_meta or {}
|
|
||||||
elif id is not None:
|
|
||||||
device_ids = [id]
|
|
||||||
id_meta = {}
|
|
||||||
else:
|
|
||||||
raise ValueError("@device 必须提供 id 或 ids")
|
|
||||||
|
|
||||||
if category is None:
|
|
||||||
raise ValueError("@device category 必填")
|
|
||||||
|
|
||||||
base_meta = {
|
|
||||||
"category": category,
|
|
||||||
"description": description,
|
|
||||||
"display_name": display_name,
|
|
||||||
"icon": icon,
|
|
||||||
"version": version,
|
|
||||||
"handles": _device_handles_to_list(handles),
|
|
||||||
"model": model,
|
|
||||||
"device_type": device_type,
|
|
||||||
"hardware_interface": (hardware_interface.model_dump(exclude_none=True) if hardware_interface else None),
|
|
||||||
}
|
|
||||||
|
|
||||||
def decorator(cls):
|
|
||||||
cls._device_registry_meta = base_meta
|
|
||||||
cls._device_registry_id_meta = id_meta
|
|
||||||
cls._device_registry_ids = device_ids
|
|
||||||
|
|
||||||
for did in device_ids:
|
|
||||||
if did in _registered_devices:
|
|
||||||
raise ValueError(f"@device id 重复: '{did}' 已被 {_registered_devices[did]} 注册")
|
|
||||||
_registered_devices[did] = cls
|
|
||||||
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# @action 方法装饰器
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 区分 "用户没传 action_type" 和 "用户传了 None"
|
|
||||||
_ACTION_TYPE_UNSET = object()
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyShadowingNames
|
|
||||||
def action(
|
|
||||||
action_type: Any = _ACTION_TYPE_UNSET,
|
|
||||||
goal: Optional[Dict[str, str]] = None,
|
|
||||||
feedback: Optional[Dict[str, str]] = None,
|
|
||||||
result: Optional[Dict[str, str]] = None,
|
|
||||||
handles: Optional[List[_ActionHandleBase]] = None,
|
|
||||||
goal_default: Optional[Dict[str, Any]] = None,
|
|
||||||
placeholder_keys: Optional[Dict[str, str]] = None,
|
|
||||||
always_free: bool = False,
|
|
||||||
is_protocol: bool = False,
|
|
||||||
description: str = "",
|
|
||||||
auto_prefix: bool = False,
|
|
||||||
parent: bool = False,
|
|
||||||
node_type: Optional["NodeType"] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
动作方法装饰器
|
|
||||||
|
|
||||||
标记方法为注册表动作。有三种用法:
|
|
||||||
1. @action(action_type=EmptyIn, ...) -- 非 auto, 使用指定 ROS Action 类型
|
|
||||||
2. @action() -- 非 auto, UniLabJsonCommand (从方法签名生成 schema)
|
|
||||||
3. 不加 @action -- auto- 前缀, UniLabJsonCommand
|
|
||||||
|
|
||||||
Protocol 用法:
|
|
||||||
@action(action_type=Add, is_protocol=True)
|
|
||||||
def AddProtocol(self): ...
|
|
||||||
标记该动作为高级协议 (protocol),运行时通过 ROS Action 路由到
|
|
||||||
protocol generator 执行。action_type 指向 unilabos_msgs 的 Action 类型。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
action_type: ROS Action 消息类型 (如 EmptyIn, SendCmd, HeatChill).
|
|
||||||
不传/默认 = UniLabJsonCommand (非 auto).
|
|
||||||
goal: Goal 字段映射 (ROS字段名 -> 设备参数名).
|
|
||||||
protocol 模式下可留空,系统自动生成 identity 映射.
|
|
||||||
feedback: Feedback 字段映射
|
|
||||||
result: Result 字段映射
|
|
||||||
handles: 动作端口列表 (ActionInputHandle / ActionOutputHandle)
|
|
||||||
goal_default: Goal 字段默认值映射 (字段名 -> 默认值), 与自动生成的 goal_default 合并
|
|
||||||
placeholder_keys: 参数占位符配置
|
|
||||||
always_free: 是否为永久闲置动作 (不受排队限制)
|
|
||||||
is_protocol: 是否为工作站协议 (protocol)。True 时运行时走 protocol generator 路径。
|
|
||||||
description: 动作描述
|
|
||||||
auto_prefix: 若为 True,动作名使用 auto-{method_name} 形式(与无 @action 时一致)
|
|
||||||
parent: 若为 True,当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数
|
|
||||||
node_type: 动作的节点类型 (NodeType.ILAB / NodeType.MANUAL_CONFIRM)。
|
|
||||||
不填写时不写入注册表。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func: F) -> F:
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
# action_type 为哨兵值 => 用户没传, 视为 None (UniLabJsonCommand)
|
|
||||||
resolved_type = None if action_type is _ACTION_TYPE_UNSET else action_type
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"action_type": resolved_type,
|
|
||||||
"goal": goal or {},
|
|
||||||
"feedback": feedback or {},
|
|
||||||
"result": result or {},
|
|
||||||
"handles": _action_handles_to_dict(handles),
|
|
||||||
"goal_default": goal_default or {},
|
|
||||||
"placeholder_keys": placeholder_keys or {},
|
|
||||||
"always_free": always_free,
|
|
||||||
"is_protocol": is_protocol,
|
|
||||||
"description": description,
|
|
||||||
"auto_prefix": auto_prefix,
|
|
||||||
"parent": parent,
|
|
||||||
}
|
|
||||||
if node_type is not None:
|
|
||||||
meta["node_type"] = node_type.value if isinstance(node_type, NodeType) else str(node_type)
|
|
||||||
wrapper._action_registry_meta = meta # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
# 设置 _is_always_free 保持与旧 @always_free 装饰器兼容
|
|
||||||
if always_free:
|
|
||||||
wrapper._is_always_free = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def get_action_meta(func) -> Optional[Dict[str, Any]]:
|
|
||||||
"""获取方法上的 @action 装饰器元数据"""
|
|
||||||
return getattr(func, "_action_registry_meta", None)
|
|
||||||
|
|
||||||
|
|
||||||
def has_action_decorator(func) -> bool:
|
|
||||||
"""检查函数是否带有 @action 装饰器"""
|
|
||||||
return hasattr(func, "_action_registry_meta")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# @resource 类/函数装饰器
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def resource(
|
|
||||||
id: str,
|
|
||||||
category: List[str],
|
|
||||||
description: str = "",
|
|
||||||
icon: str = "",
|
|
||||||
version: str = "1.0.0",
|
|
||||||
handles: Optional[List[_DeviceHandleBase]] = None,
|
|
||||||
model: Optional[Dict[str, Any]] = None,
|
|
||||||
class_type: str = "pylabrobot",
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
资源类/函数装饰器
|
|
||||||
|
|
||||||
将类或工厂函数标记为一个 UniLab-OS 资源,附加注册表元数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id: 注册表唯一标识 (必填, 不可重复)
|
|
||||||
category: 资源分类标签列表 (必填)
|
|
||||||
description: 资源描述
|
|
||||||
icon: 图标路径
|
|
||||||
version: 版本号
|
|
||||||
handles: 端口列表 (InputHandle / OutputHandle)
|
|
||||||
model: 可选的 3D 模型配置
|
|
||||||
class_type: 资源实现类型 ("python" / "pylabrobot" / "unilabos")
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(obj):
|
|
||||||
meta = {
|
|
||||||
"resource_id": id,
|
|
||||||
"category": category,
|
|
||||||
"description": description,
|
|
||||||
"icon": icon,
|
|
||||||
"version": version,
|
|
||||||
"handles": _device_handles_to_list(handles),
|
|
||||||
"model": model,
|
|
||||||
"class_type": class_type,
|
|
||||||
}
|
|
||||||
obj._resource_registry_meta = meta
|
|
||||||
|
|
||||||
if id in _registered_resources:
|
|
||||||
raise ValueError(f"@resource id 重复: '{id}' 已被 {_registered_resources[id]} 注册")
|
|
||||||
_registered_resources[id] = obj
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def get_device_meta(cls, device_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取类上的 @device 装饰器元数据。
|
|
||||||
|
|
||||||
当 device_id 存在且类使用 ids+id_meta 时,返回合并后的 meta
|
|
||||||
(base_meta 与 id_meta[device_id] 深度合并)。
|
|
||||||
"""
|
|
||||||
base = getattr(cls, "_device_registry_meta", None)
|
|
||||||
if base is None:
|
|
||||||
return None
|
|
||||||
id_meta = getattr(cls, "_device_registry_id_meta", None) or {}
|
|
||||||
if device_id is None or device_id not in id_meta:
|
|
||||||
result = dict(base)
|
|
||||||
ids = getattr(cls, "_device_registry_ids", None)
|
|
||||||
result["device_id"] = device_id if device_id is not None else (ids[0] if ids else None)
|
|
||||||
return result
|
|
||||||
|
|
||||||
overrides = id_meta[device_id]
|
|
||||||
result = dict(base)
|
|
||||||
result["device_id"] = device_id
|
|
||||||
for key in ["handles", "description", "icon", "model"]:
|
|
||||||
if key in overrides:
|
|
||||||
val = overrides[key]
|
|
||||||
if key == "handles" and isinstance(val, list):
|
|
||||||
# handles 必须是 Handle 对象列表
|
|
||||||
result[key] = [h.to_registry_dict() for h in val]
|
|
||||||
else:
|
|
||||||
result[key] = val
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_resource_meta(obj) -> Optional[Dict[str, Any]]:
|
|
||||||
"""获取对象上的 @resource 装饰器元数据"""
|
|
||||||
return getattr(obj, "_resource_registry_meta", None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_registered_devices() -> Dict[str, type]:
|
|
||||||
"""获取所有已注册的设备类"""
|
|
||||||
return _registered_devices.copy()
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_registered_resources() -> Dict[str, Any]:
|
|
||||||
"""获取所有已注册的资源"""
|
|
||||||
return _registered_resources.copy()
|
|
||||||
|
|
||||||
|
|
||||||
def clear_registry():
|
|
||||||
"""清空全局注册表 (用于测试)"""
|
|
||||||
_registered_devices.clear()
|
|
||||||
_registered_resources.clear()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 枚举值归一化
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_enum_value(raw: Any, enum_cls) -> Optional[str]:
|
|
||||||
"""将 AST 提取的枚举成员名 / YAML 值字符串 / 旧格式长路径统一归一化为枚举值。
|
|
||||||
|
|
||||||
适用于 Side、DataSource、NodeType 等继承自 ``str, Enum`` 的装饰器枚举。
|
|
||||||
|
|
||||||
处理以下格式:
|
|
||||||
- "MANUAL_CONFIRM" → NodeType["MANUAL_CONFIRM"].value = "manual_confirm"
|
|
||||||
- "manual_confirm" → NodeType("manual_confirm").value = "manual_confirm"
|
|
||||||
- "HANDLE" → DataSource["HANDLE"].value = "handle"
|
|
||||||
- "NORTH" → Side["NORTH"].value = "NORTH"
|
|
||||||
- 旧缓存长路径 "unilabos...NodeType.MANUAL_CONFIRM" → 先 rsplit 再查找
|
|
||||||
"""
|
|
||||||
if not raw:
|
|
||||||
return None
|
|
||||||
raw_str = str(raw)
|
|
||||||
if "." in raw_str:
|
|
||||||
raw_str = raw_str.rsplit(".", 1)[-1]
|
|
||||||
try:
|
|
||||||
return enum_cls[raw_str].value
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return enum_cls(raw_str).value
|
|
||||||
except ValueError:
|
|
||||||
return raw_str
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# topic_config / not_action / always_free 装饰器
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def topic_config(
|
|
||||||
period: Optional[float] = None,
|
|
||||||
print_publish: Optional[bool] = None,
|
|
||||||
qos: Optional[int] = None,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
) -> Callable[[F], F]:
|
|
||||||
"""
|
|
||||||
Topic发布配置装饰器
|
|
||||||
|
|
||||||
用于装饰 get_{attr_name} 方法或 @property,控制对应属性的ROS topic发布行为。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
period: 发布周期(秒)。None 表示使用默认值 5.0
|
|
||||||
print_publish: 是否打印发布日志。None 表示使用节点默认配置
|
|
||||||
qos: QoS深度配置。None 表示使用默认值 10
|
|
||||||
name: 自定义发布名称。None 表示使用方法名(去掉 get_ 前缀)
|
|
||||||
|
|
||||||
Note:
|
|
||||||
与 @property 连用时,@topic_config 必须放在 @property 下面,
|
|
||||||
这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func: F) -> F:
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
wrapper._topic_period = period # type: ignore[attr-defined]
|
|
||||||
wrapper._topic_print_publish = print_publish # type: ignore[attr-defined]
|
|
||||||
wrapper._topic_qos = qos # type: ignore[attr-defined]
|
|
||||||
wrapper._topic_name = name # type: ignore[attr-defined]
|
|
||||||
wrapper._has_topic_config = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def get_topic_config(func) -> dict:
|
|
||||||
"""获取函数上的 topic 配置 (period, print_publish, qos, name)"""
|
|
||||||
if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False):
|
|
||||||
return {
|
|
||||||
"period": getattr(func, "_topic_period", None),
|
|
||||||
"print_publish": getattr(func, "_topic_print_publish", None),
|
|
||||||
"qos": getattr(func, "_topic_qos", None),
|
|
||||||
"name": getattr(func, "_topic_name", None),
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def always_free(func: F) -> F:
|
|
||||||
"""
|
|
||||||
标记动作为永久闲置(不受busy队列限制)的装饰器
|
|
||||||
|
|
||||||
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制,
|
|
||||||
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
wrapper._is_always_free = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
|
|
||||||
def is_always_free(func) -> bool:
|
|
||||||
"""检查函数是否被标记为永久闲置"""
|
|
||||||
return getattr(func, "_is_always_free", False)
|
|
||||||
|
|
||||||
|
|
||||||
def not_action(func: F) -> F:
|
|
||||||
"""
|
|
||||||
标记方法为非动作的装饰器
|
|
||||||
|
|
||||||
用于装饰 driver 类中的方法,使其在注册表扫描时不被识别为动作。
|
|
||||||
适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
wrapper._is_not_action = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
|
|
||||||
def is_not_action(func) -> bool:
|
|
||||||
"""检查函数是否被标记为非动作"""
|
|
||||||
return getattr(func, "_is_not_action", False)
|
|
||||||
@@ -96,13 +96,10 @@ serial:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: string
|
type: string
|
||||||
registry_name:
|
|
||||||
type: string
|
|
||||||
resource_tracker:
|
resource_tracker:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- device_id
|
- device_id
|
||||||
- registry_name
|
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
|
|||||||
@@ -13,18 +13,21 @@ Qone_nmr:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -68,6 +71,31 @@ Qone_nmr:
|
|||||||
title: monitor_folder_for_new_content参数
|
title: monitor_folder_for_new_content参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-strings_to_txt:
|
auto-strings_to_txt:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -110,18 +138,21 @@ Qone_nmr:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -136,31 +167,32 @@ Qone_nmr:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,321 +24,39 @@ bioyond_dispensing_station:
|
|||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: brief_step_parameters参数
|
title: brief_step_parameters参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-process_order_finish_report:
|
auto-compute_experiment_design:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
|
||||||
report_request: null
|
|
||||||
used_materials: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
report_request:
|
|
||||||
type: string
|
|
||||||
used_materials:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- report_request
|
|
||||||
- used_materials
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_order_finish_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-project_order_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
order_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_id
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: project_order_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-query_resource_by_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- material_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: query_resource_by_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sample_locations:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sample_locations参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_90_10_vial_feeding_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
titration: titration
|
|
||||||
goal_default:
|
|
||||||
delay_time: null
|
|
||||||
hold_m_name: null
|
|
||||||
liquid_material_name: NMP
|
|
||||||
speed: null
|
|
||||||
temperature: null
|
|
||||||
titration: null
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: titration
|
|
||||||
data_source: handle
|
|
||||||
data_type: object
|
|
||||||
handler_key: titration
|
|
||||||
io_type: source
|
|
||||||
label: Titration Data From Calculation Node
|
|
||||||
output:
|
|
||||||
- data_key: return_info
|
|
||||||
data_source: executor
|
|
||||||
data_type: string
|
|
||||||
handler_key: BATCH_CREATE_RESULT
|
|
||||||
io_type: sink
|
|
||||||
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
title: BatchCreate9010VialFeedingTasks_Feedback
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
description: 延迟时间(秒),默认600
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
description: 库位名称,如"C01",必填参数
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
default: NMP
|
|
||||||
description: 10%物料的液体物料名称,默认为"NMP"
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
description: 搅拌速度,默认400
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
description: 温度(℃),默认40
|
|
||||||
type: string
|
|
||||||
titration:
|
|
||||||
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
|
|
||||||
titration_solvent(滴定溶液体积mL)'
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- titration
|
|
||||||
title: BatchCreate9010VialFeedingTasks_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: BatchCreate9010VialFeedingTasks_Result
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: batch_create_90_10_vial_feeding_tasks参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_diamine_solution_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
solutions: solutions
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
goal_default:
|
|
||||||
delay_time: null
|
|
||||||
liquid_material_name: NMP
|
|
||||||
solutions: null
|
|
||||||
speed: null
|
|
||||||
temperature: null
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: solutions
|
|
||||||
data_source: handle
|
|
||||||
data_type: array
|
|
||||||
handler_key: solutions
|
|
||||||
io_type: source
|
|
||||||
label: Solution Data From Python
|
|
||||||
output:
|
|
||||||
- data_key: return_info
|
|
||||||
data_source: executor
|
|
||||||
data_type: string
|
|
||||||
handler_key: BATCH_CREATE_RESULT
|
|
||||||
io_type: sink
|
|
||||||
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
title: BatchCreateDiamineSolutionTasks_Feedback
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
default: NMP
|
|
||||||
description: 液体溶剂名称,用于溶解固体物料,默认为NMP(N-甲基吡咯烷酮)
|
|
||||||
type: string
|
|
||||||
solutions:
|
|
||||||
description: '溶液列表,JSON数组格式,每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
|
|
||||||
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
|
|
||||||
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
|
|
||||||
4.5, "solvent_volume": 18}]'
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- solutions
|
|
||||||
title: BatchCreateDiamineSolutionTasks_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: BatchCreateDiamineSolutionTasks_Result
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: batch_create_diamine_solution_tasks参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
compute_experiment_design:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
m_tot: m_tot
|
|
||||||
ratio: ratio
|
|
||||||
titration_percent: titration_percent
|
|
||||||
wt_percent: wt_percent
|
|
||||||
goal_default:
|
goal_default:
|
||||||
m_tot: '70'
|
m_tot: '70'
|
||||||
ratio: null
|
ratio: null
|
||||||
titration_percent: '0.03'
|
titration_percent: '0.03'
|
||||||
wt_percent: '0.25'
|
wt_percent: '0.25'
|
||||||
handles:
|
handles: {}
|
||||||
output:
|
|
||||||
- data_key: solutions
|
|
||||||
data_source: executor
|
|
||||||
data_type: array
|
|
||||||
handler_key: solutions
|
|
||||||
io_type: sink
|
|
||||||
label: Solution Data From Python
|
|
||||||
- data_key: titration
|
|
||||||
data_source: executor
|
|
||||||
data_type: object
|
|
||||||
handler_key: titration
|
|
||||||
io_type: sink
|
|
||||||
label: Titration Data From Calculation Node
|
|
||||||
- data_key: solvents
|
|
||||||
data_source: executor
|
|
||||||
data_type: object
|
|
||||||
handler_key: solvents
|
|
||||||
io_type: sink
|
|
||||||
label: Solvents Data From Calculation Node
|
|
||||||
- data_key: feeding_order
|
|
||||||
data_source: executor
|
|
||||||
data_type: array
|
|
||||||
handler_key: feeding_order
|
|
||||||
io_type: sink
|
|
||||||
label: Feeding Order Data From Calculation Node
|
|
||||||
placeholder_keys: {}
|
placeholder_keys: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
m_tot:
|
m_tot:
|
||||||
default: '70'
|
default: '70'
|
||||||
description: 总质量(g)
|
|
||||||
type: string
|
type: string
|
||||||
ratio:
|
ratio:
|
||||||
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
|
|
||||||
type: object
|
type: object
|
||||||
titration_percent:
|
titration_percent:
|
||||||
default: '0.03'
|
default: '0.03'
|
||||||
description: 滴定比例(10%部分)
|
|
||||||
type: string
|
type: string
|
||||||
wt_percent:
|
wt_percent:
|
||||||
default: '0.25'
|
default: '0.25'
|
||||||
description: 目标固含质量分数
|
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- ratio
|
- ratio
|
||||||
@@ -377,6 +95,305 @@ bioyond_dispensing_station:
|
|||||||
title: compute_experiment_design参数
|
title: compute_experiment_design参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-process_order_finish_report:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
report_request: null
|
||||||
|
used_materials: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
report_request:
|
||||||
|
type: string
|
||||||
|
used_materials:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- report_request
|
||||||
|
- used_materials
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: process_order_finish_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-project_order_report:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
order_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: project_order_report参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-query_resource_by_name:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
material_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
material_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- material_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: query_resource_by_name参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-transfer_materials_to_reaction_station:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
target_device_id: null
|
||||||
|
transfer_groups: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
target_device_id:
|
||||||
|
type: string
|
||||||
|
transfer_groups:
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- target_device_id
|
||||||
|
- transfer_groups
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: transfer_materials_to_reaction_station参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-workflow_sample_locations:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
workflow_id: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
workflow_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- workflow_id
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: workflow_sample_locations参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
batch_create_90_10_vial_feeding_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
hold_m_name: hold_m_name
|
||||||
|
liquid_material_name: liquid_material_name
|
||||||
|
speed: speed
|
||||||
|
temperature: temperature
|
||||||
|
titration: titration
|
||||||
|
goal_default:
|
||||||
|
delay_time: '600'
|
||||||
|
hold_m_name: ''
|
||||||
|
liquid_material_name: NMP
|
||||||
|
speed: '400'
|
||||||
|
temperature: '40'
|
||||||
|
titration: ''
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: titration
|
||||||
|
data_source: handle
|
||||||
|
data_type: object
|
||||||
|
handler_key: titration
|
||||||
|
io_type: source
|
||||||
|
label: Titration Data From Calculation Node
|
||||||
|
output:
|
||||||
|
- data_key: return_info
|
||||||
|
data_source: executor
|
||||||
|
data_type: string
|
||||||
|
handler_key: BATCH_CREATE_RESULT
|
||||||
|
io_type: sink
|
||||||
|
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
default: '600'
|
||||||
|
description: 延迟时间(秒),默认600
|
||||||
|
type: string
|
||||||
|
hold_m_name:
|
||||||
|
description: 库位名称,如"C01",必填参数
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
description: 10%物料的液体物料名称,默认为"NMP"
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
default: '400'
|
||||||
|
description: 搅拌速度,默认400
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
default: '40'
|
||||||
|
description: 温度(℃),默认40
|
||||||
|
type: string
|
||||||
|
titration:
|
||||||
|
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
|
||||||
|
titration_solvent(滴定溶液体积mL)'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- titration
|
||||||
|
- hold_m_name
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: BatchCreate9010VialFeedingTasks_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: BatchCreate9010VialFeedingTasks
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
batch_create_diamine_solution_tasks:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
liquid_material_name: liquid_material_name
|
||||||
|
solutions: solutions
|
||||||
|
speed: speed
|
||||||
|
temperature: temperature
|
||||||
|
goal_default:
|
||||||
|
delay_time: '600'
|
||||||
|
liquid_material_name: NMP
|
||||||
|
solutions: ''
|
||||||
|
speed: '400'
|
||||||
|
temperature: '20'
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: solutions
|
||||||
|
data_source: handle
|
||||||
|
data_type: array
|
||||||
|
handler_key: solutions
|
||||||
|
io_type: source
|
||||||
|
label: Solution Data From Python
|
||||||
|
output:
|
||||||
|
- data_key: return_info
|
||||||
|
data_source: executor
|
||||||
|
data_type: string
|
||||||
|
handler_key: BATCH_CREATE_RESULT
|
||||||
|
io_type: sink
|
||||||
|
label: Complete Batch Create Result JSON (contains order_codes and order_ids)
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
default: '600'
|
||||||
|
description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
default: NMP
|
||||||
|
description: 液体溶剂名称,用于溶解固体物料,默认为NMP(N-甲基吡咯烷酮)
|
||||||
|
type: string
|
||||||
|
solutions:
|
||||||
|
description: '溶液列表,JSON数组格式,每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
|
||||||
|
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
|
||||||
|
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
|
||||||
|
4.5, "solvent_volume": 18}]'
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
default: '400'
|
||||||
|
description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
default: '20'
|
||||||
|
description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- solutions
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: BatchCreateDiamineSolutionTasks_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: BatchCreateDiamineSolutionTasks
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
create_90_10_vial_feeding_task:
|
create_90_10_vial_feeding_task:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -428,18 +445,17 @@ bioyond_dispensing_station:
|
|||||||
speed: ''
|
speed: ''
|
||||||
temperature: ''
|
temperature: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: DispenStationVialFeed_Feedback
|
title: DispenStationVialFeed_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
delay_time:
|
delay_time:
|
||||||
type: string
|
type: string
|
||||||
@@ -487,13 +503,38 @@ bioyond_dispensing_station:
|
|||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- order_name
|
||||||
|
- percent_90_1_assign_material_name
|
||||||
|
- percent_90_1_target_weigh
|
||||||
|
- percent_90_2_assign_material_name
|
||||||
|
- percent_90_2_target_weigh
|
||||||
|
- percent_90_3_assign_material_name
|
||||||
|
- percent_90_3_target_weigh
|
||||||
|
- percent_10_1_assign_material_name
|
||||||
|
- percent_10_1_target_weigh
|
||||||
|
- percent_10_1_volume
|
||||||
|
- percent_10_1_liquid_material_name
|
||||||
|
- percent_10_2_assign_material_name
|
||||||
|
- percent_10_2_target_weigh
|
||||||
|
- percent_10_2_volume
|
||||||
|
- percent_10_2_liquid_material_name
|
||||||
|
- percent_10_3_assign_material_name
|
||||||
|
- percent_10_3_target_weigh
|
||||||
|
- percent_10_3_volume
|
||||||
|
- percent_10_3_liquid_material_name
|
||||||
|
- speed
|
||||||
|
- temperature
|
||||||
|
- delay_time
|
||||||
|
- hold_m_name
|
||||||
title: DispenStationVialFeed_Goal
|
title: DispenStationVialFeed_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: DispenStationVialFeed_Result
|
title: DispenStationVialFeed_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -524,18 +565,17 @@ bioyond_dispensing_station:
|
|||||||
temperature: ''
|
temperature: ''
|
||||||
volume: ''
|
volume: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: DispenStationSolnPrep_Feedback
|
title: DispenStationSolnPrep_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
delay_time:
|
delay_time:
|
||||||
type: string
|
type: string
|
||||||
@@ -555,13 +595,24 @@ bioyond_dispensing_station:
|
|||||||
type: string
|
type: string
|
||||||
volume:
|
volume:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- order_name
|
||||||
|
- material_name
|
||||||
|
- target_weigh
|
||||||
|
- volume
|
||||||
|
- liquid_material_name
|
||||||
|
- speed
|
||||||
|
- temperature
|
||||||
|
- delay_time
|
||||||
|
- hold_m_name
|
||||||
title: DispenStationSolnPrep_Goal
|
title: DispenStationSolnPrep_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: DispenStationSolnPrep_Result
|
title: DispenStationSolnPrep_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -569,64 +620,6 @@ bioyond_dispensing_station:
|
|||||||
title: DispenStationSolnPrep
|
title: DispenStationSolnPrep
|
||||||
type: object
|
type: object
|
||||||
type: DispenStationSolnPrep
|
type: DispenStationSolnPrep
|
||||||
scheduler_start:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: scheduler_start结果
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: scheduler_start参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
transfer_materials_to_reaction_station:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
target_device_id: target_device_id
|
|
||||||
transfer_groups: transfer_groups
|
|
||||||
goal_default:
|
|
||||||
target_device_id: null
|
|
||||||
transfer_groups: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys:
|
|
||||||
target_device_id: unilabos_devices
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 将配液站完成的物料(溶液、样品等)转移到指定反应站的堆栈库位。支持配置多组转移任务,每组包含物料名称、目标堆栈和目标库位。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
target_device_id:
|
|
||||||
description: 目标反应站设备ID(从设备列表中选择,所有转移组都使用同一个目标设备)
|
|
||||||
type: string
|
|
||||||
transfer_groups:
|
|
||||||
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- target_device_id
|
|
||||||
- transfer_groups
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: transfer_materials_to_reaction_station参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
wait_for_multiple_orders_and_get_reports:
|
wait_for_multiple_orders_and_get_reports:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -634,9 +627,9 @@ bioyond_dispensing_station:
|
|||||||
check_interval: check_interval
|
check_interval: check_interval
|
||||||
timeout: timeout
|
timeout: timeout
|
||||||
goal_default:
|
goal_default:
|
||||||
batch_create_result: null
|
batch_create_result: ''
|
||||||
check_interval: 10
|
check_interval: '10'
|
||||||
timeout: 7200
|
timeout: '7200'
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: batch_create_result
|
- data_key: batch_create_result
|
||||||
@@ -652,38 +645,50 @@ bioyond_dispensing_station:
|
|||||||
handler_key: batch_reports_result
|
handler_key: batch_reports_result
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Batch Order Completion Reports
|
label: Batch Order Completion Reports
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。
|
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
title: WaitForMultipleOrdersAndGetReports_Feedback
|
title: WaitForMultipleOrdersAndGetReports_Feedback
|
||||||
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
batch_create_result:
|
batch_create_result:
|
||||||
description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
|
description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
|
||||||
type: string
|
type: string
|
||||||
check_interval:
|
check_interval:
|
||||||
default: 10
|
default: '10'
|
||||||
description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务
|
description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务
|
||||||
type: integer
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
default: 7200
|
default: '7200'
|
||||||
description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout
|
description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout
|
||||||
type: integer
|
type: string
|
||||||
required: []
|
required:
|
||||||
|
- batch_create_result
|
||||||
title: WaitForMultipleOrdersAndGetReports_Goal
|
title: WaitForMultipleOrdersAndGetReports_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 'JSON格式的批量任务完成信息,包含: total(总数), completed(成功数), timeout(超时数),
|
||||||
|
error(错误数), elapsed_time(总耗时), reports(报告数组,每个元素包含order_code,
|
||||||
|
order_id, status, completion_status, report, elapsed_time)'
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: WaitForMultipleOrdersAndGetReports_Result
|
title: WaitForMultipleOrdersAndGetReports_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
title: WaitForMultipleOrdersAndGetReports
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation
|
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
||||||
status_types: {}
|
status_types: {}
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
@@ -694,16 +699,15 @@ bioyond_dispensing_station:
|
|||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
type: object
|
type: string
|
||||||
deck:
|
deck:
|
||||||
type: string
|
type: string
|
||||||
protocol_type:
|
required:
|
||||||
type: string
|
- config
|
||||||
required: []
|
- deck
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
model: {}
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
78
unilabos/registry/devices/camera.yaml
Normal file
78
unilabos/registry/devices/camera.yaml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
camera:
|
||||||
|
category:
|
||||||
|
- camera
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-destroy_node:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 用于安全地关闭摄像头设备,释放摄像头资源,停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: destroy_node参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-timer_callback:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧,将OpenCV格式的图像转换为ROS Image消息格式,并发布到指定的视频话题。默认以10Hz频率执行,确保视频流的连续性和实时性。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: timer_callback参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
module: unilabos.ros.nodes.presets.camera:VideoPublisher
|
||||||
|
status_types: {}
|
||||||
|
type: ros2
|
||||||
|
config_info: []
|
||||||
|
description: VideoPublisher摄像头设备节点,用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头(如USB摄像头、内置摄像头等),定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
camera_index:
|
||||||
|
default: 0
|
||||||
|
type: string
|
||||||
|
device_id:
|
||||||
|
default: video_publisher
|
||||||
|
type: string
|
||||||
|
device_uuid:
|
||||||
|
default: ''
|
||||||
|
type: string
|
||||||
|
period:
|
||||||
|
default: 0.1
|
||||||
|
type: number
|
||||||
|
resource_tracker:
|
||||||
|
type: object
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
@@ -18,7 +18,7 @@ cameracontroller_device:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
type: object
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
@@ -42,8 +42,7 @@ cameracontroller_device:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: stop参数
|
title: stop参数
|
||||||
@@ -51,7 +50,7 @@ cameracontroller_device:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.cameraSII.cameraUSB:CameraController
|
module: unilabos.devices.cameraSII.cameraUSB:CameraController
|
||||||
status_types:
|
status_types:
|
||||||
status: Dict[str, Any]
|
status: dict
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
||||||
@@ -104,4 +103,5 @@ cameracontroller_device:
|
|||||||
required:
|
required:
|
||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
|
registry_type: device
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -141,26 +141,30 @@ hplc.agilent:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -171,6 +175,7 @@ hplc.agilent:
|
|||||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||||
status_types:
|
status_types:
|
||||||
could_run: bool
|
could_run: bool
|
||||||
|
data_file: String
|
||||||
device_status: str
|
device_status: str
|
||||||
driver_init_ok: bool
|
driver_init_ok: bool
|
||||||
finish_status: str
|
finish_status: str
|
||||||
@@ -194,6 +199,10 @@ hplc.agilent:
|
|||||||
properties:
|
properties:
|
||||||
could_run:
|
could_run:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
data_file:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
device_status:
|
device_status:
|
||||||
type: string
|
type: string
|
||||||
driver_init_ok:
|
driver_init_ok:
|
||||||
@@ -207,13 +216,14 @@ hplc.agilent:
|
|||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- could_run
|
|
||||||
- device_status
|
|
||||||
- driver_init_ok
|
|
||||||
- finish_status
|
|
||||||
- is_running
|
|
||||||
- status_text
|
- status_text
|
||||||
|
- device_status
|
||||||
|
- could_run
|
||||||
|
- driver_init_ok
|
||||||
|
- is_running
|
||||||
- success
|
- success
|
||||||
|
- finish_status
|
||||||
|
- data_file
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
hplc.agilent-zhida:
|
hplc.agilent-zhida:
|
||||||
@@ -226,25 +236,26 @@ hplc.agilent-zhida:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -304,18 +315,21 @@ hplc.agilent-zhida:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -327,35 +341,35 @@ hplc.agilent-zhida:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: string
|
string: string
|
||||||
text: text
|
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -393,7 +407,7 @@ hplc.agilent-zhida:
|
|||||||
status:
|
status:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- methods
|
|
||||||
- status
|
- status
|
||||||
|
- methods
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -120,41 +120,42 @@ raman.home_made:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
raman_cmd:
|
raman_cmd:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ separator.chinwe:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: connect参数
|
title: connect参数
|
||||||
@@ -66,145 +65,135 @@ separator.chinwe:
|
|||||||
required:
|
required:
|
||||||
- command_dict
|
- command_dict
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: execute_command_from_outer参数
|
title: execute_command_from_outer参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_rotate_quarter:
|
motor_rotate_quarter:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
direction: 顺时针
|
direction: 顺时针
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
speed: 60
|
speed: 60
|
||||||
goal_default:
|
|
||||||
direction: 顺时针
|
|
||||||
motor_id: null
|
|
||||||
speed: 60
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 电机旋转 1/4 圈
|
description: 电机旋转 1/4 圈
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
direction:
|
direction:
|
||||||
default: 顺时针
|
default: 顺时针
|
||||||
description: 旋转方向
|
description: 旋转方向
|
||||||
|
enum:
|
||||||
|
- 顺时针
|
||||||
|
- 逆时针
|
||||||
type: string
|
type: string
|
||||||
motor_id:
|
motor_id:
|
||||||
|
default: '4'
|
||||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
type: integer
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
type: string
|
||||||
speed:
|
speed:
|
||||||
default: 60
|
default: 60
|
||||||
description: 速度 (RPM)
|
description: 速度 (RPM)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
|
- speed
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: motor_rotate_quarter参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_run_continuous:
|
motor_run_continuous:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
direction: 顺时针
|
direction: 顺时针
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
speed: 60
|
speed: 60
|
||||||
goal_default:
|
|
||||||
direction: 顺时针
|
|
||||||
motor_id: null
|
|
||||||
speed: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 电机一直旋转 (速度模式)
|
description: 电机一直旋转 (速度模式)
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
direction:
|
direction:
|
||||||
default: 顺时针
|
default: 顺时针
|
||||||
description: 旋转方向
|
description: 旋转方向
|
||||||
|
enum:
|
||||||
|
- 顺时针
|
||||||
|
- 逆时针
|
||||||
type: string
|
type: string
|
||||||
motor_id:
|
motor_id:
|
||||||
|
default: '4'
|
||||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||||
type: integer
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
type: string
|
||||||
speed:
|
speed:
|
||||||
|
default: 60
|
||||||
description: 速度 (RPM)
|
description: 速度 (RPM)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
- speed
|
- speed
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: motor_run_continuous参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
motor_stop:
|
motor_stop:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
motor_id: 4
|
motor_id: 4
|
||||||
goal_default:
|
|
||||||
motor_id: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 停止指定步进电机
|
description: 停止指定步进电机
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
motor_id:
|
motor_id:
|
||||||
|
default: '4'
|
||||||
description: 选择电机
|
description: 选择电机
|
||||||
|
enum:
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
title: '注: 4=搅拌, 5=旋钮'
|
title: '注: 4=搅拌, 5=旋钮'
|
||||||
type: integer
|
type: string
|
||||||
required:
|
required:
|
||||||
- motor_id
|
- motor_id
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: motor_stop参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_aspirate:
|
pump_aspirate:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
valve_port: 1
|
valve_port: 1
|
||||||
volume: 1000
|
volume: 1000
|
||||||
goal_default:
|
|
||||||
pump_id: null
|
|
||||||
valve_port: null
|
|
||||||
volume: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 注射泵吸液
|
description: 注射泵吸液
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
pump_id:
|
pump_id:
|
||||||
|
default: '1'
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
valve_port:
|
valve_port:
|
||||||
|
default: '1'
|
||||||
description: 阀门端口
|
description: 阀门端口
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
volume:
|
volume:
|
||||||
|
default: 1000
|
||||||
description: 吸液步数
|
description: 吸液步数
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
@@ -212,38 +201,41 @@ separator.chinwe:
|
|||||||
- volume
|
- volume
|
||||||
- valve_port
|
- valve_port
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: pump_aspirate参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_dispense:
|
pump_dispense:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
valve_port: 1
|
valve_port: 1
|
||||||
volume: 1000
|
volume: 1000
|
||||||
goal_default:
|
|
||||||
pump_id: null
|
|
||||||
valve_port: null
|
|
||||||
volume: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 注射泵排液
|
description: 注射泵排液
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
pump_id:
|
pump_id:
|
||||||
|
default: '1'
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
valve_port:
|
valve_port:
|
||||||
|
default: '1'
|
||||||
description: 阀门端口
|
description: 阀门端口
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
volume:
|
volume:
|
||||||
|
default: 1000
|
||||||
description: 排液步数
|
description: 排液步数
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
@@ -251,152 +243,121 @@ separator.chinwe:
|
|||||||
- volume
|
- volume
|
||||||
- valve_port
|
- valve_port
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: pump_dispense参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_initialize:
|
pump_initialize:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
drain_port: 0
|
drain_port: 0
|
||||||
output_port: 0
|
output_port: 0
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
speed: 10
|
speed: 10
|
||||||
goal_default:
|
|
||||||
drain_port: 0
|
|
||||||
output_port: 0
|
|
||||||
pump_id: null
|
|
||||||
speed: 10
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 初始化指定注射泵
|
description: 初始化指定注射泵
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
drain_port:
|
drain_port:
|
||||||
default: 0
|
default: 0
|
||||||
description: 排液口索引
|
description: 排液口索引
|
||||||
type: string
|
type: integer
|
||||||
output_port:
|
output_port:
|
||||||
default: 0
|
default: 0
|
||||||
description: 输出口索引
|
description: 输出口索引
|
||||||
type: string
|
|
||||||
pump_id:
|
|
||||||
description: 选择泵
|
|
||||||
title: '注: 1号泵, 2号泵, 3号泵'
|
|
||||||
type: integer
|
type: integer
|
||||||
|
pump_id:
|
||||||
|
default: '1'
|
||||||
|
description: 选择泵
|
||||||
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
title: '注: 1号泵, 2号泵, 3号泵'
|
||||||
|
type: string
|
||||||
speed:
|
speed:
|
||||||
default: 10
|
default: 10
|
||||||
description: 运动速度
|
description: 运动速度
|
||||||
type: string
|
type: integer
|
||||||
required:
|
required:
|
||||||
- pump_id
|
- pump_id
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: pump_initialize参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pump_valve:
|
pump_valve:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
port: 1
|
port: 1
|
||||||
pump_id: 1
|
pump_id: 1
|
||||||
goal_default:
|
|
||||||
port: null
|
|
||||||
pump_id: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 切换指定泵的阀门端口
|
description: 切换指定泵的阀门端口
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
port:
|
port:
|
||||||
|
default: '1'
|
||||||
description: 阀门端口号 (1-8)
|
description: 阀门端口号 (1-8)
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
- '4'
|
||||||
|
- '5'
|
||||||
|
- '6'
|
||||||
|
- '7'
|
||||||
|
- '8'
|
||||||
|
type: string
|
||||||
pump_id:
|
pump_id:
|
||||||
|
default: '1'
|
||||||
description: 选择泵
|
description: 选择泵
|
||||||
type: integer
|
enum:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- pump_id
|
- pump_id
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: pump_valve参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
wait_sensor_level:
|
wait_sensor_level:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
target_state: 有液
|
target_state: 有液
|
||||||
timeout: 30
|
timeout: 30
|
||||||
goal_default:
|
|
||||||
target_state: 有液
|
|
||||||
timeout: 30
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 等待传感器液位条件
|
description: 等待传感器液位条件
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
target_state:
|
target_state:
|
||||||
default: 有液
|
default: 有液
|
||||||
description: 目标液位状态
|
description: 目标液位状态
|
||||||
|
enum:
|
||||||
|
- 有液
|
||||||
|
- 无液
|
||||||
type: string
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
default: 30
|
default: 30
|
||||||
description: 超时时间 (秒)
|
description: 超时时间 (秒)
|
||||||
type: integer
|
type: integer
|
||||||
required: []
|
required:
|
||||||
|
- target_state
|
||||||
type: object
|
type: object
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_sensor_level参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
wait_time:
|
wait_time:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
duration: 10
|
duration: 10
|
||||||
goal_default:
|
|
||||||
duration: null
|
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
schema:
|
||||||
description: 等待指定时间
|
description: 等待指定时间
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
duration:
|
duration:
|
||||||
|
default: 10
|
||||||
description: 等待时间 (秒)
|
description: 等待时间 (秒)
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- duration
|
- duration
|
||||||
type: object
|
type: object
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_time参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.separator.chinwe:ChinweDevice
|
module: unilabos.devices.separator.chinwe:ChinweDevice
|
||||||
status_types:
|
status_types:
|
||||||
@@ -445,8 +406,8 @@ separator.chinwe:
|
|||||||
sensor_rssi:
|
sensor_rssi:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- is_connected
|
|
||||||
- sensor_level
|
- sensor_level
|
||||||
- sensor_rssi
|
- sensor_rssi
|
||||||
|
- is_connected
|
||||||
type: object
|
type: object
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
|||||||
@@ -1,834 +0,0 @@
|
|||||||
coincellassemblyworkstation_device:
|
|
||||||
category:
|
|
||||||
- coin_cell_workstation
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
auto-change_hole_sheet_to_2:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
hole: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
hole:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- hole
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: change_hole_sheet_to_2参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-fill_plate:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: fill_plate参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-fun_wuliao_test:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: fun_wuliao_test参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_allpack_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: 4200
|
|
||||||
assembly_type: 7
|
|
||||||
elec_num: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: 50
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
default: 4200
|
|
||||||
type: integer
|
|
||||||
assembly_type:
|
|
||||||
default: 7
|
|
||||||
type: integer
|
|
||||||
elec_num:
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
default: 50
|
|
||||||
type: integer
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- elec_num
|
|
||||||
- elec_use_num
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_allpack_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_allpack_cmd_simp:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: 4200
|
|
||||||
assembly_type: 7
|
|
||||||
battery_clean_ignore: false
|
|
||||||
battery_pressure_mode: true
|
|
||||||
dual_drop_first_volume: 25
|
|
||||||
dual_drop_mode: false
|
|
||||||
dual_drop_start_timing: false
|
|
||||||
dual_drop_suction_timing: false
|
|
||||||
elec_num: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: 50
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
fujipian_juzhendianwei: 0
|
|
||||||
fujipian_panshu: 0
|
|
||||||
gemo_juzhendianwei: 0
|
|
||||||
gemopanshu: 0
|
|
||||||
lvbodian: true
|
|
||||||
qiangtou_juzhendianwei: 0
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 简化版电池组装函数,整合了参数设置和双滴模式
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
default: 4200
|
|
||||||
description: 电池压制力(N)
|
|
||||||
type: integer
|
|
||||||
assembly_type:
|
|
||||||
default: 7
|
|
||||||
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
|
|
||||||
type: integer
|
|
||||||
battery_clean_ignore:
|
|
||||||
default: false
|
|
||||||
description: 是否忽略电池清洁步骤
|
|
||||||
type: boolean
|
|
||||||
battery_pressure_mode:
|
|
||||||
default: true
|
|
||||||
description: 是否启用压力模式
|
|
||||||
type: boolean
|
|
||||||
dual_drop_first_volume:
|
|
||||||
default: 25
|
|
||||||
description: 二次滴液第一次排液体积(μL)
|
|
||||||
type: integer
|
|
||||||
dual_drop_mode:
|
|
||||||
default: false
|
|
||||||
description: 电解液添加模式(false=单次滴液, true=二次滴液)
|
|
||||||
type: boolean
|
|
||||||
dual_drop_start_timing:
|
|
||||||
default: false
|
|
||||||
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
|
|
||||||
type: boolean
|
|
||||||
dual_drop_suction_timing:
|
|
||||||
default: false
|
|
||||||
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
|
|
||||||
type: boolean
|
|
||||||
elec_num:
|
|
||||||
description: 电解液瓶数
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
description: 每瓶电解液组装电池数
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
default: 50
|
|
||||||
description: 电解液吸液量(μL)
|
|
||||||
type: integer
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
description: 实验记录保存路径
|
|
||||||
type: string
|
|
||||||
fujipian_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
fujipian_panshu:
|
|
||||||
default: 0
|
|
||||||
description: 负极片盘数
|
|
||||||
type: integer
|
|
||||||
gemo_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
gemopanshu:
|
|
||||||
default: 0
|
|
||||||
description: 隔膜盘数
|
|
||||||
type: integer
|
|
||||||
lvbodian:
|
|
||||||
default: true
|
|
||||||
description: 是否使用铝箔垫片
|
|
||||||
type: boolean
|
|
||||||
qiangtou_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- elec_num
|
|
||||||
- elec_use_num
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_allpack_cmd_simp参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_get_csv_export_status:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_get_csv_export_status参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_auto:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_auto参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_init:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_init参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_init_auto_start_combined:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_search_enable: false
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_search_enable:
|
|
||||||
default: false
|
|
||||||
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
|
|
||||||
type: boolean
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_init_auto_start_combined参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_start:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_start参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_device_stop:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_device_stop参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_get_msg_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
file_path: D:\coin_cell_data
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
file_path:
|
|
||||||
default: D:\coin_cell_data
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_get_msg_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_bottle_num:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
bottle_num: null
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: bottle_num
|
|
||||||
data_source: workflow
|
|
||||||
data_type: integer
|
|
||||||
handler_key: bottle_count
|
|
||||||
io_type: source
|
|
||||||
label: 配液瓶数
|
|
||||||
required: true
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
bottle_num:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- bottle_num
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_bottle_num参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_finished_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_finished_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_pack_send_msg_cmd:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: null
|
|
||||||
assembly_type: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
type: string
|
|
||||||
assembly_type:
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- elec_use_num
|
|
||||||
- elec_vol
|
|
||||||
- assembly_type
|
|
||||||
- assembly_pressure
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_pack_send_msg_cmd参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_read_data_and_output:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_read_data_and_output参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_sendbottle_allpack_multi:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
assembly_pressure: 4200
|
|
||||||
assembly_type: 7
|
|
||||||
battery_clean_ignore: false
|
|
||||||
battery_pressure_mode: true
|
|
||||||
dual_drop_first_volume: 25
|
|
||||||
dual_drop_mode: false
|
|
||||||
dual_drop_start_timing: false
|
|
||||||
dual_drop_suction_timing: false
|
|
||||||
elec_num: null
|
|
||||||
elec_use_num: null
|
|
||||||
elec_vol: 50
|
|
||||||
file_path: /Users/sml/work
|
|
||||||
fujipian_juzhendianwei: 0
|
|
||||||
fujipian_panshu: 0
|
|
||||||
gemo_juzhendianwei: 0
|
|
||||||
gemopanshu: 0
|
|
||||||
lvbodian: true
|
|
||||||
qiangtou_juzhendianwei: 0
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: elec_num
|
|
||||||
data_source: workflow
|
|
||||||
data_type: integer
|
|
||||||
handler_key: bottle_count
|
|
||||||
io_type: source
|
|
||||||
label: 配液瓶数
|
|
||||||
required: true
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 发送瓶数+简化组装函数(适用于第二批次及后续批次),合并了发送瓶数和简化组装流程
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assembly_pressure:
|
|
||||||
default: 4200
|
|
||||||
description: 电池压制力(N)
|
|
||||||
type: integer
|
|
||||||
assembly_type:
|
|
||||||
default: 7
|
|
||||||
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
|
|
||||||
type: integer
|
|
||||||
battery_clean_ignore:
|
|
||||||
default: false
|
|
||||||
description: 是否忽略电池清洁步骤
|
|
||||||
type: boolean
|
|
||||||
battery_pressure_mode:
|
|
||||||
default: true
|
|
||||||
description: 是否启用压力模式
|
|
||||||
type: boolean
|
|
||||||
dual_drop_first_volume:
|
|
||||||
default: 25
|
|
||||||
description: 二次滴液第一次排液体积(μL)
|
|
||||||
type: integer
|
|
||||||
dual_drop_mode:
|
|
||||||
default: false
|
|
||||||
description: 电解液添加模式(false=单次滴液, true=二次滴液)
|
|
||||||
type: boolean
|
|
||||||
dual_drop_start_timing:
|
|
||||||
default: false
|
|
||||||
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
|
|
||||||
type: boolean
|
|
||||||
dual_drop_suction_timing:
|
|
||||||
default: false
|
|
||||||
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
|
|
||||||
type: boolean
|
|
||||||
elec_num:
|
|
||||||
description: 电解液瓶数,如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数
|
|
||||||
type: string
|
|
||||||
elec_use_num:
|
|
||||||
description: 每瓶电解液组装电池数
|
|
||||||
type: string
|
|
||||||
elec_vol:
|
|
||||||
default: 50
|
|
||||||
description: 电解液吸液量(μL)
|
|
||||||
type: integer
|
|
||||||
file_path:
|
|
||||||
default: /Users/sml/work
|
|
||||||
description: 实验记录保存路径
|
|
||||||
type: string
|
|
||||||
fujipian_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
fujipian_panshu:
|
|
||||||
default: 0
|
|
||||||
description: 负极片盘数
|
|
||||||
type: integer
|
|
||||||
gemo_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
gemopanshu:
|
|
||||||
default: 0
|
|
||||||
description: 隔膜盘数
|
|
||||||
type: integer
|
|
||||||
lvbodian:
|
|
||||||
default: true
|
|
||||||
description: 是否使用铝箔垫片
|
|
||||||
type: boolean
|
|
||||||
qiangtou_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2)
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- elec_num
|
|
||||||
- elec_use_num
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_sendbottle_allpack_multi参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-func_stop_read_data:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: func_stop_read_data参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-modify_deck_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
resource_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
resource_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- resource_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: modify_deck_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-qiming_coin_cell_code:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
battery_clean_ignore: false
|
|
||||||
battery_pressure: 4000
|
|
||||||
battery_pressure_mode: true
|
|
||||||
fujipian_juzhendianwei: 0
|
|
||||||
fujipian_panshu: null
|
|
||||||
gemo_juzhendianwei: 0
|
|
||||||
gemopanshu: 0
|
|
||||||
lvbodian: true
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
battery_clean_ignore:
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
battery_pressure:
|
|
||||||
default: 4000
|
|
||||||
type: integer
|
|
||||||
battery_pressure_mode:
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
fujipian_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
fujipian_panshu:
|
|
||||||
type: integer
|
|
||||||
gemo_juzhendianwei:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
gemopanshu:
|
|
||||||
default: 0
|
|
||||||
type: integer
|
|
||||||
lvbodian:
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- fujipian_panshu
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: qiming_coin_cell_code参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
|
||||||
status_types:
|
|
||||||
data_assembly_coin_cell_num: int
|
|
||||||
data_assembly_pressure: int
|
|
||||||
data_assembly_time: float
|
|
||||||
data_axis_x_pos: float
|
|
||||||
data_axis_y_pos: float
|
|
||||||
data_axis_z_pos: float
|
|
||||||
data_coin_cell_code: str
|
|
||||||
data_coin_num: int
|
|
||||||
data_electrolyte_code: str
|
|
||||||
data_electrolyte_volume: int
|
|
||||||
data_glove_box_o2_content: float
|
|
||||||
data_glove_box_pressure: float
|
|
||||||
data_glove_box_water_content: float
|
|
||||||
data_open_circuit_voltage: float
|
|
||||||
data_pole_weight: float
|
|
||||||
request_rec_msg_status: bool
|
|
||||||
request_send_msg_status: bool
|
|
||||||
sys_mode: str
|
|
||||||
sys_status: str
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: ''
|
|
||||||
handles: []
|
|
||||||
icon: koudian.webp
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
address:
|
|
||||||
default: 172.16.28.102
|
|
||||||
type: string
|
|
||||||
config:
|
|
||||||
type: object
|
|
||||||
debug_mode:
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
deck:
|
|
||||||
type: string
|
|
||||||
port:
|
|
||||||
default: '502'
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties:
|
|
||||||
data_assembly_coin_cell_num:
|
|
||||||
type: integer
|
|
||||||
data_assembly_pressure:
|
|
||||||
type: integer
|
|
||||||
data_assembly_time:
|
|
||||||
type: number
|
|
||||||
data_axis_x_pos:
|
|
||||||
type: number
|
|
||||||
data_axis_y_pos:
|
|
||||||
type: number
|
|
||||||
data_axis_z_pos:
|
|
||||||
type: number
|
|
||||||
data_coin_cell_code:
|
|
||||||
type: string
|
|
||||||
data_coin_num:
|
|
||||||
type: integer
|
|
||||||
data_electrolyte_code:
|
|
||||||
type: string
|
|
||||||
data_electrolyte_volume:
|
|
||||||
type: integer
|
|
||||||
data_glove_box_o2_content:
|
|
||||||
type: number
|
|
||||||
data_glove_box_pressure:
|
|
||||||
type: number
|
|
||||||
data_glove_box_water_content:
|
|
||||||
type: number
|
|
||||||
data_open_circuit_voltage:
|
|
||||||
type: number
|
|
||||||
data_pole_weight:
|
|
||||||
type: number
|
|
||||||
request_rec_msg_status:
|
|
||||||
type: boolean
|
|
||||||
request_send_msg_status:
|
|
||||||
type: boolean
|
|
||||||
sys_mode:
|
|
||||||
type: string
|
|
||||||
sys_status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- data_assembly_coin_cell_num
|
|
||||||
- data_assembly_pressure
|
|
||||||
- data_assembly_time
|
|
||||||
- data_axis_x_pos
|
|
||||||
- data_axis_y_pos
|
|
||||||
- data_axis_z_pos
|
|
||||||
- data_coin_cell_code
|
|
||||||
- data_coin_num
|
|
||||||
- data_electrolyte_code
|
|
||||||
- data_electrolyte_volume
|
|
||||||
- data_glove_box_o2_content
|
|
||||||
- data_glove_box_pressure
|
|
||||||
- data_glove_box_water_content
|
|
||||||
- data_open_circuit_voltage
|
|
||||||
- data_pole_weight
|
|
||||||
- request_rec_msg_status
|
|
||||||
- request_send_msg_status
|
|
||||||
- sys_mode
|
|
||||||
- sys_status
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -50,25 +50,26 @@ gas_source.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -81,25 +82,26 @@ gas_source.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -114,31 +116,32 @@ gas_source.mock:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -229,25 +232,26 @@ vacuum_pump.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -260,25 +264,26 @@ vacuum_pump.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -293,31 +298,32 @@ vacuum_pump.mock:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel:
|
|||||||
action_value_mappings: {}
|
action_value_mappings: {}
|
||||||
module: unilabos.devices.resource_container.container:HotelContainer
|
module: unilabos.devices.resource_container.container:HotelContainer
|
||||||
status_types:
|
status_types:
|
||||||
rotation: ''
|
rotation: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
|
description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- degrees
|
- degrees
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: integer
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: degrees_to_steps参数
|
title: degrees_to_steps参数
|
||||||
@@ -48,8 +47,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: emergency_stop参数
|
title: emergency_stop参数
|
||||||
@@ -74,10 +72,7 @@ xyz_stepper_controller:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: enable_all_axes参数
|
title: enable_all_axes参数
|
||||||
@@ -106,8 +101,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: enable_motor参数
|
title: enable_motor参数
|
||||||
@@ -128,10 +122,7 @@ xyz_stepper_controller:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: home_all_axes参数
|
title: home_all_axes参数
|
||||||
@@ -156,8 +147,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: home_axis参数
|
title: home_axis参数
|
||||||
@@ -198,8 +188,7 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position参数
|
title: move_to_position参数
|
||||||
@@ -240,8 +229,7 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- degrees
|
- degrees
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position_degrees参数
|
title: move_to_position_degrees参数
|
||||||
@@ -282,8 +270,7 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- revolutions
|
- revolutions
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_to_position_revolutions参数
|
title: move_to_position_revolutions参数
|
||||||
@@ -314,17 +301,14 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x:
|
x:
|
||||||
type: integer
|
type: string
|
||||||
y:
|
y:
|
||||||
type: integer
|
type: string
|
||||||
z:
|
z:
|
||||||
type: integer
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz参数
|
title: move_xyz参数
|
||||||
@@ -355,17 +339,14 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x_deg:
|
x_deg:
|
||||||
type: number
|
type: string
|
||||||
y_deg:
|
y_deg:
|
||||||
type: number
|
type: string
|
||||||
z_deg:
|
z_deg:
|
||||||
type: number
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz_degrees参数
|
title: move_xyz_degrees参数
|
||||||
@@ -396,17 +377,14 @@ xyz_stepper_controller:
|
|||||||
default: 5000
|
default: 5000
|
||||||
type: integer
|
type: integer
|
||||||
x_rev:
|
x_rev:
|
||||||
type: number
|
type: string
|
||||||
y_rev:
|
y_rev:
|
||||||
type: number
|
type: string
|
||||||
z_rev:
|
z_rev:
|
||||||
type: number
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: move_xyz_revolutions参数
|
title: move_xyz_revolutions参数
|
||||||
@@ -431,8 +409,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- revolutions
|
- revolutions
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: integer
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: revolutions_to_steps参数
|
title: revolutions_to_steps参数
|
||||||
@@ -465,8 +442,7 @@ xyz_stepper_controller:
|
|||||||
- axis
|
- axis
|
||||||
- speed
|
- speed
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: set_speed_mode参数
|
title: set_speed_mode参数
|
||||||
@@ -491,8 +467,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- steps
|
- steps
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: number
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: steps_to_degrees参数
|
title: steps_to_degrees参数
|
||||||
@@ -517,8 +492,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- steps
|
- steps
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: number
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: steps_to_revolutions参数
|
title: steps_to_revolutions参数
|
||||||
@@ -539,10 +513,7 @@ xyz_stepper_controller:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
additionalProperties:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: stop_all_axes参数
|
title: stop_all_axes参数
|
||||||
@@ -571,8 +542,7 @@ xyz_stepper_controller:
|
|||||||
required:
|
required:
|
||||||
- axis
|
- axis
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: wait_for_completion参数
|
title: wait_for_completion参数
|
||||||
@@ -580,7 +550,8 @@ xyz_stepper_controller:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
|
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
|
||||||
status_types:
|
status_types:
|
||||||
all_positions: Dict[MotorAxis, MotorPosition]
|
all_positions: dict
|
||||||
|
motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 新XYZ控制器
|
description: 新XYZ控制器
|
||||||
@@ -603,10 +574,12 @@ xyz_stepper_controller:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
all_positions:
|
all_positions:
|
||||||
additionalProperties:
|
type: object
|
||||||
type: object
|
motor_status:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
- motor_status
|
||||||
- all_positions
|
- all_positions
|
||||||
type: object
|
type: object
|
||||||
|
registry_type: device
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,31 @@ neware_battery_test_system:
|
|||||||
- battery_test
|
- battery_test
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-print_status_summary:
|
auto-print_status_summary:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -41,8 +66,7 @@ neware_battery_test_system:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: boolean
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: test_connection参数
|
title: test_connection参数
|
||||||
@@ -53,8 +77,9 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 调试方法:显示所有资源的实际名称
|
description: 调试方法:显示所有资源的实际名称
|
||||||
properties:
|
properties:
|
||||||
@@ -64,10 +89,19 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 资源调试信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: debug_resource_names参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
export_status_json:
|
export_status_json:
|
||||||
@@ -77,8 +111,9 @@ neware_battery_test_system:
|
|||||||
goal_default:
|
goal_default:
|
||||||
filepath: bts_status.json
|
filepath: bts_status.json
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 导出当前状态数据到JSON文件
|
description: 导出当前状态数据到JSON文件
|
||||||
properties:
|
properties:
|
||||||
@@ -92,10 +127,19 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 导出操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 导出是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: export_status_json参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
get_device_summary:
|
get_device_summary:
|
||||||
@@ -137,8 +181,10 @@ neware_battery_test_system:
|
|||||||
goal_default:
|
goal_default:
|
||||||
plate_num: null
|
plate_num: null
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
plate_data: plate_data
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 获取指定盘或所有盘的状态信息
|
description: 获取指定盘或所有盘的状态信息
|
||||||
properties:
|
properties:
|
||||||
@@ -147,14 +193,29 @@ neware_battery_test_system:
|
|||||||
properties:
|
properties:
|
||||||
plate_num:
|
plate_num:
|
||||||
description: 盘号 (1 或 2),如果为null则返回所有盘的状态
|
description: 盘号 (1 或 2),如果为null则返回所有盘的状态
|
||||||
|
maximum: 2
|
||||||
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
plate_data:
|
||||||
|
description: 盘状态数据(单盘或所有盘)
|
||||||
|
type: object
|
||||||
|
return_info:
|
||||||
|
description: 操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 查询是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
- plate_data
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: get_plate_status参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
print_status_summary_action:
|
print_status_summary_action:
|
||||||
@@ -162,8 +223,9 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 打印通道状态摘要信息到控制台
|
description: 打印通道状态摘要信息到控制台
|
||||||
properties:
|
properties:
|
||||||
@@ -173,21 +235,28 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 打印操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 打印是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: print_status_summary_action参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
query_plate_action:
|
query_plate_action:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
plate_id: plate_id
|
string: plate_id
|
||||||
string: string
|
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
return_info: return_info
|
||||||
success: success
|
success: success
|
||||||
@@ -195,23 +264,27 @@ neware_battery_test_system:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -225,11 +298,13 @@ neware_battery_test_system:
|
|||||||
csv_path: string
|
csv_path: string
|
||||||
output_dir: string
|
output_dir: string
|
||||||
goal_default:
|
goal_default:
|
||||||
csv_path: null
|
csv_path: ''
|
||||||
output_dir: .
|
output_dir: .
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
|
submitted_count: submitted_count
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 从CSV文件批量提交Neware测试任务
|
description: 从CSV文件批量提交Neware测试任务
|
||||||
properties:
|
properties:
|
||||||
@@ -240,17 +315,31 @@ neware_battery_test_system:
|
|||||||
description: 输入CSV文件的绝对路径
|
description: 输入CSV文件的绝对路径
|
||||||
type: string
|
type: string
|
||||||
output_dir:
|
output_dir:
|
||||||
default: .
|
|
||||||
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- csv_path
|
- csv_path
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 执行结果详细信息
|
||||||
|
type: string
|
||||||
|
submitted_count:
|
||||||
|
description: 成功提交的任务数量
|
||||||
|
type: integer
|
||||||
|
success:
|
||||||
|
description: 是否成功
|
||||||
|
type: boolean
|
||||||
|
total_count:
|
||||||
|
description: CSV文件中的总行数
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: submit_from_csv参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
test_connection_action:
|
test_connection_action:
|
||||||
@@ -258,8 +347,9 @@ neware_battery_test_system:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: 测试与电池测试系统的TCP连接
|
description: 测试与电池测试系统的TCP连接
|
||||||
properties:
|
properties:
|
||||||
@@ -269,10 +359,19 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 连接测试结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 连接测试是否成功
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: test_connection_action参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
upload_backup_to_oss:
|
upload_backup_to_oss:
|
||||||
@@ -293,8 +392,12 @@ neware_battery_test_system:
|
|||||||
handler_key: uploaded_files
|
handler_key: uploaded_files
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Uploaded Files (with standard flow info)
|
label: Uploaded Files (with standard flow info)
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
failed_files: failed_files
|
||||||
|
return_info: return_info
|
||||||
|
success: success
|
||||||
|
total_count: total_count
|
||||||
|
uploaded_count: uploaded_count
|
||||||
schema:
|
schema:
|
||||||
description: 上传备份文件到阿里云OSS
|
description: 上传备份文件到阿里云OSS
|
||||||
properties:
|
properties:
|
||||||
@@ -314,17 +417,65 @@ neware_battery_test_system:
|
|||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
failed_files:
|
||||||
|
description: 上传失败的文件名列表
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
return_info:
|
||||||
|
description: 上传操作结果信息
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
description: 上传是否成功
|
||||||
|
type: boolean
|
||||||
|
total_count:
|
||||||
|
description: 总文件数
|
||||||
|
type: integer
|
||||||
|
uploaded_count:
|
||||||
|
description: 成功上传的文件数
|
||||||
|
type: integer
|
||||||
|
uploaded_files:
|
||||||
|
description: 成功上传的文件详情列表
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
Battery_Code:
|
||||||
|
description: 电池编码
|
||||||
|
type: string
|
||||||
|
Electrolyte_Code:
|
||||||
|
description: 电解液编码
|
||||||
|
type: string
|
||||||
|
filename:
|
||||||
|
description: 文件名
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
description: OSS下载链接
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- filename
|
||||||
|
- url
|
||||||
|
- Battery_Code
|
||||||
|
- Electrolyte_Code
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
- uploaded_count
|
||||||
|
- total_count
|
||||||
|
- failed_files
|
||||||
|
- uploaded_files
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: upload_backup_to_oss参数
|
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
||||||
status_types:
|
status_types:
|
||||||
channel_status: Dict[int, Dict]
|
channel_status: dict
|
||||||
connection_info: Dict[str, str]
|
connection_info: dict
|
||||||
device_summary: dict
|
device_summary: dict
|
||||||
|
plate_status: dict
|
||||||
status: str
|
status: str
|
||||||
total_channels: int
|
total_channels: int
|
||||||
type: python
|
type: python
|
||||||
@@ -366,24 +517,23 @@ neware_battery_test_system:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
channel_status:
|
channel_status:
|
||||||
additionalProperties:
|
|
||||||
type: object
|
|
||||||
type: object
|
type: object
|
||||||
connection_info:
|
connection_info:
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
type: object
|
||||||
device_summary:
|
device_summary:
|
||||||
type: object
|
type: object
|
||||||
|
plate_status:
|
||||||
|
type: object
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
total_channels:
|
total_channels:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
|
- status
|
||||||
- channel_status
|
- channel_status
|
||||||
- connection_info
|
- connection_info
|
||||||
- device_summary
|
|
||||||
- status
|
|
||||||
- total_channels
|
- total_channels
|
||||||
|
- plate_status
|
||||||
|
- device_summary
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -49,7 +49,32 @@ opcua_example:
|
|||||||
title: load_config参数
|
title: load_config参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-refresh_node_values:
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-print_cache_stats:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
@@ -67,7 +92,32 @@ opcua_example:
|
|||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: refresh_node_values参数
|
title: print_cache_stats参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-read_node:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
node_name: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
node_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- node_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: read_node参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-set_node_value:
|
auto-set_node_value:
|
||||||
@@ -99,50 +149,10 @@ opcua_example:
|
|||||||
title: set_node_value参数
|
title: set_node_value参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-start_node_refresh:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: start_node_refresh参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-stop_node_refresh:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: stop_node_refresh参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
module: unilabos.device_comms.opcua_client.client:OpcUaClient
|
||||||
status_types: {}
|
status_types:
|
||||||
|
cache_stats: dict
|
||||||
|
node_value: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: null
|
description: null
|
||||||
@@ -151,22 +161,36 @@ opcua_example:
|
|||||||
init_param_schema:
|
init_param_schema:
|
||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
|
cache_timeout:
|
||||||
|
default: 5.0
|
||||||
|
type: number
|
||||||
config_path:
|
config_path:
|
||||||
type: string
|
type: string
|
||||||
|
deck:
|
||||||
|
type: string
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
refresh_interval:
|
subscription_interval:
|
||||||
default: 1.0
|
default: 500
|
||||||
type: number
|
type: integer
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
|
use_subscription:
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- url
|
- url
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
properties: {}
|
properties:
|
||||||
required: []
|
cache_stats:
|
||||||
|
type: object
|
||||||
|
node_value:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- node_value
|
||||||
|
- cache_stats
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ opsky_ATR30007:
|
|||||||
type: string
|
type: string
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: run_once参数
|
title: run_once参数
|
||||||
|
|||||||
@@ -100,41 +100,42 @@ rotavap.one:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_timer:
|
set_timer:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -249,13 +250,9 @@ separator.homemade:
|
|||||||
feedback:
|
feedback:
|
||||||
status: status
|
status: status
|
||||||
goal:
|
goal:
|
||||||
event: event
|
|
||||||
settling_time: settling_time
|
settling_time: settling_time
|
||||||
stir_speed: stir_speed
|
stir_speed: stir_speed
|
||||||
stir_time: stir_time
|
stir_time: stir_time,
|
||||||
time: time
|
|
||||||
time_spec: time_spec
|
|
||||||
vessel: vessel
|
|
||||||
goal_default:
|
goal_default:
|
||||||
event: ''
|
event: ''
|
||||||
settling_time: ''
|
settling_time: ''
|
||||||
@@ -284,42 +281,34 @@ separator.homemade:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
type: ''
|
type: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
message: message
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: Stir_Feedback
|
title: Stir_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
event:
|
event:
|
||||||
type: string
|
type: string
|
||||||
settling_time:
|
settling_time:
|
||||||
type: string
|
type: string
|
||||||
stir_speed:
|
stir_speed:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
stir_time:
|
stir_time:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
time:
|
time:
|
||||||
type: string
|
type: string
|
||||||
time_spec:
|
time_spec:
|
||||||
type: string
|
type: string
|
||||||
vessel:
|
vessel:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
@@ -338,26 +327,16 @@ separator.homemade:
|
|||||||
parent:
|
parent:
|
||||||
type: string
|
type: string
|
||||||
pose:
|
pose:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -367,19 +346,12 @@ separator.homemade:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -409,10 +381,17 @@ separator.homemade:
|
|||||||
- data
|
- data
|
||||||
title: vessel
|
title: vessel
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- vessel
|
||||||
|
- time
|
||||||
|
- event
|
||||||
|
- time_spec
|
||||||
|
- stir_time
|
||||||
|
- stir_speed
|
||||||
|
- settling_time
|
||||||
title: Stir_Goal
|
title: Stir_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
@@ -420,6 +399,10 @@ separator.homemade:
|
|||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- message
|
||||||
|
- return_info
|
||||||
title: Stir_Result
|
title: Stir_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -435,34 +418,36 @@ separator.homemade:
|
|||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -28,6 +28,31 @@ post_process_station:
|
|||||||
title: load_config参数
|
title: load_config参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-print_cache_stats:
|
auto-print_cache_stats:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -79,41 +104,42 @@ post_process_station:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
disconnect:
|
disconnect:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -123,41 +149,42 @@ post_process_station:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
read_node:
|
read_node:
|
||||||
feedback:
|
feedback:
|
||||||
status: status
|
result: result
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: node_name
|
||||||
node_name: node_name
|
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -256,19 +283,17 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: PostProcessTriggerClean_Feedback
|
title: PostProcessTriggerClean_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
acetone_inner_wall_cleaning_count:
|
acetone_inner_wall_cleaning_count:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_inner_wall_cleaning_injection:
|
acetone_inner_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
acetone_inner_wall_cleaning_waste_time:
|
acetone_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -279,8 +304,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_outer_wall_cleaning_injection:
|
acetone_outer_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
acetone_outer_wall_cleaning_wait_time:
|
acetone_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -299,8 +322,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
acetone_stirrer_cleaning_injection:
|
acetone_stirrer_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
acetone_stirrer_cleaning_wait_time:
|
acetone_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -327,8 +348,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_inner_wall_cleaning_injection:
|
nmp_inner_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
nmp_inner_wall_cleaning_waste_time:
|
nmp_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -339,8 +358,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_outer_wall_cleaning_injection:
|
nmp_outer_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
nmp_outer_wall_cleaning_wait_time:
|
nmp_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -359,8 +376,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
nmp_stirrer_cleaning_injection:
|
nmp_stirrer_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
nmp_stirrer_cleaning_wait_time:
|
nmp_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -379,8 +394,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_inner_wall_cleaning_injection:
|
water_inner_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
water_inner_wall_cleaning_waste_time:
|
water_inner_wall_cleaning_waste_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -391,8 +404,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_outer_wall_cleaning_injection:
|
water_outer_wall_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
water_outer_wall_cleaning_wait_time:
|
water_outer_wall_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -411,8 +422,6 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
water_stirrer_cleaning_injection:
|
water_stirrer_cleaning_injection:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
water_stirrer_cleaning_wait_time:
|
water_stirrer_cleaning_wait_time:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -422,13 +431,55 @@ post_process_station:
|
|||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- nmp_outer_wall_cleaning_injection
|
||||||
|
- nmp_outer_wall_cleaning_count
|
||||||
|
- nmp_outer_wall_cleaning_wait_time
|
||||||
|
- nmp_outer_wall_cleaning_waste_time
|
||||||
|
- nmp_inner_wall_cleaning_injection
|
||||||
|
- nmp_inner_wall_cleaning_count
|
||||||
|
- nmp_pump_cleaning_suction_count
|
||||||
|
- nmp_inner_wall_cleaning_waste_time
|
||||||
|
- nmp_stirrer_cleaning_injection
|
||||||
|
- nmp_stirrer_cleaning_count
|
||||||
|
- nmp_stirrer_cleaning_wait_time
|
||||||
|
- nmp_stirrer_cleaning_waste_time
|
||||||
|
- water_outer_wall_cleaning_injection
|
||||||
|
- water_outer_wall_cleaning_count
|
||||||
|
- water_outer_wall_cleaning_wait_time
|
||||||
|
- water_outer_wall_cleaning_waste_time
|
||||||
|
- water_inner_wall_cleaning_injection
|
||||||
|
- water_inner_wall_cleaning_count
|
||||||
|
- water_pump_cleaning_suction_count
|
||||||
|
- water_inner_wall_cleaning_waste_time
|
||||||
|
- water_stirrer_cleaning_injection
|
||||||
|
- water_stirrer_cleaning_count
|
||||||
|
- water_stirrer_cleaning_wait_time
|
||||||
|
- water_stirrer_cleaning_waste_time
|
||||||
|
- acetone_outer_wall_cleaning_injection
|
||||||
|
- acetone_outer_wall_cleaning_count
|
||||||
|
- acetone_outer_wall_cleaning_wait_time
|
||||||
|
- acetone_outer_wall_cleaning_waste_time
|
||||||
|
- acetone_inner_wall_cleaning_injection
|
||||||
|
- acetone_inner_wall_cleaning_count
|
||||||
|
- acetone_pump_cleaning_suction_count
|
||||||
|
- acetone_inner_wall_cleaning_waste_time
|
||||||
|
- acetone_stirrer_cleaning_injection
|
||||||
|
- acetone_stirrer_cleaning_count
|
||||||
|
- acetone_stirrer_cleaning_wait_time
|
||||||
|
- acetone_stirrer_cleaning_waste_time
|
||||||
|
- pipe_blowing_time
|
||||||
|
- injection_pump_forward_empty_suction_count
|
||||||
|
- injection_pump_reverse_empty_suction_count
|
||||||
|
- filtration_liquid_selection
|
||||||
title: PostProcessTriggerClean_Goal
|
title: PostProcessTriggerClean_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: PostProcessTriggerClean_Result
|
title: PostProcessTriggerClean_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -451,11 +502,11 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: PostProcessGrab_Feedback
|
title: PostProcessGrab_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
raw_tank_number:
|
raw_tank_number:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -465,13 +516,17 @@ post_process_station:
|
|||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- reaction_tank_number
|
||||||
|
- raw_tank_number
|
||||||
title: PostProcessGrab_Goal
|
title: PostProcessGrab_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: PostProcessGrab_Result
|
title: PostProcessGrab_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -518,15 +573,13 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: PostProcessTriggerPostPro_Feedback
|
title: PostProcessTriggerPostPro_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
atomization_fast_speed:
|
atomization_fast_speed:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
atomization_pressure_kpa:
|
atomization_pressure_kpa:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -541,12 +594,8 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
first_wash_water_amount:
|
first_wash_water_amount:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
initial_water_amount:
|
initial_water_amount:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
injection_pump_push_speed:
|
injection_pump_push_speed:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -573,20 +622,32 @@ post_process_station:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
second_wash_water_amount:
|
second_wash_water_amount:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
wash_slow_speed:
|
wash_slow_speed:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
|
required:
|
||||||
|
- atomization_fast_speed
|
||||||
|
- wash_slow_speed
|
||||||
|
- injection_pump_suction_speed
|
||||||
|
- injection_pump_push_speed
|
||||||
|
- raw_liquid_suction_count
|
||||||
|
- first_wash_water_amount
|
||||||
|
- second_wash_water_amount
|
||||||
|
- first_powder_mixing_tim
|
||||||
|
- second_powder_mixing_time
|
||||||
|
- first_powder_wash_count
|
||||||
|
- second_powder_wash_count
|
||||||
|
- initial_water_amount
|
||||||
|
- pre_filtration_mixing_time
|
||||||
|
- atomization_pressure_kpa
|
||||||
title: PostProcessTriggerPostPro_Goal
|
title: PostProcessTriggerPostPro_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: PostProcessTriggerPostPro_Result
|
title: PostProcessTriggerPostPro_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -608,26 +669,30 @@ post_process_station:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -637,7 +702,8 @@ post_process_station:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
|
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
|
||||||
status_types:
|
status_types:
|
||||||
cache_stats: Dict[str, Any]
|
cache_stats: dict
|
||||||
|
node_value: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 后处理站
|
description: 后处理站
|
||||||
@@ -652,9 +718,7 @@ post_process_station:
|
|||||||
config_path:
|
config_path:
|
||||||
type: string
|
type: string
|
||||||
deck:
|
deck:
|
||||||
anyOf:
|
type: string
|
||||||
- type: object
|
|
||||||
- type: object
|
|
||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
subscription_interval:
|
subscription_interval:
|
||||||
@@ -674,7 +738,10 @@ post_process_station:
|
|||||||
properties:
|
properties:
|
||||||
cache_stats:
|
cache_stats:
|
||||||
type: object
|
type: object
|
||||||
|
node_value:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- node_value
|
||||||
- cache_stats
|
- cache_stats
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -136,36 +136,36 @@ solenoid_valve:
|
|||||||
set_valve_position:
|
set_valve_position:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
position: position
|
string: position
|
||||||
string: string
|
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -278,25 +278,26 @@ solenoid_valve.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -309,25 +310,26 @@ solenoid_valve.mock:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -420,27 +422,6 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-list:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: list的参数schema
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: list参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-pull_plunger:
|
auto-pull_plunger:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -714,10 +695,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
position:
|
position:
|
||||||
anyOf:
|
type: string
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- type: number
|
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
@@ -742,9 +720,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
velocity:
|
velocity:
|
||||||
anyOf:
|
type: string
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
required:
|
required:
|
||||||
- velocity
|
- velocity
|
||||||
type: object
|
type: object
|
||||||
@@ -804,13 +780,13 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
status_types:
|
status_types:
|
||||||
max_velocity: float
|
max_velocity: float
|
||||||
mode: int
|
mode: int
|
||||||
plunger_position: ''
|
plunger_position: String
|
||||||
position: float
|
position: float
|
||||||
status: str
|
status: str
|
||||||
valve_position: str
|
valve_position: str
|
||||||
velocity_end: ''
|
velocity_end: String
|
||||||
velocity_grade: ''
|
velocity_grade: String
|
||||||
velocity_init: ''
|
velocity_init: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||||
@@ -909,15 +885,15 @@ syringe_pump_with_valve.runze.SY03B-T06:
|
|||||||
velocity_init:
|
velocity_init:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- max_velocity
|
|
||||||
- mode
|
|
||||||
- plunger_position
|
|
||||||
- position
|
|
||||||
- status
|
- status
|
||||||
- valve_position
|
- mode
|
||||||
- velocity_end
|
- max_velocity
|
||||||
- velocity_grade
|
- velocity_grade
|
||||||
- velocity_init
|
- velocity_init
|
||||||
|
- velocity_end
|
||||||
|
- valve_position
|
||||||
|
- position
|
||||||
|
- plunger_position
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
syringe_pump_with_valve.runze.SY03B-T08:
|
syringe_pump_with_valve.runze.SY03B-T08:
|
||||||
@@ -967,27 +943,6 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-list:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: list的参数schema
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: list参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-pull_plunger:
|
auto-pull_plunger:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -1261,10 +1216,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
position:
|
position:
|
||||||
anyOf:
|
type: string
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- type: number
|
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
type: object
|
type: object
|
||||||
@@ -1289,9 +1241,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
velocity:
|
velocity:
|
||||||
anyOf:
|
type: string
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
required:
|
required:
|
||||||
- velocity
|
- velocity
|
||||||
type: object
|
type: object
|
||||||
@@ -1351,13 +1301,13 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
status_types:
|
status_types:
|
||||||
max_velocity: float
|
max_velocity: float
|
||||||
mode: int
|
mode: int
|
||||||
plunger_position: ''
|
plunger_position: String
|
||||||
position: float
|
position: float
|
||||||
status: str
|
status: str
|
||||||
valve_position: str
|
valve_position: str
|
||||||
velocity_end: ''
|
velocity_end: String
|
||||||
velocity_grade: ''
|
velocity_grade: String
|
||||||
velocity_init: ''
|
velocity_init: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
|
||||||
@@ -1472,14 +1422,14 @@ syringe_pump_with_valve.runze.SY03B-T08:
|
|||||||
velocity_init:
|
velocity_init:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- max_velocity
|
|
||||||
- mode
|
|
||||||
- plunger_position
|
|
||||||
- position
|
|
||||||
- status
|
- status
|
||||||
- valve_position
|
- mode
|
||||||
- velocity_end
|
- max_velocity
|
||||||
- velocity_grade
|
- velocity_grade
|
||||||
- velocity_init
|
- velocity_init
|
||||||
|
- velocity_end
|
||||||
|
- valve_position
|
||||||
|
- position
|
||||||
|
- plunger_position
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -4,78 +4,6 @@ reaction_station.bioyond:
|
|||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
add_time_constraint:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
duration: duration
|
|
||||||
end_point: end_point
|
|
||||||
end_step_key: end_step_key
|
|
||||||
start_point: start_point
|
|
||||||
start_step_key: start_step_key
|
|
||||||
goal_default:
|
|
||||||
duration: null
|
|
||||||
end_point: 0
|
|
||||||
end_step_key: ''
|
|
||||||
start_point: 0
|
|
||||||
start_step_key: ''
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 添加时间约束 - 在两个工作流之间添加时间约束
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
duration:
|
|
||||||
description: 时间(秒)
|
|
||||||
type: integer
|
|
||||||
end_point:
|
|
||||||
default: 0
|
|
||||||
description: 终点计时点 (Start=开始前, End=结束后)
|
|
||||||
type: integer
|
|
||||||
end_step_key:
|
|
||||||
default: ''
|
|
||||||
description: 终点步骤Key (可选, 默认为空则自动选择)
|
|
||||||
type: string
|
|
||||||
start_point:
|
|
||||||
default: 0
|
|
||||||
description: 起点计时点 (Start=开始前, End=结束后)
|
|
||||||
type: integer
|
|
||||||
start_step_key:
|
|
||||||
default: ''
|
|
||||||
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- duration
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: add_time_constraint参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-clear_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: clear_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-create_order:
|
auto-create_order:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -95,8 +23,7 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- json_str
|
- json_str
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: create_order参数
|
title: create_order参数
|
||||||
@@ -123,8 +50,7 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- workflow_ids
|
- workflow_ids
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: hard_delete_merged_workflows参数
|
title: hard_delete_merged_workflows参数
|
||||||
@@ -149,8 +75,7 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- json_str
|
- json_str
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: merge_workflow_with_parameters参数
|
title: merge_workflow_with_parameters参数
|
||||||
@@ -175,8 +100,7 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- report_request
|
- report_request
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_temperature_cutoff_report参数
|
title: process_temperature_cutoff_report参数
|
||||||
@@ -201,47 +125,12 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- web_workflow_json
|
- web_workflow_json
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
items:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
type: array
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_web_workflows参数
|
title: process_web_workflows参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-set_reactor_temperature:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
reactor_id: null
|
|
||||||
temperature: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
reactor_id:
|
|
||||||
type: integer
|
|
||||||
temperature:
|
|
||||||
type: number
|
|
||||||
required:
|
|
||||||
- reactor_id
|
|
||||||
- temperature
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: set_reactor_temperature参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-skip_titration_steps:
|
auto-skip_titration_steps:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -261,35 +150,12 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- preintake_id
|
- preintake_id
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: skip_titration_steps参数
|
title: skip_titration_steps参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-sync_workflow_sequence_from_bioyond:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: sync_workflow_sequence_from_bioyond参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-wait_for_multiple_orders_and_get_reports:
|
auto-wait_for_multiple_orders_and_get_reports:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -316,38 +182,10 @@ reaction_station.bioyond:
|
|||||||
type: integer
|
type: integer
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sequence:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
value: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
value:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- value
|
|
||||||
type: object
|
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: workflow_sequence参数
|
title: wait_for_multiple_orders_and_get_reports参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-workflow_step_query:
|
auto-workflow_step_query:
|
||||||
@@ -369,35 +207,12 @@ reaction_station.bioyond:
|
|||||||
required:
|
required:
|
||||||
- workflow_id
|
- workflow_id
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: workflow_step_query参数
|
title: workflow_step_query参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
clean_all_server_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 清空服务端所有非核心工作流 (保留核心流程)
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: clean_all_server_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
drip_back:
|
drip_back:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -408,14 +223,13 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
temperature: 25.0
|
temperature: ''
|
||||||
time: '90'
|
time: ''
|
||||||
titration_type: '1'
|
titration_type: ''
|
||||||
torque_variation: 2
|
torque_variation: ''
|
||||||
volume: null
|
volume: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 滴回去
|
description: 滴回去
|
||||||
@@ -427,27 +241,27 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称(不能为空)
|
description: 物料名称(不能为空)
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '90'
|
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
description: 是否滴定(1=否, 2=是)
|
||||||
description: 是否滴定(NO=否, YES=是)
|
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 2
|
description: 是否观察 (1=否, 2=是)
|
||||||
description: 是否观察 (NO=否, YES=是)
|
type: string
|
||||||
type: integer
|
|
||||||
volume:
|
volume:
|
||||||
description: 分液公式(mL)
|
description: 分液公式(μL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
|
||||||
- volume
|
- volume
|
||||||
|
- assign_material_name
|
||||||
|
- time
|
||||||
|
- torque_variation
|
||||||
|
- titration_type
|
||||||
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -460,7 +274,7 @@ reaction_station.bioyond:
|
|||||||
goal:
|
goal:
|
||||||
batch_reports_result: batch_reports_result
|
batch_reports_result: batch_reports_result
|
||||||
goal_default:
|
goal_default:
|
||||||
batch_reports_result: null
|
batch_reports_result: ''
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: batch_reports_result
|
- data_key: batch_reports_result
|
||||||
@@ -476,8 +290,8 @@ reaction_station.bioyond:
|
|||||||
handler_key: ACTUALS_EXTRACTED
|
handler_key: ACTUALS_EXTRACTED
|
||||||
io_type: sink
|
io_type: sink
|
||||||
label: Extracted Actuals
|
label: Extracted Actuals
|
||||||
placeholder_keys: {}
|
result:
|
||||||
result: {}
|
return_info: return_info
|
||||||
schema:
|
schema:
|
||||||
description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。
|
description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。
|
||||||
properties:
|
properties:
|
||||||
@@ -491,6 +305,13 @@ reaction_station.bioyond:
|
|||||||
- batch_reports_result
|
- batch_reports_result
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: JSON字符串,包含actuals数组,每项含order_code, order_id, actualTargetWeigh,
|
||||||
|
actualVolume
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: extract_actuals_from_batch_reports结果
|
title: extract_actuals_from_batch_reports结果
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -508,14 +329,13 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: BAPP
|
assign_material_name: ''
|
||||||
temperature: 25.0
|
temperature: ''
|
||||||
time: '0'
|
time: ''
|
||||||
titration_type: '1'
|
titration_type: ''
|
||||||
torque_variation: 1
|
torque_variation: ''
|
||||||
volume: '350'
|
volume: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料烧杯
|
description: 液体进料烧杯
|
||||||
@@ -524,30 +344,30 @@ reaction_station.bioyond:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
assign_material_name:
|
assign_material_name:
|
||||||
default: BAPP
|
|
||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '0'
|
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
description: 是否滴定(1=否, 2=是)
|
||||||
description: 是否滴定(NO=否, YES=是)
|
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 1
|
description: 是否观察 (1=否, 2=是)
|
||||||
description: 是否观察 (NO=否, YES=是)
|
|
||||||
type: integer
|
|
||||||
volume:
|
|
||||||
default: '350'
|
|
||||||
description: 分液公式(mL)
|
|
||||||
type: string
|
type: string
|
||||||
required: []
|
volume:
|
||||||
|
description: 分液公式(μL)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- volume
|
||||||
|
- assign_material_name
|
||||||
|
- time
|
||||||
|
- torque_variation
|
||||||
|
- titration_type
|
||||||
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -566,13 +386,13 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
solvents: null
|
solvents: ''
|
||||||
temperature: 25.0
|
temperature: '25.00'
|
||||||
time: '360'
|
time: '360'
|
||||||
titration_type: '1'
|
titration_type: '1'
|
||||||
torque_variation: 2
|
torque_variation: '2'
|
||||||
volume: null
|
volume: ''
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: solvents
|
- data_key: solvents
|
||||||
@@ -581,10 +401,9 @@ reaction_station.bioyond:
|
|||||||
handler_key: solvents
|
handler_key: solvents
|
||||||
io_type: source
|
io_type: source
|
||||||
label: Solvents Data From Calculation Node
|
label: Solvents Data From Calculation Node
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -596,23 +415,23 @@ reaction_station.bioyond:
|
|||||||
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
default: '25.00'
|
||||||
description: 温度设定(°C),默认25.00
|
description: 温度设定(°C),默认25.00
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '360'
|
default: '360'
|
||||||
description: 观察时间(分钟),默认360
|
description: 观察时间(分钟),默认360
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
default: '1'
|
||||||
description: 是否滴定(NO=否, YES=是),默认NO
|
description: 是否滴定(1=否, 2=是),默认1
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 2
|
default: '2'
|
||||||
description: 是否观察 (NO=否, YES=是),默认YES
|
description: 是否观察 (1=否, 2=是),默认2
|
||||||
type: integer
|
type: string
|
||||||
volume:
|
volume:
|
||||||
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
|
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
@@ -636,15 +455,15 @@ reaction_station.bioyond:
|
|||||||
volume_formula: volume_formula
|
volume_formula: volume_formula
|
||||||
x_value: x_value
|
x_value: x_value
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
extracted_actuals: null
|
extracted_actuals: ''
|
||||||
feeding_order_data: null
|
feeding_order_data: ''
|
||||||
temperature: 25.0
|
temperature: '25.00'
|
||||||
time: '90'
|
time: '90'
|
||||||
titration_type: '2'
|
titration_type: '2'
|
||||||
torque_variation: 2
|
torque_variation: '2'
|
||||||
volume_formula: null
|
volume_formula: ''
|
||||||
x_value: null
|
x_value: ''
|
||||||
handles:
|
handles:
|
||||||
input:
|
input:
|
||||||
- data_key: extracted_actuals
|
- data_key: extracted_actuals
|
||||||
@@ -659,7 +478,6 @@ reaction_station.bioyond:
|
|||||||
handler_key: feeding_order
|
handler_key: feeding_order
|
||||||
io_type: source
|
io_type: source
|
||||||
label: Feeding Order Data From Calculation Node
|
label: Feeding Order Data From Calculation Node
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
|
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
|
||||||
@@ -678,23 +496,23 @@ reaction_station.bioyond:
|
|||||||
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
|
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
default: '25.00'
|
||||||
description: 温度设定(°C),默认25.00
|
description: 温度设定(°C),默认25.00
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '90'
|
default: '90'
|
||||||
description: 观察时间(分钟),默认90
|
description: 观察时间(分钟),默认90
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '2'
|
default: '2'
|
||||||
description: 是否滴定(NO=否, YES=是),默认YES
|
description: 是否滴定(1=否, 2=是),默认2
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 2
|
default: '2'
|
||||||
description: 是否观察 (NO=否, YES=是),默认YES
|
description: 是否观察 (1=否, 2=是),默认2
|
||||||
type: integer
|
type: string
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
||||||
type: string
|
type: string
|
||||||
x_value:
|
x_value:
|
||||||
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
||||||
@@ -718,14 +536,13 @@ reaction_station.bioyond:
|
|||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume_formula: volume_formula
|
volume_formula: volume_formula
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
temperature: 25.0
|
temperature: ''
|
||||||
time: '0'
|
time: ''
|
||||||
titration_type: '1'
|
titration_type: ''
|
||||||
torque_variation: 1
|
torque_variation: ''
|
||||||
volume_formula: null
|
volume_formula: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体进料小瓶(非滴定)
|
description: 液体进料小瓶(非滴定)
|
||||||
@@ -737,27 +554,27 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '0'
|
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
description: 是否滴定(1=否, 2=是)
|
||||||
description: 是否滴定(NO=否, YES=是)
|
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 1
|
description: 是否观察 (1=否, 2=是)
|
||||||
description: 是否观察 (NO=否, YES=是)
|
type: string
|
||||||
type: integer
|
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(mL)
|
description: 分液公式(μL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume_formula
|
- volume_formula
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
|
- time
|
||||||
|
- torque_variation
|
||||||
|
- titration_type
|
||||||
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -771,10 +588,9 @@ reaction_station.bioyond:
|
|||||||
task_name: task_name
|
task_name: task_name
|
||||||
workflow_name: workflow_name
|
workflow_name: workflow_name
|
||||||
goal_default:
|
goal_default:
|
||||||
task_name: null
|
task_name: ''
|
||||||
workflow_name: null
|
workflow_name: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 处理并执行工作流
|
description: 处理并执行工作流
|
||||||
@@ -792,8 +608,7 @@ reaction_station.bioyond:
|
|||||||
- workflow_name
|
- workflow_name
|
||||||
- task_name
|
- task_name
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: process_and_execute_workflow参数
|
title: process_and_execute_workflow参数
|
||||||
@@ -806,11 +621,10 @@ reaction_station.bioyond:
|
|||||||
cutoff: cutoff
|
cutoff: cutoff
|
||||||
temperature: temperature
|
temperature: temperature
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
cutoff: '900000'
|
cutoff: ''
|
||||||
temperature: -10.0
|
temperature: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
|
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
|
||||||
@@ -822,14 +636,14 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
cutoff:
|
cutoff:
|
||||||
default: '900000'
|
|
||||||
description: 粘度上限
|
description: 粘度上限
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: -10.0
|
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: number
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- cutoff
|
||||||
|
- temperature
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
@@ -843,7 +657,6 @@ reaction_station.bioyond:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
|
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
|
||||||
@@ -853,35 +666,20 @@ reaction_station.bioyond:
|
|||||||
properties: {}
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
description: 操作结果代码(1表示成功,0表示失败)
|
||||||
|
type: integer
|
||||||
|
return_info:
|
||||||
|
description: 操作结果详细信息
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reactor_taken_out参数
|
title: reactor_taken_out参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
scheduler_start:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: scheduler_start结果
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: scheduler_start参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
solid_feeding_vials:
|
solid_feeding_vials:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -891,13 +689,12 @@ reaction_station.bioyond:
|
|||||||
time: time
|
time: time
|
||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: null
|
assign_material_name: ''
|
||||||
material_id: null
|
material_id: ''
|
||||||
temperature: 25.0
|
temperature: ''
|
||||||
time: '0'
|
time: ''
|
||||||
torque_variation: 1
|
torque_variation: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
|
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
|
||||||
@@ -909,22 +706,23 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称(用于获取试剂瓶位ID)
|
description: 物料名称(用于获取试剂瓶位ID)
|
||||||
type: string
|
type: string
|
||||||
material_id:
|
material_id:
|
||||||
description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟)
|
description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
default: 25.0
|
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
type: number
|
type: string
|
||||||
time:
|
time:
|
||||||
default: '0'
|
|
||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: 1
|
description: 是否观察 (1=否, 2=是)
|
||||||
description: 是否观察 (NO=否, YES=是)
|
type: string
|
||||||
type: integer
|
|
||||||
required:
|
required:
|
||||||
|
- assign_material_name
|
||||||
- material_id
|
- material_id
|
||||||
|
- time
|
||||||
|
- torque_variation
|
||||||
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
@@ -932,10 +730,10 @@ reaction_station.bioyond:
|
|||||||
title: solid_feeding_vials参数
|
title: solid_feeding_vials参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
|
||||||
protocol_type: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
workflow_sequence: str
|
workflow_sequence: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Bioyond反应站
|
description: Bioyond反应站
|
||||||
@@ -955,7 +753,9 @@ reaction_station.bioyond:
|
|||||||
data:
|
data:
|
||||||
properties:
|
properties:
|
||||||
workflow_sequence:
|
workflow_sequence:
|
||||||
type: string
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- workflow_sequence
|
- workflow_sequence
|
||||||
type: object
|
type: object
|
||||||
@@ -991,7 +791,7 @@ reaction_station.reactor:
|
|||||||
title: update_metrics参数
|
title: update_metrics参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
|
||||||
status_types: {}
|
status_types: {}
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
|
|||||||
@@ -37,41 +37,42 @@ agv.SEER:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
send_nav_task:
|
send_nav_task:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -122,6 +122,31 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
title: moveit_task参数
|
title: moveit_task参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: post_init的参数schema
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-resource_manager:
|
auto-resource_manager:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -173,41 +198,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pick_and_place:
|
pick_and_place:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -216,41 +241,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_position:
|
set_position:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -259,41 +284,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_status:
|
set_status:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -430,41 +455,42 @@ robotic_arm.UR:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
move_pos_task:
|
move_pos_task:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -506,8 +532,8 @@ robotic_arm.UR:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- arm_pose
|
- arm_pose
|
||||||
- arm_status
|
|
||||||
- gripper_pose
|
- gripper_pose
|
||||||
|
- arm_status
|
||||||
- gripper_status
|
- gripper_status
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -700,41 +726,41 @@ robotic_arm.elite:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
modbus_task_cmd:
|
modbus_task_cmd:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -744,8 +770,8 @@ robotic_arm.elite:
|
|||||||
type: SendCmd
|
type: SendCmd
|
||||||
module: unilabos.devices.arm.elite_robot:EliteRobot
|
module: unilabos.devices.arm.elite_robot:EliteRobot
|
||||||
status_types:
|
status_types:
|
||||||
actual_joint_positions: ''
|
actual_joint_positions: String
|
||||||
arm_pose: list[float]
|
arm_pose: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Elite robot arm
|
description: Elite robot arm
|
||||||
@@ -771,8 +797,8 @@ robotic_arm.elite:
|
|||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- actual_joint_positions
|
|
||||||
- arm_pose
|
- arm_pose
|
||||||
|
- actual_joint_positions
|
||||||
type: object
|
type: object
|
||||||
model:
|
model:
|
||||||
mesh: elite_robot
|
mesh: elite_robot
|
||||||
|
|||||||
@@ -114,12 +114,11 @@ gripper.misumi_rz:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: object
|
type: string
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: modbus_crc参数
|
title: modbus_crc参数
|
||||||
@@ -399,26 +398,30 @@ gripper.misumi_rz:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -501,82 +504,71 @@ gripper.mock:
|
|||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
push_to:
|
push_to:
|
||||||
feedback:
|
feedback:
|
||||||
effort: effort
|
effort: torque
|
||||||
position: position
|
position: position
|
||||||
reached_goal: reached_goal
|
|
||||||
stalled: stalled
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command.max_effort: torque
|
||||||
position: position
|
command.position: position
|
||||||
torque: torque
|
|
||||||
velocity: velocity
|
|
||||||
goal_default:
|
goal_default:
|
||||||
command:
|
command:
|
||||||
max_effort: 0.0
|
max_effort: 0.0
|
||||||
position: 0.0
|
position: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
effort: effort
|
effort: torque
|
||||||
position: position
|
position: position
|
||||||
reached_goal: reached_goal
|
|
||||||
stalled: stalled
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
effort:
|
effort:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
reached_goal:
|
reached_goal:
|
||||||
type: boolean
|
type: boolean
|
||||||
stalled:
|
stalled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- position
|
||||||
|
- effort
|
||||||
|
- stalled
|
||||||
|
- reached_goal
|
||||||
title: GripperCommand_Feedback
|
title: GripperCommand_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
max_effort:
|
max_effort:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
- max_effort
|
- max_effort
|
||||||
title: command
|
title: command
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: GripperCommand_Goal
|
title: GripperCommand_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
effort:
|
effort:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
position:
|
position:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
reached_goal:
|
reached_goal:
|
||||||
type: boolean
|
type: boolean
|
||||||
stalled:
|
stalled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- position
|
||||||
|
- effort
|
||||||
|
- stalled
|
||||||
|
- reached_goal
|
||||||
title: GripperCommand_Result
|
title: GripperCommand_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -612,8 +604,8 @@ gripper.mock:
|
|||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- position
|
- position
|
||||||
- status
|
|
||||||
- torque
|
|
||||||
- velocity
|
- velocity
|
||||||
|
- torque
|
||||||
|
- status
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -24,27 +24,6 @@ linear_motion.grbl:
|
|||||||
title: initialize参数
|
title: initialize参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-list:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: list的参数schema
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: list参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-set_position:
|
auto-set_position:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -114,39 +93,44 @@ linear_motion.grbl:
|
|||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
move_through_points:
|
move_through_points:
|
||||||
feedback:
|
feedback:
|
||||||
current_pose: current_pose
|
current_pose.pose.position: position
|
||||||
distance_remaining: distance_remaining
|
estimated_time_remaining.sec: time_remaining
|
||||||
estimated_time_remaining: estimated_time_remaining
|
navigation_time.sec: time_spent
|
||||||
navigation_time: navigation_time
|
number_of_poses_remaining: pose_number_remaining
|
||||||
number_of_poses_remaining: number_of_poses_remaining
|
|
||||||
number_of_recoveries: number_of_recoveries
|
|
||||||
goal:
|
goal:
|
||||||
behavior_tree: behavior_tree
|
poses[].pose.position: positions[]
|
||||||
poses: poses
|
|
||||||
positions: positions
|
|
||||||
goal_default:
|
goal_default:
|
||||||
behavior_tree: ''
|
behavior_tree: ''
|
||||||
poses: []
|
poses:
|
||||||
|
- header:
|
||||||
|
frame_id: ''
|
||||||
|
stamp:
|
||||||
|
nanosec: 0
|
||||||
|
sec: 0
|
||||||
|
pose:
|
||||||
|
orientation:
|
||||||
|
w: 1.0
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
position:
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
result: result
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
current_pose:
|
current_pose:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
header:
|
header:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
frame_id:
|
frame_id:
|
||||||
type: string
|
type: string
|
||||||
stamp:
|
stamp:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -167,26 +151,16 @@ linear_motion.grbl:
|
|||||||
title: header
|
title: header
|
||||||
type: object
|
type: object
|
||||||
pose:
|
pose:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -196,19 +170,12 @@ linear_motion.grbl:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -227,11 +194,8 @@ linear_motion.grbl:
|
|||||||
title: current_pose
|
title: current_pose
|
||||||
type: object
|
type: object
|
||||||
distance_remaining:
|
distance_remaining:
|
||||||
maximum: 3.4028235e+38
|
|
||||||
minimum: -3.4028235e+38
|
|
||||||
type: number
|
type: number
|
||||||
estimated_time_remaining:
|
estimated_time_remaining:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -247,7 +211,6 @@ linear_motion.grbl:
|
|||||||
title: estimated_time_remaining
|
title: estimated_time_remaining
|
||||||
type: object
|
type: object
|
||||||
navigation_time:
|
navigation_time:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -270,10 +233,16 @@ linear_motion.grbl:
|
|||||||
maximum: 32767
|
maximum: 32767
|
||||||
minimum: -32768
|
minimum: -32768
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- current_pose
|
||||||
|
- navigation_time
|
||||||
|
- estimated_time_remaining
|
||||||
|
- number_of_recoveries
|
||||||
|
- distance_remaining
|
||||||
|
- number_of_poses_remaining
|
||||||
title: NavigateThroughPoses_Feedback
|
title: NavigateThroughPoses_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
behavior_tree:
|
behavior_tree:
|
||||||
type: string
|
type: string
|
||||||
@@ -287,8 +256,12 @@ linear_motion.grbl:
|
|||||||
stamp:
|
stamp:
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
|
maximum: 4294967295
|
||||||
|
minimum: 0
|
||||||
type: integer
|
type: integer
|
||||||
sec:
|
sec:
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- sec
|
- sec
|
||||||
@@ -341,17 +314,23 @@ linear_motion.grbl:
|
|||||||
required:
|
required:
|
||||||
- header
|
- header
|
||||||
- pose
|
- pose
|
||||||
|
title: poses
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
required:
|
||||||
|
- poses
|
||||||
|
- behavior_tree
|
||||||
title: NavigateThroughPoses_Goal
|
title: NavigateThroughPoses_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
result:
|
result:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: result
|
title: result
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- result
|
||||||
title: NavigateThroughPoses_Result
|
title: NavigateThroughPoses_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -361,15 +340,9 @@ linear_motion.grbl:
|
|||||||
type: NavigateThroughPoses
|
type: NavigateThroughPoses
|
||||||
set_spindle_speed:
|
set_spindle_speed:
|
||||||
feedback:
|
feedback:
|
||||||
error: error
|
position: spindle_speed
|
||||||
header: header
|
|
||||||
position: position
|
|
||||||
velocity: velocity
|
|
||||||
goal:
|
goal:
|
||||||
max_velocity: max_velocity
|
position: spindle_speed
|
||||||
min_duration: min_duration
|
|
||||||
position: position
|
|
||||||
spindle_speed: spindle_speed
|
|
||||||
goal_default:
|
goal_default:
|
||||||
max_velocity: 0.0
|
max_velocity: 0.0
|
||||||
min_duration:
|
min_duration:
|
||||||
@@ -377,25 +350,19 @@ linear_motion.grbl:
|
|||||||
sec: 0
|
sec: 0
|
||||||
position: 0.0
|
position: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
error:
|
error:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
header:
|
header:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
frame_id:
|
frame_id:
|
||||||
type: string
|
type: string
|
||||||
stamp:
|
stamp:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -416,24 +383,21 @@ linear_motion.grbl:
|
|||||||
title: header
|
title: header
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
velocity:
|
velocity:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
|
required:
|
||||||
|
- header
|
||||||
|
- position
|
||||||
|
- velocity
|
||||||
|
- error
|
||||||
title: SingleJointPosition_Feedback
|
title: SingleJointPosition_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
max_velocity:
|
max_velocity:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
min_duration:
|
min_duration:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
nanosec:
|
nanosec:
|
||||||
maximum: 4294967295
|
maximum: 4294967295
|
||||||
@@ -449,13 +413,16 @@ linear_motion.grbl:
|
|||||||
title: min_duration
|
title: min_duration
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
|
required:
|
||||||
|
- position
|
||||||
|
- min_duration
|
||||||
|
- max_velocity
|
||||||
title: SingleJointPosition_Goal
|
title: SingleJointPosition_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: SingleJointPosition_Result
|
title: SingleJointPosition_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -465,7 +432,7 @@ linear_motion.grbl:
|
|||||||
type: SingleJointPosition
|
type: SingleJointPosition
|
||||||
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
module: unilabos.devices.cnc.grbl_sync:GrblCNC
|
||||||
status_types:
|
status_types:
|
||||||
position: Point3D
|
position: unilabos.messages:Point3D
|
||||||
spindle_speed: float
|
spindle_speed: float
|
||||||
status: str
|
status: str
|
||||||
type: python
|
type: python
|
||||||
@@ -504,9 +471,9 @@ linear_motion.grbl:
|
|||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- status
|
||||||
- position
|
- position
|
||||||
- spindle_speed
|
- spindle_speed
|
||||||
- status
|
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
linear_motion.toyo_xyz.sim:
|
linear_motion.toyo_xyz.sim:
|
||||||
@@ -633,6 +600,31 @@ linear_motion.toyo_xyz.sim:
|
|||||||
title: moveit_task参数
|
title: moveit_task参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: post_init的参数schema
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-resource_manager:
|
auto-resource_manager:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -684,41 +676,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
pick_and_place:
|
pick_and_place:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -727,41 +719,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_position:
|
set_position:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -770,41 +762,41 @@ linear_motion.toyo_xyz.sim:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_status:
|
set_status:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -947,26 +939,30 @@ motor.iCL42:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1004,8 +1000,8 @@ motor.iCL42:
|
|||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- is_executing_run
|
|
||||||
- motor_position
|
- motor_position
|
||||||
|
- is_executing_run
|
||||||
- success
|
- success
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -14,24 +14,19 @@ solid_dispenser.laiyu:
|
|||||||
powder_tube_number: 0
|
powder_tube_number: 0
|
||||||
target_tube_position: ''
|
target_tube_position: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
actual_mass_mg: actual_mass_mg
|
actual_mass_mg: actual_mass_mg
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: SolidDispenseAddPowderTube_Feedback
|
title: SolidDispenseAddPowderTube_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
compound_mass:
|
compound_mass:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
powder_tube_number:
|
powder_tube_number:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -39,19 +34,24 @@ solid_dispenser.laiyu:
|
|||||||
type: integer
|
type: integer
|
||||||
target_tube_position:
|
target_tube_position:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- powder_tube_number
|
||||||
|
- target_tube_position
|
||||||
|
- compound_mass
|
||||||
title: SolidDispenseAddPowderTube_Goal
|
title: SolidDispenseAddPowderTube_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
actual_mass_mg:
|
actual_mass_mg:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- actual_mass_mg
|
||||||
|
- success
|
||||||
title: SolidDispenseAddPowderTube_Result
|
title: SolidDispenseAddPowderTube_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -74,12 +74,11 @@ solid_dispenser.laiyu:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: object
|
type: string
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: calculate_crc参数
|
title: calculate_crc参数
|
||||||
@@ -100,12 +99,11 @@ solid_dispenser.laiyu:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: object
|
type: string
|
||||||
required:
|
required:
|
||||||
- command
|
- command
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: send_command参数
|
title: send_command参数
|
||||||
@@ -114,37 +112,36 @@ solid_dispenser.laiyu:
|
|||||||
discharge:
|
discharge:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
float_in: float_in
|
float_input: float_input
|
||||||
goal_default:
|
goal_default:
|
||||||
float_in: 0.0
|
float_in: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: FloatSingleInput_Feedback
|
title: FloatSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
float_in:
|
float_in:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
|
required:
|
||||||
|
- float_in
|
||||||
title: FloatSingleInput_Goal
|
title: FloatSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: FloatSingleInput_Result
|
title: FloatSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -159,31 +156,32 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -202,41 +200,38 @@ solid_dispenser.laiyu:
|
|||||||
y: 0.0
|
y: 0.0
|
||||||
z: 0.0
|
z: 0.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: Point3DSeparateInput_Feedback
|
title: Point3DSeparateInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
|
required:
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
- z
|
||||||
title: Point3DSeparateInput_Goal
|
title: Point3DSeparateInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: Point3DSeparateInput_Result
|
title: Point3DSeparateInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -251,33 +246,34 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- int_input
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -292,33 +288,34 @@ solid_dispenser.laiyu:
|
|||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- int_input
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -331,25 +328,26 @@ solid_dispenser.laiyu:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ chiller:
|
|||||||
- register_address
|
- register_address
|
||||||
- value
|
- value
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: build_modbus_frame参数
|
title: build_modbus_frame参数
|
||||||
@@ -64,8 +63,7 @@ chiller:
|
|||||||
required:
|
required:
|
||||||
- temperature
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: integer
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: convert_temperature_to_modbus_value参数
|
title: convert_temperature_to_modbus_value参数
|
||||||
@@ -86,12 +84,11 @@ chiller:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
type: object
|
type: string
|
||||||
required:
|
required:
|
||||||
- data
|
- data
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: modbus_crc参数
|
title: modbus_crc参数
|
||||||
@@ -119,41 +116,42 @@ chiller:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_temperature:
|
set_temperature:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -268,15 +266,9 @@ heaterstirrer.dalong:
|
|||||||
feedback:
|
feedback:
|
||||||
status: status
|
status: status
|
||||||
goal:
|
goal:
|
||||||
pressure: pressure
|
|
||||||
purpose: purpose
|
purpose: purpose
|
||||||
reflux_solvent: reflux_solvent
|
|
||||||
stir: stir
|
|
||||||
stir_speed: stir_speed
|
|
||||||
temp: temp
|
temp: temp
|
||||||
temp_spec: temp_spec
|
|
||||||
time: time
|
time: time
|
||||||
time_spec: time_spec
|
|
||||||
vessel: vessel
|
vessel: vessel
|
||||||
goal_default:
|
goal_default:
|
||||||
pressure: ''
|
pressure: ''
|
||||||
@@ -309,23 +301,20 @@ heaterstirrer.dalong:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
type: ''
|
type: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
message: message
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: HeatChill_Feedback
|
title: HeatChill_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
pressure:
|
pressure:
|
||||||
type: string
|
type: string
|
||||||
@@ -336,12 +325,8 @@ heaterstirrer.dalong:
|
|||||||
stir:
|
stir:
|
||||||
type: boolean
|
type: boolean
|
||||||
stir_speed:
|
stir_speed:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
temp:
|
temp:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
temp_spec:
|
temp_spec:
|
||||||
type: string
|
type: string
|
||||||
@@ -350,7 +335,6 @@ heaterstirrer.dalong:
|
|||||||
time_spec:
|
time_spec:
|
||||||
type: string
|
type: string
|
||||||
vessel:
|
vessel:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
@@ -369,26 +353,16 @@ heaterstirrer.dalong:
|
|||||||
parent:
|
parent:
|
||||||
type: string
|
type: string
|
||||||
pose:
|
pose:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
orientation:
|
orientation:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
w:
|
w:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -398,19 +372,12 @@ heaterstirrer.dalong:
|
|||||||
title: orientation
|
title: orientation
|
||||||
type: object
|
type: object
|
||||||
position:
|
position:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
x:
|
x:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
y:
|
y:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
z:
|
z:
|
||||||
maximum: 1.7976931348623157e+308
|
|
||||||
minimum: -1.7976931348623157e+308
|
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- x
|
- x
|
||||||
@@ -440,10 +407,20 @@ heaterstirrer.dalong:
|
|||||||
- data
|
- data
|
||||||
title: vessel
|
title: vessel
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- vessel
|
||||||
|
- temp
|
||||||
|
- time
|
||||||
|
- temp_spec
|
||||||
|
- time_spec
|
||||||
|
- pressure
|
||||||
|
- reflux_solvent
|
||||||
|
- stir
|
||||||
|
- stir_speed
|
||||||
|
- purpose
|
||||||
title: HeatChill_Goal
|
title: HeatChill_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
@@ -451,6 +428,10 @@ heaterstirrer.dalong:
|
|||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- message
|
||||||
|
- return_info
|
||||||
title: HeatChill_Result
|
title: HeatChill_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -459,42 +440,42 @@ heaterstirrer.dalong:
|
|||||||
type: object
|
type: object
|
||||||
type: HeatChill
|
type: HeatChill
|
||||||
set_temp_target:
|
set_temp_target:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: temp
|
||||||
temp: temp
|
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -503,42 +484,42 @@ heaterstirrer.dalong:
|
|||||||
type: object
|
type: object
|
||||||
type: SendCmd
|
type: SendCmd
|
||||||
set_temp_warning:
|
set_temp_warning:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: temp
|
||||||
temp: temp
|
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -588,8 +569,8 @@ heaterstirrer.dalong:
|
|||||||
- status
|
- status
|
||||||
- stir_speed
|
- stir_speed
|
||||||
- temp
|
- temp
|
||||||
- temp_target
|
|
||||||
- temp_warning
|
- temp_warning
|
||||||
|
- temp_target
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
tempsensor:
|
tempsensor:
|
||||||
@@ -710,41 +691,42 @@ tempsensor:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_warning:
|
set_warning:
|
||||||
feedback:
|
feedback: {}
|
||||||
status: status
|
|
||||||
goal:
|
goal:
|
||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result:
|
result:
|
||||||
return_info: return_info
|
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
title: SendCmd_Feedback
|
title: SendCmd_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
title: SendCmd_Goal
|
title: SendCmd_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SendCmd_Result
|
title: SendCmd_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,31 @@ xrd_d7mate:
|
|||||||
title: connect参数
|
title: connect参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
auto-start_from_string:
|
auto-start_from_string:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -60,14 +85,11 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
params:
|
params:
|
||||||
anyOf:
|
type: string
|
||||||
- type: string
|
|
||||||
- type: object
|
|
||||||
required:
|
required:
|
||||||
- params
|
- params
|
||||||
type: object
|
type: object
|
||||||
result:
|
result: {}
|
||||||
type: object
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: start_from_string参数
|
title: start_from_string参数
|
||||||
@@ -83,18 +105,21 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -105,38 +130,38 @@ xrd_d7mate:
|
|||||||
get_sample_down:
|
get_sample_down:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
int_input: int_input
|
sample_station: 1
|
||||||
sample_station: sample_station
|
|
||||||
goal_default:
|
goal_default:
|
||||||
int_input: 0
|
int_input: 0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: IntSingleInput_Feedback
|
title: IntSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
int_input:
|
int_input:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
|
required:
|
||||||
|
- int_input
|
||||||
title: IntSingleInput_Goal
|
title: IntSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: IntSingleInput_Result
|
title: IntSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -154,18 +179,21 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -183,18 +211,21 @@ xrd_d7mate:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -207,25 +238,26 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -242,35 +274,42 @@ xrd_d7mate:
|
|||||||
sample_id: ''
|
sample_id: ''
|
||||||
start_theta: 10.0
|
start_theta: 10.0
|
||||||
goal_default:
|
goal_default:
|
||||||
end_theta: null
|
end_theta: 80.0
|
||||||
exp_time: null
|
exp_time: 0.5
|
||||||
increment: null
|
increment: 0.02
|
||||||
sample_id: null
|
sample_id: Sample001
|
||||||
start_theta: null
|
start_theta: 10.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 送样完成后,发送样品信息和采集参数
|
description: 送样完成后,发送样品信息和采集参数
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
title: SampleReadyInput_Feedback
|
title: SampleReadyInput_Feedback
|
||||||
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
end_theta:
|
end_theta:
|
||||||
description: 结束角度(≥5.5°,且必须大于start_theta)
|
description: 结束角度(≥5.5°,且必须大于start_theta)
|
||||||
|
minimum: 5.5
|
||||||
type: number
|
type: number
|
||||||
exp_time:
|
exp_time:
|
||||||
description: 曝光时间(0.1-5.0秒)
|
description: 曝光时间(0.1-5.0秒)
|
||||||
|
maximum: 5.0
|
||||||
|
minimum: 0.1
|
||||||
type: number
|
type: number
|
||||||
increment:
|
increment:
|
||||||
description: 角度增量(≥0.005)
|
description: 角度增量(≥0.005)
|
||||||
|
minimum: 0.005
|
||||||
type: number
|
type: number
|
||||||
sample_id:
|
sample_id:
|
||||||
description: 样品标识符
|
description: 样品标识符
|
||||||
type: string
|
type: string
|
||||||
start_theta:
|
start_theta:
|
||||||
description: 起始角度(≥5°)
|
description: 起始角度(≥5°)
|
||||||
|
minimum: 5.0
|
||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- sample_id
|
- sample_id
|
||||||
@@ -281,11 +320,19 @@ xrd_d7mate:
|
|||||||
title: SampleReadyInput_Goal
|
title: SampleReadyInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: SampleReadyInput_Result
|
title: SampleReadyInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: send_sample_ready参数
|
title: SampleReadyInput
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
set_power_off:
|
set_power_off:
|
||||||
@@ -293,25 +340,26 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -324,25 +372,26 @@ xrd_d7mate:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -356,16 +405,18 @@ xrd_d7mate:
|
|||||||
current: 30.0
|
current: 30.0
|
||||||
voltage: 40.0
|
voltage: 40.0
|
||||||
goal_default:
|
goal_default:
|
||||||
current: null
|
current: 30.0
|
||||||
voltage: null
|
voltage: 40.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 设置高压电源电压和电流
|
description: 设置高压电源电压和电流
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
title: VoltageCurrentInput_Feedback
|
title: VoltageCurrentInput_Feedback
|
||||||
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
current:
|
current:
|
||||||
@@ -380,11 +431,19 @@ xrd_d7mate:
|
|||||||
title: VoltageCurrentInput_Goal
|
title: VoltageCurrentInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: VoltageCurrentInput_Result
|
title: VoltageCurrentInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: set_voltage_current参数
|
title: VoltageCurrentInput
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
start:
|
start:
|
||||||
@@ -394,12 +453,11 @@ xrd_d7mate:
|
|||||||
end_theta: 80.0
|
end_theta: 80.0
|
||||||
exp_time: 0.1
|
exp_time: 0.1
|
||||||
increment: 0.05
|
increment: 0.05
|
||||||
sample_id: ''
|
sample_id: 样品名称
|
||||||
start_theta: 10.0
|
start_theta: 10.0
|
||||||
string: ''
|
string: ''
|
||||||
wait_minutes: 3.0
|
wait_minutes: 3.0
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
|
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
|
||||||
@@ -408,42 +466,54 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
end_theta:
|
end_theta:
|
||||||
default: 80.0
|
|
||||||
description: 结束角度(≥5.5°,且必须大于start_theta)
|
description: 结束角度(≥5.5°,且必须大于start_theta)
|
||||||
type: number
|
minimum: 5.5
|
||||||
|
type: string
|
||||||
exp_time:
|
exp_time:
|
||||||
default: 0.1
|
|
||||||
description: 曝光时间(0.1-5.0秒)
|
description: 曝光时间(0.1-5.0秒)
|
||||||
type: number
|
maximum: 5.0
|
||||||
|
minimum: 0.1
|
||||||
|
type: string
|
||||||
increment:
|
increment:
|
||||||
default: 0.05
|
|
||||||
description: 角度增量(≥0.005)
|
description: 角度增量(≥0.005)
|
||||||
type: number
|
minimum: 0.005
|
||||||
|
type: string
|
||||||
sample_id:
|
sample_id:
|
||||||
default: ''
|
|
||||||
description: 样品标识符
|
description: 样品标识符
|
||||||
type: string
|
type: string
|
||||||
start_theta:
|
start_theta:
|
||||||
default: 10.0
|
|
||||||
description: 起始角度(≥5°)
|
description: 起始角度(≥5°)
|
||||||
type: number
|
minimum: 5.0
|
||||||
|
type: string
|
||||||
string:
|
string:
|
||||||
default: ''
|
|
||||||
description: 字符串格式的参数输入,如果提供则优先解析使用
|
description: 字符串格式的参数输入,如果提供则优先解析使用
|
||||||
type: string
|
type: string
|
||||||
wait_minutes:
|
wait_minutes:
|
||||||
default: 3.0
|
|
||||||
description: 允许上样后等待分钟数
|
description: 允许上样后等待分钟数
|
||||||
|
minimum: 0.0
|
||||||
type: number
|
type: number
|
||||||
required: []
|
required:
|
||||||
|
- sample_id
|
||||||
|
- start_theta
|
||||||
|
- end_theta
|
||||||
|
- increment
|
||||||
|
- exp_time
|
||||||
title: StartWorkflow_Goal
|
title: StartWorkflow_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StartWorkflow_Result
|
title: StartWorkflow_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: start参数
|
title: StartWorkflow
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
start_auto_mode:
|
start_auto_mode:
|
||||||
@@ -451,15 +521,17 @@ xrd_d7mate:
|
|||||||
goal:
|
goal:
|
||||||
status: true
|
status: true
|
||||||
goal_default:
|
goal_default:
|
||||||
status: null
|
status: true
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 启动或停止自动模式
|
description: 启动或停止自动模式
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
title: BoolSingleInput_Feedback
|
title: BoolSingleInput_Feedback
|
||||||
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -470,16 +542,25 @@ xrd_d7mate:
|
|||||||
title: BoolSingleInput_Goal
|
title: BoolSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: BoolSingleInput_Result
|
title: BoolSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: start_auto_mode参数
|
title: BoolSingleInput
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
|
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
|
||||||
status_types:
|
status_types:
|
||||||
current_acquire_data: dict
|
current_acquire_data: dict
|
||||||
|
sample_down: dict
|
||||||
sample_request: dict
|
sample_request: dict
|
||||||
sample_status: dict
|
sample_status: dict
|
||||||
type: python
|
type: python
|
||||||
@@ -505,13 +586,16 @@ xrd_d7mate:
|
|||||||
properties:
|
properties:
|
||||||
current_acquire_data:
|
current_acquire_data:
|
||||||
type: object
|
type: object
|
||||||
|
sample_down:
|
||||||
|
type: object
|
||||||
sample_request:
|
sample_request:
|
||||||
type: object
|
type: object
|
||||||
sample_status:
|
sample_status:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- current_acquire_data
|
|
||||||
- sample_request
|
- sample_request
|
||||||
|
- current_acquire_data
|
||||||
- sample_status
|
- sample_status
|
||||||
|
- sample_down
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
@@ -8,25 +8,26 @@ zhida_gcms:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -76,6 +77,31 @@ zhida_gcms:
|
|||||||
title: connect参数
|
title: connect参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
auto-post_init:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
ros_node: null
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
ros_node:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ros_node
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: post_init参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
get_methods:
|
get_methods:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -86,18 +112,21 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -115,18 +144,21 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -144,18 +176,21 @@ zhida_gcms:
|
|||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -168,25 +203,26 @@ zhida_gcms:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Feedback
|
title: EmptyIn_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: EmptyIn_Goal
|
title: EmptyIn_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
title: EmptyIn_Result
|
title: EmptyIn_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -198,35 +234,35 @@ zhida_gcms:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
string: string
|
string: string
|
||||||
text: text
|
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -237,36 +273,36 @@ zhida_gcms:
|
|||||||
start_with_csv_file:
|
start_with_csv_file:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
csv_file_path: csv_file_path
|
|
||||||
string: string
|
string: string
|
||||||
goal_default:
|
goal_default:
|
||||||
string: ''
|
string: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
result: {}
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
success: success
|
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback:
|
feedback:
|
||||||
additionalProperties: true
|
properties: {}
|
||||||
|
required: []
|
||||||
title: StrSingleInput_Feedback
|
title: StrSingleInput_Feedback
|
||||||
type: object
|
type: object
|
||||||
goal:
|
goal:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
string:
|
string:
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
title: StrSingleInput_Goal
|
title: StrSingleInput_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
properties:
|
||||||
return_info:
|
return_info:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
title: StrSingleInput_Result
|
title: StrSingleInput_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -307,8 +343,8 @@ zhida_gcms:
|
|||||||
version:
|
version:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- methods
|
|
||||||
- status
|
- status
|
||||||
|
- methods
|
||||||
- version
|
- version
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user