Compare commits

..

1 Commits

Author SHA1 Message Date
Xuwznln
9aeffebde1 0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>

* fix: workstation handlers and vessel_id parsing

* fix: working dir error when input config path
feat: report publish topic when error

* modify default discovery_interval to 15s

* feat: add trace log level

* feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

* feat: 优化protocol node节点运行日志

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

* refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

* update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)

* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

* fix: station自己的方法注册错误

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

* 修复工站的tracker实例追踪失效问题

* 增加handle检测,增加material edge关系上传

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

* 主机节点信息等支持自动刷新

* 注册表编辑器

* 修复status密集发送时,消息出错

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

- Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes.
- Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume.
- Both actions include response fields for return information and success status.

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

* Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

* 对于PRCXI9320的transfer_group,一对多和多对多

* 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

* Fix: run-column with correct vessel id (#86)

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

* 重新规定了版位推荐的入参

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

* 修复移液站错误的aspirate注册表

* Feature/xprbalance-zhida (#80)

* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* frontend_docs

* create/update resources with POST/PUT for big amount/ small amount data

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

* Workstation templates: Resources and its CRUD, and workstation tasks (#95)

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.

* 修复to_plr_resources

* add update remove

* 支持选择器注册表自动生成
支持转运物料

* 修复资源添加

* 修复transfer_resource_to_another生成

* 更新transfer_resource_to_another参数,支持spot入参

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

* 修复同步任务报错不显示的bug

* feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了

* update todo

* modify bioyond/plr converter, bioyond resource registry, and tests

* pass the tests

* update todo

* add conda-pack-build.yml

* add auto install script for conda-pack-build.yml

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py

* Update conda-pack-build.yml

* Update conda-pack-build.yml (with mamba)

* Update conda-pack-build.yml

* Fix FileNotFoundError

* Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined>

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

* Update registry. Update uuid loop figure method. Update install docs.

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字

* 电池装配工站二次开发教程(带目录)上传至dev (#94)

* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释

* Update prcxi driver & fix transfer_liquid mix_times (#90)

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: LccLink <1951855008@qq.com>
Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
2025-10-12 23:34:26 +08:00
233 changed files with 12478 additions and 37214 deletions

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: 0.10.10
version: 0.10.7
source:
path: ../unilabos

View File

@@ -42,7 +42,7 @@ jobs:
defaults:
run:
# Windows uses cmd for better conda/mamba compatibility, Unix uses bash
shell: ${{ matrix.platform == 'win-64' && 'cmd' || 'bash' }}
shell: ${{ matrix.platform == 'win-64' && 'cmd /C CALL {0}' || 'bash -el {0}' }}
steps:
- name: Check if platform should be built
@@ -73,6 +73,7 @@ jobs:
channels: conda-forge,robostack-staging,uni-lab,defaults
channel-priority: flexible
activate-environment: unilab
auto-activate-base: true
auto-update-conda: false
show-channel-urls: true
@@ -81,7 +82,7 @@ jobs:
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
mamba install 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'
@@ -89,15 +90,15 @@ jobs:
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
mamba install 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
conda list ros-humble-unilabos-msgs
for /f "tokens=2" %%i in ('conda list 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%
@@ -107,7 +108,7 @@ jobs:
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')")
VERSION=$(conda list 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"
@@ -118,7 +119,7 @@ jobs:
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
mamba update 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'
@@ -128,65 +129,65 @@ jobs:
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"
mamba update 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
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 .
pip install .
echo Verifying installation...
mamba run -n unilab pip show unilabos
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"
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 .
pip install .
echo "Verifying installation..."
mamba run -n unilab pip show unilabos
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
conda env list
echo.
echo === Installed Packages ===
mamba list -n unilab | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || mamba list -n unilab
conda list | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || conda list
echo.
echo === Python Packages ===
mamba run -n unilab pip list | findstr unilabos || mamba run -n unilab pip list
pip list | findstr unilabos || 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
conda env list
echo ""
echo "=== Installed Packages ==="
mamba list -n unilab | grep -E "(unilabos|ros-humble-unilabos-msgs)" || mamba list -n unilab
conda list | grep -E "(unilabos|ros-humble-unilabos-msgs)" || conda list
echo ""
echo "=== Python Packages ==="
mamba run -n unilab pip list | grep unilabos || mamba run -n unilab pip list
pip list | grep unilabos || 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}')"
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
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')"
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
python scripts\verify_installation.py || echo Warning: Verification script reported issues
echo Environment verification complete!
- name: Verify environment integrity (Unix)
@@ -194,20 +195,20 @@ jobs:
shell: bash
run: |
echo "Verifying Python version..."
mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
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"
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')"
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"
python scripts/verify_installation.py || 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
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
@@ -216,7 +217,6 @@ jobs:
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
@@ -242,10 +242,6 @@ jobs:
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
@@ -278,10 +274,6 @@ jobs:
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
@@ -291,6 +283,46 @@ jobs:
ls -lh dist-package/
echo ""
- name: Finalize Windows distribution package
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: |
echo ==========================================
echo Windows distribution package ready
echo.
echo Package will be uploaded as artifact
echo GitHub Actions will automatically create ZIP
echo.
echo Contents:
dir /b dist-package
echo.
echo Users will download a ZIP containing:
echo - install_unilab.bat
echo - unilab-env-${{ matrix.platform }}.tar.gz
echo - verify_installation.py
echo - README.txt
echo ==========================================
- name: Create Unix/Linux TAR.GZ archive
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
shell: bash
run: |
echo "=========================================="
echo "Creating Unix/Linux TAR.GZ archive..."
echo "Archive: unilab-pack-${{ matrix.platform }}.tar.gz"
echo "Contents: install_unilab.sh + unilab-env-${{ matrix.platform }}.tar.gz + extras"
tar -czf unilab-pack-${{ matrix.platform }}.tar.gz -C dist-package .
echo "=========================================="
echo ""
echo "Final package created:"
ls -lh unilab-pack-*
echo ""
echo "Users can now:"
echo " 1. Download unilab-pack-${{ matrix.platform }}.tar.gz"
echo " 2. Extract it: tar -xzf unilab-pack-${{ matrix.platform }}.tar.gz"
echo " 3. Run: bash install_unilab.sh"
echo ""
- name: Upload distribution package
if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v4
@@ -333,8 +365,12 @@ jobs:
echo "Distribution package contents:"
ls -lh dist-package/
echo ""
echo "Package size (tar.gz):"
ls -lh unilab-pack-*.tar.gz
echo ""
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
echo ""
echo "After download:"
echo " install_unilab.sh"
echo " - Windows/macOS: Extract ZIP, then: tar -xzf unilab-pack-${{ matrix.platform }}.tar.gz"
echo " - Linux: Extract ZIP (or download tar.gz directly), run install_unilab.sh"
echo "=========================================="

View File

@@ -39,39 +39,24 @@ jobs:
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
- name: Setup Python environment
uses: actions/setup-python@v5
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
python-version: '3.10'
- name: Install unilabos and dependencies
- name: Install system 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
sudo apt-get update
sudo apt-get install -y pandoc
- name: Install latest unilabos from source
- name: Install Python dependencies
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
python -m pip install --upgrade pip
# Install package in development mode to get version info
pip install -e .
# Install documentation dependencies
pip install -r docs/requirements.txt
- name: Setup Pages
id: pages
@@ -83,8 +68,8 @@ jobs:
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
# Build HTML documentation
python -m sphinx -b html . _build/html -v
- name: Check build results
run: |

View File

@@ -1,18 +1,15 @@
56 Xuwznln <18435084+Xuwznln@users.noreply.github.com>
10 wznln <18435084+Xuwznln@users.noreply.github.com>
6 Junhan Chang <changjh@dp.tech>
156 Xuwznln <18435084+Xuwznln@users.noreply.github.com>
39 Junhan Chang <changjh@dp.tech>
9 wznln <18435084+Xuwznln@users.noreply.github.com>
8 Guangxin Zhang <guangxin.zhang.bio@gmail.com>
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>
2 Xie Qiming <97236197+Andy6M@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 LccLink <1951855008@qq.com>
1 h840473807 <47357934+h840473807@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 shiyubo0410 <shiyubo@dp.tech>
1 王俊杰 <1800011822@pku.edu.cn>
1 王俊杰 <43375851+wjjxxx@users.noreply.github.com>

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://dptech-corp.github.io/Uni-Lab-OS/)
## 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://dptech-corp.github.io/Uni-Lab-OS/boot_examples/index.html)
## Message Format

View File

@@ -31,7 +31,7 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控
详细文档可在以下位置找到:
- [在线文档](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
- [在线文档](https://dptech-corp.github.io/Uni-Lab-OS/)
## 快速开始
@@ -57,7 +57,7 @@ pip install .
3. 启动 Uni-Lab 系统:
请见[文档-启动样例](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
请见[文档-启动样例](https://dptech-corp.github.io/Uni-Lab-OS/boot_examples/index.html)
## 消息格式

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

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

View File

@@ -1,26 +1,88 @@
## 简单单变量动作函数
### `SendCmd`
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
:language: yaml
```
----
---
### `StrSingleInput`
```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action
:language: yaml
```
---
### `IntSingleInput`
```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action
:language: yaml
```
---
### `FloatSingleInput`
```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action
:language: yaml
```
---
### `Point3DSeparateInput`
```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action
:language: yaml
```
---
### `Wait`
```{literalinclude} ../../unilabos_msgs/action/Wait.action
:language: yaml
```
---
## 常量有机化学操作
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
### `Clean`
```{literalinclude} ../../unilabos_msgs/action/Clean.action
:language: yaml
```
----
---
### `EvacuateAndRefill`
```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action
:language: yaml
```
---
### `Evaporate`
```{literalinclude} ../../unilabos_msgs/action/Evaporate.action
:language: yaml
```
---
### `HeatChill`
```{literalinclude} ../../unilabos_msgs/action/HeatChill.action
:language: yaml
```
---
### `HeatChillStart`
@@ -28,7 +90,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `HeatChillStop`
@@ -36,7 +98,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `PumpTransfer`
@@ -44,12 +106,195 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Separate`
```{literalinclude} ../../unilabos_msgs/action/Separate.action
:language: yaml
```
---
### `Stir`
```{literalinclude} ../../unilabos_msgs/action/Stir.action
:language: yaml
```
---
### `Add`
```{literalinclude} ../../unilabos_msgs/action/Add.action
:language: yaml
```
---
### `AddSolid`
```{literalinclude} ../../unilabos_msgs/action/AddSolid.action
:language: yaml
```
---
### `AdjustPH`
```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action
:language: yaml
```
---
### `Centrifuge`
```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action
:language: yaml
```
---
### `CleanVessel`
```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action
:language: yaml
```
---
### `Crystallize`
```{literalinclude} ../../unilabos_msgs/action/Crystallize.action
:language: yaml
```
---
### `Dissolve`
```{literalinclude} ../../unilabos_msgs/action/Dissolve.action
:language: yaml
```
---
### `Dry`
```{literalinclude} ../../unilabos_msgs/action/Dry.action
:language: yaml
```
---
### `Filter`
```{literalinclude} ../../unilabos_msgs/action/Filter.action
:language: yaml
```
---
### `FilterThrough`
```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action
:language: yaml
```
---
### `Hydrogenate`
```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action
:language: yaml
```
---
### `Purge`
```{literalinclude} ../../unilabos_msgs/action/Purge.action
:language: yaml
```
---
### `Recrystallize`
```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action
:language: yaml
```
---
### `RunColumn`
```{literalinclude} ../../unilabos_msgs/action/RunColumn.action
:language: yaml
```
---
### `StartPurge`
```{literalinclude} ../../unilabos_msgs/action/StartPurge.action
:language: yaml
```
---
### `StartStir`
```{literalinclude} ../../unilabos_msgs/action/StartStir.action
:language: yaml
```
---
### `StopPurge`
```{literalinclude} ../../unilabos_msgs/action/StopPurge.action
:language: yaml
```
---
### `StopStir`
```{literalinclude} ../../unilabos_msgs/action/StopStir.action
:language: yaml
```
---
### `Transfer`
```{literalinclude} ../../unilabos_msgs/action/Transfer.action
:language: yaml
```
---
### `WashSolid`
```{literalinclude} ../../unilabos_msgs/action/WashSolid.action
:language: yaml
```
---
## 移液工作站及相关生物自动化设备操作
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
### `LiquidHandlerAspirate`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
:language: yaml
```
---
### `LiquidHandlerDiscardTips`
@@ -57,7 +302,15 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDispense`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action
:language: yaml
```
---
### `LiquidHandlerDropTips`
@@ -65,7 +318,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDropTips96`
@@ -73,7 +326,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveLid`
@@ -81,7 +334,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMovePlate`
@@ -89,7 +342,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveResource`
@@ -97,7 +350,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips`
@@ -105,7 +358,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips96`
@@ -113,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips`
@@ -121,7 +374,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips96`
@@ -129,7 +382,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerStamp`
@@ -137,9 +390,121 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
## 多工作站及小车运行、物料转移
---
### `LiquidHandlerTransfer`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action
:language: yaml
```
---
### `LiquidHandlerAdd`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action
:language: yaml
```
---
### `LiquidHandlerIncubateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
:language: yaml
```
---
### `LiquidHandlerMix`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action
:language: yaml
```
---
### `LiquidHandlerMoveBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action
:language: yaml
```
---
### `LiquidHandlerMoveTo`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action
:language: yaml
```
---
### `LiquidHandlerOscillateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
:language: yaml
```
---
### `LiquidHandlerProtocolCreation`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action
:language: yaml
```
---
### `LiquidHandlerRemove`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action
:language: yaml
```
---
### `LiquidHandlerSetGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action
:language: yaml
```
---
### `LiquidHandlerSetLiquid`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action
:language: yaml
```
---
### `LiquidHandlerSetTipRack`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action
:language: yaml
```
---
### `LiquidHandlerTransferBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action
:language: yaml
```
---
### `LiquidHandlerTransferGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action
:language: yaml
```
---
## 多工作站及小车运行、物料转移
### `AGVTransfer`
@@ -147,7 +512,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `WorkStationRun`
@@ -155,12 +520,64 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `ResetHandling`
```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action
:language: yaml
```
---
### `ResourceCreateFromOuter`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action
:language: yaml
```
---
### `ResourceCreateFromOuterEasy`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action
:language: yaml
```
---
### `SetPumpPosition`
```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action
:language: yaml
```
---
## 固体分配与处理设备操作
### `SolidDispenseAddPowderTube`
```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action
:language: yaml
```
---
## 其他设备操作
### `EmptyIn`
```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action
:language: yaml
```
---
## 机械臂、夹爪等机器人设备
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`
### `FollowJointTrajectory`
```yaml
@@ -228,7 +645,8 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
```
----
---
### `GripperCommand`
```yaml
@@ -246,42 +664,19 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
```
----
---
### `JointTrajectory`
```yaml
trajectory_msgs/JointTrajectory trajectory
---
---
---
```
----
### `ParallelGripperCommand`
```yaml
# Parallel grippers refer to an end effector where two opposing fingers grasp an object from opposite sides.
sensor_msgs/JointState command
# name: the name(s) of the joint this command is requesting
# position: desired position of each gripper joint (radians or meters)
# velocity: (optional, not used if empty) max velocity of the joint allowed while moving (radians or meters / second)
# effort: (optional, not used if empty) max effort of the joint allowed while moving (Newtons or Newton-meters)
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
bool stalled # True if the gripper is exerting max effort and not moving
bool reached_goal # True if the gripper position has reached the commanded setpoint
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
```
----
### `PointHead`
```yaml
@@ -291,12 +686,13 @@ string pointing_frame
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
float64 pointing_angle_error
```
----
---
### `SingleJointPosition`
```yaml
@@ -304,15 +700,16 @@ float64 position
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
std_msgs/Header header
float64 position
float64 velocity
float64 error
```
----
---
### `AssistedTeleop`
```yaml
@@ -324,10 +721,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback
builtin_interfaces/Duration current_teleop_duration
```
----
---
### `BackUp`
```yaml
@@ -341,10 +738,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `ComputePathThroughPoses`
```yaml
@@ -359,10 +756,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `ComputePathToPose`
```yaml
@@ -377,10 +774,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `DriveOnHeading`
```yaml
@@ -394,10 +791,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `DummyBehavior`
```yaml
@@ -408,10 +805,10 @@ std_msgs/String command
builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
```
----
---
### `FollowPath`
```yaml
@@ -426,10 +823,10 @@ std_msgs/Empty result
#feedback definition
float32 distance_to_goal
float32 speed
```
----
---
### `FollowWaypoints`
```yaml
@@ -441,10 +838,10 @@ int32[] missed_waypoints
---
#feedback definition
uint32 current_waypoint
```
----
---
### `NavigateThroughPoses`
```yaml
@@ -462,10 +859,10 @@ builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
int16 number_of_poses_remaining
```
----
---
### `NavigateToPose`
```yaml
@@ -482,10 +879,10 @@ builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
```
----
---
### `SmoothPath`
```yaml
@@ -501,10 +898,10 @@ builtin_interfaces/Duration smoothing_duration
bool was_completed
---
#feedback definition
```
----
---
### `Spin`
```yaml
@@ -517,10 +914,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 angular_distance_traveled
```
----
---
### `Wait`
```yaml
@@ -532,7 +929,6 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
builtin_interfaces/Duration time_left
```
----
---

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,405 +0,0 @@
# 物料构建指南
## 概述
在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`: 版本号,用于版本管理

File diff suppressed because it is too large Load Diff

View File

@@ -26,19 +26,15 @@ boot_examples/index.md
## 开发者指南
```{toctree}
:maxdepth: 2
developer_guide/device_driver
developer_guide/add_device
developer_guide/add_action
developer_guide/actions
developer_guide/workstation_architecture
developer_guide/add_protocol
developer_guide/add_batteryPLC
developer_guide/materials_tutorial
developer_guide/materials_construction_guide
developer_guide/materials_tutorial.md
```
## 接口文档

View File

@@ -2,7 +2,6 @@
sphinx>=7.0.0
sphinx-rtd-theme>=2.0.0
myst-parser>=2.0.0
sphinxcontrib-mermaid
# 用于支持Jupyter notebook文档
myst-nb>=1.0.0

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.10.10
version: 0.10.7
source:
path: ../../unilabos_msgs
target_directory: src

View File

@@ -1,41 +0,0 @@
:: Generated by vinca http://github.com/RoboStack/vinca.
:: DO NOT EDIT!
setlocal EnableDelayedExpansion
set "PYTHONPATH=%LIBRARY_PREFIX%\lib\site-packages;%SP_DIR%"
:: MSVC is preferred.
set CC=cl.exe
set CXX=cl.exe
rd /s /q build
mkdir build
pushd build
:: set "CMAKE_GENERATOR=Ninja"
:: try to fix long paths issues by using default generator
set "CMAKE_GENERATOR=Visual Studio %VS_MAJOR% %VS_YEAR%"
set "SP_DIR_FORWARDSLASHES=%SP_DIR:\=/%"
set PYTHON="%PREFIX%\python.exe"
cmake ^
-G "%CMAKE_GENERATOR%" ^
-DCMAKE_INSTALL_PREFIX=%LIBRARY_PREFIX% ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP=True ^
-DPYTHON_EXECUTABLE=%PYTHON% ^
-DPython_EXECUTABLE=%PYTHON% ^
-DPython3_EXECUTABLE=%PYTHON% ^
-DSETUPTOOLS_DEB_LAYOUT=OFF ^
-DBUILD_SHARED_LIBS=ON ^
-DBUILD_TESTING=OFF ^
-DCMAKE_OBJECT_PATH_MAX=255 ^
-DPYTHON_INSTALL_DIR=%SP_DIR_FORWARDSLASHES% ^
--compile-no-warning-as-error ^
%SRC_DIR%\%PKG_NAME%\src\work
if errorlevel 1 exit 1
cmake --build . --config Release --target install
if errorlevel 1 exit 1

View File

@@ -1,71 +0,0 @@
# Generated by vinca http://github.com/RoboStack/vinca.
# DO NOT EDIT!
rm -rf build
mkdir build
cd build
# necessary for correctly linking SIP files (from python_qt_bindings)
export LINK=$CXX
if [[ "$CONDA_BUILD_CROSS_COMPILATION" != "1" ]]; then
PYTHON_EXECUTABLE=$PREFIX/bin/python
PKG_CONFIG_EXECUTABLE=$PREFIX/bin/pkg-config
OSX_DEPLOYMENT_TARGET="10.15"
else
PYTHON_EXECUTABLE=$BUILD_PREFIX/bin/python
PKG_CONFIG_EXECUTABLE=$BUILD_PREFIX/bin/pkg-config
OSX_DEPLOYMENT_TARGET="11.0"
fi
echo "USING PYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}"
echo "USING PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_EXECUTABLE}"
export ROS_PYTHON_VERSION=`$PYTHON_EXECUTABLE -c "import sys; print('%i.%i' % (sys.version_info[0:2]))"`
echo "Using Python ${ROS_PYTHON_VERSION}"
# Fix up SP_DIR which for some reason might contain a path to a wrong Python version
FIXED_SP_DIR=$(echo $SP_DIR | sed -E "s/python[0-9]+\.[0-9]+/python$ROS_PYTHON_VERSION/")
echo "Using site-package dir ${FIXED_SP_DIR}"
# see https://github.com/conda-forge/cross-python-feedstock/issues/24
if [[ "$CONDA_BUILD_CROSS_COMPILATION" == "1" ]]; then
find $PREFIX/lib/cmake -type f -exec sed -i "s~\${_IMPORT_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/share/rosidl* -type f -exec sed -i "s~$PREFIX/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/share/rosidl* -type f -exec sed -i "s~\${_IMPORT_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/lib/cmake -type f -exec sed -i "s~message(FATAL_ERROR \"The imported target~message(WARNING \"The imported target~g" {} + || true
fi
if [[ $target_platform =~ linux.* ]]; then
export CFLAGS="${CFLAGS} -D__STDC_FORMAT_MACROS=1"
export CXXFLAGS="${CXXFLAGS} -D__STDC_FORMAT_MACROS=1"
fi;
# Needed for qt-gui-cpp ..
if [[ $target_platform =~ linux.* ]]; then
ln -s $GCC ${BUILD_PREFIX}/bin/gcc
ln -s $GXX ${BUILD_PREFIX}/bin/g++
fi;
cmake \
-G "Ninja" \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_PREFIX_PATH=$PREFIX \
-DAMENT_PREFIX_PATH=$PREFIX \
-DCMAKE_INSTALL_LIBDIR=lib \
-DCMAKE_BUILD_TYPE=Release \
-DPYTHON_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython3_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython3_FIND_STRATEGY=LOCATION \
-DPKG_CONFIG_EXECUTABLE=$PKG_CONFIG_EXECUTABLE \
-DPYTHON_INSTALL_DIR=$FIXED_SP_DIR \
-DSETUPTOOLS_DEB_LAYOUT=OFF \
-DCATKIN_SKIP_TESTING=$SKIP_TESTING \
-DCMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP=True \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTING=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=$OSX_DEPLOYMENT_TARGET \
--compile-no-warning-as-error \
$SRC_DIR/$PKG_NAME/src/work
cmake --build . --config Release --target install

View File

@@ -1,61 +0,0 @@
package:
name: ros-humble-unilabos-msgs
version: 0.9.7
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
build:
script:
sel(win): bld_ament_cmake.bat
sel(unix): build_ament_cmake.sh
number: 5
about:
home: https://www.ros.org/
license: BSD-3-Clause
summary: |
Robot Operating System
extra:
recipe-maintainers:
- ros-forge
requirements:
build:
- "{{ compiler('cxx') }}"
- "{{ compiler('c') }}"
- sel(linux64): sysroot_linux-64 2.17
- ninja
- setuptools
- sel(unix): make
- sel(unix): coreutils
- sel(osx): tapi
- sel(build_platform != target_platform): pkg-config
- cmake
- cython
- sel(win): vs2022_win-64
- sel(build_platform != target_platform): python
- sel(build_platform != target_platform): cross-python_{{ target_platform }}
- sel(build_platform != target_platform): numpy
host:
- numpy
- pip
- sel(build_platform == target_platform): pkg-config
- robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ament-cmake
- robostack-staging::ros-humble-ament-lint-auto
- robostack-staging::ros-humble-ament-lint-common
- robostack-staging::ros-humble-ros-environment
- robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-generators
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
- robostack-staging::ros2-distro-mutex=0.5.*
run:
- robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-runtime
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
# - robostack-staging::ros2-distro-mutex=0.6.*
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: "0.10.10"
version: "0.10.7"
source:
path: ../..

View File

@@ -172,7 +172,7 @@ Examples:
with open(output_path, "w", encoding="utf-8") as f:
f.write(readme_content)
print(f" README.txt created: {output_path}")
print(f" README.txt created: {output_path}")
print(f" Platform: {args.platform}")
print(f" Branch: {args.branch}")

View File

@@ -8,10 +8,7 @@ This script verifies that UniLabOS and its dependencies are correctly installed.
Run this script after installing the conda-pack environment to ensure everything works.
Usage:
python verify_installation.py [--auto-install]
Options:
--auto-install Automatically install missing packages
python verify_installation.py
Or in the conda environment:
conda activate unilab
@@ -20,15 +17,14 @@ Usage:
import sys
import os
import argparse
# IMPORTANT: Set UTF-8 encoding BEFORE any other imports
# This ensures all subsequent imports (including unilabos) can output UTF-8 characters
if sys.platform == "win32":
# Method 1: Reconfigure stdout/stderr to use UTF-8 with error handling
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, OSError):
pass
@@ -53,7 +49,7 @@ CHECK_MARK = "[OK]"
CROSS_MARK = "[FAIL]"
def check_package(package_name: str, display_name: str | None = None) -> bool:
def check_package(package_name: str, display_name: str = None) -> bool:
"""
Check if a package can be imported.
@@ -91,25 +87,9 @@ def check_python_version() -> bool:
def main():
"""Run all verification checks."""
# Parse command line arguments
parser = argparse.ArgumentParser(
description="Verify UniLabOS installation",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--auto-install",
action="store_true",
help="Automatically install missing packages",
)
args = parser.parse_args()
print("=" * 60)
print("UniLabOS Installation Verification")
print("=" * 60)
if args.auto_install:
print("Mode: Auto-install missing packages")
else:
print("Mode: Verification only")
print()
all_passed = True
@@ -133,16 +113,14 @@ def main():
print(f" {CHECK_MARK} UniLabOS installed")
# Check environment with optional auto-install
# Check environment without auto-install (verification only)
# Set show_details=False to suppress detailed Chinese output that may cause encoding issues
env_check_passed = check_environment(auto_install=args.auto_install, show_details=False)
env_check_passed = check_environment(auto_install=False, show_details=False)
if env_check_passed:
print(f" {CHECK_MARK} All required packages available")
else:
print(f" {CROSS_MARK} Some optional packages are missing")
if not args.auto_install:
print(" Hint: Run with --auto-install to automatically install missing packages")
except ImportError:
print(f" {CROSS_MARK} UniLabOS not installed")
all_passed = False

View File

@@ -1,695 +0,0 @@
import json
import logging
import traceback
import uuid
import xml.etree.ElementTree as ET
from typing import Any, Dict, List
import networkx as nx
import matplotlib.pyplot as plt
import requests
logger = logging.getLogger(__name__)
class SimpleGraph:
"""简单的有向图实现,用于构建工作流图"""
def __init__(self):
self.nodes = {}
self.edges = []
def add_node(self, node_id, **attrs):
"""添加节点"""
self.nodes[node_id] = attrs
def add_edge(self, source, target, **attrs):
"""添加边"""
edge = {"source": source, "target": target, **attrs}
self.edges.append(edge)
def to_dict(self):
"""转换为工作流图格式"""
nodes_list = []
for node_id, attrs in self.nodes.items():
node_attrs = attrs.copy()
params = node_attrs.pop("parameters", {}) or {}
node_attrs.update(params)
nodes_list.append({"id": node_id, **node_attrs})
return {
"directed": True,
"multigraph": False,
"graph": {},
"nodes": nodes_list,
"links": self.edges,
}
def extract_json_from_markdown(text: str) -> str:
"""从markdown代码块中提取JSON"""
text = text.strip()
if text.startswith("```json\n"):
text = text[8:]
if text.startswith("```\n"):
text = text[4:]
if text.endswith("\n```"):
text = text[:-4]
return text
def convert_to_type(val: str) -> Any:
"""将字符串值转换为适当的数据类型"""
if val == "True":
return True
if val == "False":
return False
if val == "?":
return None
if val.endswith(" g"):
return float(val.split(" ")[0])
if val.endswith("mg"):
return float(val.split("mg")[0])
elif val.endswith("mmol"):
return float(val.split("mmol")[0]) / 1000
elif val.endswith("mol"):
return float(val.split("mol")[0])
elif val.endswith("ml"):
return float(val.split("ml")[0])
elif val.endswith("RPM"):
return float(val.split("RPM")[0])
elif val.endswith(" °C"):
return float(val.split(" ")[0])
elif val.endswith(" %"):
return float(val.split(" ")[0])
return val
def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""统一的数据重构函数,根据操作类型自动选择模板"""
refactored_data = []
# 定义操作映射,包含生物实验和有机化学的所有操作
OPERATION_MAPPING = {
# 生物实验操作
"transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid",
"transfer": "SynBioFactory-liquid_handler.biomek-transfer",
"incubation": "SynBioFactory-liquid_handler.biomek-incubation",
"move_labware": "SynBioFactory-liquid_handler.biomek-move_labware",
"oscillation": "SynBioFactory-liquid_handler.biomek-oscillation",
# 有机化学操作
"HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol",
"StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol",
"StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol",
"HeatChill": "SynBioFactory-workstation-HeatChillProtocol",
"Dissolve": "SynBioFactory-workstation-DissolveProtocol",
"Transfer": "SynBioFactory-workstation-TransferProtocol",
"Evaporate": "SynBioFactory-workstation-EvaporateProtocol",
"Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol",
"Filter": "SynBioFactory-workstation-FilterProtocol",
"Dry": "SynBioFactory-workstation-DryProtocol",
"Add": "SynBioFactory-workstation-AddProtocol",
}
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
for step in data:
operation = step.get("action")
if not operation or operation in UNSUPPORTED_OPERATIONS:
continue
# 处理重复操作
if operation == "Repeat":
times = step.get("times", step.get("parameters", {}).get("times", 1))
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
for i in range(int(times)):
sub_data = refactor_data(sub_steps)
refactored_data.extend(sub_data)
continue
# 获取模板名称
template = OPERATION_MAPPING.get(operation)
if not template:
# 自动推断模板类型
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
template = f"SynBioFactory-liquid_handler.biomek-{operation}"
else:
template = f"SynBioFactory-workstation-{operation}Protocol"
# 创建步骤数据
step_data = {
"template": template,
"description": step.get("description", step.get("purpose", f"{operation} operation")),
"lab_node_type": "Device",
"parameters": step.get("parameters", step.get("action_args", {})),
}
refactored_data.append(step_data)
return refactored_data
def build_protocol_graph(
labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str
) -> SimpleGraph:
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑"""
G = SimpleGraph()
resource_last_writer = {}
LAB_NAME = "SynBioFactory"
protocol_steps = refactor_data(protocol_steps)
# 检查协议步骤中的模板来判断协议类型
has_biomek_template = any(
("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", ""))
for step in protocol_steps
)
if has_biomek_template:
# 生物实验协议图构建
for labware_id, labware in labware_info.items():
node_id = str(uuid.uuid4())
labware_attrs = labware.copy()
labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}"))
labware_attrs["description"] = labware_id
labware_attrs["lab_node_type"] = (
"Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample"
)
labware_attrs["device_id"] = workstation_name
G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs)
resource_last_writer[labware_id] = f"{node_id}:labware"
# 处理协议步骤
prev_node = None
for i, step in enumerate(protocol_steps):
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 添加控制流边
if prev_node is not None:
G.add_edge(prev_node, node_id, source_port="ready", target_port="ready")
prev_node = node_id
# 处理物料流
params = step.get("parameters", {})
if "sources" in params and params["sources"] in resource_last_writer:
source_node, source_port = resource_last_writer[params["sources"]].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port="labware")
if "targets" in params:
resource_last_writer[params["targets"]] = f"{node_id}:labware"
# 添加协议结束节点
end_id = str(uuid.uuid4())
G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol")
if prev_node is not None:
G.add_edge(prev_node, end_id, source_port="ready", target_port="ready")
else:
# 有机化学协议图构建
WORKSTATION_ID = workstation_name
# 为所有labware创建资源节点
for item_id, item in labware_info.items():
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
node_id = str(uuid.uuid4())
# 判断节点类型
if item.get("type") == "hardware" or "reactor" in str(item_id).lower():
if "reactor" not in str(item_id).lower():
continue
lab_node_type = "Sample"
description = f"Prepare Reactor: {item_id}"
liquid_type = []
liquid_volume = []
else:
lab_node_type = "Reagent"
description = f"Add Reagent to Flask: {item_id}"
liquid_type = [item_id]
liquid_volume = [1e5]
G.add_node(
node_id,
template=f"{LAB_NAME}-host_node-create_resource",
description=description,
lab_node_type=lab_node_type,
res_id=item_id,
device_id=WORKSTATION_ID,
class_name="container",
parent=WORKSTATION_ID,
bind_locations={"x": 0.0, "y": 0.0, "z": 0.0},
liquid_input_slot=[-1],
liquid_type=liquid_type,
liquid_volume=liquid_volume,
slot_on_deck="",
role=item.get("role", ""),
)
resource_last_writer[item_id] = f"{node_id}:labware"
last_control_node_id = None
# 处理协议步骤
for step in protocol_steps:
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 控制流
if last_control_node_id is not None:
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
last_control_node_id = node_id
# 物料流
params = step.get("parameters", {})
input_resources = {
"Vessel": params.get("vessel"),
"ToVessel": params.get("to_vessel"),
"FromVessel": params.get("from_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources": params.get("sources"),
"targets": params.get("targets"),
}
for target_port, resource_name in input_resources.items():
if resource_name and resource_name in resource_last_writer:
source_node, source_port = resource_last_writer[resource_name].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
output_resources = {
"VesselOut": params.get("vessel"),
"FromVesselOut": params.get("from_vessel"),
"ToVesselOut": params.get("to_vessel"),
"FiltrateOut": params.get("filtrate_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources_out": params.get("sources"),
"targets_out": params.get("targets"),
}
for source_port, resource_name in output_resources.items():
if resource_name:
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
return G
def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str):
"""
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
G.add_node(node_id, label=label, **attrs)
for edge in protocol_graph.edges:
G.add_edge(edge["source"], edge["target"])
plt.figure(figsize=(20, 15))
try:
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
except Exception:
pos = nx.shell_layout(G) # Fallback layout
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
nx.draw(
G,
pos,
with_labels=False,
node_size=2500,
node_color="skyblue",
node_shape="o",
edge_color="gray",
width=1.5,
arrowsize=15,
)
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
plt.title("Chemical Protocol Workflow Graph", size=15)
plt.savefig(output_path, dpi=300, bbox_inches="tight")
plt.close()
print(f" - Visualization saved to '{output_path}'")
from networkx.drawing.nx_agraph import to_agraph
import re
COMPASS = {"n","e","s","w","ne","nw","se","sw","c"}
def _is_compass(port: str) -> bool:
return isinstance(port, str) and port.lower() in COMPASS
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
"""
使用 Graphviz 端口语法绘制协议工作流图。
- 若边上的 source_port/target_port 是 compassn/e/s/w/...),直接用 compass。
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
最终由 PyGraphviz 渲染并输出到 output_path后缀决定格式如 .png/.svg/.pdf
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
# 1) 先用 networkx 搭建有向图,保留端口属性
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)})
edges_data = []
in_ports_by_node = {} # 收集命名输入端口
out_ports_by_node = {} # 收集命名输出端口
for edge in protocol_graph.edges:
u = edge["source"]
v = edge["target"]
sp = edge.get("source_port")
tp = edge.get("target_port")
# 记录到图里(保留原始端口信息)
G.add_edge(u, v, source_port=sp, target_port=tp)
edges_data.append((u, v, sp, tp))
# 如果不是 compass就按“命名端口”先归类等会儿给节点造 record
if sp and not _is_compass(sp):
out_ports_by_node.setdefault(u, set()).add(str(sp))
if tp and not _is_compass(tp):
in_ports_by_node.setdefault(v, set()).add(str(tp))
# 2) 转为 AGraph使用 Graphviz 渲染
A = to_agraph(G)
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica")
A.edge_attr.update(arrowsize="0.8", color="#666666")
# 3) 为需要命名端口的节点设置 record 形状与 label
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
for n in A.nodes():
node = A.get_node(n)
core = G.nodes[n].get("_core_label", n)
in_ports = sorted(in_ports_by_node.get(n, []))
out_ports = sorted(out_ports_by_node.get(n, []))
# 如果该节点涉及命名端口,则用 record否则保留原 box
if in_ports or out_ports:
def port_fields(ports):
if not ports:
return " " # 必须留一个空槽占位
# 每个端口一个小格子,<p> name
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
left = port_fields(in_ports)
right = port_fields(out_ports)
# 三栏:左(入) | 中(节点名) | 右(出)
record_label = f"{{ {left} | {core} | {right} }}"
node.attr.update(shape="record", label=record_label)
else:
# 没有命名端口:普通盒子,显示核心标签
node.attr.update(label=str(core))
# 4) 给边设置 headport / tailport
# - 若端口为 compass直接用 compasse.g., headport="e"
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
for (u, v, sp, tp) in edges_data:
e = A.get_edge(u, v)
# Graphviz 属性tail 是源head 是目标
if sp:
if _is_compass(sp):
e.attr["tailport"] = sp.lower()
else:
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp))
if tp:
if _is_compass(tp):
e.attr["headport"] = tp.lower()
else:
e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp))
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
# e.attr["arrowhead"] = "vee"
# 5) 输出
A.draw(output_path, prog="dot")
print(f" - Port-aware workflow rendered to '{output_path}'")
def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]:
"""展平嵌套的XDL程序结构"""
flattened_operations = []
TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"]
def extract_operations(element: ET.Element):
if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]:
if element.tag not in TEMP_UNSUPPORTED_PROTOCOL:
flattened_operations.append(element)
for child in element:
extract_operations(child)
for child in procedure_elem:
extract_operations(child)
return flattened_operations
def parse_xdl_content(xdl_content: str) -> tuple:
"""解析XDL内容"""
try:
xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable())
root = ET.fromstring(xdl_content_cleaned)
synthesis_elem = root.find("Synthesis")
if synthesis_elem is None:
return None, None, None
# 解析硬件组件
hardware_elem = synthesis_elem.find("Hardware")
hardware = []
if hardware_elem is not None:
hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")]
# 解析试剂
reagents_elem = synthesis_elem.find("Reagents")
reagents = []
if reagents_elem is not None:
reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")]
# 解析程序
procedure_elem = synthesis_elem.find("Procedure")
if procedure_elem is None:
return None, None, None
flattened_operations = flatten_xdl_procedure(procedure_elem)
return hardware, reagents, flattened_operations
except ET.ParseError as e:
raise ValueError(f"Invalid XDL format: {e}")
def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]:
"""
将XDL XML格式转换为标准的字典格式
Args:
xdl_content: XDL XML内容
Returns:
转换结果,包含步骤和器材信息
"""
try:
hardware, reagents, flattened_operations = parse_xdl_content(xdl_content)
if hardware is None:
return {"error": "Failed to parse XDL content", "success": False}
# 将XDL元素转换为字典格式
steps_data = []
for elem in flattened_operations:
# 转换参数类型
parameters = {}
for key, val in elem.attrib.items():
converted_val = convert_to_type(val)
if converted_val is not None:
parameters[key] = converted_val
step_dict = {
"operation": elem.tag,
"parameters": parameters,
"description": elem.get("purpose", f"Operation: {elem.tag}"),
}
steps_data.append(step_dict)
# 合并硬件和试剂为统一的labware_info格式
labware_data = []
labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware)
labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents)
return {
"success": True,
"steps": steps_data,
"labware": labware_data,
"message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.",
}
except Exception as e:
error_msg = f"XDL conversion failed: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
def create_workflow(
steps_info: str,
labware_info: str,
workflow_name: str = "Generated Workflow",
workstation_name: str = "workstation",
workflow_description: str = "Auto-generated workflow from protocol",
) -> Dict[str, Any]:
"""
创建工作流,输入数据已经是统一的字典格式
Args:
steps_info: 步骤信息 (JSON字符串已经是list of dict格式)
labware_info: 实验器材和试剂信息 (JSON字符串已经是list of dict格式)
workflow_name: 工作流名称
workflow_description: 工作流描述
Returns:
创建结果包含工作流UUID和详细信息
"""
try:
# 直接解析JSON数据
steps_info_clean = extract_json_from_markdown(steps_info)
labware_info_clean = extract_json_from_markdown(labware_info)
steps_data = json.loads(steps_info_clean)
labware_data = json.loads(labware_info_clean)
# 统一处理所有数据
protocol_graph = build_protocol_graph(labware_data, steps_data, workstation_name=workstation_name)
# 检测协议类型(用于标签)
protocol_type = "bio" if any("biomek" in step.get("template", "") for step in refactored_steps) else "organic"
# 转换为工作流格式
data = protocol_graph.to_dict()
# 转换节点格式
for i, node in enumerate(data["nodes"]):
description = node.get("description", "")
onode = {
"template": node.pop("template"),
"id": node["id"],
"lab_node_type": node.get("lab_node_type", "Device"),
"name": description or f"Node {i + 1}",
"params": {"default": node},
"handles": {},
}
# 处理边连接
for edge in data["links"]:
if edge["source"] == node["id"]:
source_port = edge.get("source_port", "output")
if source_port not in onode["handles"]:
onode["handles"][source_port] = {"type": "source"}
if edge["target"] == node["id"]:
target_port = edge.get("target_port", "input")
if target_port not in onode["handles"]:
onode["handles"][target_port] = {"type": "target"}
data["nodes"][i] = onode
# 发送到API创建工作流
api_secret = configs.Lab.Key
if not api_secret:
return {"error": "API SecretKey is not configured", "success": False}
# Step 1: 创建工作流
workflow_url = f"{configs.Lab.Api}/api/v1/workflow/"
headers = {
"Content-Type": "application/json",
}
params = {"secret_key": api_secret}
graph_data = {"name": workflow_name, **data}
logger.info(f"Creating workflow: {workflow_name}")
response = requests.post(
workflow_url, params=params, json=graph_data, headers=headers, timeout=configs.Lab.Timeout
)
response.raise_for_status()
workflow_info = response.json()
if workflow_info.get("code") != 0:
error_msg = f"API returned an error: {workflow_info.get('msg', 'Unknown Error')}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
workflow_uuid = workflow_info.get("data", {}).get("uuid")
if not workflow_uuid:
return {"error": "Failed to get workflow UUID from response", "success": False}
# Step 2: 添加到模板库(可选)
try:
library_url = f"{configs.Lab.Api}/api/flociety/vs/workflows/library/"
lib_payload = {
"workflow_uuid": workflow_uuid,
"title": workflow_name,
"description": workflow_description,
"labels": [protocol_type.title(), "Auto-generated"],
}
library_response = requests.post(
library_url, params=params, json=lib_payload, headers=headers, timeout=configs.Lab.Timeout
)
library_response.raise_for_status()
library_info = library_response.json()
logger.info(f"Workflow added to library: {library_info}")
return {
"success": True,
"workflow_uuid": workflow_uuid,
"workflow_info": workflow_info.get("data"),
"library_info": library_info.get("data"),
"protocol_type": protocol_type,
"message": f"Workflow '{workflow_name}' created successfully",
}
except Exception as e:
# 即使添加到库失败,工作流创建仍然成功
logger.warning(f"Failed to add workflow to library: {str(e)}")
return {
"success": True,
"workflow_uuid": workflow_uuid,
"workflow_info": workflow_info.get("data"),
"protocol_type": protocol_type,
"message": f"Workflow '{workflow_name}' created successfully (library addition failed)",
}
except requests.exceptions.RequestException as e:
error_msg = f"Network error when calling API: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
except json.JSONDecodeError as e:
error_msg = f"JSON parsing error: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
except Exception as e:
error_msg = f"An unexpected error occurred: {str(e)}"
logger.error(error_msg)
logger.error(traceback.format_exc())
return {"error": error_msg, "success": False}

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.10.10',
version='0.10.7',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],

View File

@@ -1,171 +0,0 @@
{
"nodes": [
{
"id": "dispensing_station_bioyond",
"name": "dispensing_station_bioyond",
"children": [
"Bioyond_Dispensing_Deck"
],
"parent": null,
"type": "device",
"class": "bioyond_dispensing_station",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44400",
"material_type_mappings": {
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14196b-8bcf-a460-4f74-23f21ca79e72"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"分装板",
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14196c-76be-2279-4e22-7310d69aed68"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
],
"BIOYOND_PolymerStation_8StockCarrier": [
"样品板",
"3a14196e-b7a0-a5da-1931-35f3000281e9"
],
"BIOYOND_PolymerStation_Solid_Stock": [
"样品瓶",
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
]
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Dispensing_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Dispensing_Deck",
"name": "Bioyond_Dispensing_Deck",
"sample_id": null,
"children": [],
"parent": "dispensing_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerPreparationStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerPreparationStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "reaction_station_bioyond",
"name": "reaction_station_bioyond",
"parent": null,
"children": [
"Bioyond_Deck"
],
"type": "device",
"class": "reaction_station.bioyond",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402",
"workflow_mappings": {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"BIOYOND_PolymerStation_Reactor": [
"反应器",
"3a14233b-902d-0d7b-4533-3f60f1c41c1b"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14233b-56e3-6c53-a8ab-fcaac163a9ba"
],
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14233b-f0a9-ba84-eaa9-0d4718b361b6"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"样品板",
"3a142339-80de-8f25-6093-1b1b1b6c322e"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14233a-26e1-28f8-af6a-60ca06ba0165"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14233a-84a3-088d-6676-7cb4acd57c64"
],
"BIOYOND_PolymerStation_TipBox": [
"枪头盒",
"3a143890-9d51-60ac-6d6f-6edb43c12041"
]
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerReactionStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -170,16 +170,15 @@
"z": 0
},
"config": {
"max_volume": 1000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 200,
"size_y": 150,
"size_z": 0
"max_volume": 1000.0
},
"data": {
"liquids": [["DMF", 500.0]],
"pending_liquids": [["DMF", 500.0]]
"liquids": [
{
"liquid_type": "DMF",
"liquid_volume": 1000.0
}
]
}
},
{
@@ -195,16 +194,15 @@
"z": 0
},
"config": {
"max_volume": 1000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 200,
"size_y": 150,
"size_z": 0
"max_volume": 1000.0
},
"data": {
"liquids": [["ethyl_acetate", 1000.0]],
"pending_liquids": [["ethyl_acetate", 1000.0]]
"liquids": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 1000.0
}
]
}
},
{
@@ -220,16 +218,15 @@
"z": 0
},
"config": {
"max_volume": 1000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 300,
"size_y": 150,
"size_z": 0
"max_volume": 1000.0
},
"data": {
"liquids": [["hexane", 1000.0]],
"pending_liquids": [["hexane", 1000.0]]
"liquids": [
{
"liquid_type": "hexane",
"liquid_volume": 1000.0
}
]
}
},
{
@@ -245,16 +242,15 @@
"z": 0
},
"config": {
"max_volume": 1000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 900,
"size_y": 150,
"size_z": 0
"max_volume": 1000.0
},
"data": {
"liquids": [["methanol", 1000.0]],
"pending_liquids": [["methanol", 1000.0]]
"liquids": [
{
"liquid_type": "methanol",
"liquid_volume": 1000.0
}
]
}
},
{
@@ -270,16 +266,15 @@
"z": 0
},
"config": {
"max_volume": 1000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 950,
"size_y": 150,
"size_z": 0
"max_volume": 1000.0
},
"data": {
"liquids": [["water", 1000.0]],
"pending_liquids": [["water", 1000.0]]
"liquids": [
{
"liquid_type": "water",
"liquid_volume": 1000.0
}
]
}
},
{
@@ -340,16 +335,14 @@
},
"config": {
"max_volume": 500.0,
"type": "RegularContainer",
"category": "container",
"max_temp": 200.0,
"min_temp": -20.0,
"has_stirrer": true,
"has_heater": true
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -426,16 +419,11 @@
"z": 0
},
"config": {
"max_volume": 2000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 500,
"size_y": 400,
"size_z": 0
"max_volume": 2000.0
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -451,16 +439,11 @@
"z": 0
},
"config": {
"max_volume": 2000.0,
"type": "RegularContainer",
"category": "container",
"size_x": 1100,
"size_y": 500,
"size_z": 0
"max_volume": 2000.0
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -666,16 +649,11 @@
"z": 0
},
"config": {
"max_volume": 250.0,
"type": "RegularContainer",
"category": "container",
"size_x": 900,
"size_y": 500,
"size_z": 0
"max_volume": 250.0
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -691,16 +669,11 @@
"z": 0
},
"config": {
"max_volume": 250.0,
"type": "RegularContainer",
"category": "container",
"size_x": 950,
"size_y": 500,
"size_z": 0
"max_volume": 250.0
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -716,16 +689,11 @@
"z": 0
},
"config": {
"max_volume": 250.0,
"type": "RegularContainer",
"category": "container",
"size_x": 1050,
"size_y": 500,
"size_z": 0
"max_volume": 250.0
},
"data": {
"liquids": [],
"pending_liquids": []
"liquids": [
]
}
},
{
@@ -765,11 +733,6 @@
},
"config": {
"max_volume": 500.0,
"size_x": 550,
"size_y": 250,
"size_z": 0,
"type": "RegularContainer",
"category": "container",
"reagent": "sodium_chloride",
"physical_state": "solid"
},
@@ -793,11 +756,6 @@
},
"config": {
"volume": 500.0,
"size_x": 600,
"size_y": 250,
"size_z": 0,
"type": "RegularContainer",
"category": "container",
"reagent": "sodium_carbonate",
"physical_state": "solid"
},
@@ -821,11 +779,6 @@
},
"config": {
"volume": 500.0,
"size_x": 650,
"size_y": 250,
"size_z": 0,
"type": "RegularContainer",
"category": "container",
"reagent": "magnesium_chloride",
"physical_state": "solid"
},

View File

@@ -8,41 +8,11 @@
],
"parent": null,
"type": "device",
"class": "bioyond_dispensing_station",
"class": "dispensing_station.bioyond",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388",
"material_type_mappings": {
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14196b-8bcf-a460-4f74-23f21ca79e72"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"分装板",
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14196c-76be-2279-4e22-7310d69aed68"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
],
"BIOYOND_PolymerStation_8StockCarrier": [
"样品板",
"3a14196e-b7a0-a5da-1931-35f3000281e9"
],
"BIOYOND_PolymerStation_Solid_Stock": [
"样品瓶",
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
]
}
"api_host": "http://192.168.1.200:44388"
},
"deck": {
"data": {
@@ -50,6 +20,13 @@
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
}
},
"station_config": {
"station_type": "dispensing_station",
"enable_dispensing_station": true,
"enable_reaction_station": false,
"station_name": "DispensingStation_001",
"description": "Bioyond配液工作站"
},
"protocol_type": []
},
"data": {}

View File

@@ -1,19 +0,0 @@
{
"nodes": [
{
"id": "id",
"name": "name",
"children": [
],
"parent": null,
"type": "device",
"class": "opcua_example",
"config": {
"url": "url",
"config_path": "unilabos/device_comms/opcua_client/opcua_workflow_example.json"
},
"data": {
}
}
]
}

View File

@@ -10,22 +10,24 @@
"x": 620.6111111111111,
"y": 171,
"z": 0
},
},
"config": {
"data": {
"children": [
{
"_resource_child_name": "deck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
}
],
"backend": {
"type": "LiquidHandlerRvizBackend"
}
}
},
"data": {},
"children": [
"deck"
],
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
},
"backend": {
"type": "UniLiquidHandlerRvizBackend"
},
"simulator": true
}
]
},
{
"id": "deck",
@@ -9648,7 +9650,7 @@
"children": [],
"parent": null,
"type": "device",
"class": "robotic_arm.SCARA_with_slider.moveit.virtual",
"class": "robotic_arm.SCARA_with_slider.virtual",
"position": {
"x": -500,
"y": 1000,

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"type": "device",
"class": "reaction_station.bioyond",
"config": {
"config": {
"bioyond_config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402",
"workflow_mappings": {
@@ -19,39 +19,14 @@
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"BIOYOND_PolymerStation_Reactor": [
"反应器",
"3a14233b-902d-0d7b-4533-3f60f1c41c1b"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14233b-56e3-6c53-a8ab-fcaac163a9ba"
],
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14233b-f0a9-ba84-eaa9-0d4718b361b6"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"样品板",
"3a142339-80de-8f25-6093-1b1b1b6c322e"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14233a-26e1-28f8-af6a-60ca06ba0165"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14233a-84a3-088d-6676-7cb4acd57c64"
],
"BIOYOND_PolymerStation_TipBox": [
"枪头盒",
"3a143890-9d51-60ac-6d6f-6edb43c12041"
]
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
}
},
"deck": {
@@ -67,7 +42,9 @@
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [],
"sample_id": null,
"children": [
],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",

View File

@@ -18,21 +18,21 @@
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "unilabos.devices.liquid_handling.laiyu.laiyu:TransformXYZDeck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck",
"name": "deck"
},
"backend": {
"type": "UniLiquidHandlerLaiyuBackend",
"port": "/dev/ttyUSB_CH340"
"type": "UniLiquidHandlerRvizBackend"
},
"simulator": false,
"total_height": 232.5
"simulator": true,
"total_height": 300
}
},
{
"id": "deck",
"name": "deck",
"sample_id": null,
"children": [
"tip_rack",
"plate_well",
@@ -64,7 +64,7 @@
{
"id": "tip_rack",
"name": "tip_rack",
"sample_id": null,
"children": [
"tip_rack_A1"
],
@@ -102,7 +102,7 @@
{
"id": "tip_rack_A1",
"name": "tip_rack_A1",
"sample_id": null,
"children": [],
"parent": "tip_rack",
"type": "container",
@@ -144,7 +144,7 @@
{
"id": "plate_well",
"name": "plate_well",
"sample_id": null,
"children": [
"plate_well_A1"
],
@@ -156,6 +156,18 @@
"y": 116,
"z": 48.5
},
"pose": {
"position_3d": {
"x": 161,
"y": 116,
"z": 48.5
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
"config": {
"type": "Plate",
"size_x": 127.76,
@@ -183,7 +195,7 @@
{
"id": "plate_well_A1",
"name": "plate_well_A1",
"sample_id": null,
"children": [],
"parent": "plate_well",
"type": "device",
@@ -224,7 +236,7 @@
{
"id": "tube_rack",
"name": "tube_rack",
"sample_id": null,
"children": [
"tube_rack_A1"
],
@@ -259,7 +271,7 @@
{
"id": "tube_rack_A1",
"name": "tube_rack_A1",
"sample_id": null,
"children": [],
"parent": "tube_rack",
"type": "device",
@@ -303,7 +315,7 @@
{
"id": "bottle_rack",
"name": "bottle_rack",
"sample_id": null,
"children": [
"bottle_rack_A1"
],
@@ -339,7 +351,7 @@
{
"id": "bottle_rack_A1",
"name": "bottle_rack_A1",
"sample_id": null,
"children": [],
"parent": "bottle_rack",
"type": "device",

View File

@@ -1,383 +0,0 @@
{
"nodes": [
{
"id": "liquid_handler",
"name": "liquid_handler",
"parent": null,
"type": "device",
"class": "liquid_handler",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"data": {},
"children": [
"deck"
],
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "unilabos.devices.liquid_handling.laiyu.laiyu:TransformXYZDeck",
"name": "deck"
},
"backend": {
"type": "UniLiquidHandlerRvizBackend"
},
"simulator": true,
"total_height": 300,
"joint_config": "TransformXYZDeck",
"simulate_rviz": true
}
},
{
"id": "deck",
"name": "deck",
"children": [
"tip_rack",
"plate_well",
"tube_rack",
"bottle_rack"
],
"parent": "liquid_handler",
"type": "deck",
"class": "TransformXYZDeck",
"position": {
"x": 0,
"y": 0,
"z": 18
},
"config": {
"type": "TransformXYZDeck",
"size_x": 624.3,
"size_y": 565.2,
"size_z": 900,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "tip_rack",
"name": "tip_rack",
"children": [
"tip_rack_A1"
],
"parent": "deck",
"type": "tip_rack",
"class": "tiprack_box",
"position": {
"x": 150,
"y": 7,
"z": 103
},
"config": {
"type": "TipRack",
"size_x": 134,
"size_y": 96,
"size_z": 7.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tip_rack",
"model": "tiprack_box",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "tip_rack_A1",
"name": "tip_rack_A1",
"children": [],
"parent": "tip_rack",
"type": "container",
"class": "",
"position": {
"x": 11.12,
"y": 75,
"z": -91.54
},
"config": {
"type": "TipSpot",
"size_x": 9,
"size_y": 9,
"size_z": 95,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tip_spot",
"model": null,
"prototype_tip": {
"type": "Tip",
"total_tip_length": 95,
"has_filter": false,
"maximal_volume": 1000.0,
"fitting_depth": 3.29
}
},
"data": {
"tip": null,
"tip_state": null,
"pending_tip": null
}
},
{
"id": "plate_well",
"name": "plate_well",
"children": [
"plate_well_A1"
],
"parent": "deck",
"type": "plate",
"class": "plate_96",
"position": {
"x": 161,
"y": 116,
"z": 48.5
},
"config": {
"type": "Plate",
"size_x": 127.76,
"size_y": 85.48,
"size_z": 45.5,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": "plate_96",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "plate_well_A1",
"name": "plate_well_A1",
"children": [],
"parent": "plate_well",
"type": "device",
"class": "",
"position": {
"x": 10.1,
"y": 70,
"z": 6.1
},
"config": {
"type": "Well",
"size_x": 8.2,
"size_y": 8.2,
"size_z": 38,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "well",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
},
{
"id": "tube_rack",
"name": "tube_rack",
"children": [
"tube_rack_A1"
],
"parent": "deck",
"type": "container",
"class": "tube_container",
"position": {
"x": 0,
"y": 127,
"z": 0
},
"config": {
"type": "Plate",
"size_x": 151,
"size_y": 75,
"size_z": 75,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"model": "tube_container",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "tube_rack_A1",
"name": "tube_rack_A1",
"children": [],
"parent": "tube_rack",
"type": "device",
"class": "",
"position": {
"x": 6,
"y": 38,
"z": 10
},
"config": {
"type": "Well",
"size_x": 34,
"size_y": 34,
"size_z": 117,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
}
,
{
"id": "bottle_rack",
"name": "bottle_rack",
"children": [
"bottle_rack_A1"
],
"parent": "deck",
"type": "container",
"class": "bottle_container",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "Plate",
"size_x": 130,
"size_y": 117,
"size_z": 8,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube_rack",
"model": "bottle_container",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "bottle_rack_A1",
"name": "bottle_rack_A1",
"children": [],
"parent": "bottle_rack",
"type": "device",
"class": "",
"position": {
"x": 25,
"y": 18.5,
"z": 8
},
"config": {
"type": "Well",
"size_x": 80,
"size_y": 80,
"size_z": 117,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
}
],
"links": []
}

View File

@@ -3,8 +3,7 @@
"""
import asyncio
from typing import Dict, Any, List
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from typing import Dict, Any, Optional, List
class SmartPumpController:
@@ -15,8 +14,6 @@ class SmartPumpController:
适用于实验室自动化系统中的液体处理任务。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
"""
初始化智能泵控制器
@@ -33,9 +30,6 @@ class SmartPumpController:
self.calibration_factor = 1.0
self.pump_mode = "continuous" # continuous, volume, rate
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def connect_device(self, timeout: int = 10) -> bool:
"""
连接到泵设备
@@ -96,7 +90,7 @@ class SmartPumpController:
pump_time = (volume / flow_rate) * 60 # 转换为秒
self.current_flow_rate = flow_rate
await self._ros_node.sleep(min(pump_time, 3.0)) # 模拟泵送过程
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
self.total_volume_pumped += volume
self.current_flow_rate = 0.0
@@ -176,8 +170,6 @@ class AdvancedTemperatureController:
适用于需要精确温度控制的化学反应和材料处理过程。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, controller_id: str = "temp_controller_01"):
"""
初始化温度控制器
@@ -193,9 +185,6 @@ class AdvancedTemperatureController:
self.pid_enabled = True
self.temperature_history: List[Dict] = []
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
"""
设置目标温度
@@ -249,7 +238,7 @@ class AdvancedTemperatureController:
}
)
await self._ros_node.sleep(step_time)
await asyncio.sleep(step_time)
# 保持历史记录不超过100条
if len(self.temperature_history) > 100:
@@ -341,8 +330,6 @@ class MultiChannelAnalyzer:
常用于光谱分析、电化学测量等应用场景。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
"""
初始化多通道分析仪
@@ -357,9 +344,6 @@ class MultiChannelAnalyzer:
self.is_measuring = False
self.sample_rate = 1000 # Hz
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
"""
配置通道
@@ -392,7 +376,7 @@ class MultiChannelAnalyzer:
# 模拟数据采集
measurements = []
for _ in range(duration):
for second in range(duration):
timestamp = asyncio.get_event_loop().time()
frame_data = {}
@@ -407,7 +391,7 @@ class MultiChannelAnalyzer:
measurements.append({"timestamp": timestamp, "data": frame_data})
await self._ros_node.sleep(1.0) # 每秒采集一次
await asyncio.sleep(1.0) # 每秒采集一次
self.is_measuring = False
@@ -481,8 +465,6 @@ class AutomatedDispenser:
集成称重功能,确保分配精度和重现性。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, dispenser_id: str = "dispenser_01"):
"""
初始化自动分配器
@@ -497,9 +479,6 @@ class AutomatedDispenser:
self.container_capacity = 1000.0 # mL
self.precision_mode = True
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def move_to_position(self, x: float, y: float, z: float) -> bool:
"""
移动到指定位置
@@ -538,7 +517,7 @@ class AutomatedDispenser:
if viscosity == "high":
dispense_time *= 2 # 高粘度液体需要更长时间
await self._ros_node.sleep(min(dispense_time, 5.0)) # 最多等待5秒
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
self.dispensed_total += volume

View File

@@ -12,13 +12,23 @@ lab_registry.setup()
type_mapping = {
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6StockCarrier",
"分装板": "BIOYOND_PolymerStation_6VialCarrier",
"样品瓶": "BIOYOND_PolymerStation_Solid_Stock",
"90%分装小瓶": "BIOYOND_PolymerStation_Solid_Vial",
"10%分装小瓶": "BIOYOND_PolymerStation_Liquid_Vial",
}
type_uuid_mapping = {
"烧杯": "",
"试剂瓶": "",
"样品板": "",
"分装板": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
"样品瓶": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
"90%分装小瓶": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
"10%分装小瓶": "3a14196c-76be-2279-4e22-7310d69aed68",
}

View File

@@ -1,115 +0,0 @@
#!/usr/bin/env python3
"""
测试修改后的 get_child_identifier 函数
"""
from unilabos.resources.itemized_carrier import ItemizedCarrier, Bottle
from pylabrobot.resources.coordinate import Coordinate
def test_get_child_identifier_with_indices():
"""测试返回x,y,z索引的 get_child_identifier 函数"""
# 创建一些测试瓶子
bottle1 = Bottle("bottle1", diameter=25.0, height=50.0, max_volume=15.0)
bottle1.location = Coordinate(10, 20, 5)
bottle2 = Bottle("bottle2", diameter=25.0, height=50.0, max_volume=15.0)
bottle2.location = Coordinate(50, 20, 5)
bottle3 = Bottle("bottle3", diameter=25.0, height=50.0, max_volume=15.0)
bottle3.location = Coordinate(90, 20, 5)
# 创建载架,指定维度
sites = {
"A1": bottle1,
"A2": bottle2,
"A3": bottle3,
"B1": None, # 空位
"B2": None,
"B3": None
}
carrier = ItemizedCarrier(
name="test_carrier",
size_x=150,
size_y=100,
size_z=30,
num_items_x=3, # 3列
num_items_y=2, # 2行
num_items_z=1, # 1层
sites=sites
)
print("测试载架维度:")
print(f"num_items_x: {carrier.num_items_x}")
print(f"num_items_y: {carrier.num_items_y}")
print(f"num_items_z: {carrier.num_items_z}")
print()
# 测试获取bottle1的标识符信息 (A1 = idx:0, x:0, y:0, z:0)
result1 = carrier.get_child_identifier(bottle1)
print("测试bottle1 (A1):")
print(f" identifier: {result1['identifier']}")
print(f" idx: {result1['idx']}")
print(f" x index: {result1['x']}")
print(f" y index: {result1['y']}")
print(f" z index: {result1['z']}")
# Assert 验证 bottle1 (A1) 的结果
assert result1['identifier'] == 'A1', f"Expected identifier 'A1', got '{result1['identifier']}'"
assert result1['idx'] == 0, f"Expected idx 0, got {result1['idx']}"
assert result1['x'] == 0, f"Expected x index 0, got {result1['x']}"
assert result1['y'] == 0, f"Expected y index 0, got {result1['y']}"
assert result1['z'] == 0, f"Expected z index 0, got {result1['z']}"
print(" ✓ bottle1 (A1) 测试通过")
print()
# 测试获取bottle2的标识符信息 (A2 = idx:1, x:1, y:0, z:0)
result2 = carrier.get_child_identifier(bottle2)
print("测试bottle2 (A2):")
print(f" identifier: {result2['identifier']}")
print(f" idx: {result2['idx']}")
print(f" x index: {result2['x']}")
print(f" y index: {result2['y']}")
print(f" z index: {result2['z']}")
# Assert 验证 bottle2 (A2) 的结果
assert result2['identifier'] == 'A2', f"Expected identifier 'A2', got '{result2['identifier']}'"
assert result2['idx'] == 1, f"Expected idx 1, got {result2['idx']}"
assert result2['x'] == 1, f"Expected x index 1, got {result2['x']}"
assert result2['y'] == 0, f"Expected y index 0, got {result2['y']}"
assert result2['z'] == 0, f"Expected z index 0, got {result2['z']}"
print(" ✓ bottle2 (A2) 测试通过")
print()
# 测试获取bottle3的标识符信息 (A3 = idx:2, x:2, y:0, z:0)
result3 = carrier.get_child_identifier(bottle3)
print("测试bottle3 (A3):")
print(f" identifier: {result3['identifier']}")
print(f" idx: {result3['idx']}")
print(f" x index: {result3['x']}")
print(f" y index: {result3['y']}")
print(f" z index: {result3['z']}")
# Assert 验证 bottle3 (A3) 的结果
assert result3['identifier'] == 'A3', f"Expected identifier 'A3', got '{result3['identifier']}'"
assert result3['idx'] == 2, f"Expected idx 2, got {result3['idx']}"
assert result3['x'] == 2, f"Expected x index 2, got {result3['x']}"
assert result3['y'] == 0, f"Expected y index 0, got {result3['y']}"
assert result3['z'] == 0, f"Expected z index 0, got {result3['z']}"
print(" ✓ bottle3 (A3) 测试通过")
print()
# 测试错误情况:查找不存在的资源
bottle_not_exists = Bottle("bottle_not_exists", diameter=25.0, height=50.0, max_volume=15.0)
try:
carrier.get_child_identifier(bottle_not_exists)
assert False, "应该抛出 ValueError 异常"
except ValueError as e:
print("✓ 正确抛出了 ValueError 异常:", str(e))
assert "is not assigned to this carrier" in str(e), "异常消息应该包含预期的文本"
print("\n🎉 所有测试都通过了!")
if __name__ == "__main__":
test_get_child_identifier_with_indices()

View File

@@ -1,68 +0,0 @@
import pytest
import json
import os
from pylabrobot.resources import Resource as ResourcePLR
from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.registry.registry import lab_registry
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
lab_registry.setup()
type_mapping = {
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}
@pytest.fixture
def bioyond_materials_reaction() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_reaction.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.fixture
def bioyond_materials_liquidhandling_1() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_1.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.fixture
def bioyond_materials_liquidhandling_2() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_2.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.mark.parametrize("materials_fixture", [
"bioyond_materials_reaction",
"bioyond_materials_liquidhandling_1",
])
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
materials = request.getfixturevalue(materials_fixture)
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
print(deck.summary())
r = ResourceTreeSet.from_plr_resources([deck])
print(r.dump())
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)

View File

@@ -1,186 +0,0 @@
{
"workflow": [
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_1",
"targets": "Liquid_2",
"asp_vol": 66.0,
"dis_vol": 66.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 58.0,
"dis_vol": 96.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 85.0,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 63.333333333333336,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 72.0,
"dis_vol": 150.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 85.0,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 63.333333333333336,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 72.0,
"dis_vol": 150.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 20.0,
"dis_vol": 20.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_5",
"targets": "Liquid_2",
"asp_vol": 6.0,
"dis_vol": 12.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_5",
"targets": "Liquid_2",
"asp_vol": 10.666666666666666,
"dis_vol": 12.0,
"asp_flow_rate": 7.599999999999999,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_6",
"asp_vol": 12.0,
"dis_vol": 10.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
}
],
"reagent": {
"Liquid_6": {
"slot": 1,
"well": [
"A2"
],
"labware": "elution plate"
},
"Liquid_1": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_4": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_5": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_2": {
"slot": 4,
"well": [
"A2"
],
"labware": "TAG1 plate on Magnetic Module GEN2"
},
"Liquid_3": {
"slot": 12,
"well": [
"A1"
],
"labware": "Opentrons Fixed Trash"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -1,63 +0,0 @@
{
"steps_info": [
{
"step_number": 1,
"action": "transfer_liquid",
"parameters": {
"source": "sample supernatant",
"target": "antibody-coated well",
"volume": 100
}
},
{
"step_number": 2,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 3,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 4,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 5,
"action": "transfer_liquid",
"parameters": {
"source": "TMB substrate",
"target": "antibody-coated well",
"volume": 100
}
}
],
"labware_info": [
{"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1},
{"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2},
{"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3},
{"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 5},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 6},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 7},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 8},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 9},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 10},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 11},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 13}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,94 +0,0 @@
import json
import sys
from datetime import datetime
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
import pytest
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
def _normalize_steps(data):
normalized = []
for step in data:
action = step.get("action") or step.get("operation")
if not action:
continue
raw_params = step.get("parameters") or step.get("action_args") or {}
params = dict(raw_params)
if "source" in raw_params and "sources" not in raw_params:
params["sources"] = raw_params["source"]
if "target" in raw_params and "targets" not in raw_params:
params["targets"] = raw_params["target"]
description = step.get("description") or step.get("purpose")
step_dict = {"action": action, "parameters": params}
if description:
step_dict["description"] = description
normalized.append(step_dict)
return normalized
def _normalize_labware(data):
labware = {}
for item in data:
reagent_name = item.get("reagent_name")
key = reagent_name or item.get("material_name") or item.get("name")
if not key:
continue
key = str(key)
idx = 1
original_key = key
while key in labware:
idx += 1
key = f"{original_key}_{idx}"
labware[key] = {
"slot": item.get("positions") or item.get("slot"),
"labware": item.get("material_name") or item.get("labware"),
"well": item.get("well", []),
"type": item.get("type", "reagent"),
"role": item.get("role", ""),
"name": key,
}
return labware
@pytest.mark.parametrize("protocol_name", [
"example_bio",
# "bioyond_materials_liquidhandling_1",
"example_prcxi",
])
def test_build_protocol_graph(protocol_name):
data_path = Path(__file__).with_name(f"{protocol_name}.json")
with data_path.open("r", encoding="utf-8") as fp:
d = json.load(fp)
if "workflow" in d and "reagent" in d:
protocol_steps = d["workflow"]
labware_info = d["reagent"]
elif "steps_info" in d and "labware_info" in d:
protocol_steps = _normalize_steps(d["steps_info"])
labware_info = _normalize_labware(d["labware_info"])
else:
raise ValueError("Unsupported protocol format")
graph = build_protocol_graph(
labware_info=labware_info,
protocol_steps=protocol_steps,
workstation_name="PRCXi",
)
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
draw_protocol_graph_with_ports(graph, str(output_path))
print(graph)

View File

@@ -1 +1 @@
__version__ = "0.10.10"
__version__ = "0.10.7"

View File

@@ -13,7 +13,7 @@ def start_backend(
graph=None,
controllers_config: dict = {},
bridges=[],
is_slave: bool = False,
without_host: bool = False,
visual: str = "None",
resources_mesh_config: dict = {},
**kwargs,
@@ -32,7 +32,7 @@ def start_backend(
raise ValueError(f"Unsupported backend: {backend}")
backend_thread = threading.Thread(
target=main if not is_slave else slave,
target=main if not without_host else slave,
args=(
devices_config,
resources_config,

View File

@@ -11,14 +11,18 @@ from typing import Dict, Any, List
import networkx as nx
import yaml
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
if unilabos_dir not in sys.path:
sys.path.append(unilabos_dir)
from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.resources.graphio import modify_to_backend_format
def load_config_from_file(config_path):
if config_path is None:
@@ -180,7 +184,6 @@ def main():
working_dir = os.path.abspath(os.getcwd())
else:
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
if args_dict.get("working_dir"):
working_dir = args_dict.get("working_dir", "")
if config_path and not os.path.exists(config_path):
@@ -212,14 +215,6 @@ def main():
# 加载配置文件
print_status(f"当前工作目录为 {working_dir}", "info")
load_config_from_file(config_path)
# 根据配置重新设置日志级别
from unilabos.utils.log import configure_logger, logger
if hasattr(BasicConfig, "log_level"):
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
configure_logger(loglevel=BasicConfig.log_level)
if args_dict["addr"] == "test":
print_status("使用测试环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
@@ -273,8 +268,6 @@ def main():
from unilabos.app.web import http_client
from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources
from unilabos.resources.graphio import modify_to_backend_format
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
# 显示启动横幅
print_unilab_banner(args_dict)
@@ -356,7 +349,7 @@ def main():
if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk:
if args_dict.get("ak") and args_dict.get("sk"):
print_status("开始注册设备到服务端...", "info")
try:
register_devices_and_resources(lab_registry)
@@ -375,23 +368,22 @@ def main():
args_dict["bridges"] = []
# 获取通信客户端仅支持WebSocket
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client)
# 获取通信客户端仅支持WebSocket
if BasicConfig.is_host_mode:
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
if "websocket" in args_dict["app_bridges"]:
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
else:
print_status("SlaveMode跳过Websocket连接")
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D
@@ -419,23 +411,7 @@ def main():
)
server_thread.start()
asyncio.set_event_loop(asyncio.new_event_loop())
try:
resource_visualization.start()
except OSError as e:
if "AMENT_PREFIX_PATH" in str(e):
print_status(
f"ROS 2环境未正确设置跳过3D可视化启动。错误详情: {e}",
"warning"
)
print_status(
"建议解决方案:\n"
"1. 激活Conda环境: conda activate unilab\n"
"2. 或使用 --backend simple 参数\n"
"3. 或使用 --visual disable 参数禁用可视化",
"info"
)
else:
raise
resource_visualization.start()
while True:
time.sleep(1)
else:

View File

@@ -1,12 +1,11 @@
import json
import time
from typing import Optional, Tuple, Dict, Any
from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
def register_devices_and_resources(lab_registry):
"""
注册设备和资源到服务器仅支持HTTP
"""
@@ -29,8 +28,6 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
if gather_only:
return devices_to_register, resources_to_register
# 注册设备
if devices_to_register:
try:

View File

@@ -6,8 +6,6 @@ HTTP客户端模块
import json
import os
import time
from threading import Thread
from typing import List, Dict, Any, Optional
import requests
@@ -75,8 +73,6 @@ class HTTPClient:
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
# 从序列化数据中提取所有节点的UUID保存旧UUID
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
if not self.initialized or first_add:
@@ -86,18 +82,16 @@ class HTTPClient:
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=60,
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=10,
timeout=100,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
# 处理响应构建UUID映射
uuid_mapping = {}
if response.status_code == 200:
@@ -128,16 +122,12 @@ class HTTPClient:
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
response = requests.post(
f"{self.remote_addr}/edge/material/query",
json={"uuids": uuid_list, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
@@ -193,16 +183,12 @@ class HTTPClient:
Returns:
Dict: 返回的资源数据
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
response = requests.get(
f"{self.remote_addr}/lab/material",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
return response.json()
def resource_del(self, id: str) -> requests.Response:

View File

@@ -261,28 +261,29 @@ class DeviceActionManager:
device_key = job_info.device_action_key
# 如果是正在执行的任务
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
# 清理active job状态
del self.active_jobs[device_key]
job_info.status = JobStatus.ENDED
# 从all_jobs中移除
del self.all_jobs[job_id]
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
if (
device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id
): # 后面需要和cancel_goal进行联动而不是在这里进行处理现在默认等待这个job结束
# del self.active_jobs[device_key]
# job_info.status = JobStatus.ENDED
# # 从all_jobs中移除
# del self.all_jobs[job_id]
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
# logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
# 启动下一个任务
if device_key in self.device_queues and self.device_queues[device_key]:
next_job = self.device_queues[device_key].pop(0)
# 将下一个job设置为READY状态并放入active_jobs
next_job.status = JobStatus.READY
next_job.update_timestamp()
next_job.set_ready_timeout(10)
self.active_jobs[device_key] = next_job
next_job_log = format_job_log(
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
)
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
return True
# # 启动下一个任务
# if device_key in self.device_queues and self.device_queues[device_key]:
# next_job = self.device_queues[device_key].pop(0)
# # 将下一个job设置为READY状态并放入active_jobs
# next_job.status = JobStatus.READY
# next_job.update_timestamp()
# next_job.set_ready_timeout(10)
# self.active_jobs[device_key] = next_job
# next_job_log = format_job_log(next_job.job_id, next_job.task_id,
# next_job.device_id, next_job.action_name)
# logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
# return True
pass
# 如果是排队中的任务
elif device_key in self.device_queues:
@@ -740,51 +741,31 @@ class MessageProcessor:
job_info.action_name if job_info else "",
)
# 先通知HostNode取消ROS2 action如果存在
host_node = HostNode.get_instance(0)
ros_cancel_success = False
if host_node:
ros_cancel_success = host_node.cancel_goal(job_id)
if ros_cancel_success:
logger.info(f"[MessageProcessor] ROS2 cancel request sent for job {job_log}")
else:
logger.debug(
f"[MessageProcessor] Job {job_log} not in ROS2 goals " "(may be queued or already finished)"
)
# 按job_id取消单个job清理状态机
# 按job_id取消单个job
success = self.device_manager.cancel_job(job_id)
if success:
logger.info(f"[MessageProcessor] Job {job_log} cancelled from queue/active list")
# 通知HostNode取消
host_node = HostNode.get_instance(0)
if host_node:
host_node.cancel_goal(job_id)
logger.info(f"[MessageProcessor] Job {job_log} cancelled")
# 通知QueueProcessor有队列更新
if self.queue_processor:
self.queue_processor.notify_queue_update()
else:
logger.warning(f"[MessageProcessor] Failed to cancel job {job_log} from queue")
logger.warning(f"[MessageProcessor] Failed to cancel job {job_log}")
elif task_id:
# 先通知HostNode取消所有ROS2 actions
# 需要先获取所有相关job_ids
jobs_to_cancel = []
with self.device_manager.lock:
jobs_to_cancel = [
job_info for job_info in self.device_manager.all_jobs.values() if job_info.task_id == task_id
]
host_node = HostNode.get_instance(0)
if host_node and jobs_to_cancel:
ros_cancelled_count = 0
for job_info in jobs_to_cancel:
if host_node.cancel_goal(job_info.job_id):
ros_cancelled_count += 1
logger.info(
f"[MessageProcessor] Sent ROS2 cancel for " f"{ros_cancelled_count}/{len(jobs_to_cancel)} jobs"
)
# 按task_id取消所有相关job清理状态机
# 按task_id取消所有相关job
cancelled_job_ids = self.device_manager.cancel_jobs_by_task_id(task_id)
if cancelled_job_ids:
# 通知HostNode取消所有job
host_node = HostNode.get_instance(0)
if host_node:
for cancelled_job_id in cancelled_job_ids:
host_node.cancel_goal(cancelled_job_id)
logger.info(f"[MessageProcessor] Cancelled {len(cancelled_job_ids)} jobs for task_id: {task_id}")
# 通知QueueProcessor有队列更新
@@ -1075,19 +1056,11 @@ class QueueProcessor:
"""处理任务完成"""
# 获取job信息用于日志
job_info = self.device_manager.get_job_info(job_id)
# 如果job不存在说明可能已被手动取消
if not job_info:
logger.debug(
f"[QueueProcessor] Job {job_id[:8]} not found in manager " "(may have been cancelled manually)"
)
return
job_log = format_job_log(
job_id,
job_info.task_id,
job_info.device_id,
job_info.action_name,
job_info.task_id if job_info else "",
job_info.device_id if job_info else "",
job_info.action_name if job_info else "",
)
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")

View File

@@ -2,7 +2,7 @@ import base64
import traceback
import os
import importlib.util
from typing import Optional, Literal
from typing import Optional
from unilabos.utils import logger
@@ -18,7 +18,6 @@ class BasicConfig:
vis_2d_enable = False
enable_resource_load = True
communication_protocol = "websocket"
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
@classmethod
def auth_secret(cls):

View File

@@ -1,19 +0,0 @@
# OPC UA 通用客户端
本模块提供了一个通用的 OPC UA 客户端实现可以通过外部配置CSV文件来定义节点并通过JSON配置来执行工作流。
## 特点
- 支持通过 CSV 文件配置 OPC UA 节点(只需提供名称、类型和数据类型,支持节点为中文名需指定NodeLanguage
- 自动查找服务器中的节点无需知道确切的节点ID
- 提供工作流机制
- 支持通过 JSON 配置创建工作流
## 使用方法
step1: 准备opcua_nodes.csv文件
step2: 编写opcua_workflow_example.json,以定义工作流。指定opcua_nodes.csv
step3: 编写工作流对应action
step4: 编写opcua_example.yaml注册表
step5: 编写opcua_example.json组态图。指定opcua_workflow_example.json定义工作流文件

View File

@@ -1,9 +0,0 @@
from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, Object, NodeType, DataType
__all__ = [
'Variable',
'Method',
'Object',
'NodeType',
'DataType',
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, Object, NodeType, DataType, Base
__all__ = [
'Variable',
'Method',
'Object',
'NodeType',
'DataType',
'Base',
]

View File

@@ -1,180 +0,0 @@
# coding=utf-8
from enum import Enum
from abc import ABC, abstractmethod
from typing import Tuple, Union, Optional, Any, List
from opcua import Client, Node
from opcua.ua import NodeId, NodeClass, VariantType
class DataType(Enum):
BOOLEAN = VariantType.Boolean
SBYTE = VariantType.SByte
BYTE = VariantType.Byte
INT16 = VariantType.Int16
UINT16 = VariantType.UInt16
INT32 = VariantType.Int32
UINT32 = VariantType.UInt32
INT64 = VariantType.Int64
UINT64 = VariantType.UInt64
FLOAT = VariantType.Float
DOUBLE = VariantType.Double
STRING = VariantType.String
DATETIME = VariantType.DateTime
BYTESTRING = VariantType.ByteString
class NodeType(Enum):
VARIABLE = NodeClass.Variable
OBJECT = NodeClass.Object
METHOD = NodeClass.Method
OBJECTTYPE = NodeClass.ObjectType
VARIABLETYPE = NodeClass.VariableType
REFERENCETYPE = NodeClass.ReferenceType
DATATYPE = NodeClass.DataType
VIEW = NodeClass.View
class Base(ABC):
def __init__(self, client: Client, name: str, node_id: str, typ: NodeType, data_type: DataType):
self._node_id: str = node_id
self._client = client
self._name = name
self._type = typ
self._data_type = data_type
self._node: Optional[Node] = None
def _get_node(self) -> Node:
if self._node is None:
try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式
if "NumericNodeId" in self._node_id:
# 从字符串中提取命名空间和标识符
import re
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
if match:
ns = int(match.group(1))
identifier = int(match.group(2))
node_id = NodeId(identifier, ns)
self._node = self._client.get_node(node_id)
else:
raise ValueError(f"无法解析节点ID: {self._node_id}")
else:
# 直接使用节点ID字符串
self._node = self._client.get_node(self._node_id)
except Exception as e:
print(f"获取节点失败: {self._node_id}, 错误: {e}")
raise
return self._node
@abstractmethod
def read(self) -> Tuple[Any, bool]:
"""读取节点值,返回(值, 是否出错)"""
pass
@abstractmethod
def write(self, value: Any) -> bool:
"""写入节点值,返回是否出错"""
pass
@property
def type(self) -> NodeType:
return self._type
@property
def node_id(self) -> str:
return self._node_id
@property
def name(self) -> str:
return self._name
class Variable(Base):
def __init__(self, client: Client, name: str, node_id: str, data_type: DataType):
super().__init__(client, name, node_id, NodeType.VARIABLE, data_type)
def read(self) -> Tuple[Any, bool]:
try:
value = self._get_node().get_value()
return value, False
except Exception as e:
print(f"读取变量 {self._name} 失败: {e}")
return None, True
def write(self, value: Any) -> bool:
try:
self._get_node().set_value(value)
return False
except Exception as e:
print(f"写入变量 {self._name} 失败: {e}")
return True
class Method(Base):
def __init__(self, client: Client, name: str, node_id: str, parent_node_id: str, data_type: DataType):
super().__init__(client, name, node_id, NodeType.METHOD, data_type)
self._parent_node_id = parent_node_id
self._parent_node = None
def _get_parent_node(self) -> Node:
if self._parent_node is None:
try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式
if "NumericNodeId" in self._parent_node_id:
# 从字符串中提取命名空间和标识符
import re
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
if match:
ns = int(match.group(1))
identifier = int(match.group(2))
node_id = NodeId(identifier, ns)
self._parent_node = self._client.get_node(node_id)
else:
raise ValueError(f"无法解析父节点ID: {self._parent_node_id}")
else:
# 直接使用节点ID字符串
self._parent_node = self._client.get_node(self._parent_node_id)
except Exception as e:
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
raise
return self._parent_node
def read(self) -> Tuple[Any, bool]:
"""方法节点不支持读取操作"""
return None, True
def write(self, value: Any) -> bool:
"""方法节点不支持写入操作"""
return True
def call(self, *args) -> Tuple[Any, bool]:
"""调用方法,返回(返回值, 是否出错)"""
try:
result = self._get_parent_node().call_method(self._get_node(), *args)
return result, False
except Exception as e:
print(f"调用方法 {self._name} 失败: {e}")
return None, True
class Object(Base):
def __init__(self, client: Client, name: str, node_id: str):
super().__init__(client, name, node_id, NodeType.OBJECT, None)
def read(self) -> Tuple[Any, bool]:
"""对象节点不支持直接读取操作"""
return None, True
def write(self, value: Any) -> bool:
"""对象节点不支持直接写入操作"""
return True
def get_children(self) -> Tuple[List[Node], bool]:
"""获取子节点列表,返回(子节点列表, 是否出错)"""
try:
children = self._get_node().get_children()
return children, False
except Exception as e:
print(f"获取对象 {self._name} 的子节点失败: {e}")
return [], True

View File

@@ -1,98 +0,0 @@
{
"register_node_list_from_csv_path": {
"path": "simple_opcua_nodes.csv"
},
"create_flow": [
{
"name": "温度控制流程",
"action": [
{
"name": "温度控制动作",
"node_function_to_create": [
{
"func_name": "read_temperature",
"node_name": "Temperature",
"mode": "read"
},
{
"func_name": "read_heating_status",
"node_name": "HeatingStatus",
"mode": "read"
},
{
"func_name": "set_heating",
"node_name": "HeatingEnabled",
"mode": "write",
"value": true
}
],
"create_init_function": {
"func_name": "init_setpoint",
"node_name": "Setpoint",
"mode": "write",
"value": 25.0
},
"create_start_function": {
"func_name": "start_heating_control",
"node_name": "HeatingEnabled",
"mode": "write",
"write_functions": [
"set_heating"
],
"condition_functions": [
"read_temperature",
"read_heating_status"
],
"stop_condition_expression": "read_temperature >= 25.0 and read_heating_status"
},
"create_stop_function": {
"func_name": "stop_heating",
"node_name": "HeatingEnabled",
"mode": "write",
"value": false
},
"create_cleanup_function": null
}
]
},
{
"name": "报警重置流程",
"action": [
{
"name": "报警重置动作",
"node_function_to_create": [
{
"func_name": "reset_alarm",
"node_name": "ResetAlarm",
"mode": "call",
"value": []
}
],
"create_init_function": null,
"create_start_function": {
"func_name": "start_reset_alarm",
"node_name": "ResetAlarm",
"mode": "call",
"write_functions": [],
"condition_functions": [
"reset_alarm"
],
"stop_condition_expression": "True"
},
"create_stop_function": null,
"create_cleanup_function": null
}
]
},
{
"name": "完整控制流程",
"action": [
"温度控制流程",
"报警重置流程"
]
}
],
"execute_flow": [
"完整控制流程"
]
}

View File

@@ -1,2 +0,0 @@
Name,EnglishName,NodeType,DataType,NodeLanguage
中文名,EnglishName,VARIABLE,INT32,Chinese
1 Name EnglishName NodeType DataType NodeLanguage
2 中文名 EnglishName VARIABLE INT32 Chinese

View File

@@ -1,30 +0,0 @@
{
"register_node_list_from_csv_path": {
"path": "opcua_nodes_example.csv"
},
"create_flow": [
{
"name": "name",
"description": "description",
"parameters": ["parameter1", "parameter2"],
"action": [
{
"init_function": {
"func_name": "init_grab_params",
"write_nodes": ["parameter1", "parameter2"]
},
"start_function": {
"func_name": "start_grab",
"write_nodes": {"parameter_start": true},
"condition_nodes": ["parameter_condition"],
"stop_condition_expression": "parameter_condition == True"
},
"stop_function": {
"func_name": "stop_grab",
"write_nodes": {"parameter_start": false}
}
}
]
}
]
}

View File

@@ -1,311 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OPC UA测试服务器
用于测试OPC UA客户端功能特别是temperature_control和valve_control工作流
"""
import sys
import time
import logging
from opcua import Server, ua
import threading
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class OpcUaTestServer:
"""OPC UA测试服务器类"""
def __init__(self, endpoint="opc.tcp://localhost:4840/freeopcua/server/"):
"""
初始化OPC UA服务器
Args:
endpoint: 服务器端点URL
"""
self.server = Server()
self.server.set_endpoint(endpoint)
# 设置服务器名称
self.server.set_server_name("UniLabOS OPC UA Test Server")
# 设置服务器命名空间
self.idx = self.server.register_namespace("http://unilabos.com/opcua/test")
# 获取Objects节点
self.objects = self.server.get_objects_node()
# 创建设备对象
self.device = self.objects.add_object(self.idx, "TestDevice")
# 存储所有节点的字典
self.nodes = {}
# 初始化标志
self.running = False
# 控制标志
self.simulation_active = True
def add_variable(self, name, value, data_type=None):
"""
添加变量节点
Args:
name: 变量名称
value: 初始值
data_type: 数据类型 (可选)
"""
if data_type is None:
var = self.device.add_variable(self.idx, name, value)
else:
var = self.device.add_variable(self.idx, name, value, data_type)
# 设置变量可写
var.set_writable()
# 存储节点
self.nodes[name] = var
logger.info(f"添加变量节点: {name}, 初始值: {value}")
return var
def add_method(self, name, callback, inputs=None, outputs=None):
"""
添加方法节点
Args:
name: 方法名称
callback: 回调函数
inputs: 输入参数列表 [(name, type), ...]
outputs: 输出参数列表 [(name, type), ...]
"""
if inputs is None:
inputs = []
if outputs is None:
outputs = []
# 创建输入参数
input_args = []
for arg_name, arg_type in inputs:
input_args.append(ua.Argument())
input_args[-1].Name = arg_name
input_args[-1].DataType = arg_type
input_args[-1].ValueRank = -1
# 创建输出参数
output_args = []
for arg_name, arg_type in outputs:
output_args.append(ua.Argument())
output_args[-1].Name = arg_name
output_args[-1].DataType = arg_type
output_args[-1].ValueRank = -1
# 添加方法
method = self.device.add_method(
self.idx,
name,
callback,
input_args,
output_args
)
# 存储节点
self.nodes[name] = method
logger.info(f"添加方法节点: {name}")
return method
def start(self):
"""启动服务器"""
if not self.running:
self.server.start()
self.running = True
logger.info("OPC UA服务器已启动")
# 启动模拟线程
self.simulation_thread = threading.Thread(target=self.run_simulation)
self.simulation_thread.daemon = True
self.simulation_thread.start()
def stop(self):
"""停止服务器"""
if self.running:
self.simulation_active = False
if hasattr(self, 'simulation_thread'):
self.simulation_thread.join(timeout=2)
self.server.stop()
self.running = False
logger.info("OPC UA服务器已停止")
def get_node(self, name):
"""获取节点"""
if name in self.nodes:
return self.nodes[name]
return None
def update_variable(self, name, value):
"""更新变量值"""
if name in self.nodes:
self.nodes[name].set_value(value)
logger.debug(f"更新变量 {name} = {value}")
return True
logger.warning(f"变量 {name} 不存在")
return False
def run_simulation(self):
"""运行模拟线程"""
logger.info("启动模拟线程")
temp = 20.0
valve_position = 0.0
flow_rate = 0.0
while self.simulation_active and self.running:
try:
# 温度控制模拟
heating_enabled = self.get_node("HeatingEnabled").get_value()
setpoint = self.get_node("Setpoint").get_value()
if heating_enabled:
self.update_variable("HeatingStatus", True)
if temp < setpoint:
temp += 0.5 # 加快温度上升速度
else:
temp -= 0.1
else:
self.update_variable("HeatingStatus", False)
if temp > 20.0:
temp -= 0.2
# 更新温度
self.update_variable("Temperature", round(temp, 2))
# 阀门控制模拟
valve_control = self.get_node("ValveControl").get_value()
valve_setpoint = self.get_node("ValveSetpoint").get_value()
if valve_control:
if valve_position < valve_setpoint:
valve_position += 5.0 # 加快阀门开启速度
if valve_position > valve_setpoint:
valve_position = valve_setpoint
else:
valve_position -= 1.0
if valve_position < 0:
valve_position = 0
else:
if valve_position > 0:
valve_position -= 5.0
if valve_position < 0:
valve_position = 0
# 更新阀门位置
self.update_variable("ValvePosition", round(valve_position, 2))
# 流量模拟 - 与阀门位置成正比
flow_rate = valve_position * 0.2 # 简单线性关系
self.update_variable("FlowRate", round(flow_rate, 2))
# 更新系统状态
status = []
if heating_enabled:
status.append("Heating")
if valve_control:
status.append("Valve_Open")
if status:
self.update_variable("SystemStatus", "_".join(status))
else:
self.update_variable("SystemStatus", "Idle")
# 每200毫秒更新一次
time.sleep(0.2)
except Exception as e:
logger.error(f"模拟线程错误: {e}")
time.sleep(1) # 出错时稍等一会再继续
logger.info("模拟线程已停止")
def reset_alarm_callback(parent, *args):
"""重置报警的回调函数"""
logger.info("调用了重置报警方法")
return True
def start_process_callback(parent, *args):
"""启动流程的回调函数"""
process_id = args[0] if args else 0
logger.info(f"启动流程 ID: {process_id}")
return process_id
def stop_process_callback(parent, *args):
"""停止流程的回调函数"""
process_id = args[0] if args else 0
logger.info(f"停止流程 ID: {process_id}")
return True
def main():
"""主函数"""
try:
# 创建服务器
server = OpcUaTestServer()
# 添加变量节点 - 温度控制相关
server.add_variable("Temperature", 20.0, ua.VariantType.Float)
server.add_variable("Setpoint", 22.0, ua.VariantType.Float)
server.add_variable("HeatingEnabled", False, ua.VariantType.Boolean)
server.add_variable("HeatingStatus", False, ua.VariantType.Boolean)
# 添加变量节点 - 阀门控制相关
server.add_variable("ValvePosition", 0.0, ua.VariantType.Float)
server.add_variable("ValveSetpoint", 0.0, ua.VariantType.Float)
server.add_variable("ValveControl", False, ua.VariantType.Boolean)
server.add_variable("FlowRate", 0.0, ua.VariantType.Float)
# 其他状态变量
server.add_variable("SystemStatus", "Idle", ua.VariantType.String)
# 添加方法节点
server.add_method(
"ResetAlarm",
reset_alarm_callback,
[],
[("Result", ua.VariantType.Boolean)]
)
server.add_method(
"StartProcess",
start_process_callback,
[("ProcessId", ua.VariantType.Int32)],
[("Result", ua.VariantType.Int32)]
)
server.add_method(
"StopProcess",
stop_process_callback,
[("ProcessId", ua.VariantType.Int32)],
[("Result", ua.VariantType.Boolean)]
)
# 启动服务器
server.start()
logger.info("服务器已启动按Ctrl+C停止")
# 保持服务器运行
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("收到键盘中断,正在停止服务器...")
# 停止服务器
server.stop()
except Exception as e:
logger.error(f"服务器错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View File

@@ -1,25 +0,0 @@
dummy2_robot:
kinematics:
# DH parameters for Dummy2 6-DOF robot arm
# [theta, d, a, alpha] for each joint
joint_1: [0.0, 0.1, 0.0, 1.5708] # Base rotation
joint_2: [0.0, 0.0, 0.2, 0.0] # Shoulder
joint_3: [0.0, 0.0, 0.15, 0.0] # Elbow
joint_4: [0.0, 0.1, 0.0, 1.5708] # Wrist roll
joint_5: [0.0, 0.0, 0.0, -1.5708] # Wrist pitch
joint_6: [0.0, 0.06, 0.0, 0.0] # Wrist yaw
# Tool center point offset from last joint
tcp_offset:
x: 0.0
y: 0.0
z: 0.04
# Workspace limits
workspace:
x_min: -0.5
x_max: 0.5
y_min: -0.5
y_max: 0.5
z_min: 0.0
z_max: 0.6

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--This does not replace URDF, and is not an extension of URDF.
This is a format for representing semantic information about the robot structure.
A URDF file must exist for this robot as well, where the joints and the links that are referenced are defined
-->
<robot name="dummy2">
<!--GROUPS: Representation of a set of joints and links. This can be useful for specifying DOF to plan for, defining arms, end effectors, etc-->
<!--LINKS: When a link is specified, the parent joint of that link (if it exists) is automatically included-->
<!--JOINTS: When a joint is specified, the child link of that joint (which will always exist) is automatically included-->
<!--CHAINS: When a chain is specified, all the links along the chain (including endpoints) are included in the group. Additionally, all the joints that are parents to included links are also included. This means that joints along the chain and the parent joint of the base link are included in the group-->
<!--SUBGROUPS: Groups can also be formed by referencing to already defined group names-->
<group name="dummy2_arm">
<joint name="virtual_joint"/>
<joint name="Joint1"/>
<joint name="Joint2"/>
<joint name="Joint3"/>
<joint name="Joint4"/>
<joint name="Joint5"/>
<joint name="Joint6"/>
</group>
<!--GROUP STATES: Purpose: Define a named state for a particular group, in terms of joint values. This is useful to define states like 'folded arms'-->
<group_state name="home" group="dummy2_arm">
<joint name="Joint1" value="0"/>
<joint name="Joint2" value="0"/>
<joint name="Joint3" value="0"/>
<joint name="Joint4" value="0"/>
<joint name="Joint5" value="0"/>
<joint name="Joint6" value="0"/>
</group_state>
<!--VIRTUAL JOINT: Purpose: this element defines a virtual joint between a robot link and an external frame of reference (considered fixed with respect to the robot)-->
<virtual_joint name="virtual_joint" type="fixed" parent_frame="world" child_link="base_link"/>
<!--DISABLE COLLISIONS: By default it is assumed that any link of the robot could potentially come into collision with any other link in the robot. This tag disables collision checking between a specified pair of links. -->
<disable_collisions link1="J1_1" link2="J2_1" reason="Adjacent"/>
<disable_collisions link1="J1_1" link2="J3_1" reason="Never"/>
<disable_collisions link1="J1_1" link2="J4_1" reason="Never"/>
<disable_collisions link1="J1_1" link2="base_link" reason="Adjacent"/>
<disable_collisions link1="J2_1" link2="J3_1" reason="Adjacent"/>
<disable_collisions link1="J3_1" link2="J4_1" reason="Adjacent"/>
<disable_collisions link1="J3_1" link2="J5_1" reason="Never"/>
<disable_collisions link1="J3_1" link2="J6_1" reason="Never"/>
<disable_collisions link1="J3_1" link2="base_link" reason="Never"/>
<disable_collisions link1="J4_1" link2="J5_1" reason="Adjacent"/>
<disable_collisions link1="J4_1" link2="J6_1" reason="Never"/>
<disable_collisions link1="J5_1" link2="J6_1" reason="Adjacent"/>
</robot>

View File

@@ -1,70 +0,0 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro" >
<transmission name="Joint1_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint1">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint1_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint2_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint2">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint2_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint3_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint3">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint3_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint4_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint4">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint4_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint5_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint5">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint5_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint6_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint6">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint6_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
</robot>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="dummy2">
<xacro:arg name="initial_positions_file" default="initial_positions.yaml" />
<!-- Import dummy2 urdf file -->
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.xacro" />
<!-- Import control_xacro -->
<xacro:include filename="dummy2.ros2_control.xacro" />
<xacro:dummy2_ros2_control name="FakeSystem" initial_positions_file="$(arg initial_positions_file)"/>
</robot>

View File

@@ -1,73 +0,0 @@
###############################################
# Modify all parameters related to servoing here
###############################################
# adapt to dummy2 by Muzhxiaowen, check out the details on bilibili.com
use_gazebo: false # Whether the robot is started in a Gazebo simulation environment
## Properties of incoming commands
command_in_type: "unitless" # "unitless"> in the range [-1:1], as if from joystick. "speed_units"> cmds are in m/s and rad/s
scale:
# Scale parameters are only used if command_in_type=="unitless"
linear: 0.4 # Max linear velocity. Unit is [m/s]. Only used for Cartesian commands.
rotational: 0.8 # Max angular velocity. Unit is [rad/s]. Only used for Cartesian commands.
# Max joint angular/linear velocity. Only used for joint commands on joint_command_in_topic.
joint: 0.5
# Optionally override Servo's internal velocity scaling when near singularity or collision (0.0 = use internal velocity scaling)
# override_velocity_scaling_factor = 0.0 # valid range [0.0:1.0]
## Properties of outgoing commands
publish_period: 0.034 # 1/Nominal publish rate [seconds]
low_latency_mode: false # Set this to true to publish as soon as an incoming Twist command is received (publish_period is ignored)
# What type of topic does your robot driver expect?
# Currently supported are std_msgs/Float64MultiArray or trajectory_msgs/JointTrajectory
command_out_type: trajectory_msgs/JointTrajectory
# What to publish? Can save some bandwidth as most robots only require positions or velocities
publish_joint_positions: true
publish_joint_velocities: true
publish_joint_accelerations: false
## Plugins for smoothing outgoing commands
smoothing_filter_plugin_name: "online_signal_smoothing::ButterworthFilterPlugin"
# If is_primary_planning_scene_monitor is set to true, the Servo server's PlanningScene advertises the /get_planning_scene service,
# which other nodes can use as a source for information about the planning environment.
# NOTE: If a different node in your system is responsible for the "primary" planning scene instance (e.g. the MoveGroup node),
# then is_primary_planning_scene_monitor needs to be set to false.
is_primary_planning_scene_monitor: true
## MoveIt properties
move_group_name: dummy2_arm # Often 'manipulator' or 'arm'
planning_frame: base_link # The MoveIt planning frame. Often 'base_link' or 'world'
## Other frames
ee_frame_name: J6_1 # The name of the end effector link, used to return the EE pose
robot_link_command_frame: base_link # commands must be given in the frame of a robot link. Usually either the base or end effector
## Stopping behaviour
incoming_command_timeout: 0.1 # Stop servoing if X seconds elapse without a new command
# If 0, republish commands forever even if the robot is stationary. Otherwise, specify num. to publish.
# Important because ROS may drop some messages and we need the robot to halt reliably.
num_outgoing_halt_msgs_to_publish: 4
## Configure handling of singularities and joint limits
lower_singularity_threshold: 170.0 # Start decelerating when the condition number hits this (close to singularity)
hard_stop_singularity_threshold: 3000.0 # Stop when the condition number hits this
joint_limit_margin: 0.1 # added as a buffer to joint limits [radians]. If moving quickly, make this larger.
leaving_singularity_threshold_multiplier: 2.0 # Multiply the hard stop limit by this when leaving singularity (see https://github.com/ros-planning/moveit2/pull/620)
## Topic names
cartesian_command_in_topic: ~/delta_twist_cmds # Topic for incoming Cartesian twist commands
joint_command_in_topic: ~/delta_joint_cmds # Topic for incoming joint angle commands
joint_topic: /joint_states
status_topic: ~/status # Publish status to this topic
command_out_topic: /dummy2_arm_controller/joint_trajectory # Publish outgoing commands here
## Collision checking for the entire robot body
check_collisions: true # Check collisions?
collision_check_rate: 10.0 # [Hz] Collision-checking can easily bog down a CPU if done too often.
self_collision_proximity_threshold: 0.001 # Start decelerating when a self-collision is this far [m]
scene_collision_proximity_threshold: 0.002 # Start decelerating when a scene collision is this far [m]

View File

@@ -1,9 +0,0 @@
# Default initial positions for dummy2's ros2_control fake system
initial_positions:
Joint1: 0
Joint2: 0
Joint3: 0
Joint4: 0
Joint5: 0
Joint6: 0

View File

@@ -1,40 +0,0 @@
# joint_limits.yaml allows the dynamics properties specified in the URDF to be overwritten or augmented as needed
# For beginners, we downscale velocity and acceleration limits.
# You can always specify higher scaling factors (<= 1.0) in your motion requests. # Increase the values below to 1.0 to always move at maximum speed.
default_velocity_scaling_factor: 0.1
default_acceleration_scaling_factor: 0.1
# Specific joint properties can be changed with the keys [max_position, min_position, max_velocity, max_acceleration]
# Joint limits can be turned off with [has_velocity_limits, has_acceleration_limits]
joint_limits:
joint_1:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_2:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_3:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_4:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_5:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_6:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0

View File

@@ -1,4 +0,0 @@
dummy2_arm:
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
kinematics_solver_search_resolution: 0.0050000000000000001
kinematics_solver_timeout: 0.5

View File

@@ -1,60 +0,0 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot_ros2_control" params="device_name mesh_path">
<xacro:property name="initial_positions" value="${load_yaml(mesh_path + '/devices/dummy2_robot/config/initial_positions.yaml')['initial_positions']}"/>
<ros2_control name="${device_name}dummy2" type="system">
<hardware>
<!-- By default, set up controllers for simulation. This won't work on real hardware -->
<plugin>mock_components/GenericSystem</plugin>
</hardware>
<!-- <plugin>mock_components/GenericSystem</plugin> -->
<joint name="${device_name}Joint1">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint1']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint2">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint2']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint3">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint3']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint4">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint4']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint5">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint5']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint6">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint6']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
</ros2_control>
</xacro:macro>
</robot>

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot_srdf" params="device_name">
<!--GROUPS: Representation of a set of joints and links. This can be useful for specifying DOF to plan for, defining arms, end effectors, etc-->
<!--LINKS: When a link is specified, the parent joint of that link (if it exists) is automatically included-->
<!--JOINTS: When a joint is specified, the child link of that joint (which will always exist) is automatically included-->
<!--CHAINS: When a chain is specified, all the links along the chain (including endpoints) are included in the group. Additionally, all the joints that are parents to included links are also included. This means that joints along the chain and the parent joint of the base link are included in the group-->
<!--SUBGROUPS: Groups can also be formed by referencing to already defined group names
This is a format for representing semantic information about the robot structure.
A URDF file must exist for this robot as well, where the joints and the links that are referenced are defined
-->
<group name="${device_name}dummy2_arm">
<joint name="${device_name}virtual_joint"/>
<joint name="${device_name}Joint1"/>
<joint name="${device_name}Joint2"/>
<joint name="${device_name}Joint3"/>
<joint name="${device_name}Joint4"/>
<joint name="${device_name}Joint5"/>
<joint name="${device_name}Joint6"/>
</group>
<!--GROUP STATES: Purpose: Define a named state for a particular group, in terms of joint values. This is useful to define states like 'folded arms'-->
<group_state name="home" group="${device_name}dummy2_arm">
<joint name="${device_name}Joint1" value="0"/>
<joint name="${device_name}Joint2" value="0"/>
<joint name="${device_name}Joint3" value="0"/>
<joint name="${device_name}Joint4" value="0"/>
<joint name="${device_name}Joint5" value="0"/>
<joint name="${device_name}Joint6" value="0"/>
</group_state>
<!--VIRTUAL JOINT: Purpose: this element defines a virtual joint between a robot link and an external frame of reference (considered fixed with respect to the robot)-->
<virtual_joint name="${device_name}virtual_joint" type="fixed" parent_frame="world" child_link="${device_name}base_link"/>
<!--DISABLE COLLISIONS: By default it is assumed that any link of the robot could potentially come into collision with any other link in the robot. This tag disables collision checking between a specified pair of links. -->
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J2_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J3_1" reason="Never"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J4_1" reason="Never"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}base_link" reason="Adjacent"/>
<disable_collisions link1="${device_name}J2_1" link2="${device_name}J3_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J4_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J5_1" reason="Never"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J6_1" reason="Never"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}base_link" reason="Never"/>
<disable_collisions link1="${device_name}J4_1" link2="${device_name}J5_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J4_1" link2="${device_name}J6_1" reason="Never"/>
<disable_collisions link1="${device_name}J5_1" link2="${device_name}J6_1" reason="Adjacent"/>
</xacro:macro>
</robot>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro" >
<material name="silver">
<color rgba="0.700 0.700 0.700 1.000"/>
</material>
</robot>

View File

@@ -1,14 +0,0 @@
{
"arm": {
"joint_names": [
"joint_1",
"joint_2",
"joint_3",
"joint_4",
"joint_5",
"joint_6"
],
"base_link_name": "base_link",
"end_effector_name": "J6_1"
}
}

View File

@@ -1,51 +0,0 @@
Panels:
- Class: rviz_common/Displays
Name: Displays
Property Tree Widget:
Expanded:
- /MotionPlanning1
- Class: rviz_common/Help
Name: Help
- Class: rviz_common/Views
Name: Views
Visualization Manager:
Displays:
- Class: rviz_default_plugins/Grid
Name: Grid
Value: true
- Class: moveit_rviz_plugin/MotionPlanning
Name: MotionPlanning
Planned Path:
Loop Animation: true
State Display Time: 0.05 s
Trajectory Topic: display_planned_path
Planning Scene Topic: monitored_planning_scene
Robot Description: robot_description
Scene Geometry:
Scene Alpha: 1
Scene Robot:
Robot Alpha: 0.5
Value: true
Global Options:
Fixed Frame: base_link
Tools:
- Class: rviz_default_plugins/Interact
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
Value: true
Views:
Current:
Class: rviz_default_plugins/Orbit
Distance: 2.0
Focal Point:
X: -0.1
Y: 0.25
Z: 0.30
Name: Current View
Pitch: 0.5
Target Frame: base_link
Yaw: -0.623
Window Geometry:
Height: 975
QMainWindow State: 000000ff00000000fd0000000100000000000002b400000375fc0200000005fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000004100fffffffb000000100044006900730070006c006100790073010000003d00000123000000c900fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e00670100000166000001910000018800fffffffb0000000800480065006c0070000000029a0000006e0000006e00fffffffb0000000a0056006900650077007301000002fd000000b5000000a400ffffff000001f60000037500000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
Width: 1200

View File

@@ -1,21 +0,0 @@
# MoveIt uses this configuration for controller management
moveit_controller_manager: moveit_simple_controller_manager/MoveItSimpleControllerManager
moveit_simple_controller_manager:
controller_names:
- dummy2_arm_controller
dummy2_arm_controller:
type: FollowJointTrajectory
action_ns: follow_joint_trajectory
default: true
joints:
- Joint1
- Joint2
- Joint3
- Joint4
- Joint5
- Joint6
action_ns: follow_joint_trajectory
default: true

View File

@@ -1,39 +0,0 @@
dummy2_robot:
# Physical properties for each link
link_masses:
base_link: 5.0
link_1: 3.0
link_2: 2.5
link_3: 2.0
link_4: 1.5
link_5: 1.0
link_6: 0.5
# Center of mass for each link (relative to joint frame)
link_com:
base_link: [0.0, 0.0, 0.05]
link_1: [0.0, 0.0, 0.05]
link_2: [0.1, 0.0, 0.0]
link_3: [0.08, 0.0, 0.0]
link_4: [0.0, 0.0, 0.05]
link_5: [0.0, 0.0, 0.03]
link_6: [0.0, 0.0, 0.02]
# Moment of inertia matrices
link_inertias:
base_link: [0.02, 0.0, 0.0, 0.02, 0.0, 0.02]
link_1: [0.01, 0.0, 0.0, 0.01, 0.0, 0.01]
link_2: [0.008, 0.0, 0.0, 0.008, 0.0, 0.008]
link_3: [0.006, 0.0, 0.0, 0.006, 0.0, 0.006]
link_4: [0.004, 0.0, 0.0, 0.004, 0.0, 0.004]
link_5: [0.002, 0.0, 0.0, 0.002, 0.0, 0.002]
link_6: [0.001, 0.0, 0.0, 0.001, 0.0, 0.001]
# Motor specifications
motor_specs:
joint_1: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_2: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_3: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_4: { max_torque: 50.0, max_speed: 2.0, gear_ratio: 50 }
joint_5: { max_torque: 50.0, max_speed: 2.0, gear_ratio: 50 }
joint_6: { max_torque: 25.0, max_speed: 2.0, gear_ratio: 25 }

View File

@@ -1,6 +0,0 @@
# Limits for the Pilz planner
cartesian_limits:
max_trans_vel: 1.0
max_trans_acc: 2.25
max_trans_dec: -5.0
max_rot_vel: 1.57

View File

@@ -1,26 +0,0 @@
# This config file is used by ros2_control
controller_manager:
ros__parameters:
update_rate: 100 # Hz
dummy2_arm_controller:
type: joint_trajectory_controller/JointTrajectoryController
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
dummy2_arm_controller:
ros__parameters:
joints:
- Joint1
- Joint2
- Joint3
- Joint4
- Joint5
- Joint6
command_interfaces:
- position
state_interfaces:
- position
- velocity

View File

@@ -1,35 +0,0 @@
dummy2_robot:
# Visual appearance settings
materials:
base_material:
color: [0.8, 0.8, 0.8, 1.0] # Light gray
metallic: 0.1
roughness: 0.3
link_material:
color: [0.2, 0.2, 0.8, 1.0] # Blue
metallic: 0.3
roughness: 0.2
joint_material:
color: [0.6, 0.6, 0.6, 1.0] # Dark gray
metallic: 0.5
roughness: 0.1
camera_material:
color: [0.1, 0.1, 0.1, 1.0] # Black
metallic: 0.0
roughness: 0.8
# Mesh scaling factors
mesh_scale: [0.001, 0.001, 0.001] # Convert mm to m
# Collision geometry simplification
collision_geometries:
base_link: "cylinder" # radius: 0.08, height: 0.1
link_1: "cylinder" # radius: 0.05, height: 0.15
link_2: "box" # size: [0.2, 0.08, 0.08]
link_3: "box" # size: [0.15, 0.06, 0.06]
link_4: "cylinder" # radius: 0.03, height: 0.1
link_5: "cylinder" # radius: 0.025, height: 0.06
link_6: "cylinder" # radius: 0.02, height: 0.04

View File

@@ -1,237 +0,0 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find dummy2_description)/urdf/materials.xacro" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.trans" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.gazebo" />
<link name="world" />
<joint name="world_joint" type="fixed">
<parent link="world" />
<child link = "base_link" />
<origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0" />
</joint>
<link name="base_link">
<inertial>
<origin xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="J1_1"/>
<child link="J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="J2_1"/>
<child link="J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="J3_1"/>
<child link="J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="J4_1"/>
<child link="J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</robot>

View File

@@ -1,37 +0,0 @@
joint_limits:
joint_1:
effort: 150
velocity: 2.0
lower: !degrees -180
upper: !degrees 180
joint_2:
effort: 150
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_3:
effort: 150
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_4:
effort: 50
velocity: 2.0
lower: !degrees -180
upper: !degrees 180
joint_5:
effort: 50
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_6:
effort: 25
velocity: 2.0
lower: !degrees -180
upper: !degrees 180

View File

@@ -1,249 +0,0 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot" params="mesh_path:='' parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 rx:=0 ry:=0 r:=0">
<xacro:include filename="${mesh_path}/devices/dummy2_robot/config/materials.xacro" />
<xacro:include filename="${mesh_path}/devices/dummy2_robot/config/dummy2.trans" />
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="${rx} ${ry} ${r}" />
<parent link="${parent_link}"/>
<child link="${station_name}${device_name}device_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}device_link"/>
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
<origin xyz="0 0 0" rpy="0 0 0" />
<parent link="${station_name}${device_name}device_link"/>
<child link="${station_name}${device_name}base_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}base_link">
<inertial>
<origin xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J1_1"/>
<child link="${station_name}${device_name}J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J2_1"/>
<child link="${station_name}${device_name}J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J3_1"/>
<child link="${station_name}${device_name}J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J4_1"/>
<child link="${station_name}${device_name}J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J5_1"/>
<child link="${station_name}${device_name}J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="${station_name}${device_name}camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J5_1"/>
<child link="${station_name}${device_name}camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</xacro:macro>
</robot>

View File

@@ -1,237 +0,0 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find dummy2_description)/urdf/materials.xacro" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.trans" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.gazebo" />
<link name="world" />
<joint name="world_joint" type="fixed">
<parent link="world" />
<child link = "base_link" />
<origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0" />
</joint>
<link name="base_link">
<inertial>
<origin xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="J1_1"/>
<child link="J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="J2_1"/>
<child link="J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="J3_1"/>
<child link="J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="J4_1"/>
<child link="J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</robot>

View File

View File

View File

View File

View File

@@ -14,7 +14,6 @@ from launch_ros.parameter_descriptions import ParameterFile
from unilabos.registry.registry import lab_registry
from ament_index_python.packages import get_package_share_directory
def get_pattern_matches(folder, pattern):
"""Given all the files in the folder, find those that match the pattern.
@@ -52,7 +51,7 @@ class ResourceVisualization:
self.launch_description = LaunchDescription()
self.resource_dict = resource
self.resource_model = {}
self.resource_type = ['deck', 'plate', 'container', 'tip_rack']
self.resource_type = ['deck', 'plate', 'container']
self.mesh_path = Path(__file__).parent.absolute()
self.enable_rviz = enable_rviz
registry = lab_registry
@@ -129,9 +128,9 @@ class ResourceVisualization:
# if node["parent"] is not None:
# new_dev.set("station_name", node["parent"]+'_')
new_dev.set("x",str(float(node["position"]["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["position"]["z"])/1000))
new_dev.set("x",str(float(node["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["z"])/1000))
if "rotation" in node["config"]:
new_dev.set("rx",str(float(node["config"]["rotation"]["x"])))
new_dev.set("ry",str(float(node["config"]["rotation"]["y"])))
@@ -141,7 +140,7 @@ class ResourceVisualization:
new_dev.set(key, str(value))
# 添加ros2_controller
if node['class'].find('moveit.')!= -1:
if node['class'].startswith('moveit.'):
new_include_controller = etree.SubElement(self.root, f"{{{xacro_uri}}}include")
new_include_controller.set("filename", f"{str(self.mesh_path)}/devices/{model_config['mesh']}/config/macro.ros2_control.xacro")
new_controller = etree.SubElement(self.root, f"{{{xacro_uri}}}{model_config['mesh']}_ros2_control")
@@ -204,24 +203,7 @@ class ResourceVisualization:
Returns:
LaunchDescription: launch描述对象
"""
# 检查ROS 2环境变量
if "AMENT_PREFIX_PATH" not in os.environ:
raise OSError(
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
"请确保:\n"
"1. 已安装ROS 2 (推荐使用 ros-humble-desktop-full)\n"
"2. 已激活Conda环境: conda activate unilab\n"
"3. 或手动source ROS 2 setup文件: source /opt/ros/humble/setup.bash\n"
"4. 或者使用 --backend simple 参数跳过ROS依赖"
)
try:
moveit_configs_utils_path = Path(get_package_share_directory("moveit_configs_utils"))
except Exception as e:
raise OSError(
f"无法找到moveit_configs_utils包。请确保ROS 2和MoveIt 2已正确安装。\n"
f"原始错误: {e}"
)
moveit_configs_utils_path = Path(get_package_share_directory("moveit_configs_utils"))
default_folder = moveit_configs_utils_path / "default_configs"
planning_pattern = re.compile("^(.*)_planning.yaml$")
pipelines = []
@@ -282,8 +264,7 @@ class ResourceVisualization:
parameters=[
{"robot_description": robot_description},
ros2_controllers,
],
env=dict(os.environ)
]
)
)
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
@@ -293,7 +274,6 @@ class ResourceVisualization:
executable="spawner",
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
output="screen",
env=dict(os.environ)
)
)
controllers.append(
@@ -302,7 +282,6 @@ class ResourceVisualization:
executable="spawner",
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
output="screen",
env=dict(os.environ)
)
)
for i in controllers:
@@ -321,8 +300,7 @@ class ResourceVisualization:
'use_sim_time': False
},
# kinematics_dict
],
env=dict(os.environ)
]
)
@@ -353,8 +331,7 @@ class ResourceVisualization:
package='moveit_ros_move_group',
executable='move_group',
output='screen',
parameters=moveit_params,
env=dict(os.environ)
parameters=moveit_params
)
@@ -377,8 +354,7 @@ class ResourceVisualization:
robot_description_planning,
planning_pipelines,
],
env=dict(os.environ)
]
)
self.launch_description.add_action(rviz_node)

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="bottle">
<link name='bottle'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/bottle.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="bottle_container">
<link name='bottle_container'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/bottle_container.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

Some files were not shown because too many files have changed in this diff Show More