Compare commits
4 Commits
154048107d
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5805f94e9a | ||
|
|
3adcc41ce8 | ||
|
|
243922caf4 | ||
|
|
079ec9d1b4 |
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.11
|
||||
version: 0.10.6
|
||||
|
||||
source:
|
||||
path: ../unilabos
|
||||
@@ -31,14 +31,11 @@ requirements:
|
||||
- python ==3.11.11
|
||||
- pip
|
||||
- setuptools
|
||||
- zstd
|
||||
- zstandard
|
||||
run:
|
||||
- conda-forge::python ==3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- zstd
|
||||
- zstandard
|
||||
- ninja
|
||||
- if: unix
|
||||
then:
|
||||
|
||||
340
.github/workflows/conda-pack-build.yml
vendored
@@ -1,340 +0,0 @@
|
||||
name: Build Conda-Pack Environment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: '选择要构建的分支'
|
||||
required: true
|
||||
default: 'dev'
|
||||
type: string
|
||||
platforms:
|
||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||
required: false
|
||||
default: 'win-64'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-conda-pack:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux-64
|
||||
env_file: unilabos-linux-64.yaml
|
||||
script_ext: sh
|
||||
- os: macos-13 # Intel
|
||||
platform: osx-64
|
||||
env_file: unilabos-osx-64.yaml
|
||||
script_ext: sh
|
||||
- os: macos-latest # ARM64
|
||||
platform: osx-arm64
|
||||
env_file: unilabos-osx-arm64.yaml
|
||||
script_ext: sh
|
||||
- os: windows-latest
|
||||
platform: win-64
|
||||
env_file: unilabos-win64.yaml
|
||||
script_ext: bat
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# Windows uses cmd for better conda/mamba compatibility, Unix uses bash
|
||||
shell: ${{ matrix.platform == 'win-64' && 'cmd' || 'bash' }}
|
||||
|
||||
steps:
|
||||
- name: Check if platform should be built
|
||||
id: should_build
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -z "${{ github.event.inputs.platforms }}" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Miniforge (with mamba)
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.11'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install conda-pack, unilabos and dependencies (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Installing unilabos and dependencies to unilab environment...
|
||||
echo Using mamba for faster and more reliable dependency resolution...
|
||||
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)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
|
||||
- name: Get latest ros-humble-unilabos-msgs version (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
id: msgs_version_win
|
||||
run: |
|
||||
echo Checking installed ros-humble-unilabos-msgs version...
|
||||
conda list -n unilab ros-humble-unilabos-msgs
|
||||
for /f "tokens=2" %%i in ('conda list -n unilab ros-humble-unilabos-msgs --json ^| python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')"') do set VERSION=%%i
|
||||
echo installed_version=%VERSION% >> %GITHUB_OUTPUT%
|
||||
echo Installed ros-humble-unilabos-msgs version: %VERSION%
|
||||
|
||||
- name: Get latest ros-humble-unilabos-msgs version (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
id: msgs_version_unix
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking installed ros-humble-unilabos-msgs version..."
|
||||
VERSION=$(conda list -n unilab ros-humble-unilabos-msgs --json | python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')")
|
||||
echo "installed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Installed ros-humble-unilabos-msgs version: $VERSION"
|
||||
|
||||
- name: Check for newer ros-humble-unilabos-msgs (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Checking for available ros-humble-unilabos-msgs versions...
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed
|
||||
echo.
|
||||
echo Updating ros-humble-unilabos-msgs to latest version...
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
|
||||
|
||||
- name: Check for newer ros-humble-unilabos-msgs (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking for available ros-humble-unilabos-msgs versions..."
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed"
|
||||
echo ""
|
||||
echo "Updating ros-humble-unilabos-msgs to latest version..."
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
|
||||
|
||||
- name: Install latest unilabos from source (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Uninstalling existing unilabos...
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo unilabos not installed via pip
|
||||
echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})...
|
||||
mamba run -n unilab pip install .
|
||||
echo Verifying installation...
|
||||
mamba run -n unilab pip show unilabos
|
||||
|
||||
- name: Install latest unilabos from source (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uninstalling existing unilabos..."
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..."
|
||||
mamba run -n unilab pip install .
|
||||
echo "Verifying installation..."
|
||||
mamba run -n unilab pip show unilabos
|
||||
|
||||
- name: Display environment info (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo === Environment Information ===
|
||||
mamba env list
|
||||
echo.
|
||||
echo === Installed Packages ===
|
||||
mamba list -n unilab | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || mamba list -n unilab
|
||||
echo.
|
||||
echo === Python Packages ===
|
||||
mamba run -n unilab pip list | findstr unilabos || mamba run -n unilab pip list
|
||||
|
||||
- name: Display environment info (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=== Environment Information ==="
|
||||
mamba env list
|
||||
echo ""
|
||||
echo "=== Installed Packages ==="
|
||||
mamba list -n unilab | grep -E "(unilabos|ros-humble-unilabos-msgs)" || mamba list -n unilab
|
||||
echo ""
|
||||
echo "=== Python Packages ==="
|
||||
mamba run -n unilab pip list | grep unilabos || mamba run -n unilab pip list
|
||||
|
||||
- name: Verify environment integrity (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Verifying Python version...
|
||||
mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
echo Verifying unilabos import...
|
||||
mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo Warning: Could not import unilabos
|
||||
echo Checking critical packages...
|
||||
mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
echo Running comprehensive verification script...
|
||||
mamba run -n unilab python scripts\verify_installation.py --auto-install || echo Warning: Verification script reported issues
|
||||
echo Environment verification complete!
|
||||
|
||||
- name: Verify environment integrity (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Verifying Python version..."
|
||||
mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
|
||||
echo "Verifying unilabos import..."
|
||||
mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo "Warning: Could not import unilabos"
|
||||
echo "Checking critical packages..."
|
||||
mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
|
||||
echo "Running comprehensive verification script..."
|
||||
mamba run -n unilab python scripts/verify_installation.py --auto-install || echo "Warning: Verification script reported issues"
|
||||
echo "Environment verification complete!"
|
||||
|
||||
- name: Pack conda environment (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Packing unilab environment with conda-pack...
|
||||
mamba activate unilab && conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo Pack file created:
|
||||
dir unilab-env-${{ matrix.platform }}.tar.gz
|
||||
|
||||
- name: Pack conda environment (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Packing unilab environment with conda-pack..."
|
||||
mamba install conda-pack -c conda-forge -y
|
||||
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo "Pack file created:"
|
||||
ls -lh unilab-env-${{ matrix.platform }}.tar.gz
|
||||
|
||||
- name: Prepare Windows distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo ==========================================
|
||||
echo Creating distribution package...
|
||||
echo Platform: ${{ matrix.platform }}
|
||||
echo ==========================================
|
||||
mkdir dist-package 2>nul || cd .
|
||||
|
||||
rem Copy packed environment
|
||||
echo Adding: unilab-env-${{ matrix.platform }}.tar.gz
|
||||
copy unilab-env-${{ matrix.platform }}.tar.gz dist-package\
|
||||
|
||||
rem Copy installation script
|
||||
echo Adding: install_unilab.bat
|
||||
copy scripts\install_unilab.bat dist-package\
|
||||
|
||||
rem Copy verification script
|
||||
echo Adding: verify_installation.py
|
||||
copy scripts\verify_installation.py dist-package\
|
||||
|
||||
rem Copy source code repository (including .git)
|
||||
echo Adding: Uni-Lab-OS source repository
|
||||
robocopy . dist-package\Uni-Lab-OS /E /XD dist-package /NFL /NDL /NJH /NJS /NC /NS || if %ERRORLEVEL% LSS 8 exit /b 0
|
||||
|
||||
rem Create README using Python script
|
||||
echo Creating: README.txt
|
||||
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt
|
||||
|
||||
echo.
|
||||
echo Distribution package contents:
|
||||
dir /b dist-package
|
||||
echo.
|
||||
|
||||
- name: Prepare Unix/Linux distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "Creating distribution package..."
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "=========================================="
|
||||
mkdir -p dist-package
|
||||
|
||||
# Copy packed environment
|
||||
echo "Adding: unilab-env-${{ matrix.platform }}.tar.gz"
|
||||
cp unilab-env-${{ matrix.platform }}.tar.gz dist-package/
|
||||
|
||||
# Copy installation script
|
||||
echo "Adding: install_unilab.sh"
|
||||
cp scripts/install_unilab.sh dist-package/
|
||||
chmod +x dist-package/install_unilab.sh
|
||||
|
||||
# Copy verification script
|
||||
echo "Adding: verify_installation.py"
|
||||
cp scripts/verify_installation.py dist-package/
|
||||
|
||||
# Copy source code repository (including .git)
|
||||
echo "Adding: Uni-Lab-OS source repository"
|
||||
rsync -a --exclude='dist-package' . dist-package/Uni-Lab-OS
|
||||
|
||||
# Create README using Python script
|
||||
echo "Creating: README.txt"
|
||||
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
|
||||
|
||||
echo ""
|
||||
echo "Distribution package contents:"
|
||||
ls -lh dist-package/
|
||||
echo ""
|
||||
|
||||
- name: Upload distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||
path: dist-package/
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Display package info (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo ==========================================
|
||||
echo Build Summary
|
||||
echo ==========================================
|
||||
echo Platform: ${{ matrix.platform }}
|
||||
echo Branch: ${{ github.event.inputs.branch }}
|
||||
echo Python version: 3.11.11
|
||||
echo.
|
||||
echo Distribution package contents:
|
||||
dir dist-package
|
||||
echo.
|
||||
echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||
echo.
|
||||
echo After download, extract the ZIP and run:
|
||||
echo install_unilab.bat
|
||||
echo ==========================================
|
||||
|
||||
- name: Display package info (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "Build Summary"
|
||||
echo "=========================================="
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "Branch: ${{ github.event.inputs.branch }}"
|
||||
echo "Python version: 3.11.11"
|
||||
echo ""
|
||||
echo "Distribution package contents:"
|
||||
ls -lh dist-package/
|
||||
echo ""
|
||||
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
|
||||
echo ""
|
||||
echo "After download:"
|
||||
echo " install_unilab.sh"
|
||||
echo "=========================================="
|
||||
113
.github/workflows/deploy-docs.yml
vendored
@@ -1,113 +0,0 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: '要部署文档的分支'
|
||||
required: false
|
||||
default: 'main'
|
||||
type: string
|
||||
deploy_to_pages:
|
||||
description: '是否部署到 GitHub Pages'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
# 设置 GITHUB_TOKEN 权限以部署到 GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# 只允许一个并发部署,跳过正在进行和最新排队之间的运行
|
||||
# 但是不取消正在进行的运行,因为我们希望允许这些生产部署完成
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build documentation
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Miniforge (with mamba)
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.11'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install unilabos and dependencies
|
||||
run: |
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
|
||||
- name: Install latest unilabos from source
|
||||
run: |
|
||||
echo "Uninstalling existing unilabos..."
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
echo "Installing unilabos from source..."
|
||||
mamba run -n unilab pip install .
|
||||
echo "Verifying installation..."
|
||||
mamba run -n unilab pip show unilabos
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: |
|
||||
echo "Installing documentation build dependencies..."
|
||||
mamba run -n unilab pip install -r docs/requirements.txt
|
||||
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v4
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
|
||||
- name: Build Sphinx documentation
|
||||
run: |
|
||||
cd docs
|
||||
# Clean previous builds
|
||||
rm -rf _build
|
||||
# Build HTML documentation in conda environment
|
||||
mamba run -n unilab python -m sphinx -b html . _build/html -v
|
||||
|
||||
- name: Check build results
|
||||
run: |
|
||||
echo "Documentation build completed, checking output directory:"
|
||||
ls -la docs/_build/html/
|
||||
echo "Checking for index.html:"
|
||||
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
with:
|
||||
path: docs/_build/html
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
1
.gitignore
vendored
@@ -2,7 +2,6 @@ configs/
|
||||
temp/
|
||||
output/
|
||||
unilabos_data/
|
||||
pyrightconfig.json
|
||||
## Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
18
CONTRIBUTORS
@@ -1,18 +0,0 @@
|
||||
56 Xuwznln <18435084+Xuwznln@users.noreply.github.com>
|
||||
10 wznln <18435084+Xuwznln@users.noreply.github.com>
|
||||
6 Junhan Chang <changjh@dp.tech>
|
||||
5 ZiWei <131428629+ZiWei09@users.noreply.github.com>
|
||||
2 Guangxin Zhang <guangxin.zhang.bio@gmail.com>
|
||||
2 Junhan Chang <changjh@pku.edu.cn>
|
||||
2 WenzheG <wenzheguo32@gmail.com>
|
||||
1 Harry Liu <113173203+ALITTLELZ@users.noreply.github.com>
|
||||
1 Harvey Que <103566763+Mile-Away@users.noreply.github.com>
|
||||
1 Junhan Chang <1700011741@pku.edu.cn>
|
||||
1 Xianwei Qi <qxw@stu.pku.edu.cn>
|
||||
1 hh.(SII) <103566763+Mile-Away@users.noreply.github.com>
|
||||
1 lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
|
||||
1 q434343 <73513873+q434343@users.noreply.github.com>
|
||||
1 tt <166512503+tt11142023@users.noreply.github.com>
|
||||
1 xyc <49015816+xiaoyu10031@users.noreply.github.com>
|
||||
1 王俊杰 <1800011822@pku.edu.cn>
|
||||
1 王俊杰 <43375851+wjjxxx@users.noreply.github.com>
|
||||
@@ -1,4 +1,3 @@
|
||||
recursive-include unilabos/test *
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/web/static *
|
||||
recursive-include unilabos/app/web/templates *
|
||||
|
||||
@@ -31,7 +31,7 @@ Join the [Intelligent Organic Chemistry Synthesis Competition](https://bohrium.d
|
||||
|
||||
Detailed documentation can be found at:
|
||||
|
||||
- [Online Documentation](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
|
||||
- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -55,7 +55,7 @@ pip install .
|
||||
|
||||
3. Start Uni-Lab System:
|
||||
|
||||
Please refer to [Documentation - Boot Examples](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
|
||||
Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
|
||||
## Message Format
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控
|
||||
|
||||
详细文档可在以下位置找到:
|
||||
|
||||
- [在线文档](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
|
||||
- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/)
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -57,7 +57,7 @@ pip install .
|
||||
|
||||
3. 启动 Uni-Lab 系统:
|
||||
|
||||
请见[文档-启动样例](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
|
||||
请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html)
|
||||
|
||||
## 消息格式
|
||||
|
||||
|
||||
@@ -1,726 +0,0 @@
|
||||
# Uni-Lab 配置指南
|
||||
|
||||
本文档详细介绍 Uni-Lab 配置文件的结构、配置项、命令行覆盖和环境变量的使用方法。
|
||||
|
||||
## 配置文件概述
|
||||
|
||||
Uni-Lab 使用 Python 格式的配置文件(`.py`),默认为 `unilabos_data/local_config.py`。配置文件采用类属性的方式定义各种配置项,比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
|
||||
## 获取实验室密钥
|
||||
|
||||
在配置文件或启动命令中,您需要提供实验室的访问密钥(ak)和私钥(sk)。
|
||||
|
||||
**获取方式:**
|
||||
|
||||
进入 [Uni-Lab 实验室](https://uni-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk:
|
||||
|
||||

|
||||
|
||||
## 配置文件格式
|
||||
|
||||
### 默认配置示例
|
||||
|
||||
首次使用时,系统会自动创建一个基础配置文件 `local_config.py`:
|
||||
|
||||
```python
|
||||
# unilabos的配置文件
|
||||
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室网页给您提供的ak代码
|
||||
sk = "" # 实验室网页给您提供的sk代码
|
||||
|
||||
|
||||
# WebSocket配置,一般无需调整
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
```
|
||||
|
||||
### 完整配置示例
|
||||
|
||||
您可以根据需要添加更多配置选项:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""Uni-Lab 配置文件"""
|
||||
|
||||
# 基础配置
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室访问密钥
|
||||
sk = "" # 实验室私钥
|
||||
working_dir = "" # 工作目录(通常自动设置)
|
||||
config_path = "" # 配置文件路径(自动设置)
|
||||
is_host_mode = True # 是否为主站模式
|
||||
slave_no_host = False # 从站模式下是否跳过等待主机服务
|
||||
upload_registry = False # 是否上传注册表
|
||||
machine_name = "undefined" # 机器名称(自动获取)
|
||||
vis_2d_enable = False # 是否启用2D可视化
|
||||
enable_resource_load = True # 是否启用资源加载
|
||||
communication_protocol = "websocket" # 通信协议
|
||||
log_level = "DEBUG" # 日志级别:TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
# WebSocket配置
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
|
||||
# ROS配置
|
||||
class ROSConfig:
|
||||
modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
"control_msgs.msg",
|
||||
"control_msgs.action",
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
] # 需要加载的ROS模块
|
||||
```
|
||||
|
||||
## 配置优先级
|
||||
|
||||
配置项的生效优先级从高到低为:
|
||||
|
||||
1. **命令行参数**:最高优先级
|
||||
2. **环境变量**:中等优先级
|
||||
3. **配置文件**:基础优先级
|
||||
|
||||
这意味着命令行参数会覆盖环境变量和配置文件,环境变量会覆盖配置文件。
|
||||
|
||||
## 推荐配置方式
|
||||
|
||||
根据参数特性,不同配置项有不同的推荐配置方式:
|
||||
|
||||
### 建议通过命令行指定的参数(不需要写入配置文件)
|
||||
|
||||
以下参数推荐通过命令行或环境变量指定,**一般不需要在配置文件中配置**:
|
||||
|
||||
| 参数 | 命令行参数 | 原因 |
|
||||
| ----------------- | ------------------- | ------------------------------------ |
|
||||
| `ak` / `sk` | `--ak` / `--sk` | **安全考虑**:避免敏感信息泄露 |
|
||||
| `working_dir` | `--working_dir` | **灵活性**:不同环境可能使用不同目录 |
|
||||
| `is_host_mode` | `--is_slave` | **运行模式**:由启动场景决定,不固定 |
|
||||
| `slave_no_host` | `--slave_no_host` | **运行模式**:从站特殊配置,按需使用 |
|
||||
| `upload_registry` | `--upload_registry` | **临时操作**:仅首次启动或更新时需要 |
|
||||
| `vis_2d_enable` | `--2d_vis` | **调试功能**:按需临时启用 |
|
||||
| `remote_addr` | `--addr` | **环境切换**:测试/生产环境快速切换 |
|
||||
|
||||
**推荐用法示例:**
|
||||
|
||||
```bash
|
||||
# 标准启动命令(所有必要参数通过命令行指定)
|
||||
unilab --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 测试环境
|
||||
unilab --addr test --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 从站模式
|
||||
unilab --is_slave --ak your_ak --sk your_sk
|
||||
|
||||
# 首次启动上传注册表
|
||||
unilab --ak your_ak --sk your_sk -g graph.json --upload_registry
|
||||
```
|
||||
|
||||
### 适合在配置文件中配置的参数
|
||||
|
||||
以下参数适合在配置文件中配置,通常不会频繁更改:
|
||||
|
||||
| 参数 | 配置类 | 说明 |
|
||||
| ------------------------ | ----------- | ---------------------- |
|
||||
| `log_level` | BasicConfig | 日志级别配置 |
|
||||
| `reconnect_interval` | WSConfig | WebSocket 重连间隔 |
|
||||
| `max_reconnect_attempts` | WSConfig | WebSocket 最大重连次数 |
|
||||
| `ping_interval` | WSConfig | WebSocket 心跳间隔 |
|
||||
| `modules` | ROSConfig | ROS 模块列表 |
|
||||
|
||||
**配置文件示例(推荐最小配置):**
|
||||
|
||||
```python
|
||||
# unilabos的配置文件
|
||||
|
||||
class BasicConfig:
|
||||
log_level = "INFO" # 生产环境建议 INFO,调试时用 DEBUG
|
||||
|
||||
# WebSocket配置,一般保持默认即可
|
||||
class WSConfig:
|
||||
reconnect_interval = 5
|
||||
max_reconnect_attempts = 999
|
||||
ping_interval = 30
|
||||
```
|
||||
|
||||
**注意:** `ak` 和 `sk` 不建议写在配置文件中,始终通过命令行参数或环境变量传递。
|
||||
|
||||
## 命令行参数覆盖配置
|
||||
|
||||
Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。
|
||||
|
||||
### 支持命令行覆盖的配置项
|
||||
|
||||
| 配置类 | 配置字段 | 命令行参数 | 说明 |
|
||||
| ------------- | ----------------- | ------------------- | -------------------------------- |
|
||||
| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 |
|
||||
| `BasicConfig` | `sk` | `--sk` | 实验室私钥 |
|
||||
| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 |
|
||||
| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) |
|
||||
| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 |
|
||||
| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 |
|
||||
| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 |
|
||||
| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 |
|
||||
|
||||
### 特殊命令行参数
|
||||
|
||||
除了直接覆盖配置项的参数外,还有一些特殊的命令行参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| `--config` | 指定配置文件路径 |
|
||||
| `--port` | Web 服务端口(不影响配置文件) |
|
||||
| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) |
|
||||
| `--visual` | 可视化工具选择(不影响配置文件) |
|
||||
| `--skip_env_check` | 跳过环境检查(不影响配置文件) |
|
||||
|
||||
### 命令行覆盖使用示例
|
||||
|
||||
```bash
|
||||
# 通过命令行覆盖认证信息
|
||||
unilab --ak "new_access_key" --sk "new_secret_key" -g graph.json
|
||||
|
||||
# 覆盖服务器地址
|
||||
unilab --ak ak --sk sk --addr "https://custom.server.com/api/v1" -g graph.json
|
||||
|
||||
# 启用从站模式并跳过等待主机
|
||||
unilab --is_slave --slave_no_host --ak ak --sk sk
|
||||
|
||||
# 启用上传注册表和2D可视化
|
||||
unilab --upload_registry --2d_vis --ak ak --sk sk -g graph.json
|
||||
|
||||
# 组合使用多个覆盖参数
|
||||
unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g graph.json
|
||||
```
|
||||
|
||||
### 预设环境地址
|
||||
|
||||
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL:
|
||||
|
||||
- `test` → `https://uni-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- `local` → `http://127.0.0.1:48197/api/v1`
|
||||
- 其他值 → 直接使用作为完整 URL
|
||||
|
||||
## 配置选项详解
|
||||
|
||||
### 1. BasicConfig - 基础配置
|
||||
|
||||
基础配置包含了系统运行的核心参数:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------------- | ------------------------------------------ |
|
||||
| `ak` | str | `""` | 实验室访问密钥(必需) |
|
||||
| `sk` | str | `""` | 实验室私钥(必需) |
|
||||
| `working_dir` | str | `""` | 工作目录,通常自动设置 |
|
||||
| `config_path` | str | `""` | 配置文件路径,自动设置 |
|
||||
| `is_host_mode` | bool | `True` | 是否为主站模式 |
|
||||
| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 |
|
||||
| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 |
|
||||
| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) |
|
||||
| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 |
|
||||
| `enable_resource_load` | bool | `True` | 是否启用资源加载 |
|
||||
| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket |
|
||||
| `log_level` | str | `"DEBUG"` | 日志级别 |
|
||||
|
||||
#### 日志级别选项
|
||||
|
||||
- `TRACE` - 追踪级别(最详细)
|
||||
- `DEBUG` - 调试级别(默认)
|
||||
- `INFO` - 信息级别
|
||||
- `WARNING` - 警告级别
|
||||
- `ERROR` - 错误级别
|
||||
- `CRITICAL` - 严重错误级别(最简略)
|
||||
|
||||
#### 认证配置(ak / sk)
|
||||
|
||||
`ak` 和 `sk` 是必需的认证参数:
|
||||
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
|
||||
2. **配置方式**:
|
||||
- **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级,推荐)
|
||||
- **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK`
|
||||
- **配置文件**:在 `BasicConfig` 类中设置(不推荐,安全风险)
|
||||
3. **安全注意**:请妥善保管您的密钥信息,不要提交到版本控制
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
- **开发环境**:使用命令行参数或环境变量
|
||||
- **生产环境**:使用环境变量
|
||||
- **临时测试**:使用命令行参数
|
||||
|
||||
### 2. WSConfig - WebSocket 配置
|
||||
|
||||
WebSocket 是 Uni-Lab 的主要通信方式:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------ | ------------------ |
|
||||
| `reconnect_interval` | int | `5` | 断线重连间隔(秒) |
|
||||
| `max_reconnect_attempts` | int | `999` | 最大重连次数 |
|
||||
| `ping_interval` | int | `30` | 心跳检测间隔(秒) |
|
||||
|
||||
### 3. HTTPConfig - HTTP 配置
|
||||
|
||||
HTTP 客户端配置用于与云端服务通信:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | -------------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"https://uni-lab.bohrium.com/api/v1"` | 远程服务地址 |
|
||||
|
||||
**预设环境地址**:
|
||||
|
||||
- 生产环境:`https://uni-lab.bohrium.com/api/v1`(默认)
|
||||
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### 4. ROSConfig - ROS 配置
|
||||
|
||||
配置 ROS 消息转换器需要加载的模块:
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| --------- | ---- | ---------- | ------------ |
|
||||
| `modules` | list | 见下方示例 | ROS 模块列表 |
|
||||
|
||||
**默认模块列表:**
|
||||
|
||||
```python
|
||||
class ROSConfig:
|
||||
modules = [
|
||||
"std_msgs.msg", # 标准消息类型
|
||||
"geometry_msgs.msg", # 几何消息类型
|
||||
"control_msgs.msg", # 控制消息类型
|
||||
"control_msgs.action", # 控制动作类型
|
||||
"nav2_msgs.action", # 导航动作类型
|
||||
"unilabos_msgs.msg", # UniLab 自定义消息类型
|
||||
"unilabos_msgs.action", # UniLab 自定义动作类型
|
||||
]
|
||||
```
|
||||
|
||||
您可以根据实际使用的设备和功能添加其他 ROS 模块。
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
Uni-Lab 支持通过环境变量覆盖配置文件中的设置。
|
||||
|
||||
### 环境变量命名规则
|
||||
|
||||
```
|
||||
UNILABOS_<配置类名>_<配置项名>
|
||||
```
|
||||
|
||||
**注意:**
|
||||
|
||||
- 环境变量名不区分大小写
|
||||
- 配置类名和配置项名都会转换为大写进行匹配
|
||||
|
||||
### 设置环境变量
|
||||
|
||||
#### Linux / macOS
|
||||
|
||||
```bash
|
||||
# 临时设置(当前终端)
|
||||
export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
|
||||
# 永久设置(添加到 ~/.bashrc 或 ~/.zshrc)
|
||||
echo 'export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
#### Windows (cmd)
|
||||
|
||||
```cmd
|
||||
# 临时设置
|
||||
set UNILABOS_BASICCONFIG_LOG_LEVEL=INFO
|
||||
set UNILABOS_BASICCONFIG_AK=your_access_key
|
||||
|
||||
# 永久设置(系统环境变量)
|
||||
setx UNILABOS_BASICCONFIG_LOG_LEVEL INFO
|
||||
```
|
||||
|
||||
#### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
# 临时设置
|
||||
$env:UNILABOS_BASICCONFIG_LOG_LEVEL="INFO"
|
||||
$env:UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
|
||||
# 永久设置
|
||||
[Environment]::SetEnvironmentVariable("UNILABOS_BASICCONFIG_LOG_LEVEL", "INFO", "User")
|
||||
```
|
||||
|
||||
### 环境变量类型转换
|
||||
|
||||
系统会根据配置项的原始类型自动转换环境变量值:
|
||||
|
||||
| 原始类型 | 转换规则 |
|
||||
| -------- | --------------------------------------- |
|
||||
| `bool` | "true", "1", "yes" → True;其他 → False |
|
||||
| `int` | 转换为整数 |
|
||||
| `float` | 转换为浮点数 |
|
||||
| `str` | 直接使用字符串值 |
|
||||
|
||||
**示例:**
|
||||
|
||||
```bash
|
||||
# 布尔值
|
||||
export UNILABOS_BASICCONFIG_IS_HOST_MODE=true # 将设置为 True
|
||||
export UNILABOS_BASICCONFIG_IS_HOST_MODE=false # 将设置为 False
|
||||
|
||||
# 整数
|
||||
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL=10 # 将设置为 10
|
||||
|
||||
# 字符串
|
||||
export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO # 将设置为 "INFO"
|
||||
```
|
||||
|
||||
### 环境变量示例
|
||||
|
||||
```bash
|
||||
# 设置基础配置
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
export UNILABOS_BASICCONFIG_IS_HOST_MODE="true"
|
||||
|
||||
# 设置WebSocket配置
|
||||
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
|
||||
|
||||
# 设置HTTP配置
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
## 配置文件使用方法
|
||||
|
||||
### 1. 使用默认配置文件(推荐)
|
||||
|
||||
系统会自动查找并加载配置文件:
|
||||
|
||||
```bash
|
||||
# 直接启动,使用默认的 unilabos_data/local_config.py
|
||||
unilab --ak your_ak --sk your_sk -g graph.json
|
||||
```
|
||||
|
||||
查找顺序:
|
||||
|
||||
1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径
|
||||
2. 工作目录下的 `local_config.py`
|
||||
3. 首次使用时会引导创建配置文件
|
||||
|
||||
### 2. 指定配置文件启动
|
||||
|
||||
```bash
|
||||
# 使用指定配置文件启动
|
||||
unilab --config /path/to/your/config.py --ak ak --sk sk -g graph.json
|
||||
```
|
||||
|
||||
### 3. 配置文件验证
|
||||
|
||||
系统启动时会自动验证配置文件:
|
||||
|
||||
- **语法检查**:确保 Python 语法正确
|
||||
- **类型检查**:验证配置项类型是否匹配
|
||||
- **加载确认**:控制台输出加载成功信息
|
||||
|
||||
## 常用配置场景
|
||||
|
||||
### 场景 1:调整日志级别
|
||||
|
||||
**配置文件方式:**
|
||||
|
||||
```python
|
||||
class BasicConfig:
|
||||
log_level = "INFO" # 生产环境建议使用 INFO 或 WARNING
|
||||
```
|
||||
|
||||
**环境变量方式:**
|
||||
|
||||
```bash
|
||||
export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO
|
||||
unilab --ak ak --sk sk -g graph.json
|
||||
```
|
||||
|
||||
**命令行方式**(需要配置文件已包含):
|
||||
|
||||
```bash
|
||||
# 配置文件无直接命令行参数,需通过环境变量
|
||||
UNILABOS_BASICCONFIG_LOG_LEVEL=INFO unilab --ak ak --sk sk -g graph.json
|
||||
```
|
||||
|
||||
### 场景 2:配置 WebSocket 重连
|
||||
|
||||
**配置文件方式:**
|
||||
|
||||
```python
|
||||
class WSConfig:
|
||||
reconnect_interval = 10 # 增加重连间隔到 10 秒
|
||||
max_reconnect_attempts = 100 # 减少最大重连次数到 100 次
|
||||
```
|
||||
|
||||
**环境变量方式:**
|
||||
|
||||
```bash
|
||||
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL=10
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS=100
|
||||
```
|
||||
|
||||
### 场景 3:切换服务器环境
|
||||
|
||||
**配置文件方式:**
|
||||
|
||||
```python
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
**环境变量方式:**
|
||||
|
||||
```bash
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://uni-lab.test.bohrium.com/api/v1
|
||||
```
|
||||
|
||||
**命令行方式(推荐):**
|
||||
|
||||
```bash
|
||||
unilab --addr test --ak your_ak --sk your_sk -g graph.json
|
||||
```
|
||||
|
||||
### 场景 4:从站模式配置
|
||||
|
||||
**配置文件方式:**
|
||||
|
||||
```python
|
||||
class BasicConfig:
|
||||
is_host_mode = False # 从站模式
|
||||
slave_no_host = True # 不等待主机服务
|
||||
```
|
||||
|
||||
**命令行方式(推荐):**
|
||||
|
||||
```bash
|
||||
unilab --is_slave --slave_no_host --ak your_ak --sk your_sk
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 安全配置
|
||||
|
||||
**不要在配置文件中存储敏感信息**
|
||||
|
||||
- ❌ **不推荐**:在配置文件中明文存储 ak/sk
|
||||
- ✅ **推荐**:使用环境变量或命令行参数
|
||||
|
||||
```bash
|
||||
# 生产环境 - 使用环境变量(推荐)
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
unilab -g graph.json
|
||||
|
||||
# 或使用命令行参数
|
||||
unilab --ak "your_access_key" --sk "your_secret_key" -g graph.json
|
||||
```
|
||||
|
||||
**其他安全建议:**
|
||||
|
||||
- 不要将包含密钥的配置文件提交到版本控制系统
|
||||
- 限制配置文件权限:`chmod 600 local_config.py`
|
||||
- 定期更换访问密钥
|
||||
- 使用 `.gitignore` 排除配置文件
|
||||
|
||||
### 2. 多环境配置
|
||||
|
||||
为不同环境创建不同的配置文件:
|
||||
|
||||
```
|
||||
configs/
|
||||
├── base_config.py # 基础配置(非敏感)
|
||||
├── dev_config.py # 开发环境
|
||||
├── test_config.py # 测试环境
|
||||
├── prod_config.py # 生产环境
|
||||
└── example_config.py # 示例配置
|
||||
```
|
||||
|
||||
**环境切换示例**:
|
||||
|
||||
```bash
|
||||
# 本地开发环境
|
||||
unilab --config configs/dev_config.py --addr local --ak ak --sk sk -g graph.json
|
||||
|
||||
# 测试环境
|
||||
unilab --config configs/test_config.py --addr test --ak ak --sk sk --upload_registry -g graph.json
|
||||
|
||||
# 生产环境
|
||||
unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK" -g graph.json
|
||||
```
|
||||
|
||||
### 3. 配置管理
|
||||
|
||||
**配置文件最佳实践:**
|
||||
|
||||
- 保持配置文件简洁,只包含需要修改的配置项
|
||||
- 为配置项添加注释说明其作用
|
||||
- 定期检查和更新配置文件
|
||||
- 版本控制仅保存示例配置,不包含实际密钥
|
||||
|
||||
**命令行参数优先使用场景:**
|
||||
|
||||
- 临时测试不同配置
|
||||
- CI/CD 流水线中的动态配置
|
||||
- 不同环境间快速切换
|
||||
- 敏感信息的安全传递
|
||||
|
||||
### 4. 灵活配置策略
|
||||
|
||||
**基础配置文件 + 命令行覆盖**的推荐方式:
|
||||
|
||||
```python
|
||||
# base_config.py - 基础配置(非敏感信息)
|
||||
class BasicConfig:
|
||||
# 非敏感配置写在文件中
|
||||
is_host_mode = True
|
||||
upload_registry = False
|
||||
vis_2d_enable = False
|
||||
log_level = "INFO"
|
||||
|
||||
class WSConfig:
|
||||
reconnect_interval = 5
|
||||
max_reconnect_attempts = 999
|
||||
ping_interval = 30
|
||||
```
|
||||
|
||||
```bash
|
||||
# 启动时通过命令行覆盖关键参数
|
||||
unilab --config base_config.py \
|
||||
--ak "$AK" \
|
||||
--sk "$SK" \
|
||||
--addr "test" \
|
||||
--upload_registry \
|
||||
--2d_vis \
|
||||
-g graph.json
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 配置文件加载失败
|
||||
|
||||
**错误信息**:`[ENV] 配置文件 xxx 不存在`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认配置文件路径正确
|
||||
- 检查文件权限是否可读
|
||||
- 确保配置文件是 `.py` 格式
|
||||
- 使用绝对路径或相对于当前目录的路径
|
||||
|
||||
### 2. 语法错误
|
||||
|
||||
**错误信息**:`[ENV] 加载配置文件 xxx 失败`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 检查 Python 语法是否正确
|
||||
- 确认类名和字段名拼写正确
|
||||
- 验证缩进是否正确(使用空格而非制表符)
|
||||
- 确保字符串使用引号包裹
|
||||
|
||||
### 3. 认证失败
|
||||
|
||||
**错误信息**:`后续运行必须拥有一个实验室`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认 `ak` 和 `sk` 已正确配置
|
||||
- 检查密钥是否有效(未过期或撤销)
|
||||
- 确认网络连接正常
|
||||
- 验证密钥是否来自正确的实验室
|
||||
|
||||
### 4. 环境变量不生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认环境变量名格式正确(`UNILABOS_<类名>_<字段名>`)
|
||||
- 检查环境变量是否已正确设置(`echo $VARIABLE_NAME`)
|
||||
- 重启终端或重新加载环境变量
|
||||
- 确认环境变量值的类型正确
|
||||
|
||||
### 5. 命令行参数不生效
|
||||
|
||||
**错误现象**:设置了命令行参数但配置没有生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`)
|
||||
- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值)
|
||||
- 确认参数位置正确(所有参数都应在 `unilab` 之后)
|
||||
- 查看启动日志确认参数是否被正确解析
|
||||
- 检查是否有配置文件或环境变量与之冲突
|
||||
|
||||
### 6. 配置优先级混淆
|
||||
|
||||
**错误现象**:不确定哪个配置生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 记住优先级:**命令行参数 > 环境变量 > 配置文件**
|
||||
- 使用 `--ak` 和 `--sk` 参数时会看到提示信息:"传入了 ak 参数,优先采用传入参数!"
|
||||
- 检查启动日志中的配置加载信息
|
||||
- 临时移除低优先级配置来测试高优先级配置是否生效
|
||||
- 使用 `printenv | grep UNILABOS` 查看所有相关环境变量
|
||||
|
||||
## 配置验证
|
||||
|
||||
### 检查配置是否生效
|
||||
|
||||
启动 Uni-Lab 时,控制台会输出配置加载信息:
|
||||
|
||||
```
|
||||
[ENV] 配置文件 /path/to/config.py 加载成功
|
||||
[ENV] 设置 BasicConfig.log_level = INFO
|
||||
传入了ak参数,优先采用传入参数!
|
||||
传入了sk参数,优先采用传入参数!
|
||||
```
|
||||
|
||||
### 常见配置错误
|
||||
|
||||
1. **配置文件格式错误**
|
||||
|
||||
```
|
||||
[ENV] 加载配置文件 /path/to/config.py 失败
|
||||
```
|
||||
|
||||
**解决方案**:检查 Python 语法,确保配置类定义正确
|
||||
|
||||
2. **环境变量格式错误**
|
||||
|
||||
```
|
||||
[ENV] 环境变量格式不正确:UNILABOS_INVALID_VAR
|
||||
```
|
||||
|
||||
**解决方案**:确保环境变量遵循 `UNILABOS_<类名>_<字段名>` 格式
|
||||
|
||||
3. **类或字段不存在**
|
||||
```
|
||||
[ENV] 未找到类:UNKNOWNCONFIG
|
||||
[ENV] 类 BasicConfig 中未找到字段:UNKNOWN_FIELD
|
||||
```
|
||||
**解决方案**:检查配置类名和字段名是否正确
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [工作目录详解](working_directory.md)
|
||||
- [启动参数详解](../user_guide/launch.md)
|
||||
- [快速安装指南](../user_guide/quick_install_guide.md)
|
||||
|
Before Width: | Height: | Size: 526 KiB |
@@ -1,218 +0,0 @@
|
||||
# 工作目录详解
|
||||
|
||||
本文档详细介绍 Uni-Lab 工作目录(`working_dir`)的判断逻辑和详细用法。
|
||||
|
||||
## 什么是工作目录
|
||||
|
||||
工作目录是 Uni-Lab 存储配置文件、日志和运行数据的目录。默认情况下,工作目录为 `当前目录/unilabos_data`。
|
||||
|
||||
## 工作目录判断逻辑
|
||||
|
||||
系统按以下决策树自动确定工作目录:
|
||||
|
||||
### 第一步:初始判断
|
||||
|
||||
```python
|
||||
# 检查当前目录
|
||||
if 当前目录以 "unilabos_data" 结尾:
|
||||
working_dir = 当前目录的绝对路径
|
||||
else:
|
||||
working_dir = 当前目录/unilabos_data
|
||||
```
|
||||
|
||||
**解释:**
|
||||
- 如果您已经在 `unilabos_data` 目录内启动,系统直接使用当前目录
|
||||
- 否则,系统会在当前目录下创建或使用 `unilabos_data` 子目录
|
||||
|
||||
### 第二步:处理 `--working_dir` 参数
|
||||
|
||||
如果用户指定了 `--working_dir` 参数:
|
||||
|
||||
```python
|
||||
working_dir = 用户指定的路径
|
||||
```
|
||||
|
||||
此时还会检查配置文件:
|
||||
- 如果同时指定了 `--config` 但该文件不存在
|
||||
- 系统会尝试在 `working_dir/local_config.py` 查找
|
||||
- 如果仍未找到,报错退出
|
||||
|
||||
### 第三步:处理 `--config` 参数
|
||||
|
||||
如果用户指定了 `--config` 且文件存在:
|
||||
|
||||
```python
|
||||
# 工作目录改为配置文件所在目录
|
||||
working_dir = config_path 的父目录
|
||||
```
|
||||
|
||||
**重要:** 这意味着配置文件的位置会影响工作目录的判断。
|
||||
|
||||
## 使用场景示例
|
||||
|
||||
### 场景 1:默认场景(推荐)
|
||||
|
||||
```bash
|
||||
# 当前目录:/home/user/project
|
||||
unilab --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 结果:
|
||||
# working_dir = /home/user/project/unilabos_data
|
||||
# config_path = /home/user/project/unilabos_data/local_config.py
|
||||
```
|
||||
|
||||
### 场景 2:在 unilabos_data 目录内启动
|
||||
|
||||
```bash
|
||||
cd /home/user/project/unilabos_data
|
||||
unilab --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 结果:
|
||||
# working_dir = /home/user/project/unilabos_data
|
||||
# config_path = /home/user/project/unilabos_data/local_config.py
|
||||
```
|
||||
|
||||
### 场景 3:手动指定工作目录
|
||||
|
||||
```bash
|
||||
unilab --working_dir /custom/path --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 结果:
|
||||
# working_dir = /custom/path
|
||||
# config_path = /custom/path/local_config.py (如果存在)
|
||||
```
|
||||
|
||||
### 场景 4:通过配置文件路径推断工作目录
|
||||
|
||||
```bash
|
||||
unilab --config /data/lab_a/local_config.py --ak your_ak --sk your_sk -g graph.json
|
||||
|
||||
# 结果:
|
||||
# working_dir = /data/lab_a
|
||||
# config_path = /data/lab_a/local_config.py
|
||||
```
|
||||
|
||||
## 高级用法:管理多个实验室配置
|
||||
|
||||
### 方法 1:使用不同的工作目录
|
||||
|
||||
```bash
|
||||
# 实验室 A
|
||||
unilab --working_dir ~/labs/lab_a --ak ak_a --sk sk_a -g graph_a.json
|
||||
|
||||
# 实验室 B
|
||||
unilab --working_dir ~/labs/lab_b --ak ak_b --sk sk_b -g graph_b.json
|
||||
```
|
||||
|
||||
### 方法 2:使用不同的配置文件
|
||||
|
||||
```bash
|
||||
# 实验室 A
|
||||
unilab --config ~/labs/lab_a/config.py --ak ak_a --sk sk_a -g graph_a.json
|
||||
|
||||
# 实验室 B
|
||||
unilab --config ~/labs/lab_b/config.py --ak ak_b --sk sk_b -g graph_b.json
|
||||
```
|
||||
|
||||
### 方法 3:使用shell脚本管理
|
||||
|
||||
创建 `start_lab_a.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd ~/labs/lab_a
|
||||
unilab --ak your_ak_a --sk your_sk_a -g graph_a.json
|
||||
```
|
||||
|
||||
创建 `start_lab_b.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd ~/labs/lab_b
|
||||
unilab --ak your_ak_b --sk your_sk_b -g graph_b.json
|
||||
```
|
||||
|
||||
## 完整决策流程图
|
||||
|
||||
```
|
||||
开始
|
||||
↓
|
||||
判断当前目录是否以 unilabos_data 结尾?
|
||||
├─ 是 → working_dir = 当前目录
|
||||
└─ 否 → working_dir = 当前目录/unilabos_data
|
||||
↓
|
||||
用户是否指定 --working_dir?
|
||||
└─ 是 → working_dir = 指定路径
|
||||
↓
|
||||
用户是否指定 --config 且文件存在?
|
||||
└─ 是 → working_dir = config 文件所在目录
|
||||
↓
|
||||
检查 working_dir/local_config.py 是否存在?
|
||||
├─ 是 → 加载配置文件 → 继续启动
|
||||
└─ 否 → 询问是否首次使用
|
||||
├─ 是 → 创建目录和配置文件 → 继续启动
|
||||
└─ 否 → 退出程序
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何查看当前使用的工作目录?
|
||||
|
||||
启动 Uni-Lab 时,系统会在控制台输出:
|
||||
|
||||
```
|
||||
当前工作目录为 /path/to/working_dir
|
||||
```
|
||||
|
||||
### 2. 可以在同一台机器上运行多个实验室吗?
|
||||
|
||||
可以。使用不同的工作目录或配置文件即可:
|
||||
|
||||
```bash
|
||||
# 终端 1
|
||||
unilab --working_dir ~/lab1 --ak ak1 --sk sk1 -g graph1.json
|
||||
|
||||
# 终端 2
|
||||
unilab --working_dir ~/lab2 --ak ak2 --sk sk2 -g graph2.json
|
||||
```
|
||||
|
||||
### 3. 工作目录中存储了什么?
|
||||
|
||||
- `local_config.py` - 配置文件
|
||||
- 日志文件
|
||||
- 临时运行数据
|
||||
- 缓存文件
|
||||
|
||||
### 4. 可以删除工作目录吗?
|
||||
|
||||
可以,但会丢失:
|
||||
- 配置文件(需要重新创建)
|
||||
- 历史日志
|
||||
- 缓存数据
|
||||
|
||||
建议定期备份配置文件。
|
||||
|
||||
### 5. 如何迁移到新的工作目录?
|
||||
|
||||
```bash
|
||||
# 1. 复制旧的工作目录
|
||||
cp -r ~/old_path/unilabos_data ~/new_path/unilabos_data
|
||||
|
||||
# 2. 在新位置启动
|
||||
cd ~/new_path
|
||||
unilab --ak your_ak --sk your_sk -g graph.json
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用默认工作目录**:对于单一实验室,使用默认的 `./unilabos_data` 即可
|
||||
2. **组织多实验室**:为每个实验室创建独立的目录结构
|
||||
3. **版本控制**:将配置文件纳入版本控制,但排除日志和缓存
|
||||
4. **备份配置**:定期备份 `local_config.py` 文件
|
||||
5. **使用脚本**:为不同实验室创建启动脚本,简化操作
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [配置文件指南](configuration.md)
|
||||
- [启动参数详解](../user_guide/launch.md)
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
使用以下命令启动模拟反应器:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/mock_reactor.json
|
||||
unilab -g test/experiments/mock_reactor.json --app_bridges ""
|
||||
```
|
||||
|
||||
### 2. 执行抽真空和充气操作
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(instructions)=
|
||||
# 设备抽象、指令集与通信中间件
|
||||
|
||||
Uni-Lab-OS的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
|
||||
Uni-Lab 操作系统的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
|
||||
|
||||
## 设备间通信模式
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||
"sphinx_rtd_theme",
|
||||
"sphinxcontrib.mermaid"
|
||||
"sphinx_rtd_theme"
|
||||
]
|
||||
|
||||
source_suffix = {
|
||||
@@ -43,8 +42,6 @@ myst_enable_extensions = [
|
||||
"substitution",
|
||||
]
|
||||
|
||||
myst_fence_as_directive = ["mermaid"]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
@@ -206,5 +203,3 @@ def generate_action_includes(app):
|
||||
|
||||
def setup(app):
|
||||
app.connect("builder-inited", generate_action_includes)
|
||||
app.add_js_file("https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js")
|
||||
app.add_js_file(None, body="mermaid.initialize({startOnLoad:true});")
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
# 添加设备:编写驱动
|
||||
# 添加新设备
|
||||
|
||||
在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的这三种机制映射到 Uni-Lab 标准指令集上。
|
||||
在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。回顾 {ref}`instructions` 中的概念,抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的三种机制映射到 Uni-Lab 标准指令集上。
|
||||
|
||||
> **💡 提示:** 本文档介绍如何使用已有的设备驱动(SDK)。若设备没有现成的驱动程序,需要自己开发驱动,请参考 {doc}`add_old_device`。
|
||||
能被 Uni-Lab 添加的驱动程序类型有以下种类:
|
||||
|
||||
## 支持的驱动类型
|
||||
|
||||
Uni-Lab 支持以下两种驱动程序:
|
||||
|
||||
### 1. Python Class(推荐)
|
||||
|
||||
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,无需额外编译。
|
||||
|
||||
**示例:**
|
||||
1. Python Class,如
|
||||
|
||||
```python
|
||||
class MockGripper:
|
||||
@@ -39,11 +31,12 @@ class MockGripper:
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
@status.setter
|
||||
def status(self, target):
|
||||
self._status = target
|
||||
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
def push_to(self, position: float, torque: float, velocity: float = 0.0):
|
||||
self._status = "Running"
|
||||
current_pos = self.position
|
||||
@@ -60,11 +53,9 @@ class MockGripper:
|
||||
self._status = "Idle"
|
||||
```
|
||||
|
||||
### 2. C# Class
|
||||
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 使用。
|
||||
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用(仅需一次)。
|
||||
|
||||
**示例:**
|
||||
2. C# Class,如
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
@@ -93,7 +84,7 @@ public class MockGripper
|
||||
position = currentPos + (Position - currentPos) / 20 * (i + 1);
|
||||
torque = Torque / (20 - i);
|
||||
velocity = Velocity;
|
||||
await Task.Delay((int)(moveTime * 1000 / 20));
|
||||
await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds
|
||||
}
|
||||
torque = Torque;
|
||||
status = "Idle";
|
||||
@@ -101,16 +92,12 @@ public class MockGripper
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
|
||||
|
||||
## 快速开始:两种方式添加设备
|
||||
|
||||
### 方式 1:使用注册表编辑器(推荐)
|
||||
## 快速开始:使用注册表编辑器(推荐)
|
||||
|
||||
推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. 启动 Uni-Lab-OS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择您的 Python 设备驱动文件
|
||||
@@ -119,18 +106,13 @@ public class MockGripper
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 保存到 `devices/` 目录下
|
||||
|
||||
**优点:**
|
||||
---
|
||||
|
||||
- 自动识别设备属性和方法
|
||||
- 可视化界面,易于操作
|
||||
- 自动生成完整配置
|
||||
- 减少手动配置错误
|
||||
|
||||
### 方式 2:手动编写注册表(简化版)
|
||||
## 手动编写注册表(简化版)
|
||||
|
||||
如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
|
||||
|
||||
**最小配置示例:**
|
||||
### 最小配置示例
|
||||
|
||||
```yaml
|
||||
my_device: # 设备唯一标识符
|
||||
@@ -139,22 +121,22 @@ my_device: # 设备唯一标识符
|
||||
type: python # 驱动类型
|
||||
```
|
||||
|
||||
**注册表文件位置:**
|
||||
### 注册表文件位置
|
||||
|
||||
- 默认路径:`unilabos/registry/devices`
|
||||
- 自定义路径:启动时使用 `--registry_path` 参数指定
|
||||
- 可将多个设备写在同一个 YAML 文件中
|
||||
- 自定义路径:启动时使用 `--registry` 参数指定
|
||||
- 可将多个设备写在同一个 yaml 文件中
|
||||
|
||||
**系统自动生成的内容:**
|
||||
### 系统自动生成的内容
|
||||
|
||||
系统会自动分析您的 Python 驱动类并生成:
|
||||
|
||||
- `status_types`:从 `@property` 装饰的方法自动识别状态属性
|
||||
- `status_types`:从 `get_*` 方法自动识别状态属性
|
||||
- `action_value_mappings`:从类方法自动生成动作映射
|
||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||
- `schema`:前端显示用的属性类型定义
|
||||
|
||||
**完整结构概览:**
|
||||
### 完整结构概览
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
@@ -169,848 +151,4 @@ my_device:
|
||||
schema: {} # 自动生成
|
||||
```
|
||||
|
||||
> 💡 **提示:** 详细的注册表编写指南和高级配置,请参考 {doc}`03_add_device_registry`。
|
||||
|
||||
---
|
||||
|
||||
## Python 类结构要求
|
||||
|
||||
Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
|
||||
|
||||
```python
|
||||
from typing import Dict, Any
|
||||
|
||||
class MyDevice:
|
||||
"""设备类文档字符串
|
||||
|
||||
说明设备的功能、连接方式等
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""初始化设备
|
||||
|
||||
Args:
|
||||
config: 配置字典,来自图文件或注册表
|
||||
"""
|
||||
self.port = config.get('port', '/dev/ttyUSB0')
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self._status = "idle"
|
||||
# 初始化硬件连接
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态(会自动广播)"""
|
||||
return self._status
|
||||
|
||||
def my_action(self, param: float) -> Dict[str, Any]:
|
||||
"""执行动作
|
||||
|
||||
Args:
|
||||
param: 参数说明
|
||||
|
||||
Returns:
|
||||
{"success": True, "result": ...}
|
||||
"""
|
||||
# 执行设备操作
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
## 状态属性 vs 动作方法
|
||||
|
||||
### 状态属性(@property)
|
||||
|
||||
状态属性会被自动识别并定期广播:
|
||||
|
||||
```python
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
"""当前温度"""
|
||||
return self._read_temperature()
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""设备状态: idle, running, error"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""设备是否就绪"""
|
||||
return self._status == "idle"
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 使用`@property`装饰器
|
||||
- 只读,不能有参数
|
||||
- 自动添加到注册表的`status_types`
|
||||
- 定期发布到 ROS2 topic
|
||||
|
||||
### 动作方法
|
||||
|
||||
动作方法是设备可以执行的操作:
|
||||
|
||||
```python
|
||||
def start_heating(self, target_temp: float, rate: float = 1.0) -> Dict[str, Any]:
|
||||
"""开始加热
|
||||
|
||||
Args:
|
||||
target_temp: 目标温度(°C)
|
||||
rate: 升温速率(°C/min)
|
||||
|
||||
Returns:
|
||||
{"success": bool, "message": str}
|
||||
"""
|
||||
self._status = "heating"
|
||||
self._target_temp = target_temp
|
||||
# 发送命令到硬件
|
||||
return {"success": True, "message": f"Heating to {target_temp}°C"}
|
||||
|
||||
async def async_operation(self, duration: float) -> Dict[str, Any]:
|
||||
"""异步操作(长时间运行)
|
||||
|
||||
Args:
|
||||
duration: 持续时间(秒)
|
||||
"""
|
||||
# 使用 self.sleep 而不是 asyncio.sleep(ROS2 异步机制)
|
||||
await self.sleep(duration)
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 普通方法或 async 方法
|
||||
- 返回 Dict 类型的结果
|
||||
- 自动注册为 ROS2 Action
|
||||
- 支持参数和返回值
|
||||
|
||||
### 返回值设计指南
|
||||
|
||||
> **⚠️ 重要:返回值会自动显示在前端**
|
||||
>
|
||||
> 动作方法的返回值(字典)会自动显示在 Web 界面的工作流执行结果中。因此,**强烈建议**设计结构化、可读的返回值字典。
|
||||
|
||||
**推荐的返回值结构:**
|
||||
|
||||
```python
|
||||
def my_action(self, param: float) -> Dict[str, Any]:
|
||||
"""执行操作"""
|
||||
try:
|
||||
# 执行操作...
|
||||
result = self._do_something(param)
|
||||
|
||||
return {
|
||||
"success": True, # 必需:操作是否成功
|
||||
"message": "操作完成", # 推荐:用户友好的消息
|
||||
"result": result, # 可选:具体结果数据
|
||||
"param_used": param, # 可选:记录使用的参数
|
||||
# 其他有用的信息...
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "操作失败"
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践示例(参考 `host_node.test_latency`):**
|
||||
|
||||
```python
|
||||
def test_latency(self) -> Dict[str, Any]:
|
||||
"""测试网络延迟
|
||||
|
||||
返回值会在前端显示,包含详细的测试结果
|
||||
"""
|
||||
# 执行测试...
|
||||
avg_rtt_ms = 25.5
|
||||
avg_time_diff_ms = 10.2
|
||||
test_count = 5
|
||||
|
||||
# 返回结构化的测试结果
|
||||
return {
|
||||
"status": "success", # 状态标识
|
||||
"avg_rtt_ms": avg_rtt_ms, # 平均往返时间
|
||||
"avg_time_diff_ms": avg_time_diff_ms, # 平均时间差
|
||||
"max_time_error_ms": 5.3, # 最大误差
|
||||
"task_delay_ms": 15.7, # 任务延迟
|
||||
"test_count": test_count, # 测试次数
|
||||
}
|
||||
```
|
||||
|
||||
**前端显示效果:**
|
||||
|
||||
当用户在 Web 界面执行工作流时,返回的字典会以 JSON 格式显示在结果面板中:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"avg_rtt_ms": 25.5,
|
||||
"avg_time_diff_ms": 10.2,
|
||||
"max_time_error_ms": 5.3,
|
||||
"task_delay_ms": 15.7,
|
||||
"test_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**返回值设计建议:**
|
||||
|
||||
1. **始终包含 `success` 字段**:布尔值,表示操作是否成功
|
||||
2. **包含 `message` 字段**:字符串,提供用户友好的描述
|
||||
3. **使用有意义的键名**:使用描述性的键名(如 `avg_rtt_ms` 而不是 `v1`)
|
||||
4. **包含单位**:在键名中包含单位(如 `_ms`、`_ml`、`_celsius`)
|
||||
5. **记录重要参数**:返回使用的关键参数值,便于追溯
|
||||
6. **错误信息详细**:失败时包含 `error` 字段和详细的错误描述
|
||||
7. **避免返回大数据**:不要返回大型数组或二进制数据,这会影响前端性能
|
||||
|
||||
**错误处理示例:**
|
||||
|
||||
```python
|
||||
def risky_operation(self, param: float) -> Dict[str, Any]:
|
||||
"""可能失败的操作"""
|
||||
if param < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "参数不能为负数",
|
||||
"message": f"无效参数: {param}",
|
||||
"param": param
|
||||
}
|
||||
|
||||
try:
|
||||
result = self._execute(param)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "操作成功",
|
||||
"result": result,
|
||||
"param": param
|
||||
}
|
||||
except IOError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "通信错误",
|
||||
"message": str(e),
|
||||
"device_status": self._status
|
||||
}
|
||||
```
|
||||
|
||||
## 特殊参数类型:ResourceSlot 和 DeviceSlot
|
||||
|
||||
Uni-Lab 提供特殊的参数类型,用于在方法中声明需要选择资源或设备。
|
||||
|
||||
### 导入类型
|
||||
|
||||
```python
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
from typing import List
|
||||
```
|
||||
|
||||
### ResourceSlot - 资源选择
|
||||
|
||||
用于需要选择物料资源的场景:
|
||||
|
||||
```python
|
||||
def pipette_liquid(
|
||||
self,
|
||||
source: ResourceSlot, # 单个源容器
|
||||
target: ResourceSlot, # 单个目标容器
|
||||
volume: float
|
||||
) -> Dict[str, Any]:
|
||||
"""从源容器吸取液体到目标容器
|
||||
|
||||
Args:
|
||||
source: 源容器(前端会显示资源选择下拉框)
|
||||
target: 目标容器(前端会显示资源选择下拉框)
|
||||
volume: 体积(μL)
|
||||
"""
|
||||
print(f"Pipetting {volume}μL from {source.id} to {target.id}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**多选示例**:
|
||||
|
||||
```python
|
||||
def mix_multiple(
|
||||
self,
|
||||
containers: List[ResourceSlot], # 多个容器选择
|
||||
speed: float
|
||||
) -> Dict[str, Any]:
|
||||
"""混合多个容器
|
||||
|
||||
Args:
|
||||
containers: 容器列表(前端会显示多选下拉框)
|
||||
speed: 混合速度
|
||||
"""
|
||||
for container in containers:
|
||||
print(f"Mixing {container.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
### DeviceSlot - 设备选择
|
||||
|
||||
用于需要选择其他设备的场景:
|
||||
|
||||
```python
|
||||
def coordinate_with_device(
|
||||
self,
|
||||
other_device: DeviceSlot, # 单个设备选择
|
||||
command: str
|
||||
) -> Dict[str, Any]:
|
||||
"""与另一个设备协同工作
|
||||
|
||||
Args:
|
||||
other_device: 协同设备(前端会显示设备选择下拉框)
|
||||
command: 命令
|
||||
"""
|
||||
print(f"Coordinating with {other_device.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**多设备示例**:
|
||||
|
||||
```python
|
||||
def sync_devices(
|
||||
self,
|
||||
devices: List[DeviceSlot], # 多个设备选择
|
||||
sync_signal: str
|
||||
) -> Dict[str, Any]:
|
||||
"""同步多个设备
|
||||
|
||||
Args:
|
||||
devices: 设备列表(前端会显示多选下拉框)
|
||||
sync_signal: 同步信号
|
||||
"""
|
||||
for dev in devices:
|
||||
print(f"Syncing {dev.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
### 完整示例:液体处理工作站
|
||||
|
||||
```python
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class LiquidHandler:
|
||||
"""液体处理工作站"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.simulation = config.get('simulation', False)
|
||||
self._status = "idle"
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def transfer_liquid(
|
||||
self,
|
||||
source: ResourceSlot, # 源容器选择
|
||||
target: ResourceSlot, # 目标容器选择
|
||||
volume: float,
|
||||
tip: ResourceSlot = None # 可选的枪头选择
|
||||
) -> Dict[str, Any]:
|
||||
"""转移液体
|
||||
|
||||
前端效果:
|
||||
- source: 下拉框,列出所有可用容器
|
||||
- target: 下拉框,列出所有可用容器
|
||||
- volume: 数字输入框
|
||||
- tip: 下拉框(可选),列出所有枪头
|
||||
"""
|
||||
self._status = "transferring"
|
||||
|
||||
# source和target会被解析为实际的资源对象
|
||||
print(f"Transferring {volume}μL")
|
||||
print(f" From: {source.id} ({source.name})")
|
||||
print(f" To: {target.id} ({target.name})")
|
||||
|
||||
if tip:
|
||||
print(f" Using tip: {tip.id}")
|
||||
|
||||
# 执行实际的液体转移
|
||||
# ...
|
||||
|
||||
self._status = "idle"
|
||||
return {
|
||||
"success": True,
|
||||
"volume_transferred": volume,
|
||||
"source_id": source.id,
|
||||
"target_id": target.id
|
||||
}
|
||||
|
||||
def multi_dispense(
|
||||
self,
|
||||
source: ResourceSlot, # 单个源
|
||||
targets: List[ResourceSlot], # 多个目标
|
||||
volumes: List[float]
|
||||
) -> Dict[str, Any]:
|
||||
"""从一个源分配到多个目标
|
||||
|
||||
前端效果:
|
||||
- source: 单选下拉框
|
||||
- targets: 多选下拉框(可选择多个容器)
|
||||
- volumes: 数组输入(每个目标对应一个体积)
|
||||
"""
|
||||
results = []
|
||||
for target, vol in zip(targets, volumes):
|
||||
print(f"Dispensing {vol}μL to {target.name}")
|
||||
results.append({
|
||||
"target": target.id,
|
||||
"volume": vol
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"dispense_results": results
|
||||
}
|
||||
|
||||
def test_with_balance(
|
||||
self,
|
||||
target: ResourceSlot, # 容器
|
||||
balance: DeviceSlot # 天平设备
|
||||
) -> Dict[str, Any]:
|
||||
"""使用天平测量容器
|
||||
|
||||
前端效果:
|
||||
- target: 容器选择下拉框
|
||||
- balance: 设备选择下拉框(仅显示天平类型)
|
||||
"""
|
||||
print(f"Weighing {target.name} on {balance.name}")
|
||||
|
||||
# 可以调用balance的方法
|
||||
# weight = balance.get_weight()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"container": target.id,
|
||||
"balance_used": balance.id
|
||||
}
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
|
||||
#### 1. 类型识别
|
||||
|
||||
注册表扫描方法签名时:
|
||||
|
||||
```python
|
||||
def my_method(self, resource: ResourceSlot, device: DeviceSlot):
|
||||
pass
|
||||
```
|
||||
|
||||
系统识别到`ResourceSlot`和`DeviceSlot`类型。
|
||||
|
||||
#### 2. 自动添加 placeholder_keys
|
||||
|
||||
在注册表中自动生成:
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
class:
|
||||
action_value_mappings:
|
||||
my_method:
|
||||
goal:
|
||||
resource: resource
|
||||
device: device
|
||||
placeholder_keys:
|
||||
resource: unilabos_resources # 自动添加!
|
||||
device: unilabos_devices # 自动添加!
|
||||
```
|
||||
|
||||
#### 3. 前端 UI 生成
|
||||
|
||||
- `unilabos_resources`: 渲染为资源选择下拉框
|
||||
- `unilabos_devices`: 渲染为设备选择下拉框
|
||||
|
||||
#### 4. 运行时解析
|
||||
|
||||
用户选择资源/设备后,实际调用时会传入完整的资源/设备对象:
|
||||
|
||||
```python
|
||||
# 用户在前端选择了 plate_1
|
||||
# 运行时,source参数会收到完整的Resource对象
|
||||
source.id # "plate_1"
|
||||
source.name # "96孔板"
|
||||
source.type # "resource"
|
||||
source.class_ # "corning_96_wellplate_360ul_flat"
|
||||
```
|
||||
|
||||
## 支持的通信方式
|
||||
|
||||
### 1. 串口(Serial)
|
||||
|
||||
```python
|
||||
import serial
|
||||
|
||||
class SerialDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.port = config['port']
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self.ser = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
def send_command(self, cmd: str) -> str:
|
||||
"""发送命令并读取响应"""
|
||||
self.ser.write(f"{cmd}\r\n".encode())
|
||||
response = self.ser.readline().decode().strip()
|
||||
return response
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
```
|
||||
|
||||
### 2. TCP/IP Socket
|
||||
|
||||
```python
|
||||
import socket
|
||||
|
||||
class TCPDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.host = config['host']
|
||||
self.port = config['port']
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((self.host, self.port))
|
||||
|
||||
def send_command(self, cmd: str) -> str:
|
||||
self.sock.sendall(cmd.encode())
|
||||
response = self.sock.recv(1024).decode()
|
||||
return response
|
||||
```
|
||||
|
||||
### 3. Modbus
|
||||
|
||||
```python
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
|
||||
class ModbusDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.host = config['host']
|
||||
self.port = config.get('port', 502)
|
||||
self.client = ModbusTcpClient(self.host, port=self.port)
|
||||
self.client.connect()
|
||||
|
||||
def read_register(self, address: int) -> int:
|
||||
result = self.client.read_holding_registers(address, 1)
|
||||
return result.registers[0]
|
||||
|
||||
def write_register(self, address: int, value: int):
|
||||
self.client.write_register(address, value)
|
||||
```
|
||||
|
||||
### 4. OPC UA
|
||||
|
||||
```python
|
||||
from opcua import Client
|
||||
|
||||
class OPCUADevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.url = config['url']
|
||||
self.client = Client(self.url)
|
||||
self.client.connect()
|
||||
|
||||
def read_node(self, node_id: str):
|
||||
node = self.client.get_node(node_id)
|
||||
return node.get_value()
|
||||
|
||||
def write_node(self, node_id: str, value):
|
||||
node = self.client.get_node(node_id)
|
||||
node.set_value(value)
|
||||
```
|
||||
|
||||
### 5. HTTP/RPC
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class HTTPDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.base_url = config['url']
|
||||
self.auth_token = config.get('token')
|
||||
|
||||
def send_command(self, endpoint: str, data: Dict) -> Dict:
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
headers = {'Authorization': f'Bearer {self.auth_token}'}
|
||||
response = requests.post(url, json=data, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## 异步 vs 同步方法
|
||||
|
||||
### 同步方法(适合快速操作)
|
||||
|
||||
```python
|
||||
def quick_operation(self, param: float) -> Dict[str, Any]:
|
||||
"""快速操作,立即返回"""
|
||||
result = self._do_something(param)
|
||||
return {"success": True, "result": result}
|
||||
```
|
||||
|
||||
### 异步方法(适合耗时操作)
|
||||
|
||||
```python
|
||||
async def long_operation(self, duration: float) -> Dict[str, Any]:
|
||||
"""长时间运行的操作"""
|
||||
self._status = "running"
|
||||
|
||||
# 使用 ROS2 提供的 sleep 方法(而不是 asyncio.sleep)
|
||||
await self.sleep(duration)
|
||||
|
||||
# 可以在过程中发送feedback
|
||||
# 需要配合ROS2 Action的feedback机制
|
||||
|
||||
self._status = "idle"
|
||||
return {"success": True, "duration": duration}
|
||||
```
|
||||
|
||||
> **⚠️ 重要提示:ROS2 异步机制 vs Python asyncio**
|
||||
>
|
||||
> Uni-Lab 的设备驱动虽然使用 `async def` 语法,但**底层是 ROS2 的异步机制,而不是 Python 的 asyncio**。
|
||||
>
|
||||
> **不能使用的 asyncio 功能:**
|
||||
>
|
||||
> - ❌ `asyncio.sleep()` - 会导致 ROS2 事件循环阻塞
|
||||
> - ❌ `asyncio.create_task()` - 任务不会被 ROS2 正确调度
|
||||
> - ❌ `asyncio.gather()` - 无法与 ROS2 集成
|
||||
> - ❌ 其他 asyncio 标准库函数
|
||||
>
|
||||
> **应该使用的方法(继承自 BaseROS2DeviceNode):**
|
||||
>
|
||||
> - ✅ `await self.sleep(seconds)` - ROS2 兼容的睡眠
|
||||
> - ✅ `await self.create_task(func, **kwargs)` - ROS2 兼容的任务创建
|
||||
> - ✅ ROS2 的 Action/Service 回调机制
|
||||
>
|
||||
> **示例:**
|
||||
>
|
||||
> ```python
|
||||
> async def complex_operation(self, duration: float) -> Dict[str, Any]:
|
||||
> """正确使用 ROS2 异步方法"""
|
||||
> self._status = "processing"
|
||||
>
|
||||
> # ✅ 正确:使用 self.sleep
|
||||
> await self.sleep(duration)
|
||||
>
|
||||
> # ✅ 正确:创建并发任务
|
||||
> task = await self.create_task(self._background_work)
|
||||
>
|
||||
> # ❌ 错误:不要使用 asyncio
|
||||
> # await asyncio.sleep(duration) # 这会导致问题!
|
||||
> # task = asyncio.create_task(...) # 这也不行!
|
||||
>
|
||||
> self._status = "idle"
|
||||
> return {"success": True}
|
||||
>
|
||||
> async def _background_work(self):
|
||||
> """后台任务"""
|
||||
> await self.sleep(1.0)
|
||||
> self.lab_logger().info("Background work completed")
|
||||
> ```
|
||||
>
|
||||
> **为什么不能混用?**
|
||||
>
|
||||
> ROS2 使用 `rclpy` 的事件循环来管理所有异步操作。如果使用 `asyncio` 的函数,这些操作会在不同的事件循环中运行,导致:
|
||||
>
|
||||
> - ROS2 回调无法正确执行
|
||||
> - 任务可能永远不会完成
|
||||
> - 程序可能死锁或崩溃
|
||||
>
|
||||
> **参考实现:**
|
||||
>
|
||||
> `BaseROS2DeviceNode` 提供的方法定义(`base_device_node.py:563-572`):
|
||||
>
|
||||
> ```python
|
||||
> async def sleep(self, rel_time: float, callback_group=None):
|
||||
> """ROS2 兼容的异步睡眠"""
|
||||
> if callback_group is None:
|
||||
> callback_group = self.callback_group
|
||||
> await ROS2DeviceNode.async_wait_for(self, rel_time, callback_group)
|
||||
>
|
||||
> @classmethod
|
||||
> async def create_task(cls, func, trace_error=True, **kwargs) -> Task:
|
||||
> """ROS2 兼容的任务创建"""
|
||||
> return ROS2DeviceNode.run_async_func(func, trace_error, **kwargs)
|
||||
> ```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 基本错误处理
|
||||
|
||||
```python
|
||||
def operation_with_error_handling(self, param: float) -> Dict[str, Any]:
|
||||
"""带错误处理的操作"""
|
||||
try:
|
||||
result = self._risky_operation(param)
|
||||
return {
|
||||
"success": True,
|
||||
"result": result
|
||||
}
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid parameter",
|
||||
"message": str(e)
|
||||
}
|
||||
except IOError as e:
|
||||
self._status = "error"
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Communication error",
|
||||
"message": str(e)
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义异常
|
||||
|
||||
```python
|
||||
class DeviceError(Exception):
|
||||
"""设备错误基类"""
|
||||
pass
|
||||
|
||||
class DeviceNotReadyError(DeviceError):
|
||||
"""设备未就绪"""
|
||||
pass
|
||||
|
||||
class DeviceTimeoutError(DeviceError):
|
||||
"""设备超时"""
|
||||
pass
|
||||
|
||||
class MyDevice:
|
||||
def operation(self) -> Dict[str, Any]:
|
||||
if self._status != "idle":
|
||||
raise DeviceNotReadyError(f"Device is {self._status}")
|
||||
|
||||
# 执行操作
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 类型注解
|
||||
|
||||
```python
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
def method(
|
||||
self,
|
||||
param1: float,
|
||||
param2: str,
|
||||
optional_param: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""完整的类型注解有助于自动生成注册表"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 文档字符串
|
||||
|
||||
```python
|
||||
def method(self, param: float) -> Dict[str, Any]:
|
||||
"""方法简短描述
|
||||
|
||||
更详细的说明...
|
||||
|
||||
Args:
|
||||
param: 参数说明,包括单位和范围
|
||||
|
||||
Returns:
|
||||
Dict包含:
|
||||
- success (bool): 是否成功
|
||||
- result (Any): 结果数据
|
||||
|
||||
Raises:
|
||||
DeviceError: 错误情况说明
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. 配置验证
|
||||
|
||||
```python
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
# 验证必需参数
|
||||
required = ['port', 'baudrate']
|
||||
for key in required:
|
||||
if key not in config:
|
||||
raise ValueError(f"Missing required config: {key}")
|
||||
|
||||
self.port = config['port']
|
||||
self.baudrate = config['baudrate']
|
||||
```
|
||||
|
||||
### 4. 资源清理
|
||||
|
||||
```python
|
||||
def __del__(self):
|
||||
"""析构函数,清理资源"""
|
||||
if hasattr(self, 'connection') and self.connection:
|
||||
self.connection.close()
|
||||
```
|
||||
|
||||
### 5. 设计前端友好的返回值
|
||||
|
||||
**记住:返回值会直接显示在 Web 界面**
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def measure_temperature(self) -> Dict[str, Any]:
|
||||
"""测量温度
|
||||
|
||||
✅ 好的返回值设计:
|
||||
- 包含 success 状态
|
||||
- 使用描述性键名
|
||||
- 在键名中包含单位
|
||||
- 记录测量时间
|
||||
"""
|
||||
temp = self._read_temperature()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"temperature_celsius": temp, # 键名包含单位
|
||||
"timestamp": time.time(), # 记录时间
|
||||
"sensor_status": "normal", # 额外状态信息
|
||||
"message": f"温度测量完成: {temp}°C" # 用户友好的消息
|
||||
}
|
||||
|
||||
def bad_example(self) -> Dict[str, Any]:
|
||||
"""❌ 不好的返回值设计"""
|
||||
return {
|
||||
"s": True, # ❌ 键名不明确
|
||||
"v": 25.5, # ❌ 没有说明单位
|
||||
"t": 1234567890, # ❌ 不清楚是什么时间戳
|
||||
}
|
||||
```
|
||||
|
||||
**参考 `host_node.test_latency` 方法**(第 1216-1340 行),它返回详细的测试结果,在前端清晰显示:
|
||||
|
||||
```python
|
||||
return {
|
||||
"status": "success",
|
||||
"avg_rtt_ms": 25.5, # 有意义的键名 + 单位
|
||||
"avg_time_diff_ms": 10.2,
|
||||
"max_time_error_ms": 5.3,
|
||||
"task_delay_ms": 15.7,
|
||||
"test_count": 5, # 记录重要信息
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
看完本文档后,建议继续阅读:
|
||||
|
||||
- {doc}`add_action` - 了解如何添加新的动作指令
|
||||
- {doc}`add_yaml` - 学习如何编写和完善 YAML 注册表
|
||||
|
||||
进阶主题:
|
||||
|
||||
- {doc}`03_add_device_registry` - 了解如何配置注册表
|
||||
- {doc}`04_add_device_testing` - 学习如何测试设备
|
||||
- {doc}`add_old_device` - 没有 SDK 时如何开发设备驱动
|
||||
|
||||
## 参考
|
||||
|
||||
- [Python 类型注解](https://docs.python.org/3/library/typing.html)
|
||||
- [ROS2 rclpy 异步编程](https://docs.ros.org/en/humble/Tutorials/Intermediate/Writing-an-Action-Server-Client/Py.html) - Uni-Lab 使用 ROS2 的异步机制
|
||||
- [串口通信](https://pyserial.readthedocs.io/)
|
||||
|
||||
> **注意:** 虽然设备驱动使用 `async def` 语法,但请**不要参考** Python 标准的 [asyncio 文档](https://docs.python.org/3/library/asyncio.html)。Uni-Lab 使用的是 ROS2 的异步机制,两者不兼容。请使用 `self.sleep()` 和 `self.create_task()` 等 BaseROS2DeviceNode 提供的方法。
|
||||
详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <add_yaml>`。
|
||||
|
||||
@@ -111,8 +111,8 @@ new_device: # 设备名,要唯一
|
||||
|
||||
1. 以 `auto-` 开头的动作:从你 Python 类的方法自动生成
|
||||
2. 通用的驱动动作:
|
||||
- `_execute_driver_command`:同步执行驱动命令(仅本地可用)
|
||||
- `_execute_driver_command_async`:异步执行驱动命令(仅本地可用)
|
||||
- `_execute_driver_command`:同步执行驱动命令
|
||||
- `_execute_driver_command_async`:异步执行驱动命令
|
||||
|
||||
### 如果要手动定义动作
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# 设备 Driver 开发(无 SDK 设备)
|
||||
# 设备 Driver 开发
|
||||
|
||||
我们对设备 Driver 的定义,是一个 Python/C++/C# 类,类的方法可以用于获取传感器数据、执行设备动作、更新物料信息。它们经过 Uni-Lab 的通信中间件包装,就能成为高效分布式通信的设备节点。
|
||||
|
||||
因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。**仅当没有 SDK (Driver) 时,请参考本章进行驱动开发。**
|
||||
|
||||
> **💡 提示:** 本文档介绍如何为没有现成驱动的老设备开发驱动程序。如果您的设备已经有 SDK 或驱动,请直接参考 {doc}`add_device`。
|
||||
因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。仅当没有 SDK (Driver) 时,请参考本章作开发。
|
||||
|
||||
## 有串口字符串指令集文档的设备:Python 串口通信(常见 RS485, RS232, USB)
|
||||
|
||||
@@ -14,13 +12,13 @@
|
||||
|
||||
Modbus 与 RS485、RS232 不一样的地方在于,会有更多直接寄存器的读写,以及涉及字节序转换(Big Endian, Little Endian)。
|
||||
|
||||
Uni-Lab 开发团队在仓库中提供了 3 个样例:
|
||||
Uni-Lab 开发团队在仓库中提供了3个样例:
|
||||
|
||||
- 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
|
||||
- 单一通信设备**IO 板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
|
||||
- 执行多设备复杂任务逻辑的**PLC**,Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
|
||||
* 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
|
||||
* 单一通信设备**IO板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
|
||||
* 执行多设备复杂任务逻辑的**PLC**,Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
|
||||
|
||||
---
|
||||
****
|
||||
|
||||
## 其他工业通信协议:CANopen, Ethernet, OPCUA...
|
||||
|
||||
@@ -28,32 +26,32 @@ Uni-Lab 开发团队在仓库中提供了 3 个样例:
|
||||
|
||||
## 没有接口的老设备老软件:使用 PyWinAuto
|
||||
|
||||
**pywinauto**是一个 Python 库,用于自动化 Windows GUI 操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI 自动化等场景。它支持通过两个后端进行操作:
|
||||
**pywinauto**是一个 Python 库,用于自动化Windows GUI操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI自动化等场景。它支持通过两个后端进行操作:
|
||||
|
||||
- **win32**后端:适用于大多数 Windows 应用程序,使用 native Win32 API。(pywinauto_recorder 默认使用 win32 后端)
|
||||
- **uia**后端:基于 Microsoft UI Automation,适用于较新的应用程序,特别是基于 WPF 或 UWP 的应用程序。(在 win10 上,会有更全的目录,有的窗口 win32 会识别不到)
|
||||
* **win32**后端:适用于大多数Windows应用程序,使用native Win32 API。(pywinauto_recorder默认使用win32后端)
|
||||
* **uia**后端:基于Microsoft UI Automation,适用于较新的应用程序,特别是基于WPF或UWP的应用程序。(在win10上,会有更全的目录,有的窗口win32会识别不到)
|
||||
|
||||
### windows 平台安装 pywinauto 和 pywinauto_recorder
|
||||
### windows平台安装pywinauto和pywinauto_recorder
|
||||
|
||||
直接安装会造成环境崩溃,需要下载并解压已经修改好的文件。
|
||||
|
||||
cd 到对应目录,执行安装
|
||||
cd到对应目录,执行安装
|
||||
|
||||
` pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple `
|
||||
`pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
|
||||

|
||||
|
||||
windows 平台测试 python pywinauto_recorder.py,退出使用两次 ctrl+alt+r 取消选中,关闭命令提示符。
|
||||
windows平台测试 python pywinauto_recorder.py,退出使用两次ctrl+alt+r取消选中,关闭命令提示符。
|
||||
|
||||
### 计算器例子
|
||||
|
||||
你可以先打开 windows 的计算器,然后在 ilab 的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的 pywinauto 用法:
|
||||
你可以先打开windows的计算器,然后在ilab的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的pywinauto用法:
|
||||
|
||||
- 连接到指定进程
|
||||
- 利用 dump_tree 查找需要的窗口
|
||||
- 获取某个位置的信息
|
||||
- 模拟点击
|
||||
- 模拟输入
|
||||
* 连接到指定进程
|
||||
* 利用dump_tree查找需要的窗口
|
||||
* 获取某个位置的信息
|
||||
* 模拟点击
|
||||
* 模拟输入
|
||||
|
||||
#### 代码学习
|
||||
|
||||
@@ -264,13 +262,13 @@ r, g, b = pyautogui.pixel(point_x, point_y)
|
||||
|
||||
### pywinauto_recorder
|
||||
|
||||
pywinauto_recorder 是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用 DLL 的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用 pywinauto 的一些查找 UI 步骤。
|
||||
pywinauto_recorder是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用DLL的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用pywinauto的一些查找UI步骤。
|
||||
|
||||
#### 运行尝试
|
||||
|
||||
请参照 上手尝试-环境创建-3 开启 pywinauto_recorder
|
||||
请参照 上手尝试-环境创建-3 开启pywinauto_recorder
|
||||
|
||||
例如我们这里先启动一个 windows 自带的计算器软件
|
||||
例如我们这里先启动一个windows自带的计算器软件
|
||||
|
||||

|
||||
|
||||
@@ -288,7 +286,7 @@ with UIPath(u"计算器||Window"):
|
||||
click(u"九||Button")
|
||||
```
|
||||
|
||||
执行该 python 脚本,可以观察到新开启的计算器被点击了数字 9
|
||||
执行该python脚本,可以观察到新开启的计算器被点击了数字9
|
||||
|
||||

|
||||
|
||||
@@ -317,31 +315,16 @@ child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
"""
|
||||
```
|
||||
|
||||
这里以上面计算器的例子对 dump_tree 进行解读
|
||||
这里以上面计算器的例子对dump_tree进行解读
|
||||
|
||||
2~4 行为当前对象的窗口
|
||||
2~4行为当前对象的窗口
|
||||
|
||||
- 第 2 行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
|
||||
- 第 3 行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
|
||||
- 第 4 行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
|
||||
* 第2行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
|
||||
* 第3行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
|
||||
* 第4行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
|
||||
|
||||
6~8 行为当前对象窗口所包含的子窗口信息,信息类型对应 2~4 行
|
||||
6~8行为当前对象窗口所包含的子窗口信息,信息类型对应2~4行
|
||||
|
||||
### 窗口获取注意事项
|
||||
|
||||
1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
完成设备驱动开发后,建议继续阅读:
|
||||
|
||||
- {doc}`add_device` - 了解如何将驱动添加到 Uni-Lab 中
|
||||
- {doc}`add_action` - 学习如何添加新的动作指令
|
||||
- {doc}`add_yaml` - 编写和完善 YAML 注册表
|
||||
|
||||
进阶主题:
|
||||
|
||||
- {doc}`03_add_device_registry` - 详细的注册表配置
|
||||
- {doc}`04_add_device_testing` - 设备测试指南
|
||||
@@ -1,202 +0,0 @@
|
||||
# 实例:电池装配工站接入(PLC 控制)
|
||||
|
||||
> **文档类型**:实际应用案例
|
||||
> **适用场景**:使用 PLC 控制的电池装配工站接入
|
||||
> **前置知识**:{doc}`../add_device` | {doc}`../add_registry`
|
||||
|
||||
本指南以电池装配工站为实际案例,引导你完成 PLC 控制设备的完整接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。
|
||||
|
||||
## 案例概述
|
||||
|
||||
**设备类型**:电池装配工站
|
||||
**通信方式**:Modbus TCP (PLC)
|
||||
**工站基类**:`WorkstationBase`
|
||||
**主要功能**:电池组装、寄存器读写、数据采集
|
||||
|
||||
## 1. 新建工站文件
|
||||
|
||||
### 1.1 创建工站文件
|
||||
|
||||
在 `unilabos/devices/workstation/coin_cell_assembly` 目录下新建工站文件,如 `coin_cell_assembly.py`。工站类需继承 `WorkstationBase`,并在构造函数中初始化通信客户端与寄存器映射。
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
# 工站基类
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
# Modbus 通讯与寄存器 CSV 支持
|
||||
from unilabos.device_comms.modbus_plc.client import TCPClient, BaseClient
|
||||
|
||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
def __init__(
|
||||
self,
|
||||
station_resource,
|
||||
address: str = "192.168.1.20",
|
||||
port: str = "502",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(station_resource=station_resource, *args, **kwargs)
|
||||
self.station_resource = station_resource # 物料台面(Deck)
|
||||
self.success: bool = False
|
||||
self.allow_data_read: bool = False
|
||||
self.csv_export_thread = None
|
||||
self.csv_export_running = False
|
||||
self.csv_export_file: Optional[str] = None
|
||||
|
||||
# 连接 PLC,并注册寄存器节点
|
||||
tcp = TCPClient(addr=address, port=port)
|
||||
tcp.client.connect()
|
||||
self.nodes = BaseClient.load_csv(".../PLC_register.csv")
|
||||
self.client = tcp.register_node_list(self.nodes)
|
||||
```
|
||||
|
||||
## 2. 编写驱动与寄存器读写
|
||||
|
||||
### 2.1 寄存器示例
|
||||
|
||||
- `COIL_SYS_START_CMD`(BOOL,地址 8010):启动命令(脉冲式)
|
||||
- `COIL_SYS_START_STATUS`(BOOL,地址 8210):启动状态
|
||||
- `REG_DATA_OPEN_CIRCUIT_VOLTAGE`(FLOAT32,地址 10002):开路电压
|
||||
- `REG_DATA_ASSEMBLY_PRESSURE`(INT16,地址 10014):压制扣电压力
|
||||
|
||||
### 2.2 最小驱动示例
|
||||
|
||||
```python
|
||||
from unilabos.device_comms.modbus_plc.modbus import WorderOrder
|
||||
|
||||
def start_and_read_metrics(self):
|
||||
# 1) 下发启动(置 True 再复位 False)
|
||||
self.client.use_node('COIL_SYS_START_CMD').write(True)
|
||||
self.client.use_node('COIL_SYS_START_CMD').write(False)
|
||||
|
||||
# 2) 等待进入启动状态
|
||||
while True:
|
||||
status, _ = self.client.use_node('COIL_SYS_START_STATUS').read(1)
|
||||
if bool(status[0]):
|
||||
break
|
||||
|
||||
# 3) 读取关键数据(FLOAT32 需读 2 个寄存器并指定字节序)
|
||||
voltage, _ = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(
|
||||
2, word_order=WorderOrder.LITTLE
|
||||
)
|
||||
pressure, _ = self.client.use_node('REG_DATA_ASSEMBLY_PRESSURE').read(1)
|
||||
|
||||
return {
|
||||
'open_circuit_voltage': voltage,
|
||||
'assembly_pressure': pressure,
|
||||
}
|
||||
```
|
||||
|
||||
> 提示:若需参数下发,可在 PLC 端设置标志寄存器并完成握手复位,避免粘连与竞争。
|
||||
|
||||
## 3. 本地生成注册表并校验
|
||||
|
||||
完成工站类与驱动后,需要生成(或更新)工站注册表供系统识别。
|
||||
|
||||
### 3.1 新增工站设备(或资源)首次生成注册表
|
||||
|
||||
首先通过以下命令启动 unilab。进入 unilab 系统状态检查页面
|
||||
|
||||
```bash
|
||||
python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
|
||||
```
|
||||
|
||||
点击注册表编辑,进入注册表编辑页面
|
||||
|
||||

|
||||
|
||||
按照图示步骤填写自动生成注册表信息:
|
||||
|
||||

|
||||
|
||||
步骤说明:
|
||||
|
||||
1. 选择新增的工站`coin_cell_assembly.py`文件
|
||||
2. 点击分析按钮,分析`coin_cell_assembly.py`文件
|
||||
3. 选择`coin_cell_assembly.py`文件中继承`WorkstationBase`类
|
||||
4. 填写新增的工站.py 文件与`unilabos`目录的距离。例如,新增的工站文件`coin_cell_assembly.py`路径为`unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly.py`,则此处填写`unilabos.devices.workstation.coin_cell_assembly`。
|
||||
5. 此处填写新定义工站的类的名字(名称可以自拟)
|
||||
6. 填写新的工站注册表备注信息
|
||||
7. 生成注册表
|
||||
|
||||
以上操作步骤完成,则会生成的新的注册表 YAML 文件,如下图:
|
||||
|
||||

|
||||
|
||||
### 3.2 添加新生成注册表
|
||||
|
||||
在`unilabos\registry\devices`目录下新建一个 yaml 文件,此处新建文件命名为`coincellassemblyworkstation_device.yaml`,将上面生成的新的注册表信息粘贴到`coincellassemblyworkstation_device.yaml`文件中。
|
||||
|
||||
在终端输入以下命令进行注册表补全操作。
|
||||
|
||||
```bash
|
||||
python unilabos\app\register.py --complete_registry
|
||||
```
|
||||
|
||||
### 3.3 启动并上传注册表
|
||||
|
||||
新增设备之后,启动 unilab 需要增加`--upload_registry`参数,来上传注册表信息。
|
||||
|
||||
```bash
|
||||
python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK> --upload_registry
|
||||
```
|
||||
|
||||
## 4. 注意事项
|
||||
|
||||
### 4.1 验证模块路径
|
||||
|
||||
在新生成的 YAML 中,确认 `module` 指向新工站类。本例中需检查 `coincellassemblyworkstation_device.yaml` 文件中是否正确指向了 `CoinCellAssemblyWorkstation` 类:
|
||||
|
||||
```yaml
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||
```
|
||||
|
||||
### 4.2 首次接入流程
|
||||
|
||||
首次新增设备(或资源)需要完整流程:
|
||||
|
||||
1. ✅ 在网页端生成注册表信息
|
||||
2. ✅ 使用 `--complete_registry` 补全注册表
|
||||
3. ✅ 使用 `--upload_registry` 上传注册表信息
|
||||
|
||||
### 4.3 驱动更新流程
|
||||
|
||||
如果不是新增设备,仅修改了工站驱动的 `.py` 文件:
|
||||
|
||||
1. ✅ 运行 `--complete_registry` 补全注册表
|
||||
2. ✅ 运行 `--upload_registry` 上传注册表
|
||||
3. ❌ 不需要在网页端重新生成注册表
|
||||
|
||||
### 4.4 PLC 通信注意事项
|
||||
|
||||
- **握手机制**:若需参数下发,建议在 PLC 端设置标志寄存器并完成握手复位,避免粘连与竞争
|
||||
- **字节序**:FLOAT32 等多字节数据类型需要正确指定字节序(如 `WorderOrder.LITTLE`)
|
||||
- **寄存器映射**:确保 CSV 文件中的寄存器地址与 PLC 实际配置一致
|
||||
- **连接稳定性**:在初始化时检查 PLC 连接状态,建议添加重连机制
|
||||
|
||||
## 5. 扩展阅读
|
||||
|
||||
### 相关文档
|
||||
|
||||
- {doc}`../add_device` - 设备驱动编写通用指南
|
||||
- {doc}`../add_registry` - 注册表配置完整指南
|
||||
- {doc}`../workstation_architecture` - 工站架构详解
|
||||
|
||||
### 技术要点
|
||||
|
||||
- **Modbus TCP 通信**:PLC 通信协议和寄存器读写
|
||||
- **WorkstationBase**:工站基类的继承和使用
|
||||
- **寄存器映射**:CSV 格式的寄存器配置
|
||||
- **注册表生成**:自动化工具使用
|
||||
|
||||
## 6. 总结
|
||||
|
||||
通过本案例,你应该掌握:
|
||||
|
||||
1. ✅ 如何创建 PLC 控制的工站驱动
|
||||
2. ✅ Modbus TCP 通信和寄存器读写
|
||||
3. ✅ 使用可视化编辑器生成注册表
|
||||
4. ✅ 注册表的补全和上传流程
|
||||
5. ✅ 新增设备与更新驱动的区别
|
||||
|
||||
这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。
|
||||
|
Before Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 66 KiB |
@@ -1,409 +0,0 @@
|
||||
# 实例:物料构建指南
|
||||
|
||||
> **文档类型**:物料系统实战指南
|
||||
> **适用场景**:工作站物料系统构建、Deck/Warehouse/Carrier/Bottle 配置
|
||||
> **前置知识**:PyLabRobot 基础 | 资源管理概念
|
||||
|
||||
## 概述
|
||||
|
||||
在UniLab-OS系统中,任何工作站中所需要用到的物料主要包括四个核心组件:
|
||||
|
||||
1. **桌子(Deck)** - 工作台面,定义整个工作空间的布局
|
||||
2. **堆栈(Warehouse)** - 存储区域,用于放置载具和物料
|
||||
3. **载具(Carriers)** - 承载瓶子等物料的容器架
|
||||
4. **瓶子(Bottles)** - 实际的物料容器
|
||||
|
||||
本文档以BioYond工作站为例,详细说明如何构建这些物料组件。
|
||||
|
||||
## 文件结构
|
||||
|
||||
物料定义文件位于 `unilabos/resources/` 文件夹中:
|
||||
|
||||
```
|
||||
unilabos/resources/bioyond/
|
||||
├── decks.py # 桌子定义
|
||||
├── YB_warehouses.py # 堆栈定义
|
||||
├── YB_bottle_carriers.py # 载具定义
|
||||
└── YB_bottles.py # 瓶子定义
|
||||
```
|
||||
|
||||
对应的注册表文件位于 `unilabos/registry/resources/bioyond/` 文件夹中:
|
||||
|
||||
```
|
||||
unilabos/registry/resources/bioyond/
|
||||
├── deck.yaml # 桌子注册表
|
||||
├── YB_bottle_carriers.yaml # 载具注册表
|
||||
└── YB_bottle.yaml # 瓶子注册表
|
||||
```
|
||||
|
||||
## 1. 桌子(Deck)构建
|
||||
|
||||
桌子是整个工作站的基础,定义了工作空间的尺寸和各个组件的位置。
|
||||
|
||||
### 代码示例 (decks.py)
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Coordinate, Deck
|
||||
from unilabos.resources.bioyond.YB_warehouses import (
|
||||
bioyond_warehouse_2x2x1,
|
||||
bioyond_warehouse_3x5x1,
|
||||
bioyond_warehouse_20x1x1,
|
||||
bioyond_warehouse_3x3x1,
|
||||
bioyond_warehouse_10x1x1
|
||||
)
|
||||
|
||||
class BIOYOND_YB_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "YB_Deck",
|
||||
size_x: float = 4150, # 桌子X方向尺寸 (mm)
|
||||
size_y: float = 1400.0, # 桌子Y方向尺寸 (mm)
|
||||
size_z: float = 2670.0, # 桌子Z方向尺寸 (mm)
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
|
||||
if setup:
|
||||
self.setup() # 当在工作站配置中setup为True时,自动创建并放置所有预定义的堆栈
|
||||
|
||||
def setup(self) -> None:
|
||||
# 定义桌子上的各个仓库区域
|
||||
self.warehouses = {
|
||||
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"),
|
||||
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
|
||||
"手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"),
|
||||
"手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"),
|
||||
"粉末加样头堆栈": bioyond_warehouse_20x1x1("粉末加样头堆栈"),
|
||||
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
|
||||
"试剂替换仓库": bioyond_warehouse_10x1x1("试剂替换仓库"),
|
||||
}
|
||||
|
||||
# 定义各个仓库在桌子上的坐标位置
|
||||
self.warehouse_locations = {
|
||||
"自动堆栈-左": Coordinate(-100.3, 171.5, 0.0),
|
||||
"自动堆栈-右": Coordinate(3960.1, 155.9, 0.0),
|
||||
"手动堆栈-左": Coordinate(-213.3, 804.4, 0.0),
|
||||
"手动堆栈-右": Coordinate(3960.1, 807.6, 0.0),
|
||||
"粉末加样头堆栈": Coordinate(415.0, 1301.0, 0.0),
|
||||
"配液站内试剂仓库": Coordinate(2162.0, 437.0, 0.0),
|
||||
"试剂替换仓库": Coordinate(1173.0, 802.0, 0.0),
|
||||
}
|
||||
|
||||
# 将仓库分配到桌子的指定位置
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
```
|
||||
|
||||
### 在工作站配置中的使用
|
||||
|
||||
当在工作站配置文件中定义桌子时,可以通过`setup`参数控制是否自动建立所有堆栈:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "YB_Bioyond_Deck",
|
||||
"name": "YB_Bioyond_Deck",
|
||||
"children": [],
|
||||
"parent": "bioyond_cell_workstation",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_YB_Deck",
|
||||
"config": {
|
||||
"type": "BIOYOND_YB_Deck",
|
||||
"setup": true
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**重要说明**:
|
||||
- 当 `"setup": true` 时,系统会自动调用桌子的 `setup()` 方法
|
||||
- 这将创建并放置所有预定义的堆栈到桌子上的指定位置
|
||||
- 如果 `"setup": false` 或省略该参数,则只创建空桌子,需要手动添加堆栈
|
||||
|
||||
### 关键要点注释
|
||||
|
||||
- `size_x`, `size_y`, `size_z`: 定义桌子的物理尺寸
|
||||
- `warehouses`: 字典类型,包含桌子上所有的仓库区域
|
||||
- `warehouse_locations`: 定义每个仓库在桌子坐标系中的位置
|
||||
- `assign_child_resource()`: 将仓库资源分配到桌子的指定位置
|
||||
- `setup()`: 可选的自动设置方法,初始化时可调用
|
||||
|
||||
## 2. 堆栈(Warehouse)构建
|
||||
|
||||
堆栈定义了存储区域的规格和布局,用于放置载具。
|
||||
|
||||
### 代码示例 (YB_warehouses.py)
|
||||
|
||||
```python
|
||||
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
|
||||
|
||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||
"""创建BioYond 1x4x4仓库
|
||||
|
||||
Args:
|
||||
name: 仓库名称
|
||||
|
||||
Returns:
|
||||
WareHouse: 仓库对象
|
||||
"""
|
||||
return YB_warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=1, # X方向位置数量
|
||||
num_items_y=4, # Y方向位置数量
|
||||
num_items_z=4, # Z方向位置数量(层数)
|
||||
dx=10.0, # X方向起始偏移
|
||||
dy=10.0, # Y方向起始偏移
|
||||
dz=10.0, # Z方向起始偏移
|
||||
item_dx=137.0, # X方向间距
|
||||
item_dy=96.0, # Y方向间距
|
||||
item_dz=120.0, # Z方向间距(层高)
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
|
||||
"""创建BioYond 2x2x1仓库(自动堆栈)"""
|
||||
return YB_warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=2,
|
||||
num_items_y=2,
|
||||
num_items_z=1, # 单层
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="YB_warehouse",
|
||||
)
|
||||
```
|
||||
|
||||
### 关键要点注释
|
||||
|
||||
- `num_items_x/y/z`: 定义仓库在各个方向的位置数量
|
||||
- `dx/dy/dz`: 第一个位置的起始偏移坐标
|
||||
- `item_dx/dy/dz`: 相邻位置之间的间距
|
||||
- `category`: 仓库类别,用于分类管理
|
||||
- `YB_warehouse_factory`: 统一的仓库创建工厂函数
|
||||
|
||||
## 3. 载具(Carriers)构建
|
||||
|
||||
载具是承载瓶子的容器架,定义了瓶子的排列方式和位置。
|
||||
|
||||
### 代码示例 (YB_bottle_carriers.py)
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||
from unilabos.resources.bioyond.YB_bottles import YB_pei_ye_xiao_Bottle
|
||||
|
||||
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
|
||||
"""配液瓶(小)板 - 4x2布局,8个位置
|
||||
|
||||
Args:
|
||||
name: 载具名称
|
||||
|
||||
Returns:
|
||||
BottleCarrier: 载具对象,包含8个配液瓶位置
|
||||
"""
|
||||
|
||||
# 载具物理尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 65.0
|
||||
|
||||
# 瓶位参数
|
||||
bottle_diameter = 35.0 # 瓶子直径
|
||||
bottle_spacing_x = 42.0 # X方向瓶子间距
|
||||
bottle_spacing_y = 35.0 # Y方向瓶子间距
|
||||
|
||||
# 计算起始位置 (居中排列)
|
||||
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||
|
||||
# 创建瓶位布局:4列x2行
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=4, # 4列
|
||||
num_items_y=2, # 2行
|
||||
dx=start_x,
|
||||
dy=start_y,
|
||||
dz=5.0, # 瓶子底部高度
|
||||
item_dx=bottle_spacing_x,
|
||||
item_dy=bottle_spacing_y,
|
||||
size_x=bottle_diameter,
|
||||
size_y=bottle_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
|
||||
# 为每个瓶位设置名称
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}"
|
||||
|
||||
# 创建载具对象
|
||||
carrier = BottleCarrier(
|
||||
name=name,
|
||||
size_x=carrier_size_x,
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_peiyepingxiaoban",
|
||||
)
|
||||
|
||||
# 设置载具布局参数
|
||||
carrier.num_items_x = 4
|
||||
carrier.num_items_y = 2
|
||||
carrier.num_items_z = 1
|
||||
|
||||
# 定义瓶子排列顺序
|
||||
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
||||
|
||||
# 为每个位置创建瓶子实例
|
||||
for i in range(8):
|
||||
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
|
||||
|
||||
return carrier
|
||||
```
|
||||
|
||||
### 关键要点注释
|
||||
|
||||
- `carrier_size_x/y/z`: 载具的物理尺寸
|
||||
- `bottle_diameter`: 瓶子的直径,用于计算瓶位大小
|
||||
- `bottle_spacing_x/y`: 瓶子之间的间距
|
||||
- `create_ordered_items_2d`: 创建二维排列的瓶位
|
||||
- `sites`: 瓶位字典,存储所有瓶子位置信息
|
||||
- `ordering`: 定义瓶位的命名规则(如A1, A2, B1等)
|
||||
|
||||
## 4. 瓶子(Bottles)构建
|
||||
|
||||
瓶子是最终的物料容器,定义了容器的物理属性。
|
||||
|
||||
### 代码示例 (YB_bottles.py)
|
||||
|
||||
```python
|
||||
from unilabos.resources.itemized_carrier import Bottle
|
||||
|
||||
def YB_pei_ye_xiao_Bottle(
|
||||
name: str,
|
||||
diameter: float = 35.0, # 瓶子直径 (mm)
|
||||
height: float = 60.0, # 瓶子高度 (mm)
|
||||
max_volume: float = 30000.0, # 最大容量 (μL) - 30mL
|
||||
barcode: str = None, # 条码
|
||||
) -> Bottle:
|
||||
"""创建配液瓶(小)
|
||||
|
||||
Args:
|
||||
name: 瓶子名称
|
||||
diameter: 瓶子直径
|
||||
height: 瓶子高度
|
||||
max_volume: 最大容量(微升)
|
||||
barcode: 条码标识
|
||||
|
||||
Returns:
|
||||
Bottle: 瓶子对象
|
||||
"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_pei_ye_xiao_Bottle",
|
||||
)
|
||||
|
||||
def YB_ye_Bottle(
|
||||
name: str,
|
||||
diameter: float = 40.0,
|
||||
height: float = 70.0,
|
||||
max_volume: float = 50000.0, # 最大容量
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建液体瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_ye_Bottle",
|
||||
)
|
||||
```
|
||||
|
||||
### 关键要点注释
|
||||
|
||||
- `diameter`: 瓶子直径,影响瓶位大小计算
|
||||
- `height`: 瓶子高度,用于碰撞检测和移液计算
|
||||
- `max_volume`: 最大容量,单位为微升(μL)
|
||||
- `barcode`: 条码标识,用于瓶子追踪
|
||||
- `model`: 型号标识,用于区分不同类型的瓶子
|
||||
|
||||
## 5. 注册表配置
|
||||
|
||||
创建完物料定义后,需要在注册表中注册这些物料,使系统能够识别和使用它们。
|
||||
|
||||
在 `unilabos/registry/resources/bioyond/` 目录下创建:
|
||||
|
||||
- `deck.yaml` - 桌子注册表
|
||||
- `YB_bottle_carriers.yaml` - 载具注册表
|
||||
- `YB_bottle.yaml` - 瓶子注册表
|
||||
|
||||
### 5.1 桌子注册表 (deck.yaml)
|
||||
|
||||
```yaml
|
||||
BIOYOND_YB_Deck:
|
||||
category:
|
||||
- deck # 前端显示的分类存放
|
||||
class:
|
||||
module: unilabos.resources.bioyond.decks:BIOYOND_YB_Deck # 定义桌子的类的路径
|
||||
type: pylabrobot
|
||||
description: BIOYOND_YB_Deck # 描述信息
|
||||
handles: []
|
||||
icon: 配液站.webp # 图标文件
|
||||
init_param_schema: {}
|
||||
registry_type: resource # 注册类型
|
||||
version: 1.0.0 # 版本号
|
||||
```
|
||||
|
||||
### 5.2 载具注册表 (YB_bottle_carriers.yaml)
|
||||
|
||||
```yaml
|
||||
YB_peiyepingxiaoban:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
|
||||
type: pylabrobot
|
||||
description: YB_peiyepingxiaoban
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
### 5.3 瓶子注册表 (YB_bottle.yaml)
|
||||
|
||||
```yaml
|
||||
YB_pei_ye_xiao_Bottle:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
|
||||
type: pylabrobot
|
||||
description: YB_pei_ye_xiao_Bottle
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
### 注册表关键要点注释
|
||||
|
||||
- `category`: 物料分类,用于在云端(网页界面)中的分类中显示
|
||||
- `module`: Python模块路径,格式为 `模块路径:类名`
|
||||
- `type`: 框架类型,通常为 `pylabrobot`(默认即可)
|
||||
- `description`: 描述信息,显示在用户界面中
|
||||
- `icon`: (名称唯一自动匹配后端上传的图标文件名,显示在云端)
|
||||
- `registry_type`: 固定为 `resource`
|
||||
- `version`: 版本号,用于版本管理
|
||||
@@ -1,413 +0,0 @@
|
||||
# 实例:物料教程(Resource)
|
||||
|
||||
> **文档类型**:物料系统完整教程
|
||||
> **适用场景**:物料格式转换、多系统物料对接、资源结构理解
|
||||
> **前置知识**:Python 基础 | JSON 数据结构
|
||||
|
||||
本教程面向 Uni-Lab-OS 的开发者,讲解"物料"的核心概念、3种物料格式(UniLab、PyLabRobot、奔耀Bioyond)及其相互转换方法,并说明4种 children 结构表现形式及使用场景。
|
||||
|
||||
---
|
||||
|
||||
## 1. 物料是什么
|
||||
|
||||
- **物料(Resource)**:指实验工作站中的实体对象,包括设备(device)、操作甲板 (deck)、试剂、实验耗材,也包括设备上承载的具体物料或者包含的容器(如container/plate/well/瓶/孔/片等)。
|
||||
- **物料基本信息**(以 UniLab list格式为例):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"id": "plate", // 某一类物料的唯一名称
|
||||
"name": "50ml瓶装试剂托盘", // 在云端显示的名称
|
||||
"sample_id": null, // 同类物料的不同样品
|
||||
"children": [
|
||||
"50ml试剂瓶" // 表示托盘上有一个 50ml 试剂瓶
|
||||
],
|
||||
"parent": "deck", // 此物料放置在 deck 上
|
||||
"type": "plate", // 物料类型
|
||||
"class": "plate", // 物料对应的注册/类名
|
||||
"position": {
|
||||
"x": 0, // 初始放置位置
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": { // 固有配置(尺寸、旋转等)
|
||||
"size_x": 400.0,
|
||||
"size_y": 400.0,
|
||||
"size_z": 400.0,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"bottle_number": 1 // 动态数据(可变化)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 3种物料格式概览(UniLab、PyLabRobot、奔耀Bioyond)
|
||||
|
||||
### 2.1 UniLab 物料格式(云端/项目内通用)
|
||||
|
||||
- 结构特征:顶层通常是 `nodes` 列表;每个节点是扁平字典,`children` 是子节点 `id` 列表;`parent` 为父节点 `id` 或 `null`。
|
||||
- 用途:
|
||||
- 云端数据存储、前端可视化、与图结构算法互操作
|
||||
- 在上传/下载/部署配置时作为标准交换格式
|
||||
|
||||
示例片段(UniLab 物料格式):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"nodes": [
|
||||
|
||||
{
|
||||
"id": "a",
|
||||
"name": "name_a",
|
||||
"sample_id": 1,
|
||||
"type": "deck",
|
||||
"class": "deck",
|
||||
"parent": null,
|
||||
"children": ["b1"],
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"config": {},
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
|
||||
"id": "b1",
|
||||
"name": "name_b1",
|
||||
"sample_id": 1,
|
||||
"type": "plate",
|
||||
"class": "plate",
|
||||
"parent": "a1",
|
||||
"children": [],
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"config": {},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 PyLabRobot(PLR)物料格式(实验流程运行时)
|
||||
|
||||
- 结构特征:严格的层级树,`children` 为“子资源字典列表”(每个子节点本身是完整对象)。
|
||||
- 用途:
|
||||
- 实验流程执行与调度,PLR 运行时期望的资源对象格式
|
||||
- 通过 `Resource.deserialize/serialize`、`load_all_state/serialize_all_state` 与对象交互
|
||||
|
||||
示例片段(PRL 物料格式)::
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "deck",
|
||||
"type": "Deck",
|
||||
"category": "deck",
|
||||
"location": {"x": 0, "y": 0, "z": 0, "type": "Coordinate"},
|
||||
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
|
||||
"parent_name": null,
|
||||
"children": [
|
||||
{
|
||||
"name": "plate_1",
|
||||
"type": "Plate",
|
||||
"category": "plate_96",
|
||||
"location": {"x": 100, "y": 0, "z": 0, "type": "Coordinate"},
|
||||
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
|
||||
"parent_name": "deck",
|
||||
"children": [
|
||||
{
|
||||
"name": "A1",
|
||||
"type": "Well",
|
||||
"category": "well",
|
||||
"location": {"x": 0, "y": 0, "z": 0, "type": "Coordinate"},
|
||||
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
|
||||
"parent_name": "plate_1",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 2.3 奔耀 Bioyond 物料格式(第三方来源)
|
||||
一般是厂商自己定义的json格式和字段,信息需要提取和对应。以下为示例说明。
|
||||
|
||||
- 结构特征:顶层 `data` 列表,每项包含 `typeName`、`code`、`barCode`、`name`、`quantity`、`unit`、`locations`(仓位 `whName`、`x/y/z`)、`detail`(细粒度内容,如瓶内液体或孔位物料)。
|
||||
- 用途:
|
||||
- 第三方 WMS/设备的物料清单输入
|
||||
- 需要自定义映射表将 `typeName` → PLR 类名,对 `locations`/`detail` 进行落位/赋值
|
||||
|
||||
示例片段(奔耀Bioyond 物料格式):
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "3a1b5c10-d4f3-01ac-1e64-5b4be2add4b1",
|
||||
"typeName": "液",
|
||||
"code": "0006-00014",
|
||||
"barCode": "",
|
||||
"name": "EMC",
|
||||
"quantity": 50,
|
||||
"lockQuantity": 2.057,
|
||||
"unit": "瓶",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
|
||||
"whid": "3a19da43-57b4-a2a8-3f52-91dbbeb836db",
|
||||
"whName": "配液站内试剂仓库",
|
||||
"code": "0003-0003",
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"code": "0006-00014-01",
|
||||
"name": "EMC-瓶-1",
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"quantity": 500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 0
|
||||
}
|
||||
```
|
||||
### 2.4 3种物料格式关键字段对应(UniLab、PyLabRobot、奔耀Bioyond)
|
||||
|
||||
| 含义 | UniLab | PyLabRobot (PLR) | 奔耀 Bioyond |
|
||||
| - | - | - | - |
|
||||
| 节点唯一名 | `id` | `name` | `name` |
|
||||
| 父节点引用 | `parent` | `parent_name` | `locations` 坐标(无直接父名,需映射坐标下的物料) |
|
||||
| 子节点集合 | `children`(id 列表或对象列表,视结构而定) | `children`(对象列表) | `detail`(明细,非严格树结构,需要自定义映射) |
|
||||
| 类型(抽象类别) | `type`(device/container/plate/deck/…) | `category`(plate/well/…),以及类名 `type` | `typeName`(厂商自定义,如“液”、“加样头(大)”) |
|
||||
| 运行/业务数据 | `data` | 通过 `serialize_all_state()`/`load_all_state()` 管理的状态 | `quantity`、`lockQuantity` 等业务数值 |
|
||||
| 固有配置 | `config`(size_x/size_y/size_z/model/ordering…) | 资源字典中的同名键(反序列化时按构造签名取用) | 厂商自定义字段(需映射入 PLR/UniLab 的 `config` 或 `data`) |
|
||||
| 空间位置 | `position`(x/y/z) | `location`(Coordinate) + `rotation`(Rotation) | `locations`(whName、x/y/z),不含旋转 |
|
||||
| 条码/标识 | `config.barcode`(可选) | 常放在配置键中(如 `barcode`) | `barCode` |
|
||||
| 数量单位 | 无固定键,通常在 `data` | 无固定键,通常在配置或状态中 | `unit` |
|
||||
| 物料编码 | 通常在 `config` 或 `data` 自定义 | 通常在配置中自定义 | `code` |
|
||||
|
||||
说明:
|
||||
- Bioyond 不提供显式的树形父子关系,通常通过 `locations` 将物料落位到某仓位/坐标。用 `detail` 表示子级明细。
|
||||
|
||||
---
|
||||
|
||||
## 3. children 的四种结构表示
|
||||
|
||||
- **list(扁平列表)**:每个节点是扁平字典,`children` 为子节点 `id` 数组。示例:UniLab `nodes` 中的单个节点。
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{ "id": "root", "parent": null, "children": ["child1"] },
|
||||
{ "id": "child1", "parent": "root", "children": [] }
|
||||
]
|
||||
}
|
||||
```
|
||||
- **dict(嵌套字典)**:节点的 `children` 是 `{ child_id: child_node_dict }` 字典。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "root",
|
||||
"parent": null,
|
||||
"children": {
|
||||
"child1": { "id": "child1", "parent": "root", "children": {} }
|
||||
}
|
||||
}
|
||||
```
|
||||
- **tree(树形列表)**:顶层是 `[root_node, ...]`,每个 `node.children` 是“子节点对象列表”(而非 id 列表)。
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"parent": null,
|
||||
"children": [
|
||||
{ "id": "child1", "parent": "root", "children": [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **nestdict(顶层嵌套字典)**:顶层是 `{root_id: root_node, ...}`,或者根节点自身带 `children: {id: node}` 形态。
|
||||
|
||||
```json
|
||||
{
|
||||
"root": {
|
||||
"id": "root",
|
||||
"parent": null,
|
||||
"children": {
|
||||
"child1": { "id": "child1", "parent": "root", "children": {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这些结构之间可使用 `graphio.py` 中的工具函数互转(见下一节)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 转换函数及调用
|
||||
|
||||
核心代码文件:`unilabos/resources/graphio.py`
|
||||
|
||||
### 4.1 结构互转(list/dict/tree/nestdict)
|
||||
|
||||
代码引用:
|
||||
|
||||
```217:239:unilabos/resources/graphio.py
|
||||
def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
# ... 由扁平 dict(id->node)生成树(children 为对象列表)
|
||||
```
|
||||
|
||||
```241:267:unilabos/resources/graphio.py
|
||||
def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
|
||||
# ... 由扁平 dict 生成嵌套字典(children 为 {id:node})
|
||||
```
|
||||
|
||||
```270:273:unilabos/resources/graphio.py
|
||||
def list_to_nested_dict(nodes: list[dict]) -> dict:
|
||||
# ... 由扁平列表(children 为 id 列表)转嵌套字典
|
||||
```
|
||||
|
||||
```275:286:unilabos/resources/graphio.py
|
||||
def tree_to_list(tree: list[dict]) -> list[dict]:
|
||||
# ... 由树形列表转回扁平列表(children 还原为 id 列表)
|
||||
```
|
||||
|
||||
```289:337:unilabos/resources/graphio.py
|
||||
def nested_dict_to_list(nested_dict: dict) -> list[dict]:
|
||||
# ... 由嵌套字典转回扁平列表
|
||||
```
|
||||
|
||||
常见路径:
|
||||
|
||||
- UniLab 扁平列表 → 树:`dict_to_tree({r["id"]: r for r in resources})`
|
||||
- 树 → UniLab 扁平列表:`tree_to_list(resources_tree)`
|
||||
- 扁平列表 ↔ 嵌套字典:`list_to_nested_dict` / `nested_dict_to_list`
|
||||
|
||||
### 4.2 UniLab ↔ PyLabRobot(PLR)
|
||||
|
||||
高层封装:
|
||||
|
||||
```339:368:unilabos/resources/graphio.py
|
||||
def convert_resources_to_type(resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False):
|
||||
# UniLab -> (NestedDict or PLR)
|
||||
```
|
||||
|
||||
```371:395:unilabos/resources/graphio.py
|
||||
def convert_resources_from_type(resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False):
|
||||
# (NestedDict or PLR) -> UniLab 扁平列表
|
||||
```
|
||||
|
||||
底层转换:
|
||||
|
||||
```398:441:unilabos/resources/graphio.py
|
||||
def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
|
||||
# UniLab 单节点(树根) -> PLR Resource 对象
|
||||
```
|
||||
|
||||
```443:481:unilabos/resources/graphio.py
|
||||
def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, with_children=True):
|
||||
# PLR Resource -> UniLab 单节点(dict)
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
|
||||
# UniLab 扁平列表 -> PLR 根资源对象
|
||||
plr_root = convert_resources_to_type(resources_list=ulab_list, resource_type=ResourcePLR)
|
||||
|
||||
# PLR 资源对象 -> UniLab 扁平列表(用于保存/上传)
|
||||
ulab_flat = convert_resources_from_type(resources_list=plr_root, resource_type=ResourcePLR)
|
||||
```
|
||||
|
||||
可选项:
|
||||
|
||||
- `plr_model=True`:保留 `model` 字段(默认会移除)。
|
||||
- `with_children=False`:`resource_plr_to_ulab` 仅转换当前节点。
|
||||
|
||||
### 4.3 奔耀(Bioyond)→ PLR(及进一步到 UniLab)
|
||||
|
||||
转换入口:
|
||||
|
||||
```483:527:unilabos/resources/graphio.py
|
||||
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
|
||||
# Bioyond 列表 -> PLR 资源列表,并可根据 deck.warehouses 将资源落位
|
||||
```
|
||||
|
||||
使用示例:
|
||||
|
||||
```python
|
||||
import json
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr, convert_resources_from_type
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
|
||||
resp = json.load(open("unilabos/devices/workstation/bioyond_cell/bioyond_test_yibin.json", encoding="utf-8"))
|
||||
materials = resp["data"]
|
||||
|
||||
# 将第三方类型name映射到 PLR 资源类名(需根据现场定义)
|
||||
type_mapping = {
|
||||
"液": "RegularContainer",
|
||||
"加样头(大)": "RegularContainer"
|
||||
}
|
||||
|
||||
plr_list = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=None)
|
||||
|
||||
# 如需上传云端(UniLab 扁平格式):
|
||||
ulab_flat = convert_resources_from_type(plr_list, [ResourcePLR])
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `type_mapping` 必须由开发者根据设备/物料种类人工维护。
|
||||
- 如传入 `deck`,且 `deck.warehouses` 命名与 `whName` 对应,可将物料安放到仓库坐标(x/y/z)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 何时使用哪种格式
|
||||
|
||||
- **云端/持久化**:使用 UniLab 物料格式(扁平 `nodes` 列表,children 为 id 列表)。便于版本化、可视化与网络传输。
|
||||
- **实验工作流执行**:使用 PyLabRobot(PLR)格式。PLR 运行时依赖严格的树形资源结构与对象 API。
|
||||
- **第三方设备/系统(Bioyond)输入**:保持来源格式不变,使用 `resource_bioyond_to_plr` + 人工 `type_mapping` 将其转换为 PLR(必要时再转 UniLab)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题与注意事项
|
||||
|
||||
- **children 形态不一致**:不同函数期望不同 children 形态,注意在进入转换前先用“结构互转”工具函数标准化形态。
|
||||
- **devices_only**:`dict_to_tree/dict_to_nested_dict` 支持仅保留 `type == device` 的节点。
|
||||
- **模型/类型字段**:PLR 对象序列化参数有所差异,`resource_ulab_to_plr` 内部会根据构造签名移除不兼容字段(如 `category`)。
|
||||
- **驱动初始化**:`initialize_resource(s)` 支持从注册表/类路径创建 PLR/UniLab 资源或列表。
|
||||
|
||||
参考代码:
|
||||
|
||||
```530:577:unilabos/resources/graphio.py
|
||||
def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]:
|
||||
# 从注册类/模块反射创建资源,或将 UniLab 字典包装为列表
|
||||
```
|
||||
|
||||
```580:597:unilabos/resources/graphio.py
|
||||
def initialize_resources(resources_config) -> list[dict]:
|
||||
# 批量初始化
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,782 +0,0 @@
|
||||
# 实例:工作站模板架构设计与对接指南
|
||||
|
||||
> **文档类型**:架构设计指南与实战案例
|
||||
> **适用场景**:大型工作站接入、子设备管理、物料系统集成
|
||||
> **前置知识**:{doc}`../add_device` | {doc}`../add_registry`
|
||||
|
||||
## 0. 问题简介
|
||||
|
||||
我们可以从以下几类例子,来理解对接大型工作站需要哪些设计。本文档之后的实战案例也将由这些组成。
|
||||
|
||||
### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合;
|
||||
|
||||
1. 工作站包含大量已经注册的子设备,可能各自通信组态很不相同;部分设备可能会拥有同一个通信设备作为出口,如 2 个泵共用 1 个串口、所有设备共同接入 PLC 等。
|
||||
2. 任务系统是统一实现的 protocols,protocols 中会将高层指令处理成各子设备配合的工作流 json 并管理执行、同时更改物料信息
|
||||
3. 物料系统较为简单直接,如常量有机化学仅为工作站内固定的瓶子,初始化时就已固定;随后在任务执行过程中,记录试剂量更改信息
|
||||
|
||||
### 0.2 移液工作站:物料系统和工作流模板管理
|
||||
|
||||

|
||||
|
||||
1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供
|
||||
2. 所有任务系统均由工作站本身实现并下发指令,有统一的抽象函数可实现(pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。
|
||||
3. 物料系统为固定的板位系统:台面上有多个可摆放位置,摆放标准孔板。
|
||||
|
||||
### 0.3 厂家开发的定制大型工站
|
||||
|
||||

|
||||
|
||||
由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信
|
||||
|
||||
1. 在监控状态时,希望展现子设备的状态;但子设备仅为逻辑概念,通信由工作站上位机接口提供;部分情况下,子设备状态是被记录在文件中的,需要读取
|
||||
2. 工作站有自己的工作流系统甚至调度系统;可以通过脚本/PLC 连续读写来配置工作站可用的工作流;
|
||||
3. 部分拥有完善的物料入库、出库、过程记录,需要与 Uni-Lab-OS 物料系统对接
|
||||
|
||||
## 1. 整体架构图
|
||||
|
||||
### 1.1 工作站核心架构
|
||||
|
||||
```{mermaid}
|
||||
graph TB
|
||||
subgraph "工作站模板组成"
|
||||
WB[WorkstationBase<br/>工作流状态管理]
|
||||
RPN[ROS2WorkstationNode<br/>Protocol执行引擎]
|
||||
WB -.post_init关联.-> RPN
|
||||
end
|
||||
|
||||
subgraph "物料管理系统"
|
||||
DECK[Deck<br/>PLR本地物料系统]
|
||||
RS[ResourceSynchronizer<br/>外部物料同步器]
|
||||
WB --> DECK
|
||||
WB --> RS
|
||||
RS --> DECK
|
||||
end
|
||||
|
||||
subgraph "通信与子设备管理"
|
||||
HW[hardware_interface<br/>硬件通信接口]
|
||||
SUBDEV[子设备集合<br/>pumps/grippers/sensors]
|
||||
WB --> HW
|
||||
RPN --> SUBDEV
|
||||
HW -.代理模式.-> RPN
|
||||
end
|
||||
|
||||
subgraph "工作流任务系统"
|
||||
PROTO[Protocol定义<br/>LiquidHandling/PlateHandling]
|
||||
WORKFLOW[Workflow执行器<br/>步骤管理与编排]
|
||||
RPN --> PROTO
|
||||
RPN --> WORKFLOW
|
||||
WORKFLOW --> SUBDEV
|
||||
end
|
||||
```
|
||||
|
||||
### 1.2 外部系统对接关系
|
||||
|
||||
```{mermaid}
|
||||
graph LR
|
||||
subgraph "Uni-Lab-OS工作站"
|
||||
WS[WorkstationBase + ROS2WorkstationNode]
|
||||
DECK2[物料系统<br/>Deck]
|
||||
HW2[通信接口<br/>hardware_interface]
|
||||
HTTP[HTTP服务<br/>WorkstationHTTPService]
|
||||
end
|
||||
|
||||
subgraph "外部物料系统"
|
||||
BIOYOND[Bioyond物料管理]
|
||||
LIMS[LIMS系统]
|
||||
WAREHOUSE[第三方仓储]
|
||||
end
|
||||
|
||||
subgraph "外部硬件系统"
|
||||
PLC[PLC设备]
|
||||
SERIAL[串口设备]
|
||||
ROBOT[机械臂/机器人]
|
||||
end
|
||||
|
||||
subgraph "云端系统"
|
||||
CLOUD[UniLab云端<br/>资源管理]
|
||||
MONITOR[监控与调度]
|
||||
end
|
||||
|
||||
BIOYOND <-->|RPC双向同步| DECK2
|
||||
LIMS -->|HTTP报送| HTTP
|
||||
WAREHOUSE <-->|API对接| DECK2
|
||||
|
||||
PLC <-->|Modbus TCP| HW2
|
||||
SERIAL <-->|串口通信| HW2
|
||||
ROBOT <-->|SDK/API| HW2
|
||||
|
||||
WS -->|ROS消息| CLOUD
|
||||
CLOUD -->|任务下发| WS
|
||||
MONITOR -->|状态查询| WS
|
||||
```
|
||||
|
||||
### 1.3 具体实现示例
|
||||
|
||||
```{mermaid}
|
||||
graph TB
|
||||
subgraph "工作站基类"
|
||||
BASE[WorkstationBase<br/>抽象基类]
|
||||
end
|
||||
|
||||
subgraph "Bioyond集成工作站"
|
||||
BW[BioyondWorkstation]
|
||||
BW_DECK[Deck + Warehouses]
|
||||
BW_SYNC[BioyondResourceSynchronizer]
|
||||
BW_HW[BioyondV1RPC]
|
||||
BW_HTTP[HTTP报送服务]
|
||||
|
||||
BW --> BW_DECK
|
||||
BW --> BW_SYNC
|
||||
BW --> BW_HW
|
||||
BW --> BW_HTTP
|
||||
end
|
||||
|
||||
subgraph "纯协议节点"
|
||||
PN[ProtocolNode]
|
||||
PN_SUB[子设备集合]
|
||||
PN_PROTO[Protocol工作流]
|
||||
|
||||
PN --> PN_SUB
|
||||
PN --> PN_PROTO
|
||||
end
|
||||
|
||||
subgraph "PLC控制工作站"
|
||||
PW[PLCWorkstation]
|
||||
PW_DECK[Deck物料系统]
|
||||
PW_PLC[Modbus PLC客户端]
|
||||
PW_WF[工作流定义]
|
||||
|
||||
PW --> PW_DECK
|
||||
PW --> PW_PLC
|
||||
PW --> PW_WF
|
||||
end
|
||||
|
||||
BASE -.继承.-> BW
|
||||
BASE -.继承.-> PN
|
||||
BASE -.继承.-> PW
|
||||
```
|
||||
|
||||
## 2. 类关系图
|
||||
|
||||
```{mermaid}
|
||||
classDiagram
|
||||
class WorkstationBase {
|
||||
<<abstract>>
|
||||
+_ros_node: ROS2WorkstationNode
|
||||
+deck: Deck
|
||||
+plr_resources: Dict[str, PLRResource]
|
||||
+resource_synchronizer: ResourceSynchronizer
|
||||
+hardware_interface: Union[Any, str]
|
||||
+current_workflow_status: WorkflowStatus
|
||||
+supported_workflows: Dict[str, WorkflowInfo]
|
||||
|
||||
+post_init(ros_node)*
|
||||
+set_hardware_interface(interface)
|
||||
+call_device_method(method, *args, **kwargs)
|
||||
+get_device_status()
|
||||
+is_device_available()
|
||||
|
||||
+get_deck()
|
||||
+get_all_resources()
|
||||
+find_resource_by_name(name)
|
||||
+find_resources_by_type(type)
|
||||
+sync_with_external_system()
|
||||
|
||||
+execute_workflow(name, params)
|
||||
+stop_workflow(emergency)
|
||||
+workflow_status
|
||||
+is_busy
|
||||
}
|
||||
|
||||
class ROS2WorkstationNode {
|
||||
+device_id: str
|
||||
+children: Dict[str, Any]
|
||||
+sub_devices: Dict
|
||||
+protocol_names: List[str]
|
||||
+_action_clients: Dict
|
||||
+_action_servers: Dict
|
||||
+resource_tracker: DeviceNodeResourceTracker
|
||||
|
||||
+initialize_device(device_id, config)
|
||||
+create_ros_action_server(action_name, mapping)
|
||||
+execute_single_action(device_id, action, kwargs)
|
||||
+update_resource(resources)
|
||||
+transfer_resource_to_another(resources, target, sites)
|
||||
+_setup_hardware_proxy(device, comm_device, read, write)
|
||||
}
|
||||
|
||||
%% 物料管理相关类
|
||||
class Deck {
|
||||
+name: str
|
||||
+children: List
|
||||
+assign_child_resource()
|
||||
}
|
||||
|
||||
class ResourceSynchronizer {
|
||||
<<abstract>>
|
||||
+workstation: WorkstationBase
|
||||
+sync_from_external()*
|
||||
+sync_to_external(plr_resource)*
|
||||
+handle_external_change(change_info)*
|
||||
}
|
||||
|
||||
class BioyondResourceSynchronizer {
|
||||
+bioyond_api_client: BioyondV1RPC
|
||||
+sync_interval: int
|
||||
+last_sync_time: float
|
||||
|
||||
+initialize()
|
||||
+sync_from_external()
|
||||
+sync_to_external(resource)
|
||||
+handle_external_change(change_info)
|
||||
}
|
||||
|
||||
%% 硬件接口相关类
|
||||
class HardwareInterface {
|
||||
<<interface>>
|
||||
}
|
||||
|
||||
class BioyondV1RPC {
|
||||
+base_url: str
|
||||
+api_key: str
|
||||
+stock_material()
|
||||
+add_material()
|
||||
+material_inbound()
|
||||
}
|
||||
|
||||
%% 服务类
|
||||
class WorkstationHTTPService {
|
||||
+workstation: WorkstationBase
|
||||
+host: str
|
||||
+port: int
|
||||
+server: HTTPServer
|
||||
+running: bool
|
||||
|
||||
+start()
|
||||
+stop()
|
||||
+_handle_step_finish_report()
|
||||
+_handle_sample_finish_report()
|
||||
+_handle_order_finish_report()
|
||||
+_handle_material_change_report()
|
||||
+_handle_error_handling_report()
|
||||
}
|
||||
|
||||
%% 具体实现类
|
||||
class BioyondWorkstation {
|
||||
+bioyond_config: Dict
|
||||
+workflow_mappings: Dict
|
||||
+workflow_sequence: List
|
||||
|
||||
+post_init(ros_node)
|
||||
+transfer_resource_to_another()
|
||||
+resource_tree_add(resources)
|
||||
+append_to_workflow_sequence(name)
|
||||
+get_all_workflows()
|
||||
+get_bioyond_status()
|
||||
}
|
||||
|
||||
class ProtocolNode {
|
||||
+post_init(ros_node)
|
||||
}
|
||||
|
||||
%% 核心关系
|
||||
WorkstationBase o-- ROS2WorkstationNode : post_init关联
|
||||
WorkstationBase o-- WorkstationHTTPService : 可选服务
|
||||
|
||||
%% 物料管理侧
|
||||
WorkstationBase *-- Deck : deck
|
||||
WorkstationBase *-- ResourceSynchronizer : 可选组合
|
||||
ResourceSynchronizer <|-- BioyondResourceSynchronizer
|
||||
|
||||
%% 硬件接口侧
|
||||
WorkstationBase o-- HardwareInterface : hardware_interface
|
||||
HardwareInterface <|.. BioyondV1RPC : 实现
|
||||
BioyondResourceSynchronizer --> BioyondV1RPC : 使用
|
||||
|
||||
%% 继承关系
|
||||
BioyondWorkstation --|> WorkstationBase
|
||||
ProtocolNode --|> WorkstationBase
|
||||
ROS2WorkstationNode --|> BaseROS2DeviceNode : 继承
|
||||
```
|
||||
|
||||
## 3. 工作站启动时序图
|
||||
|
||||
```{mermaid}
|
||||
sequenceDiagram
|
||||
participant APP as Application
|
||||
participant WS as WorkstationBase
|
||||
participant DECK as PLR Deck
|
||||
participant SYNC as ResourceSynchronizer
|
||||
participant HW as HardwareInterface
|
||||
participant ROS as ROS2WorkstationNode
|
||||
participant HTTP as HTTPService
|
||||
|
||||
APP->>WS: 创建工作站实例(__init__)
|
||||
WS->>DECK: 初始化PLR Deck
|
||||
DECK->>DECK: 创建Warehouse等子资源
|
||||
DECK-->>WS: Deck创建完成
|
||||
|
||||
WS->>HW: 创建硬件接口(如BioyondV1RPC)
|
||||
HW->>HW: 建立连接(PLC/RPC/串口等)
|
||||
HW-->>WS: 硬件接口就绪
|
||||
|
||||
WS->>SYNC: 创建ResourceSynchronizer(可选)
|
||||
SYNC->>HW: 使用hardware_interface
|
||||
SYNC->>SYNC: 初始化同步配置
|
||||
SYNC-->>WS: 同步器创建完成
|
||||
|
||||
WS->>SYNC: sync_from_external()
|
||||
SYNC->>HW: 查询外部物料系统
|
||||
HW-->>SYNC: 返回物料数据
|
||||
SYNC->>DECK: 转换并添加到Deck
|
||||
SYNC-->>WS: 同步完成
|
||||
|
||||
Note over WS: __init__完成,等待ROS节点
|
||||
|
||||
APP->>ROS: 初始化ROS2WorkstationNode
|
||||
ROS->>ROS: 初始化子设备(children)
|
||||
ROS->>ROS: 创建Action客户端
|
||||
ROS->>ROS: 设置硬件接口代理
|
||||
ROS-->>APP: ROS节点就绪
|
||||
|
||||
APP->>WS: post_init(ros_node)
|
||||
WS->>WS: self._ros_node = ros_node
|
||||
WS->>ROS: update_resource([deck])
|
||||
ROS->>ROS: 上传物料到云端
|
||||
ROS-->>WS: 上传完成
|
||||
|
||||
WS->>HTTP: 创建WorkstationHTTPService(可选)
|
||||
HTTP->>HTTP: 启动HTTP服务器线程
|
||||
HTTP-->>WS: HTTP服务启动
|
||||
|
||||
WS-->>APP: 工作站完全就绪
|
||||
```
|
||||
|
||||
## 4. 工作流执行时序图(Protocol 模式)
|
||||
|
||||
```{mermaid}
|
||||
sequenceDiagram
|
||||
participant CLIENT as 客户端
|
||||
participant ROS as ROS2WorkstationNode
|
||||
participant WS as WorkstationBase
|
||||
participant HW as HardwareInterface
|
||||
participant DECK as PLR Deck
|
||||
participant CLOUD as 云端资源管理
|
||||
participant DEV as 子设备
|
||||
|
||||
CLIENT->>ROS: 发送Protocol Action请求
|
||||
ROS->>ROS: execute_protocol回调
|
||||
ROS->>ROS: 从Goal提取参数
|
||||
ROS->>ROS: 调用protocol_steps_generator
|
||||
ROS->>ROS: 生成action步骤列表
|
||||
|
||||
ROS->>WS: 更新workflow_status = RUNNING
|
||||
|
||||
loop 执行每个步骤
|
||||
alt 调用子设备
|
||||
ROS->>ROS: execute_single_action(device_id, action, params)
|
||||
ROS->>DEV: 发送Action Goal(通过Action Client)
|
||||
DEV->>DEV: 执行设备动作
|
||||
DEV-->>ROS: 返回Result
|
||||
else 调用工作站自身
|
||||
ROS->>WS: call_device_method(method, *args)
|
||||
alt 直接模式
|
||||
WS->>HW: 调用hardware_interface方法
|
||||
HW->>HW: 执行硬件操作
|
||||
HW-->>WS: 返回结果
|
||||
else 代理模式
|
||||
WS->>ROS: 转发到子设备
|
||||
ROS->>DEV: 调用子设备方法
|
||||
DEV-->>ROS: 返回结果
|
||||
ROS-->>WS: 返回结果
|
||||
end
|
||||
WS-->>ROS: 返回结果
|
||||
end
|
||||
|
||||
ROS->>DECK: 更新本地物料状态
|
||||
DECK->>DECK: 修改PLR资源属性
|
||||
end
|
||||
|
||||
ROS->>CLOUD: 同步物料到云端(可选)
|
||||
CLOUD-->>ROS: 同步完成
|
||||
|
||||
ROS->>WS: 更新workflow_status = COMPLETED
|
||||
ROS-->>CLIENT: 返回Protocol Result
|
||||
```
|
||||
|
||||
## 5. HTTP 报送处理时序图
|
||||
|
||||
```{mermaid}
|
||||
sequenceDiagram
|
||||
participant EXT as 外部工作站/LIMS
|
||||
participant HTTP as HTTPService
|
||||
participant WS as WorkstationBase
|
||||
participant DECK as PLR Deck
|
||||
participant SYNC as ResourceSynchronizer
|
||||
participant CLOUD as 云端
|
||||
|
||||
EXT->>HTTP: POST /report/step_finish
|
||||
HTTP->>HTTP: 解析请求数据
|
||||
HTTP->>HTTP: 验证LIMS协议字段
|
||||
HTTP->>WS: process_step_finish_report(request)
|
||||
|
||||
WS->>WS: 增加接收计数(_reports_received_count++)
|
||||
WS->>WS: 记录步骤完成事件
|
||||
WS->>DECK: 更新相关物料状态(可选)
|
||||
DECK->>DECK: 修改PLR资源状态
|
||||
|
||||
WS->>WS: 保存报送记录到内存
|
||||
|
||||
WS-->>HTTP: 返回处理结果
|
||||
HTTP->>HTTP: 构造HTTP响应
|
||||
HTTP-->>EXT: 200 OK + acknowledgment_id
|
||||
|
||||
Note over EXT,CLOUD: 类似处理sample_finish, order_finish等报送
|
||||
|
||||
alt 物料变更报送
|
||||
EXT->>HTTP: POST /report/material_change
|
||||
HTTP->>WS: process_material_change_report(data)
|
||||
WS->>DECK: 查找或创建物料
|
||||
WS->>SYNC: sync_to_external(resource)
|
||||
SYNC->>SYNC: 同步到外部系统(如Bioyond)
|
||||
SYNC-->>WS: 同步完成
|
||||
WS->>CLOUD: update_resource(通过ROS节点)
|
||||
CLOUD-->>WS: 上传完成
|
||||
WS-->>HTTP: 返回结果
|
||||
HTTP-->>EXT: 200 OK
|
||||
end
|
||||
```
|
||||
|
||||
## 6. 错误处理时序图
|
||||
|
||||
```{mermaid}
|
||||
sequenceDiagram
|
||||
participant DEV as 子设备/外部系统
|
||||
participant ROS as ROS2WorkstationNode
|
||||
participant WS as WorkstationBase
|
||||
participant HW as HardwareInterface
|
||||
participant HTTP as HTTPService
|
||||
participant LOG as 日志系统
|
||||
|
||||
alt 设备错误(ROS Action失败)
|
||||
DEV->>ROS: Action返回失败结果
|
||||
ROS->>ROS: 记录错误信息
|
||||
ROS->>WS: 更新workflow_status = ERROR
|
||||
ROS->>LOG: 记录错误日志
|
||||
else 外部系统错误报送
|
||||
DEV->>HTTP: POST /report/error_handling
|
||||
HTTP->>WS: handle_external_error(error_data)
|
||||
WS->>WS: 记录错误历史
|
||||
WS->>LOG: 记录错误日志
|
||||
end
|
||||
|
||||
alt 关键错误需要停止
|
||||
WS->>ROS: stop_workflow(emergency=True)
|
||||
ROS->>ROS: 取消所有进行中的Action
|
||||
ROS->>HW: 调用emergency_stop()(如果支持)
|
||||
HW->>HW: 执行紧急停止
|
||||
WS->>WS: 更新workflow_status = ERROR
|
||||
else 可恢复错误
|
||||
WS->>WS: 标记步骤失败
|
||||
WS->>ROS: 触发重试逻辑(可选)
|
||||
ROS->>DEV: 重新发送Action
|
||||
end
|
||||
|
||||
WS-->>HTTP: 返回错误处理结果
|
||||
HTTP-->>DEV: 200 OK + 处理状态
|
||||
```
|
||||
|
||||
## 7. 典型工作站实现示例
|
||||
|
||||
### 7.1 Bioyond 集成工作站实现
|
||||
|
||||
```python
|
||||
class BioyondWorkstation(WorkstationBase):
|
||||
def __init__(self, bioyond_config: Dict, deck: Deck, *args, **kwargs):
|
||||
# 初始化deck
|
||||
super().__init__(deck=deck, *args, **kwargs)
|
||||
|
||||
# 设置硬件接口为Bioyond RPC客户端
|
||||
self.hardware_interface = BioyondV1RPC(bioyond_config)
|
||||
|
||||
# 创建资源同步器
|
||||
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||
|
||||
# 从Bioyond同步物料到本地deck
|
||||
self.resource_synchronizer.sync_from_external()
|
||||
|
||||
# 配置工作流
|
||||
self.workflow_mappings = bioyond_config.get("workflow_mappings", {})
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
"""ROS节点就绪后的初始化"""
|
||||
self._ros_node = ros_node
|
||||
|
||||
# 上传deck(包括所有物料)到云端
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource,
|
||||
True,
|
||||
resources=[self.deck]
|
||||
)
|
||||
|
||||
def resource_tree_add(self, resources: List[ResourcePLR]):
|
||||
"""添加物料并同步到Bioyond"""
|
||||
for resource in resources:
|
||||
self.deck.assign_child_resource(resource, location)
|
||||
self.resource_synchronizer.sync_to_external(resource)
|
||||
```
|
||||
|
||||
### 7.2 纯协议节点实现
|
||||
|
||||
```python
|
||||
class ProtocolNode(WorkstationBase):
|
||||
"""纯协议节点,不需要物料管理和外部通信"""
|
||||
|
||||
def __init__(self, deck: Optional[Deck] = None, *args, **kwargs):
|
||||
super().__init__(deck=deck, *args, **kwargs)
|
||||
# 不设置hardware_interface和resource_synchronizer
|
||||
# 所有功能通过子设备协同完成
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
self._ros_node = ros_node
|
||||
# 不需要上传物料或其他初始化
|
||||
```
|
||||
|
||||
### 7.3 PLC 直接控制工作站
|
||||
|
||||
```python
|
||||
class PLCWorkstation(WorkstationBase):
|
||||
def __init__(self, plc_config: Dict, deck: Deck, *args, **kwargs):
|
||||
super().__init__(deck=deck, *args, **kwargs)
|
||||
|
||||
# 设置硬件接口为Modbus客户端
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
self.hardware_interface = ModbusTcpClient(
|
||||
host=plc_config["host"],
|
||||
port=plc_config["port"]
|
||||
)
|
||||
self.hardware_interface.connect()
|
||||
|
||||
# 定义支持的工作流
|
||||
self.supported_workflows = {
|
||||
"battery_assembly": WorkflowInfo(
|
||||
name="电池组装",
|
||||
description="自动化电池组装流程",
|
||||
estimated_duration=300.0,
|
||||
required_materials=["battery_cell", "connector"],
|
||||
output_product="battery_pack",
|
||||
parameters_schema={"quantity": int, "model": str}
|
||||
)
|
||||
}
|
||||
|
||||
def execute_workflow(self, workflow_name: str, parameters: Dict):
|
||||
"""通过PLC执行工作流"""
|
||||
workflow_id = self._get_workflow_id(workflow_name)
|
||||
|
||||
# 写入PLC寄存器启动工作流
|
||||
self.hardware_interface.write_register(100, workflow_id)
|
||||
self.hardware_interface.write_register(101, parameters["quantity"])
|
||||
|
||||
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||
return True
|
||||
```
|
||||
|
||||
## 8. 核心接口说明
|
||||
|
||||
### 8.1 WorkstationBase 核心属性
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
| ------------------------- | ----------------------- | ------------------------------- |
|
||||
| `_ros_node` | ROS2WorkstationNode | ROS 节点引用,由 post_init 设置 |
|
||||
| `deck` | Deck | PyLabRobot Deck,本地物料系统 |
|
||||
| `plr_resources` | Dict[str, PLRResource] | 物料资源映射 |
|
||||
| `resource_synchronizer` | ResourceSynchronizer | 外部物料同步器(可选) |
|
||||
| `hardware_interface` | Union[Any, str] | 硬件接口或代理字符串 |
|
||||
| `current_workflow_status` | WorkflowStatus | 当前工作流状态 |
|
||||
| `supported_workflows` | Dict[str, WorkflowInfo] | 支持的工作流定义 |
|
||||
|
||||
### 8.2 必须实现的方法
|
||||
|
||||
- `post_init(ros_node)`: ROS 节点就绪后的初始化,必须实现
|
||||
|
||||
### 8.3 硬件接口相关方法
|
||||
|
||||
- `set_hardware_interface(interface)`: 设置硬件接口
|
||||
- `call_device_method(method, *args, **kwargs)`: 统一设备方法调用
|
||||
- 支持直接模式: 直接调用 hardware_interface 的方法
|
||||
- 支持代理模式: hardware_interface="proxy:device_id"通过 ROS 转发
|
||||
- `get_device_status()`: 获取设备状态
|
||||
- `is_device_available()`: 检查设备可用性
|
||||
|
||||
### 8.4 物料管理方法
|
||||
|
||||
- `get_deck()`: 获取 PLR Deck
|
||||
- `get_all_resources()`: 获取所有物料
|
||||
- `find_resource_by_name(name)`: 按名称查找物料
|
||||
- `find_resources_by_type(type)`: 按类型查找物料
|
||||
- `sync_with_external_system()`: 触发外部同步
|
||||
|
||||
### 8.5 工作流控制方法
|
||||
|
||||
- `execute_workflow(name, params)`: 执行工作流
|
||||
- `stop_workflow(emergency)`: 停止工作流
|
||||
- `workflow_status`: 获取工作流状态(属性)
|
||||
- `is_busy`: 检查是否忙碌(属性)
|
||||
- `workflow_runtime`: 获取运行时间(属性)
|
||||
|
||||
### 8.6 可选的 HTTP 报送处理方法
|
||||
|
||||
- `process_step_finish_report()`: 步骤完成处理
|
||||
- `process_sample_finish_report()`: 样本完成处理
|
||||
- `process_order_finish_report()`: 订单完成处理
|
||||
- `process_material_change_report()`: 物料变更处理
|
||||
- `handle_external_error()`: 错误处理
|
||||
|
||||
### 8.7 ROS2WorkstationNode 核心方法
|
||||
|
||||
- `initialize_device(device_id, config)`: 初始化子设备
|
||||
- `create_ros_action_server(action_name, mapping)`: 创建 Action 服务器
|
||||
- `execute_single_action(device_id, action, kwargs)`: 执行单个动作
|
||||
- `update_resource(resources)`: 同步物料到云端
|
||||
- `transfer_resource_to_another(...)`: 跨设备物料转移
|
||||
|
||||
## 9. 配置参数说明
|
||||
|
||||
### 9.1 工作站初始化配置
|
||||
|
||||
```python
|
||||
# 示例1: Bioyond集成工作站
|
||||
bioyond_config = {
|
||||
"base_url": "http://192.168.1.100:8080",
|
||||
"api_key": "your_api_key",
|
||||
"sync_interval": 600, # 同步间隔(秒)
|
||||
"workflow_mappings": {
|
||||
"样品制备": "workflow_uuid_1",
|
||||
"质检流程": "workflow_uuid_2"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"plate": "板",
|
||||
"tube": "试管"
|
||||
},
|
||||
"warehouse_mapping": {
|
||||
"冷藏区": {
|
||||
"uuid": "warehouse_uuid_1",
|
||||
"locations": {...}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 创建Deck
|
||||
from pylabrobot.resources import Deck
|
||||
deck = Deck(name="main_deck", size_x=1000, size_y=800, size_z=200)
|
||||
|
||||
workstation = BioyondWorkstation(
|
||||
bioyond_config=bioyond_config,
|
||||
deck=deck
|
||||
)
|
||||
```
|
||||
|
||||
### 9.2 子设备配置(children)
|
||||
|
||||
```python
|
||||
# 在devices.json中配置
|
||||
{
|
||||
"bioyond_workstation": {
|
||||
"type": "protocol", # 表示这是工作站节点
|
||||
"protocol_type": ["LiquidHandling", "PlateHandling"],
|
||||
"children": {
|
||||
"pump_1": {
|
||||
"type": "device",
|
||||
"driver": "TricontInnovaDriver",
|
||||
"communication": "serial_1",
|
||||
"config": {...}
|
||||
},
|
||||
"gripper_1": {
|
||||
"type": "device",
|
||||
"driver": "RobotiqGripperDriver",
|
||||
"communication": "io_modbus_1",
|
||||
"config": {...}
|
||||
},
|
||||
"serial_1": {
|
||||
"type": "communication",
|
||||
"protocol": "serial",
|
||||
"port": "/dev/ttyUSB0",
|
||||
"baudrate": 9600
|
||||
},
|
||||
"io_modbus_1": {
|
||||
"type": "communication",
|
||||
"protocol": "modbus_tcp",
|
||||
"host": "192.168.1.101",
|
||||
"port": 502
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 HTTP 服务配置
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
|
||||
# 创建HTTP服务(可选)
|
||||
http_service = WorkstationHTTPService(
|
||||
workstation_instance=workstation,
|
||||
host="0.0.0.0", # 监听所有网卡
|
||||
port=8081
|
||||
)
|
||||
http_service.start()
|
||||
```
|
||||
|
||||
## 10. 架构设计特点总结
|
||||
|
||||
这个简化后的架构设计具有以下特点:
|
||||
|
||||
### 10.1 清晰的职责分离
|
||||
|
||||
- **WorkstationBase**: 负责物料管理(deck)、硬件接口(hardware_interface)、工作流状态管理
|
||||
- **ROS2WorkstationNode**: 负责子设备管理、Protocol 执行、云端物料同步
|
||||
- **ResourceSynchronizer**: 可选的外部物料系统同步(如 Bioyond)
|
||||
- **WorkstationHTTPService**: 可选的 HTTP 报送接收服务
|
||||
|
||||
### 10.2 灵活的硬件接口模式
|
||||
|
||||
1. **直接模式**: hardware_interface 是具体对象(如 BioyondV1RPC、ModbusClient)
|
||||
2. **代理模式**: hardware_interface="proxy:device_id",通过 ROS 节点转发到子设备
|
||||
3. **混合模式**: 工作站有自己的接口,同时管理多个子设备
|
||||
|
||||
### 10.3 统一的物料系统
|
||||
|
||||
- 基于 PyLabRobot Deck 的标准化物料表示
|
||||
- 通过 ResourceSynchronizer 实现与外部系统(如 Bioyond、LIMS)的双向同步
|
||||
- 通过 ROS2WorkstationNode 实现与云端的物料状态同步
|
||||
|
||||
### 10.4 Protocol 驱动的工作流
|
||||
|
||||
- ROS2WorkstationNode 负责 Protocol 的执行和步骤管理
|
||||
- 支持子设备协同(通过 Action Client 调用)
|
||||
- 支持工作站直接控制(通过 hardware_interface)
|
||||
|
||||
### 10.5 可选的 HTTP 报送服务
|
||||
|
||||
- 基于 LIMS 协议规范的统一报送接口
|
||||
- 支持步骤完成、样本完成、任务完成、物料变更等多种报送类型
|
||||
- 与工作站解耦,可独立启停
|
||||
|
||||
### 10.6 简化的初始化流程
|
||||
|
||||
```
|
||||
1. __init__: 创建deck、设置hardware_interface、创建resource_synchronizer
|
||||
2. 从外部系统同步物料(如果有)
|
||||
3. ROS节点初始化子设备
|
||||
4. post_init: 关联ROS节点、上传物料到云端
|
||||
5. (可选)启动HTTP服务
|
||||
```
|
||||
|
||||
这种设计既保持了灵活性,又避免了过度抽象,更适合实际的工作站对接场景。
|
||||
@@ -1,334 +0,0 @@
|
||||
# HTTP API 指南
|
||||
|
||||
本文档介绍如何通过 HTTP API 与 Uni-Lab-OS 进行交互,包括查询设备、提交任务和获取结果。
|
||||
|
||||
## 概述
|
||||
|
||||
Uni-Lab-OS 提供 RESTful HTTP API,允许外部系统通过标准 HTTP 请求控制实验室设备。API 基于 FastAPI 构建,默认运行在 `http://localhost:8002`。
|
||||
|
||||
### 基础信息
|
||||
|
||||
- **Base URL**: `http://localhost:8002/api/v1`
|
||||
- **Content-Type**: `application/json`
|
||||
- **响应格式**: JSON
|
||||
|
||||
### 通用响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": { ... },
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | ------ | ------------------ |
|
||||
| `code` | int | 状态码,0 表示成功 |
|
||||
| `data` | object | 响应数据 |
|
||||
| `message` | string | 响应消息 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
以下是一个完整的工作流示例:查询设备 → 获取动作 → 提交任务 → 获取结果。
|
||||
|
||||
### 步骤 1: 获取在线设备
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8002/api/v1/online-devices"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"online_devices": {
|
||||
"host_node": {
|
||||
"device_key": "/host_node",
|
||||
"namespace": "",
|
||||
"machine_name": "本地",
|
||||
"uuid": "xxx-xxx-xxx",
|
||||
"node_name": "host_node"
|
||||
}
|
||||
},
|
||||
"total_count": 1,
|
||||
"timestamp": 1732612345.123
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2: 获取设备可用动作
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8002/api/v1/devices/host_node/actions"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"device_id": "host_node",
|
||||
"actions": {
|
||||
"test_latency": {
|
||||
"type_name": "unilabos_msgs.action._empty_in.EmptyIn",
|
||||
"type_name_convert": "unilabos_msgs/action/_empty_in/EmptyIn",
|
||||
"action_path": "/devices/host_node/test_latency",
|
||||
"goal_info": "{}",
|
||||
"is_busy": false,
|
||||
"current_job_id": null
|
||||
},
|
||||
"create_resource": {
|
||||
"type_name": "unilabos_msgs.action._resource_create_from_outer_easy.ResourceCreateFromOuterEasy",
|
||||
"action_path": "/devices/host_node/create_resource",
|
||||
"goal_info": "{res_id: '', device_id: '', class_name: '', ...}",
|
||||
"is_busy": false,
|
||||
"current_job_id": null
|
||||
}
|
||||
},
|
||||
"action_count": 5
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**动作状态字段说明**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ---------------- | ----------------------------- |
|
||||
| `type_name` | 动作类型的完整名称 |
|
||||
| `action_path` | ROS2 动作路径 |
|
||||
| `goal_info` | 动作参数模板 |
|
||||
| `is_busy` | 动作是否正在执行 |
|
||||
| `current_job_id` | 当前执行的任务 ID(如果繁忙) |
|
||||
|
||||
### 步骤 3: 提交任务
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8002/api/v1/job/add" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id":"host_node","action":"test_latency","action_args":{}}'
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "host_node",
|
||||
"action": "test_latency",
|
||||
"action_args": {}
|
||||
}
|
||||
```
|
||||
|
||||
**请求参数说明**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ---------------------------------- |
|
||||
| `device_id` | string | ✓ | 目标设备 ID |
|
||||
| `action` | string | ✓ | 动作名称 |
|
||||
| `action_args` | object | ✓ | 动作参数(根据动作类型不同而变化) |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"jobId": "b6acb586-733a-42ab-9f73-55c9a52aa8bd",
|
||||
"status": 1,
|
||||
"result": {}
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**任务状态码**:
|
||||
|
||||
| 状态码 | 含义 | 说明 |
|
||||
| ------ | --------- | ------------------------------ |
|
||||
| 0 | UNKNOWN | 未知状态 |
|
||||
| 1 | ACCEPTED | 任务已接受,等待执行 |
|
||||
| 2 | EXECUTING | 任务执行中 |
|
||||
| 3 | CANCELING | 任务取消中 |
|
||||
| 4 | SUCCEEDED | 任务成功完成 |
|
||||
| 5 | CANCELED | 任务已取消 |
|
||||
| 6 | ABORTED | 任务中止(设备繁忙或执行失败) |
|
||||
|
||||
### 步骤 4: 查询任务状态和结果
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8002/api/v1/job/b6acb586-733a-42ab-9f73-55c9a52aa8bd/status"
|
||||
```
|
||||
|
||||
**响应示例(执行中)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"jobId": "b6acb586-733a-42ab-9f73-55c9a52aa8bd",
|
||||
"status": 2,
|
||||
"result": {}
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例(执行完成)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"jobId": "b6acb586-733a-42ab-9f73-55c9a52aa8bd",
|
||||
"status": 4,
|
||||
"result": {
|
||||
"error": "",
|
||||
"suc": true,
|
||||
"return_value": {
|
||||
"avg_rtt_ms": 103.99,
|
||||
"avg_time_diff_ms": 7181.55,
|
||||
"max_time_error_ms": 7210.57,
|
||||
"task_delay_ms": -1,
|
||||
"raw_delay_ms": 33.19,
|
||||
"test_count": 5,
|
||||
"status": "success"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: 任务结果在首次查询后会被自动删除,请确保保存返回的结果数据。
|
||||
|
||||
## API 端点列表
|
||||
|
||||
### 设备相关
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
| ---------------------------------------------------------- | ---- | ---------------------- |
|
||||
| `/api/v1/online-devices` | GET | 获取在线设备列表 |
|
||||
| `/api/v1/devices` | GET | 获取设备配置 |
|
||||
| `/api/v1/devices/{device_id}/actions` | GET | 获取指定设备的可用动作 |
|
||||
| `/api/v1/devices/{device_id}/actions/{action_name}/schema` | GET | 获取动作参数 Schema |
|
||||
| `/api/v1/actions` | GET | 获取所有设备的可用动作 |
|
||||
|
||||
### 任务相关
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
| ----------------------------- | ---- | ------------------ |
|
||||
| `/api/v1/job/add` | POST | 提交新任务 |
|
||||
| `/api/v1/job/{job_id}/status` | GET | 查询任务状态和结果 |
|
||||
|
||||
### 资源相关
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
| ------------------- | ---- | ------------ |
|
||||
| `/api/v1/resources` | GET | 获取资源列表 |
|
||||
|
||||
## 常见动作示例
|
||||
|
||||
### test_latency - 延迟测试
|
||||
|
||||
测试系统延迟,无需参数。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8002/api/v1/job/add" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id":"host_node","action":"test_latency","action_args":{}}'
|
||||
```
|
||||
|
||||
### create_resource - 创建资源
|
||||
|
||||
在设备上创建新资源。
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8002/api/v1/job/add" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": "host_node",
|
||||
"action": "create_resource",
|
||||
"action_args": {
|
||||
"res_id": "my_plate",
|
||||
"device_id": "host_node",
|
||||
"class_name": "Plate",
|
||||
"parent": "deck",
|
||||
"bind_locations": {"x": 0, "y": 0, "z": 0}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 设备繁忙
|
||||
|
||||
当设备正在执行其他任务时,提交新任务会返回 `status: 6`(ABORTED):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"jobId": "xxx",
|
||||
"status": 6,
|
||||
"result": {}
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
此时应等待当前任务完成后重试,或使用 `/devices/{device_id}/actions` 检查动作的 `is_busy` 状态。
|
||||
|
||||
### 参数错误
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 2002,
|
||||
"data": { ... },
|
||||
"message": "device_id is required"
|
||||
}
|
||||
```
|
||||
|
||||
## 轮询策略
|
||||
|
||||
推荐的任务状态轮询策略:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
def wait_for_job(job_id, timeout=60, interval=0.5):
|
||||
"""等待任务完成并返回结果"""
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
response = requests.get(f"http://localhost:8002/api/v1/job/{job_id}/status")
|
||||
data = response.json()["data"]
|
||||
|
||||
status = data["status"]
|
||||
if status in (4, 5, 6): # SUCCEEDED, CANCELED, ABORTED
|
||||
return data
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
|
||||
|
||||
# 使用示例
|
||||
response = requests.post(
|
||||
"http://localhost:8002/api/v1/job/add",
|
||||
json={"device_id": "host_node", "action": "test_latency", "action_args": {}}
|
||||
)
|
||||
job_id = response.json()["data"]["jobId"]
|
||||
result = wait_for_job(job_id)
|
||||
print(result)
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [设备注册指南](add_device.md)
|
||||
- [动作定义指南](add_action.md)
|
||||
- [网络架构概述](networking_overview.md)
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 629 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 269 KiB |
@@ -1,594 +0,0 @@
|
||||
# 组网部署与主从模式配置
|
||||
|
||||
本文档介绍 Uni-Lab-OS 的组网架构、部署方式和主从模式的详细配置。
|
||||
|
||||
## 目录
|
||||
|
||||
- [架构概览](#架构概览)
|
||||
- [节点类型](#节点类型)
|
||||
- [通信机制](#通信机制)
|
||||
- [典型拓扑](#典型拓扑)
|
||||
- [主从模式配置](#主从模式配置)
|
||||
- [网络配置](#网络配置)
|
||||
- [示例:多房间部署](#示例多房间部署)
|
||||
- [故障处理](#故障处理)
|
||||
- [监控和维护](#监控和维护)
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
Uni-Lab-OS 支持多种部署模式:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Cloud Platform/Self-hosted Platform │
|
||||
│ uni-lab.bohrium.com │
|
||||
│ (Resource Management, Task Scheduling, │
|
||||
│ Monitoring) │
|
||||
└────────────────────┬─────────────────────────┘
|
||||
│ WebSocket / HTTP
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
┌────▼─────┐ ┌────▼─────┐
|
||||
│ Master │◄──ROS2──►│ Slave │
|
||||
│ Node │ │ Node │
|
||||
│ (Host) │ │ (Slave) │
|
||||
└────┬─────┘ └────┬─────┘
|
||||
│ │
|
||||
┌────┴────┐ ┌────┴────┐
|
||||
│ Device A│ │ Device B│
|
||||
│ Device C│ │ Device D│
|
||||
└─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 节点类型
|
||||
|
||||
### 主节点(Host Node)
|
||||
|
||||
**功能**:
|
||||
|
||||
- 创建和管理全局资源
|
||||
- 提供 host_node 服务
|
||||
- 连接云端平台
|
||||
- 协调多个从节点
|
||||
- 提供 Web 管理界面
|
||||
|
||||
**启动命令**:
|
||||
|
||||
```bash
|
||||
unilab --ak your_ak --sk your_sk -g host_devices.json
|
||||
```
|
||||
|
||||
### 从节点(Slave Node)
|
||||
|
||||
**功能**:
|
||||
|
||||
- 管理本地设备
|
||||
- 不连接云端(可选)
|
||||
- 向主节点注册
|
||||
- 执行分配的任务
|
||||
|
||||
**启动命令**:
|
||||
|
||||
```bash
|
||||
unilab --ak your_ak --sk your_sk -g slave_devices.json --is_slave
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通信机制
|
||||
|
||||
### ROS2 通信
|
||||
|
||||
**用途**: 节点间实时通信
|
||||
|
||||
**通信方式**:
|
||||
|
||||
- **Topic**: 状态广播(设备状态、传感器数据)
|
||||
- **Service**: 同步请求(资源查询、配置获取)
|
||||
- **Action**: 异步任务(设备操作、长时间运行)
|
||||
|
||||
**示例**:
|
||||
|
||||
```bash
|
||||
# 查看ROS2节点
|
||||
ros2 node list
|
||||
|
||||
# 查看topic
|
||||
ros2 topic list
|
||||
|
||||
# 查看action
|
||||
ros2 action list
|
||||
```
|
||||
|
||||
### WebSocket 通信
|
||||
|
||||
**用途**: 主节点与云端通信
|
||||
|
||||
**特点**:
|
||||
|
||||
- 实时双向通信
|
||||
- 自动重连
|
||||
- 心跳保持
|
||||
|
||||
**配置**:
|
||||
|
||||
```python
|
||||
# local_config.py
|
||||
BasicConfig.ak = "your_ak"
|
||||
BasicConfig.sk = "your_sk"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 典型拓扑
|
||||
|
||||
### 单节点模式
|
||||
|
||||
**适用场景**: 小型实验室、开发测试
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ Uni-Lab Node │
|
||||
│ ┌────────────┐ │
|
||||
│ │ Device A │ │
|
||||
│ │ Device B │ │
|
||||
│ │ Device C │ │
|
||||
│ └────────────┘ │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
**优点**:
|
||||
|
||||
- 配置简单
|
||||
- 无网络延迟
|
||||
- 适合快速原型
|
||||
|
||||
**启动**:
|
||||
|
||||
```bash
|
||||
unilab --ak your_ak --sk your_sk -g all_devices.json
|
||||
```
|
||||
|
||||
### 主从模式
|
||||
|
||||
**适用场景**: 多房间、分布式设备
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐
|
||||
│ Master Node │◄────►│ Slave Node 1 │
|
||||
│ Coordinator │ │ Liquid │
|
||||
│ Web UI │ │ Handling │
|
||||
└──────┬──────┘ └──────────────┘
|
||||
│
|
||||
│ ┌──────────────┐
|
||||
└────────────►│ Slave Node 2 │
|
||||
│ Analytical │
|
||||
│ (NMR/GC) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**优点**:
|
||||
|
||||
- 物理分隔
|
||||
- 独立故障域
|
||||
- 易于扩展
|
||||
|
||||
**适用场景**:
|
||||
|
||||
- 设备物理位置分散
|
||||
- 不同房间的设备
|
||||
- 需要独立故障域
|
||||
- 分阶段扩展系统
|
||||
|
||||
**主节点**:
|
||||
|
||||
```bash
|
||||
unilab --ak your_ak --sk your_sk -g host.json
|
||||
```
|
||||
|
||||
**从节点**:
|
||||
|
||||
```bash
|
||||
unilab --ak your_ak --sk your_sk -g slave1.json --is_slave
|
||||
unilab --ak your_ak --sk your_sk -g slave2.json --is_slave --port 8003
|
||||
```
|
||||
|
||||
### 云端集成模式
|
||||
|
||||
**适用场景**: 远程监控、多实验室协作
|
||||
|
||||
```
|
||||
Cloud Platform
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ │
|
||||
Laboratory A Laboratory B
|
||||
(Master Node) (Master Node)
|
||||
```
|
||||
|
||||
**优点**:
|
||||
|
||||
- 远程访问
|
||||
- 数据同步
|
||||
- 任务调度
|
||||
|
||||
**启动**:
|
||||
|
||||
```bash
|
||||
# 实验室A
|
||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||
|
||||
# 实验室B
|
||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 主从模式配置
|
||||
|
||||
### 主节点配置
|
||||
|
||||
#### 1. 创建主节点设备图
|
||||
|
||||
`host.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [],
|
||||
"links": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 启动主节点
|
||||
|
||||
```bash
|
||||
# 基本启动
|
||||
unilab --ak your_ak --sk your_sk -g host.json
|
||||
|
||||
# 带云端集成
|
||||
unilab --ak your_ak --sk your_sk -g host.json --upload_registry
|
||||
|
||||
# 指定端口
|
||||
unilab --ak your_ak --sk your_sk -g host.json --port 8002
|
||||
```
|
||||
|
||||
#### 3. 验证主节点
|
||||
|
||||
```bash
|
||||
# 检查ROS2节点
|
||||
ros2 node list
|
||||
# 应该看到 /host_node
|
||||
|
||||
# 检查服务
|
||||
ros2 service list | grep host_node
|
||||
|
||||
# Web界面
|
||||
# 访问 http://localhost:8002
|
||||
```
|
||||
|
||||
### 从节点配置
|
||||
|
||||
#### 1. 创建从节点设备图
|
||||
|
||||
`slave1.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "liquid_handler_1",
|
||||
"name": "液体处理工作站",
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {
|
||||
"simulation": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 启动从节点
|
||||
|
||||
```bash
|
||||
# 基本从节点启动
|
||||
unilab --ak your_ak --sk your_sk -g slave1.json --is_slave
|
||||
|
||||
# 指定不同端口(如果多个从节点在同一台机器)
|
||||
unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --port 8003
|
||||
|
||||
# 跳过等待主节点(独立测试)
|
||||
unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --slave_no_host
|
||||
```
|
||||
|
||||
#### 3. 验证从节点
|
||||
|
||||
```bash
|
||||
# 检查节点连接
|
||||
ros2 node list
|
||||
|
||||
# 检查设备状态
|
||||
ros2 topic echo /liquid_handler_1/status
|
||||
```
|
||||
|
||||
### 跨节点通信
|
||||
|
||||
#### 资源访问
|
||||
|
||||
主节点可以访问从节点的资源:
|
||||
|
||||
```bash
|
||||
# 在主节点或其他节点调用从节点设备
|
||||
ros2 action send_goal /liquid_handler_1/transfer_liquid \
|
||||
unilabos_msgs/action/TransferLiquid \
|
||||
"{source: {...}, target: {...}, volume: 100.0}"
|
||||
```
|
||||
|
||||
#### 状态监控
|
||||
|
||||
主节点监控所有从节点状态:
|
||||
|
||||
```bash
|
||||
# 订阅从节点状态
|
||||
ros2 topic echo /liquid_handler_1/status
|
||||
|
||||
# 查看所有设备状态
|
||||
ros2 topic list | grep status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 网络配置
|
||||
|
||||
### ROS2 DDS 配置
|
||||
|
||||
确保主从节点在同一网络:
|
||||
|
||||
```bash
|
||||
# 检查网络可达性
|
||||
ping <slave_node_ip>
|
||||
|
||||
# 设置ROS_DOMAIN_ID(可选,用于隔离)
|
||||
export ROS_DOMAIN_ID=42
|
||||
```
|
||||
|
||||
### 防火墙配置
|
||||
|
||||
**建议做法**:
|
||||
|
||||
为了确保 ROS2 DDS 通信正常,建议直接关闭防火墙,而不是配置特定端口。ROS2 使用动态端口范围,配置特定端口可能导致通信问题。
|
||||
|
||||
**Linux**:
|
||||
|
||||
```bash
|
||||
# 关闭防火墙
|
||||
sudo ufw disable
|
||||
|
||||
# 或者临时停止防火墙
|
||||
sudo systemctl stop ufw
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
|
||||
```powershell
|
||||
# 在Windows安全中心关闭防火墙
|
||||
# 控制面板 -> 系统和安全 -> Windows Defender 防火墙 -> 启用或关闭Windows Defender防火墙
|
||||
```
|
||||
|
||||
### 验证网络连通性
|
||||
|
||||
在配置完成后,使用 ROS2 自带的 demo 节点来验证跨节点通信是否正常:
|
||||
|
||||
**在主节点机器上**(激活 unilab 环境后):
|
||||
|
||||
```bash
|
||||
# 启动talker
|
||||
ros2 run demo_nodes_cpp talker
|
||||
|
||||
# 同时在另一个终端启动listener
|
||||
ros2 run demo_nodes_cpp listener
|
||||
```
|
||||
|
||||
**在从节点机器上**(激活 unilab 环境后):
|
||||
|
||||
```bash
|
||||
# 启动talker
|
||||
ros2 run demo_nodes_cpp talker
|
||||
|
||||
# 同时在另一个终端启动listener
|
||||
ros2 run demo_nodes_cpp listener
|
||||
```
|
||||
|
||||
**注意**:必须在两台机器上**互相启动** talker 和 listener,否则可能出现只能收不能发的单向通信问题。
|
||||
|
||||
**预期结果**:
|
||||
|
||||
- 每台机器的 listener 应该能同时接收到本地和远程 talker 发送的消息
|
||||
- 如果只能看到本地消息,说明网络配置有问题
|
||||
- 如果两台机器都能互相收发消息,则组网配置正确
|
||||
|
||||
### 本地网络要求
|
||||
|
||||
**ROS2 通信**:
|
||||
|
||||
- 同一局域网或 VPN
|
||||
- 端口:默认 DDS 端口(7400-7500)
|
||||
- 组播支持(或配置 unicast)
|
||||
|
||||
**检查连通性**:
|
||||
|
||||
```bash
|
||||
# Ping测试
|
||||
ping <target_ip>
|
||||
|
||||
# ROS2节点发现
|
||||
ros2 node list
|
||||
ros2 daemon stop && ros2 daemon start
|
||||
```
|
||||
|
||||
### 云端连接
|
||||
|
||||
**要求**:
|
||||
|
||||
- HTTPS (443)
|
||||
- WebSocket 支持
|
||||
- 稳定的互联网连接
|
||||
|
||||
**测试连接**:
|
||||
|
||||
```bash
|
||||
# 测试云端连接
|
||||
curl https://uni-lab.bohrium.com/api/v1/health
|
||||
|
||||
# 测试WebSocket
|
||||
# 启动Uni-Lab后查看日志
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示例:多房间部署
|
||||
|
||||
### 场景描述
|
||||
|
||||
- **房间 A**: 主控室,有 Web 界面
|
||||
- **房间 B**: 液体处理室
|
||||
- **房间 C**: 分析仪器室
|
||||
|
||||
### 房间 A - 主节点
|
||||
|
||||
```bash
|
||||
# host.json
|
||||
unilab --ak your_ak --sk your_sk -g host.json --port 8002
|
||||
```
|
||||
|
||||
### 房间 B - 从节点 1
|
||||
|
||||
```bash
|
||||
# liquid_handler.json
|
||||
unilab --ak your_ak --sk your_sk -g liquid_handler.json --is_slave --port 8003
|
||||
```
|
||||
|
||||
### 房间 C - 从节点 2
|
||||
|
||||
```bash
|
||||
# analytical.json
|
||||
unilab --ak your_ak --sk your_sk -g analytical.json --is_slave --port 8004
|
||||
| < | ||||