4 Commits

Author SHA1 Message Date
h840473807
5805f94e9a 扣电驱动中增加多个组装参数,更新驱动与注册表 (#120)
扣电驱动中增加多个组装参数,elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200,更新驱动与注册表
2025-10-21 16:27:02 +08:00
Calvin Cao
3adcc41ce8 Merge pull request #118 from h840473807/workstation_dev_YB2
Workstation dev yb2
2025-10-21 10:32:41 +08:00
h840473807
243922caf4 宜宾配液+扣电工站注册表文件
宜宾配液+扣电工站注册表文件
2025-10-20 15:43:21 +08:00
h840473807
079ec9d1b4 workstation_by_hhm
宜宾扣电工站与奔曜配液工站,更新截止10月20日
2025-10-20 15:36:53 +08:00
427 changed files with 37794 additions and 69406 deletions

View File

@@ -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:

View File

@@ -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 "=========================================="

View File

@@ -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
View File

@@ -2,7 +2,6 @@ configs/
temp/
output/
unilabos_data/
pyrightconfig.json
## Python
# Byte-compiled / optimized / DLL files

View File

@@ -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>

View File

@@ -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 *

View File

@@ -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

View File

@@ -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)
## 消息格式

View File

@@ -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
![copy_aksk.gif](image/copy_aksk.gif)
## 配置文件格式
### 默认配置示例
首次使用时,系统会自动创建一个基础配置文件 `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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

View File

@@ -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)

View File

@@ -91,7 +91,7 @@
使用以下命令启动模拟反应器:
```bash
unilab -g test/experiments/mock_reactor.json
unilab -g test/experiments/mock_reactor.json --app_bridges ""
```
### 2. 执行抽真空和充气操作

View File

@@ -1,7 +1,7 @@
(instructions)=
# 设备抽象、指令集与通信中间件
Uni-Lab-OS的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
Uni-Lab 操作系统的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
## 设备间通信模式

View File

@@ -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});")

File diff suppressed because it is too large Load Diff

View File

@@ -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.sleepROS2 异步机制)
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>`

File diff suppressed because it is too large Load Diff

View File

@@ -111,8 +111,8 @@ new_device: # 设备名,要唯一
1.`auto-` 开头的动作:从你 Python 类的方法自动生成
2. 通用的驱动动作:
- `_execute_driver_command`:同步执行驱动命令(仅本地可用)
- `_execute_driver_command_async`:异步执行驱动命令(仅本地可用)
- `_execute_driver_command`:同步执行驱动命令
- `_execute_driver_command_async`:异步执行驱动命令
### 如果要手动定义动作

View File

@@ -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适用于较新的应用程序特别是基于 WPFUWP 的应用程序。(在 win10 上,会有更全的目录,有的窗口 win32 会识别不到)
* **win32**后端适用于大多数Windows应用程序使用native Win32 API。pywinauto_recorder默认使用win32后端
* **uia**后端基于Microsoft UI Automation适用于较新的应用程序特别是基于WPFUWP的应用程序。在win10上会有更全的目录有的窗口win32会识别不到
### windows 平台安装 pywinautopywinauto_recorder
### windows平台安装pywinautopywinauto_recorder
直接安装会造成环境崩溃,需要下载并解压已经修改好的文件。
cd 到对应目录,执行安装
cd到对应目录执行安装
` pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple `
`pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple`
![pywinauto_install](image/device_driver/pywinauto_install.png)
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自带的计算器软件
![calculator_01](image/device_driver/calculator_01.png)
@@ -288,7 +286,7 @@ with UIPath(u"计算器||Window"):
click(u"九||Button")
```
执行该 python 脚本,可以观察到新开启的计算器被点击了数字 9
执行该python脚本可以观察到新开启的计算器被点击了数字9
![calculator_03](image/device_driver/calculator_03.png)
@@ -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` - 设备测试指南

View File

@@ -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>
```
点击注册表编辑,进入注册表编辑页面
![系统状态页面](image_battery_plc/unilab_sys_status.png)
按照图示步骤填写自动生成注册表信息:
![注册表生成流程](image_battery_plc/unilab_registry_process.png)
步骤说明:
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 文件,如下图:
![生成的YAML文件](image_battery_plc/unilab_new_yaml.png)
### 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 设备接入流程,可以作为其他类似设备接入的参考模板。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -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`: 版本号,用于版本管理

View File

@@ -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 PyLabRobotPLR物料格式实验流程运行时
- 结构特征:严格的层级树,`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]:
# ... 由扁平 dictid->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 ↔ PyLabRobotPLR
高层封装:
```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 列表)。便于版本化、可视化与网络传输。
- **实验工作流执行**:使用 PyLabRobotPLR格式。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]:
# 批量初始化
```

View File

@@ -1,782 +0,0 @@
# 实例:工作站模板架构设计与对接指南
> **文档类型**:架构设计指南与实战案例
> **适用场景**:大型工作站接入、子设备管理、物料系统集成
> **前置知识**{doc}`../add_device` | {doc}`../add_registry`
## 0. 问题简介
我们可以从以下几类例子,来理解对接大型工作站需要哪些设计。本文档之后的实战案例也将由这些组成。
### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发
![workstation_organic_yed](../image/workstation_architecture/workstation_organic_yed.png)
![workstation_organic](../image/workstation_architecture/workstation_organic.png)
这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合;
1. 工作站包含大量已经注册的子设备,可能各自通信组态很不相同;部分设备可能会拥有同一个通信设备作为出口,如 2 个泵共用 1 个串口、所有设备共同接入 PLC 等。
2. 任务系统是统一实现的 protocolsprotocols 中会将高层指令处理成各子设备配合的工作流 json 并管理执行、同时更改物料信息
3. 物料系统较为简单直接,如常量有机化学仅为工作站内固定的瓶子,初始化时就已固定;随后在任务执行过程中,记录试剂量更改信息
### 0.2 移液工作站:物料系统和工作流模板管理
![workstation_liquid_handler](../image/workstation_architecture/workstation_liquid_handler.png)
1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供
2. 所有任务系统均由工作站本身实现并下发指令有统一的抽象函数可实现pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。
3. 物料系统为固定的板位系统:台面上有多个可摆放位置,摆放标准孔板。
### 0.3 厂家开发的定制大型工站
![workstation_by_supplier](../image/workstation_architecture/workstation_by_supplier.png)
由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 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服务
```
这种设计既保持了灵活性,又避免了过度抽象,更适合实际的工作站对接场景。

View File

@@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -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
<